From c929672b8c0c0b26776a034dd972e33e4c6833fc Mon Sep 17 00:00:00 2001 From: YaleChen299 Date: Fri, 28 May 2021 23:23:15 +0800 Subject: [PATCH 001/174] update test.exs to include optional test setting --- config/test.exs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/config/test.exs b/config/test.exs index b16cf8c2f..9fbcf5f73 100644 --- a/config/test.exs +++ b/config/test.exs @@ -85,3 +85,6 @@ config :cadet, ] config :arc, storage: Arc.Storage.Local + +if "test.secrets.exs" |> Path.expand(__DIR__) |> File.exists?(), + do: import_config("test.secrets.exs") From 4398684354544e951bb1793f31fd914e5e083f8f Mon Sep 17 00:00:00 2001 From: YaleChen299 Date: Fri, 28 May 2021 17:16:04 +0800 Subject: [PATCH 002/174] initial schema change to account --- lib/cadet/accounts/accounts.ex | 21 ++++++++++++++++----- lib/cadet/accounts/query.ex | 2 ++ lib/cadet/accounts/user.ex | 18 ++++++++++-------- 3 files changed, 28 insertions(+), 13 deletions(-) diff --git a/lib/cadet/accounts/accounts.ex b/lib/cadet/accounts/accounts.ex index 35b349f14..d5e14ccb1 100644 --- a/lib/cadet/accounts/accounts.ex +++ b/lib/cadet/accounts/accounts.ex @@ -14,13 +14,17 @@ defmodule Cadet.Accounts do Returns {:ok, user} on success, otherwise {:error, changeset} """ - def register(attrs = %{username: username}, role) when is_binary(username) do - attrs |> Map.put(:role, role) |> insert_or_update_user() + # def register(attrs = %{username: username}, role) when is_binary(username) do + # attrs |> Map.put(:role, role) |> insert_or_update_user() + # end + def register(attrs = %{username: username}) when is_binary(username) do + attrs |> insert_or_update_user() end @doc """ Creates User entity with specified attributes. """ + # :TODO recheck if deprecated def create_user(attrs \\ %{}) do %User{} |> User.changeset(attrs) @@ -56,6 +60,7 @@ defmodule Cadet.Accounts do @doc """ Returns users matching a given set of criteria. """ + # :TODO to pipe thru some join functon with mapping table so van get group id in a course def get_users(filter \\ []) do User |> join(:left, [u], g in assoc(u, :group)) @@ -79,9 +84,14 @@ defmodule Cadet.Accounts do case Repo.one(Query.username(username)) do nil -> # user is not registered in our database - with {:ok, role} <- Provider.get_role(provider, token), - {:ok, name} <- Provider.get_name(provider, token), - {:ok, _} <- register(%{name: name, username: username}, role) do + # :TODO recheck when designing onboarding process (assign role to module) + # :TODO get_role process to be put in course creation? + # with {:ok, role} <- Provider.get_role(provider, token), + # {:ok, name} <- Provider.get_name(provider, token), + # {:ok, _} <- register(%{name: name, username: username}, role) do + # sign_in(username, name, token) + with {:ok, name} <- Provider.get_name(provider, token), + {:ok, _} <- register(%{name: name, username: username}) do sign_in(username, name, token) else {:error, :invalid_credentials, err} -> @@ -99,6 +109,7 @@ defmodule Cadet.Accounts do end end + # :TODO Pipe through module def update_game_states(user = %User{}, new_game_state = %{}) do case user |> User.changeset(%{game_states: new_game_state}) diff --git a/lib/cadet/accounts/query.ex b/lib/cadet/accounts/query.ex index 3921a65e7..408b9f349 100644 --- a/lib/cadet/accounts/query.ex +++ b/lib/cadet/accounts/query.ex @@ -21,6 +21,7 @@ defmodule Cadet.Accounts.Query do |> of_username(username) end + # :TODO this one need to pipe through course info @spec students_of(%User{}) :: Ecto.Query.t() def students_of(%User{id: id, role: :staff}) do User @@ -28,6 +29,7 @@ defmodule Cadet.Accounts.Query do |> where([_, g], g.leader_id == ^id) end + # :TODO this one need to pipe through course info def avenger_of?(avenger, student_id) do students = students_of(avenger) diff --git a/lib/cadet/accounts/user.ex b/lib/cadet/accounts/user.ex index 3bda352e9..57c56b9af 100644 --- a/lib/cadet/accounts/user.ex +++ b/lib/cadet/accounts/user.ex @@ -7,25 +7,27 @@ defmodule Cadet.Accounts.User do """ use Cadet, :model - alias Cadet.Accounts.Role - alias Cadet.Courses.Group + # alias Cadet.Accounts.Role + # alias Cadet.Course.Group schema "users" do field(:name, :string) - field(:role, Role) + # field(:role, Role) field(:username, :string) - field(:game_states, :map) - belongs_to(:group, Group) + # field(:game_states, :map) + # belongs_to(:group, Group) timestamps() end - @required_fields ~w(name role)a - @optional_fields ~w(username group_id game_states)a + # @required_fields ~w(name role)a + @required_fields ~w(name)a + # @optional_fields ~w(username group_id game_states)a + @optional_fields ~w(username)a def changeset(user, params \\ %{}) do user |> cast(params, @required_fields ++ @optional_fields) - |> add_belongs_to_id_from_model(:group, params) + # |> add_belongs_to_id_from_model(:group, params) |> validate_required(@required_fields) end end From de88eb18d451f7f1494ce4e88c00e1d0d1a8985b Mon Sep 17 00:00:00 2001 From: YaleChen299 Date: Tue, 1 Jun 2021 23:22:30 +0800 Subject: [PATCH 003/174] added schema for course_registration --- lib/cadet/accounts/course_registration.ex | 27 +++++++++++++++++++++++ 1 file changed, 27 insertions(+) create mode 100644 lib/cadet/accounts/course_registration.ex diff --git a/lib/cadet/accounts/course_registration.ex b/lib/cadet/accounts/course_registration.ex new file mode 100644 index 000000000..beeb8a0dc --- /dev/null +++ b/lib/cadet/accounts/course_registration.ex @@ -0,0 +1,27 @@ +defmodule Cadet.Accounts.CourseRegistration do + @moduledoc """ + The mapping table representing the registration of a user to a course. + """ + use Cadet, :model + + alias Cadet.Course.{Courses, Group} + + schema "course_registrations" do + field(:role, Role) + field(:game_states, :map) + + belongs_to(:group, Courses.Group) + belongs_to(:user, User) + belongs_to(:course, Courses) + + timestamps() + end + + # @optional_fields ~w(name leader_id mentor_id)a + + # def changeset(group, attrs \\ %{}) do + # group + # |> cast(attrs, @optional_fields) + # |> add_belongs_to_id_from_model([:leader, :mentor], attrs) + # end +end From e4c3121284c12c52d27f7728446d3732141d3e6b Mon Sep 17 00:00:00 2001 From: YaleChen299 Date: Wed, 2 Jun 2021 22:45:04 +0800 Subject: [PATCH 004/174] updated accounts query --- lib/cadet/accounts/course_registration.ex | 6 ++-- lib/cadet/accounts/query.ex | 43 +++++++++++++++-------- lib/cadet/assessments/assessments.ex | 8 +++-- 3 files changed, 36 insertions(+), 21 deletions(-) diff --git a/lib/cadet/accounts/course_registration.ex b/lib/cadet/accounts/course_registration.ex index beeb8a0dc..c229a7346 100644 --- a/lib/cadet/accounts/course_registration.ex +++ b/lib/cadet/accounts/course_registration.ex @@ -4,15 +4,15 @@ defmodule Cadet.Accounts.CourseRegistration do """ use Cadet, :model - alias Cadet.Course.{Courses, Group} + alias Cadet.Course.{Course, Group} schema "course_registrations" do field(:role, Role) field(:game_states, :map) - belongs_to(:group, Courses.Group) + belongs_to(:group, Group) belongs_to(:user, User) - belongs_to(:course, Courses) + belongs_to(:course, Course) timestamps() end diff --git a/lib/cadet/accounts/query.ex b/lib/cadet/accounts/query.ex index 408b9f349..023a9a528 100644 --- a/lib/cadet/accounts/query.ex +++ b/lib/cadet/accounts/query.ex @@ -4,14 +4,15 @@ defmodule Cadet.Accounts.Query do """ import Ecto.Query - alias Cadet.Accounts.User - alias Cadet.Courses.Group + alias Cadet.Accounts.{User, CourseRegistration} + alias Cadet.Course.Group alias Cadet.Repo - # This gets all users where each and every user is a student. - def all_students do + # :TODO test + def all_students(course_id) do User - |> where([u], u.role == "student") + |> in_course(course_id) + |> where([u, cr], cr.role == "student") |> preload(:group) |> Repo.all() end @@ -21,22 +22,27 @@ defmodule Cadet.Accounts.Query do |> of_username(username) end - # :TODO this one need to pipe through course info - @spec students_of(%User{}) :: Ecto.Query.t() - def students_of(%User{id: id, role: :staff}) do + # :TODO test + @spec students_of(%CourseRegistration{}) :: Ecto.Query.t() + def students_of(%CourseRegistration{user_id: id, role: :staff, course_id: course_id}) do User - |> join(:inner, [u], g in Group, on: u.group_id == g.id) - |> where([_, g], g.leader_id == ^id) + |> in_course(course_id) + |> join(:inner, [cr], g in Group, on: cr.group_id == g.id) + |> where([cr, g], g.leader_id == ^id) end - # :TODO this one need to pipe through course info - def avenger_of?(avenger, student_id) do - students = students_of(avenger) + # :TODO test + def avenger_of?(avenger_id, course_id, student_id) do + avengerInCourse = CourseRegistration + |> where([cr], cr.course_id = ^course_id) + |> where([cr], cr.user_id = ^avenger_id) + + students = students_of(avengerInCourse) students - |> Repo.get(student_id) + |> Repo.get_by(user_id: ^student_id) |> case do - nil -> false + nil -> false _ -> true end end @@ -44,4 +50,11 @@ defmodule Cadet.Accounts.Query do defp of_username(query, username) do query |> where([a], a.username == ^username) end + + # :TODO test + defp in_course(user, course_id) do + user + |> join(:inner, [u], cr in CourseRegistration, on: u.id == cr.user_id) + |> where([_, cr], cr.id == ^course_id) + end end diff --git a/lib/cadet/assessments/assessments.ex b/lib/cadet/assessments/assessments.ex index b856a3ef3..1bc6439f0 100644 --- a/lib/cadet/assessments/assessments.ex +++ b/lib/cadet/assessments/assessments.ex @@ -6,7 +6,7 @@ defmodule Cadet.Assessments do use Cadet, [:context, :display] import Ecto.Query - alias Cadet.Accounts.{Notification, Notifications, User} + alias Cadet.Accounts.{Notification, Notifications, User, CourseRegistration} alias Cadet.Assessments.{Answer, Assessment, Query, Question, Submission, SubmissionVotes} alias Cadet.Autograder.GradingJob alias Cadet.Courses.Group @@ -708,7 +708,9 @@ defmodule Cadet.Assessments do end end - def unsubmit_submission(submission_id, user = %User{id: user_id, role: role}) + # :TODO update avenger_of? call + # def unsubmit_submission(submission_id, user = %User{id: user_id, role: role}) + def unsubmit_submission(submission_id, userCourse = %CourseRegistration{user_id: user_id, role: role, course_id: course_id}) when is_ecto_id(submission_id) do submission = Submission @@ -724,7 +726,7 @@ defmodule Cadet.Assessments do {:allowed_to_unsubmit?, true} <- {:allowed_to_unsubmit?, role == :admin or bypass or - Cadet.Accounts.Query.avenger_of?(user, submission.student_id)} do + Cadet.Accounts.Query.avenger_of?(userCourse, submission.student_id)} do Multi.new() |> Multi.run( :rollback_submission, From a1d3caf901ada85bef3467464479bdd9dfecefba Mon Sep 17 00:00:00 2001 From: Chow En Rong <53928333+chownces@users.noreply.github.com> Date: Thu, 3 Jun 2021 12:49:27 +0800 Subject: [PATCH 005/174] Removed settings context and shifted sublanguages to courses context --- lib/cadet/courses/courses.ex | 41 ++++++++++- lib/cadet/settings/settings.ex | 49 ------------- lib/cadet/settings/sublanguage.ex | 32 --------- .../admin_courses_controller.ex | 69 +++++++++++++++++++ .../admin_settings_controller.ex | 69 ------------------- .../controllers/courses_controller.ex | 55 +++++++++++++++ .../controllers/settings_controller.ex | 56 --------------- lib/cadet_web/router.ex | 5 +- lib/cadet_web/views/courses_view.ex | 9 +++ lib/cadet_web/views/settings_view.ex | 9 --- ...0210531155751_add_course_configuration.exs | 2 + 11 files changed, 178 insertions(+), 218 deletions(-) delete mode 100644 lib/cadet/settings/settings.ex delete mode 100644 lib/cadet/settings/sublanguage.ex delete mode 100644 lib/cadet_web/admin_controllers/admin_settings_controller.ex delete mode 100644 lib/cadet_web/controllers/settings_controller.ex create mode 100644 lib/cadet_web/views/courses_view.ex delete mode 100644 lib/cadet_web/views/settings_view.ex diff --git a/lib/cadet/courses/courses.ex b/lib/cadet/courses/courses.ex index 6a3d518f6..51d1be117 100644 --- a/lib/cadet/courses/courses.ex +++ b/lib/cadet/courses/courses.ex @@ -8,7 +8,46 @@ defmodule Cadet.Courses do import Ecto.Query alias Cadet.Accounts.User - alias Cadet.Courses.{Group, Sourcecast, SourcecastUpload} + alias Cadet.Courses.{Course, Group, Sourcecast, SourcecastUpload} + + @doc """ + Returns the default Source sublanguage of the Playground for the specified course. + """ + @spec get_sublanguage(integer) :: + {:ok, %{source_chapter: integer, source_variant: String.t()}} + | {:error, {:bad_request, String.t()}} + def get_sublanguage(course_id) when is_ecto_id(course_id) do + case retrieve_course(course_id) do + nil -> + {:error, {:bad_request, "Invalid course id"}} + + course -> + {:ok, %{source_chapter: course.source_chapter, source_variant: course.source_variant}} + end + end + + @doc """ + Updates the default Source sublanguage of the Playground for the specified course. + """ + @spec update_sublanguage(integer, integer, String.t()) :: + {:ok, %Course{}} | {:error, {:bad_request, String.t()} | {:error, Ecto.Changeset.t()}} + def update_sublanguage(course_id, chapter, variant) when is_ecto_id(course_id) do + case retrieve_course(course_id) do + nil -> + {:error, {:bad_request, "Invalid course id"}} + + course -> + course + |> Course.changeset(%{chapter: chapter, variant: variant}) + |> Repo.update() + end + end + + defp retrieve_course(course_id) when is_ecto_id(course_id) do + Course + |> where(id: ^course_id) + |> Repo.one() + end @doc """ Get a group based on the group name or create one if it doesn't exist diff --git a/lib/cadet/settings/settings.ex b/lib/cadet/settings/settings.ex deleted file mode 100644 index d46524867..000000000 --- a/lib/cadet/settings/settings.ex +++ /dev/null @@ -1,49 +0,0 @@ -defmodule Cadet.Settings do - @moduledoc """ - The Settings context contains functions to retrieve and configure Academy-wide - settings. - """ - use Cadet, [:context, :display] - - import Ecto.Query - - alias Cadet.Settings.Sublanguage - - @doc """ - Returns the default Source sublanguage of the Playground, from the most recent - entry in the Sublanguage table (there should only be 1, as seeded). - - If no entries exist, returns Source 1 as the default sublanguage. - """ - @spec get_sublanguage :: {:ok, %Sublanguage{}} - def get_sublanguage do - {:ok, retrieve_sublanguage() || %Sublanguage{chapter: 1, variant: "default"}} - end - - @doc """ - Updates the most recent entry in the Sublanguage table to the new chapter and - variant. - - If no entries exist, inserts a new entry in the Sublanguage table with the - given chapter and variant. - """ - @spec update_sublanguage(integer(), String.t()) :: - {:ok, %Sublanguage{}} | {:error, Ecto.Changeset.t()} - def update_sublanguage(chapter, variant) do - case retrieve_sublanguage() do - nil -> - %Sublanguage{} - |> Sublanguage.changeset(%{chapter: chapter, variant: variant}) - |> Repo.insert() - - sublanguage -> - sublanguage - |> Sublanguage.changeset(%{chapter: chapter, variant: variant}) - |> Repo.update() - end - end - - defp retrieve_sublanguage do - Sublanguage |> order_by(desc: :id) |> limit(1) |> Repo.one() - end -end diff --git a/lib/cadet/settings/sublanguage.ex b/lib/cadet/settings/sublanguage.ex deleted file mode 100644 index ed1d22162..000000000 --- a/lib/cadet/settings/sublanguage.ex +++ /dev/null @@ -1,32 +0,0 @@ -defmodule Cadet.Settings.Sublanguage do - @moduledoc """ - The Sublanguage entity stores the chapter and variant of the default - sublanguage in use by the Playground. - """ - use Cadet, :model - use Arc.Ecto.Schema - - schema "sublanguages" do - field(:chapter, :integer) - field(:variant, :string) - end - - @required_fields ~w(chapter variant)a - - def changeset(sublanguage, params) do - sublanguage - |> cast(params, @required_fields) - |> validate_required(@required_fields) - |> validate_allowed_combination() - end - - defp validate_allowed_combination(changeset) do - case get_field(changeset, :chapter) do - 1 -> validate_inclusion(changeset, :variant, ["default", "lazy", "wasm"]) - 2 -> validate_inclusion(changeset, :variant, ["default", "lazy"]) - 3 -> validate_inclusion(changeset, :variant, ["default", "concurrent", "non-det"]) - 4 -> validate_inclusion(changeset, :variant, ["default", "gpu"]) - _ -> add_error(changeset, :chapter, "is invalid") - end - end -end diff --git a/lib/cadet_web/admin_controllers/admin_courses_controller.ex b/lib/cadet_web/admin_controllers/admin_courses_controller.ex index 8b1378917..66eedc515 100644 --- a/lib/cadet_web/admin_controllers/admin_courses_controller.ex +++ b/lib/cadet_web/admin_controllers/admin_courses_controller.ex @@ -1 +1,70 @@ +defmodule CadetWeb.AdminCoursesController do + use CadetWeb, :controller + use PhoenixSwagger + + alias Cadet.Courses + + def update_sublanguage(conn, %{ + "courseid" => course_id, + "chapter" => chapter, + "variant" => variant + }) + when is_ecto_id(course_id) do + case Courses.update_sublanguage(course_id, chapter, variant) do + {:ok, _} -> + text(conn, "OK") + + {:error, {status, message}} -> + send_resp(conn, status, message) + + {:error, _} -> + conn + |> put_status(:bad_request) + |> text("Invalid parameter(s)") + end + end + + def update_sublanguage(conn, _) do + send_resp(conn, :bad_request, "Missing parameter(s)") + end + + swagger_path :update do + put("/admin/courses/{courseId}/sublanguage") + + summary("Updates the default Source sublanguage of the Playground for the specified course") + + security([%{JWT: []}]) + + consumes("application/json") + + parameters do + courseId(:path, :integer, "Course ID", required: true) + sublanguage(:body, Schema.ref(:AdminSublanguage), "sublanguage object", required: true) + end + + response(200, "OK") + response(400, "Missing or invalid parameter(s)") + response(403, "Forbidden") + end + + def swagger_definitions do + %{ + AdminSublanguage: + swagger_schema do + title("AdminSublanguage") + + properties do + chapter(:integer, "Chapter number from 1 to 4", required: true, minimum: 1, maximum: 4) + + variant(Schema.ref(:SourceVariant), "Variant name", required: true) + end + + example(%{ + chapter: 2, + variant: "lazy" + }) + end + } + end +end diff --git a/lib/cadet_web/admin_controllers/admin_settings_controller.ex b/lib/cadet_web/admin_controllers/admin_settings_controller.ex deleted file mode 100644 index d1e37bb5c..000000000 --- a/lib/cadet_web/admin_controllers/admin_settings_controller.ex +++ /dev/null @@ -1,69 +0,0 @@ -defmodule CadetWeb.AdminSettingsController do - @moduledoc """ - Receives authorized requests involving Academy-wide configuration settings. - """ - use CadetWeb, :controller - - use PhoenixSwagger - - alias Cadet.Settings - - @doc """ - Receives a /settings/sublanguage PUT request with valid attributes. - - Overrides the stored default Source sublanguage of the Playground. - """ - def update(conn, %{"chapter" => chapter, "variant" => variant}) do - case Settings.update_sublanguage(chapter, variant) do - {:ok, _} -> - text(conn, "OK") - - {:error, _} -> - conn - |> put_status(:bad_request) - |> text("Invalid parameter(s)") - end - end - - def update(conn, _) do - send_resp(conn, :bad_request, "Missing parameter(s)") - end - - swagger_path :update do - put("/admin/settings/sublanguage") - - summary("Updates the default Source sublanguage of the Playground") - - security([%{JWT: []}]) - - consumes("application/json") - - parameters do - sublanguage(:body, Schema.ref(:AdminSublanguage), "sublanguage object", required: true) - end - - response(200, "OK") - response(400, "Missing or invalid parameter(s)") - response(403, "Forbidden") - end - - def swagger_definitions do - %{ - AdminSublanguage: - swagger_schema do - title("AdminSublanguage") - - properties do - chapter(:integer, "Chapter number from 1 to 4", required: true, minimum: 1, maximum: 4) - - variant(Schema.ref(:SourceVariant), "Variant name", required: true) - end - - example(%{ - chapter: 2, - variant: "lazy" - }) - end - } - end -end diff --git a/lib/cadet_web/controllers/courses_controller.ex b/lib/cadet_web/controllers/courses_controller.ex index 8b1378917..564192534 100644 --- a/lib/cadet_web/controllers/courses_controller.ex +++ b/lib/cadet_web/controllers/courses_controller.ex @@ -1 +1,56 @@ +defmodule CadetWeb.CoursesController do + use CadetWeb, :controller + use PhoenixSwagger + + alias Cadet.Courses + + def get_sublanguage(conn, %{"courseid" => course_id}) when is_ecto_id(course_id) do + case Courses.get_sublanguage(course_id) do + {:ok, sublanguage} -> render(conn, "sublanguage.json", sublanguage: sublanguage) + {:error, {status, message}} -> send_resp(conn, status, message) + end + end + + swagger_path :get_sublanguage do + get("/courses/{courseId}/sublanguage") + + summary("Retrieves the default Source sublanguage of the Playground for the specified course") + + security([%{JWT: []}]) + + produces("application/json") + + parameters do + courseId(:path, :integer, "Course ID", required: true) + end + + response(200, "OK", Schema.ref(:Sublanguage)) + response(400, "Invalid courseId") + end + + def swagger_definitions do + %{ + Sublanguage: + swagger_schema do + title("Sublanguage") + + properties do + chapter(:integer, "Chapter number from 1 to 4", required: true, minimum: 1, maximum: 4) + + variant(Schema.ref(:SourceVariant), "Variant name", required: true) + end + + example(%{ + chapter: 1, + variant: "default" + }) + end, + SourceVariant: + swagger_schema do + type(:string) + enum([:default, :concurrent, :gpu, :lazy, "non-det", :wasm]) + end + } + end +end diff --git a/lib/cadet_web/controllers/settings_controller.ex b/lib/cadet_web/controllers/settings_controller.ex deleted file mode 100644 index 901cf37c9..000000000 --- a/lib/cadet_web/controllers/settings_controller.ex +++ /dev/null @@ -1,56 +0,0 @@ -defmodule CadetWeb.SettingsController do - @moduledoc """ - Receives public requests involving Academy-wide configuration settings. - """ - use CadetWeb, :controller - - use PhoenixSwagger - - alias Cadet.Settings - - @doc """ - Receives a /settings/sublanguage GET request. - - Returns the default Source sublanguage of the Playground. - """ - def index(conn, _) do - {:ok, sublanguage} = Settings.get_sublanguage() - - render(conn, "show.json", sublanguage: sublanguage) - end - - swagger_path :index do - get("/settings/sublanguage") - - summary("Retrieves the default Source sublanguage of the Playground") - - produces("application/json") - - response(200, "OK", Schema.ref(:Sublanguage)) - end - - def swagger_definitions do - %{ - Sublanguage: - swagger_schema do - title("Sublanguage") - - properties do - chapter(:integer, "Chapter number from 1 to 4", required: true, minimum: 1, maximum: 4) - - variant(Schema.ref(:SourceVariant), "Variant name", required: true) - end - - example(%{ - chapter: 1, - variant: "default" - }) - end, - SourceVariant: - swagger_schema do - type(:string) - enum([:default, :concurrent, :gpu, :lazy, "non-det", :wasm]) - end - } - end -end diff --git a/lib/cadet_web/router.ex b/lib/cadet_web/router.ex index a66cd6abf..23b8d40a3 100644 --- a/lib/cadet_web/router.ex +++ b/lib/cadet_web/router.ex @@ -34,7 +34,6 @@ defmodule CadetWeb.Router do post("/auth/refresh", AuthController, :refresh) post("/auth/login", AuthController, :create) post("/auth/logout", AuthController, :logout) - get("/settings/sublanguage", SettingsController, :index) end scope "/v2", CadetWeb do @@ -71,6 +70,8 @@ defmodule CadetWeb.Router do get("/user", UserController, :index) put("/user/game_states", UserController, :update_game_states) + get("/courses/:courseid/sublanguage", CoursesController, :get_sublanguage) + get("/devices", DevicesController, :index) post("/devices", DevicesController, :register) post("/devices/:id", DevicesController, :edit) @@ -123,7 +124,7 @@ defmodule CadetWeb.Router do put("/goals/:uuid", AdminGoalsController, :update) delete("/goals/:uuid", AdminGoalsController, :delete) - put("/settings/sublanguage", AdminSettingsController, :update) + put("/courses/:courseid/sublanguage", AdminCoursesController, :update_sublanguage) end # Other scopes may use custom stacks. diff --git a/lib/cadet_web/views/courses_view.ex b/lib/cadet_web/views/courses_view.ex new file mode 100644 index 000000000..233a2daa1 --- /dev/null +++ b/lib/cadet_web/views/courses_view.ex @@ -0,0 +1,9 @@ +defmodule CadetWeb.CoursesView do + use CadetWeb, :view + + def render("sublanguage.json", %{sublanguage: sublanguage}) do + %{ + sublanguage: transform_map_for_view(sublanguage, [:source_chapter, :source_variant]) + } + end +end diff --git a/lib/cadet_web/views/settings_view.ex b/lib/cadet_web/views/settings_view.ex deleted file mode 100644 index c812aebdc..000000000 --- a/lib/cadet_web/views/settings_view.ex +++ /dev/null @@ -1,9 +0,0 @@ -defmodule CadetWeb.SettingsView do - use CadetWeb, :view - - def render("show.json", %{sublanguage: sublanguage}) do - %{ - sublanguage: transform_map_for_view(sublanguage, [:chapter, :variant]) - } - end -end diff --git a/priv/repo/migrations/20210531155751_add_course_configuration.exs b/priv/repo/migrations/20210531155751_add_course_configuration.exs index 8d0449a6b..104dd760a 100644 --- a/priv/repo/migrations/20210531155751_add_course_configuration.exs +++ b/priv/repo/migrations/20210531155751_add_course_configuration.exs @@ -29,5 +29,7 @@ defmodule Cadet.Repo.Migrations.AddCourseConfiguration do add(:course_id, references(:courses), null: false) timestamps() end + + drop_if_exists(table(:sublanguages)) end end From f4f2aab32245b4ac520c3b2b56c124538c35fcfd Mon Sep 17 00:00:00 2001 From: Chow En Rong <53928333+chownces@users.noreply.github.com> Date: Thu, 3 Jun 2021 14:28:19 +0800 Subject: [PATCH 006/174] Added initial tests for courses context and shifted settings tests over --- lib/cadet/courses/course.ex | 11 +- test/cadet/course/course_test.exs | 91 ------------ test/cadet/courses/courses_test.exs | 130 ++++++++++++++++++ test/cadet/{course => courses}/group_test.exs | 0 .../{course => courses}/sourcecast_test.exs | 0 .../admin_courses_controller_test.exs | 1 + .../controllers/courses_controller_test.exs | 1 + test/factories/courses/course_factory.ex | 25 ++++ .../{course => courses}/group_factory.ex | 0 .../{course => courses}/sourcecast_factory.ex | 0 test/factories/factory.ex | 4 +- 11 files changed, 163 insertions(+), 100 deletions(-) delete mode 100644 test/cadet/course/course_test.exs create mode 100644 test/cadet/courses/courses_test.exs rename test/cadet/{course => courses}/group_test.exs (100%) rename test/cadet/{course => courses}/sourcecast_test.exs (100%) create mode 100644 test/cadet_web/admin_controllers/admin_courses_controller_test.exs create mode 100644 test/cadet_web/controllers/courses_controller_test.exs create mode 100644 test/factories/courses/course_factory.ex rename test/factories/{course => courses}/group_factory.ex (100%) rename test/factories/{course => courses}/sourcecast_factory.ex (100%) diff --git a/lib/cadet/courses/course.ex b/lib/cadet/courses/course.ex index ec7f45e55..3136d673e 100644 --- a/lib/cadet/courses/course.ex +++ b/lib/cadet/courses/course.ex @@ -24,18 +24,17 @@ defmodule Cadet.Courses.Course do def changeset(course, params) do course |> cast(params, @optional_fields) - |> validate_required(@required_fields) |> validate_allowed_combination() end # Validates combination of Source chapter and variant defp validate_allowed_combination(changeset) do case get_field(changeset, :source_chapter) do - 1 -> validate_inclusion(changeset, :variant, ["default", "lazy", "wasm"]) - 2 -> validate_inclusion(changeset, :variant, ["default", "lazy"]) - 3 -> validate_inclusion(changeset, :variant, ["default", "concurrent", "non-det"]) - 4 -> validate_inclusion(changeset, :variant, ["default", "gpu"]) - _ -> add_error(changeset, :chapter, "is invalid") + 1 -> validate_inclusion(changeset, :source_variant, ["default", "lazy", "wasm"]) + 2 -> validate_inclusion(changeset, :source_variant, ["default", "lazy"]) + 3 -> validate_inclusion(changeset, :source_variant, ["default", "concurrent", "non-det"]) + 4 -> validate_inclusion(changeset, :source_variant, ["default", "gpu"]) + _ -> add_error(changeset, :source_chapter, "is invalid") end end end diff --git a/test/cadet/course/course_test.exs b/test/cadet/course/course_test.exs deleted file mode 100644 index c3c66d3be..000000000 --- a/test/cadet/course/course_test.exs +++ /dev/null @@ -1,91 +0,0 @@ -defmodule Cadet.CourseTest do - use Cadet.DataCase - - alias Cadet.{Course, Repo} - alias Cadet.Courses.{Group, Sourcecast, SourcecastUpload} - - describe "Sourcecast" do - setup do - on_exit(fn -> File.rm_rf!("uploads/test/sourcecasts") end) - end - - test "upload file to folder then delete it" do - uploader = insert(:user, %{role: :staff}) - - upload = %Plug.Upload{ - content_type: "audio/wav", - filename: "upload.wav", - path: "test/fixtures/upload.wav" - } - - result = - Course.upload_sourcecast_file(uploader, %{ - title: "Test Upload", - audio: upload, - playbackData: - "{\"init\":{\"editorValue\":\"// Type your program in here!\"},\"inputs\":[]}" - }) - - assert {:ok, sourcecast} = result - path = SourcecastUpload.url({sourcecast.audio, sourcecast}) - assert path =~ "/uploads/test/sourcecasts/upload.wav" - - deleter = insert(:user, %{role: :staff}) - assert {:ok, _} = Course.delete_sourcecast_file(deleter, sourcecast.id) - assert Repo.get(Sourcecast, sourcecast.id) == nil - refute File.exists?("uploads/test/sourcecasts/upload.wav") - end - end - - describe "get_or_create_group" do - test "existing group" do - group = insert(:group) - - {:ok, group_db} = Course.get_or_create_group(group.name) - - assert group_db.id == group.id - assert group_db.leader_id == group.leader_id - end - - test "non-existent group" do - group_name = params_for(:group).name - - {:ok, _} = Course.get_or_create_group(group_name) - - group_db = - Group - |> where(name: ^group_name) - |> Repo.one() - - refute is_nil(group_db) - end - end - - describe "insert_or_update_group" do - test "existing group" do - group = insert(:group) - group_params = params_with_assocs(:group, name: group.name) - Course.insert_or_update_group(group_params) - - updated_group = - Group - |> where(name: ^group.name) - |> Repo.one() - - assert updated_group.id == group.id - assert updated_group.leader_id == group_params.leader_id - end - - test "non-existent group" do - group_params = params_with_assocs(:group) - Course.insert_or_update_group(group_params) - - updated_group = - Group - |> where(name: ^group_params.name) - |> Repo.one() - - assert updated_group.leader_id == group_params.leader_id - end - end -end diff --git a/test/cadet/courses/courses_test.exs b/test/cadet/courses/courses_test.exs new file mode 100644 index 000000000..c264ad316 --- /dev/null +++ b/test/cadet/courses/courses_test.exs @@ -0,0 +1,130 @@ +defmodule Cadet.CourseTest do + use Cadet.DataCase + + alias Cadet.{Courses, Repo} + alias Cadet.Courses.{Group, Sourcecast, SourcecastUpload} + + describe "get sublanguage" do + test "succeeds" do + course = insert(:course, %{source_chapter: 3, source_variant: "concurrent"}) + {:ok, sublanguage} = Courses.get_sublanguage(course.id) + assert sublanguage.source_chapter == 3 + assert sublanguage.source_variant == "concurrent" + end + + test "returns with error for invalid course id" do + course = insert(:course) + assert {:error, _} = Courses.get_sublanguage(course.id + 1) + end + end + + describe "update sublanguage" do + test "succeeds" do + course = insert(:course) + new_chapter = Enum.random(1..4) + {:ok, sublanguage} = Courses.update_sublanguage(course.id, new_chapter, "default") + assert sublanguage.source_chapter == new_chapter + assert sublanguage.source_variant == "default" + end + + test "returns with error for invalid course id" do + course = insert(:course) + new_chapter = Enum.random(1..4) + assert {:error, _} = Courses.update_sublanguage(course.id + 1, new_chapter, "default") + end + + test "returns with error for failed updates" do + course = insert(:course) + assert {:error, changeset} = Courses.update_sublanguage(course.id, 0, "default") + assert %{source_chapter: ["is invalid"]} = errors_on(changeset) + + assert {:error, changeset} = Courses.update_sublanguage(course.id, 2, "gpu") + assert %{source_variant: ["is invalid"]} = errors_on(changeset) + end + end + + # describe "Sourcecast" do + # setup do + # on_exit(fn -> File.rm_rf!("uploads/test/sourcecasts") end) + # end + + # test "upload file to folder then delete it" do + # uploader = insert(:user, %{role: :staff}) + + # upload = %Plug.Upload{ + # content_type: "audio/wav", + # filename: "upload.wav", + # path: "test/fixtures/upload.wav" + # } + + # result = + # Courses.upload_sourcecast_file(uploader, %{ + # title: "Test Upload", + # audio: upload, + # playbackData: + # "{\"init\":{\"editorValue\":\"// Type your program in here!\"},\"inputs\":[]}" + # }) + + # assert {:ok, sourcecast} = result + # path = SourcecastUpload.url({sourcecast.audio, sourcecast}) + # assert path =~ "/uploads/test/sourcecasts/upload.wav" + + # deleter = insert(:user, %{role: :staff}) + # assert {:ok, _} = Courses.delete_sourcecast_file(deleter, sourcecast.id) + # assert Repo.get(Sourcecast, sourcecast.id) == nil + # refute File.exists?("uploads/test/sourcecasts/upload.wav") + # end + # end + + # describe "get_or_create_group" do + # test "existing group" do + # group = insert(:group) + + # {:ok, group_db} = Courses.get_or_create_group(group.name) + + # assert group_db.id == group.id + # assert group_db.leader_id == group.leader_id + # end + + # test "non-existent group" do + # group_name = params_for(:group).name + + # {:ok, _} = Courses.get_or_create_group(group_name) + + # group_db = + # Group + # |> where(name: ^group_name) + # |> Repo.one() + + # refute is_nil(group_db) + # end + # end + + # describe "insert_or_update_group" do + # test "existing group" do + # group = insert(:group) + # group_params = params_with_assocs(:group, name: group.name) + # Courses.insert_or_update_group(group_params) + + # updated_group = + # Group + # |> where(name: ^group.name) + # |> Repo.one() + + # assert updated_group.id == group.id + # assert updated_group.leader_id == group_params.leader_id + # end + + # test "non-existent group" do + # group_params = params_with_assocs(:group) + # Courses.insert_or_update_group(group_params) + + # updated_group = + # Group + # |> where(name: ^group_params.name) + # |> Repo.one() + + # assert updated_group.leader_id == group_params.leader_id + # end + # end +end diff --git a/test/cadet/course/group_test.exs b/test/cadet/courses/group_test.exs similarity index 100% rename from test/cadet/course/group_test.exs rename to test/cadet/courses/group_test.exs diff --git a/test/cadet/course/sourcecast_test.exs b/test/cadet/courses/sourcecast_test.exs similarity index 100% rename from test/cadet/course/sourcecast_test.exs rename to test/cadet/courses/sourcecast_test.exs diff --git a/test/cadet_web/admin_controllers/admin_courses_controller_test.exs b/test/cadet_web/admin_controllers/admin_courses_controller_test.exs new file mode 100644 index 000000000..8b1378917 --- /dev/null +++ b/test/cadet_web/admin_controllers/admin_courses_controller_test.exs @@ -0,0 +1 @@ + diff --git a/test/cadet_web/controllers/courses_controller_test.exs b/test/cadet_web/controllers/courses_controller_test.exs new file mode 100644 index 000000000..8b1378917 --- /dev/null +++ b/test/cadet_web/controllers/courses_controller_test.exs @@ -0,0 +1 @@ + diff --git a/test/factories/courses/course_factory.ex b/test/factories/courses/course_factory.ex new file mode 100644 index 000000000..c3711b586 --- /dev/null +++ b/test/factories/courses/course_factory.ex @@ -0,0 +1,25 @@ +defmodule Cadet.Courses.CourseFactory do + @moduledoc """ + Factory for the Course entity + """ + + defmacro __using__(_opts) do + quote do + alias Cadet.Courses.Course + + def course_factory do + %Course{ + name: "Programming Methodology", + module_code: "CS1101S", + viewable: true, + enable_game: true, + enable_achievements: true, + enable_sourcecast: true, + source_chapter: 1, + source_variant: "default", + module_help_text: "Help Text" + } + end + end + end +end diff --git a/test/factories/course/group_factory.ex b/test/factories/courses/group_factory.ex similarity index 100% rename from test/factories/course/group_factory.ex rename to test/factories/courses/group_factory.ex diff --git a/test/factories/course/sourcecast_factory.ex b/test/factories/courses/sourcecast_factory.ex similarity index 100% rename from test/factories/course/sourcecast_factory.ex rename to test/factories/courses/sourcecast_factory.ex diff --git a/test/factories/factory.ex b/test/factories/factory.ex index 0a43563a8..c419d28f3 100644 --- a/test/factories/factory.ex +++ b/test/factories/factory.ex @@ -22,9 +22,7 @@ defmodule Cadet.Factory do GoalFactory } - use Cadet.Settings.{SublanguageFactory} - - use Cadet.Courses.{GroupFactory, SourcecastFactory} + use Cadet.Courses.{CourseFactory, GroupFactory, SourcecastFactory} use Cadet.Devices.DeviceFactory From 37d70d30d5da95355e8b11989d59a51c65d6d62e Mon Sep 17 00:00:00 2001 From: Chow En Rong <53928333+chownces@users.noreply.github.com> Date: Thu, 3 Jun 2021 16:09:35 +0800 Subject: [PATCH 007/174] Added initial tests for courses context controllers --- lib/cadet/courses/courses.ex | 2 +- .../admin_courses_controller.ex | 2 +- .../admin_courses_controller_test.exs | 56 +++++++++++++++++++ .../controllers/courses_controller_test.exs | 41 ++++++++++++++ 4 files changed, 99 insertions(+), 2 deletions(-) diff --git a/lib/cadet/courses/courses.ex b/lib/cadet/courses/courses.ex index 51d1be117..e17882aa1 100644 --- a/lib/cadet/courses/courses.ex +++ b/lib/cadet/courses/courses.ex @@ -38,7 +38,7 @@ defmodule Cadet.Courses do course -> course - |> Course.changeset(%{chapter: chapter, variant: variant}) + |> Course.changeset(%{source_chapter: chapter, source_variant: variant}) |> Repo.update() end end diff --git a/lib/cadet_web/admin_controllers/admin_courses_controller.ex b/lib/cadet_web/admin_controllers/admin_courses_controller.ex index 66eedc515..bcd1650fa 100644 --- a/lib/cadet_web/admin_controllers/admin_courses_controller.ex +++ b/lib/cadet_web/admin_controllers/admin_courses_controller.ex @@ -29,7 +29,7 @@ defmodule CadetWeb.AdminCoursesController do send_resp(conn, :bad_request, "Missing parameter(s)") end - swagger_path :update do + swagger_path :update_sublanguage do put("/admin/courses/{courseId}/sublanguage") summary("Updates the default Source sublanguage of the Playground for the specified course") diff --git a/test/cadet_web/admin_controllers/admin_courses_controller_test.exs b/test/cadet_web/admin_controllers/admin_courses_controller_test.exs index 8b1378917..fab877dca 100644 --- a/test/cadet_web/admin_controllers/admin_courses_controller_test.exs +++ b/test/cadet_web/admin_controllers/admin_courses_controller_test.exs @@ -1 +1,57 @@ +defmodule CadetWeb.AdminCoursesControllerTest do + use CadetWeb.ConnCase + alias CadetWeb.AdminCoursesController + + test "swagger" do + AdminCoursesController.swagger_definitions() + AdminCoursesController.swagger_path_update_sublanguage(nil) + end + + describe "PUT /courses/{courseId}/sublanguage" do + @tag authenticate: :admin + test "succeeds", %{conn: conn} do + course = insert(:course, %{source_chapter: 4, source_variant: "gpu"}) + + conn = + put(conn, build_url(course.id), %{ + "chapter" => Enum.random(1..4), + "variant" => "default" + }) + + assert response(conn, 200) == "OK" + end + + @tag authenticate: :student + test "rejects forbidden request for non-staff users", %{conn: conn} do + course = insert(:course) + conn = put(conn, build_url(course.id), %{"chapter" => 3, "variant" => "concurrent"}) + + assert response(conn, 403) == "Forbidden" + end + + @tag authenticate: :staff + test "rejects requests with invalid course id", %{conn: conn} do + course = insert(:course) + conn = put(conn, build_url(course.id + 1), %{"chapter" => 3, "variant" => "concurrent"}) + end + + @tag authenticate: :staff + test "rejects requests with invalid params", %{conn: conn} do + course = insert(:course) + conn = put(conn, build_url(course.id), %{"chapter" => 4, "variant" => "wasm"}) + + assert response(conn, 400) == "Invalid parameter(s)" + end + + @tag authenticate: :staff + test "rejects requests with missing params", %{conn: conn} do + course = insert(:course) + conn = put(conn, build_url(course.id), %{"variant" => "default"}) + + assert response(conn, 400) == "Missing parameter(s)" + end + end + + defp build_url(course_id), do: "/v2/admin/courses/#{course_id}/sublanguage" +end diff --git a/test/cadet_web/controllers/courses_controller_test.exs b/test/cadet_web/controllers/courses_controller_test.exs index 8b1378917..1629e4147 100644 --- a/test/cadet_web/controllers/courses_controller_test.exs +++ b/test/cadet_web/controllers/courses_controller_test.exs @@ -1 +1,42 @@ +defmodule CadetWeb.CoursesControllerTest do + use CadetWeb.ConnCase + alias CadetWeb.CoursesController + + test "swagger" do + CoursesController.swagger_definitions() + CoursesController.swagger_path_get_sublanguage(nil) + end + + describe "GET /, unauthenticated" do + test "unauthorized", %{conn: conn} do + course = insert(:course) + conn = get(conn, build_url(course.id)) + assert response(conn, 401) =~ "Unauthorised" + end + end + + describe "GET /courses/course_id/sublanguage" do + @tag authenticate: :student + test "succeeds", %{conn: conn} do + course = insert(:course, %{source_chapter: 2, source_variant: "lazy"}) + + resp = conn |> get(build_url(course.id)) |> json_response(200) + + assert %{"sublanguage" => %{"source_chapter" => 2, "source_variant" => "lazy"}} = resp + end + + @tag authenticate: :student + test "returns with error for invalid course id", %{conn: conn} do + course = insert(:course) + + conn = + conn + |> get(build_url(course.id + 1)) + + assert response(conn, 400) == "Invalid course id" + end + end + + defp build_url(course_id), do: "/v2/courses/#{course_id}/sublanguage" +end From 92ed28e591eb98e6faeda76bb4d9ca7546ec7b94 Mon Sep 17 00:00:00 2001 From: Chow En Rong <53928333+chownces@users.noreply.github.com> Date: Thu, 3 Jun 2021 17:53:05 +0800 Subject: [PATCH 008/174] Added course table changeset tests --- lib/cadet/courses/course.ex | 8 ++++---- lib/cadet/courses/courses.ex | 2 +- test/cadet/courses/course_text.exs | 30 ++++++++++++++++++++++++++++++ 3 files changed, 35 insertions(+), 5 deletions(-) create mode 100644 test/cadet/courses/course_text.exs diff --git a/lib/cadet/courses/course.ex b/lib/cadet/courses/course.ex index 3136d673e..302177d08 100644 --- a/lib/cadet/courses/course.ex +++ b/lib/cadet/courses/course.ex @@ -18,12 +18,12 @@ defmodule Cadet.Courses.Course do timestamps() end - @optional_fields ~w(name source_chapter source_variant module_code viewable enable_game - enable_achievements enable_sourcecast module_help_text)a + @required_fields_sublanguage ~w(source_chapter source_variant)a - def changeset(course, params) do + def sublanguage_changeset(course, params) do course - |> cast(params, @optional_fields) + |> cast(params, @required_fields_sublanguage) + |> validate_required(@required_fields_sublanguage) |> validate_allowed_combination() end diff --git a/lib/cadet/courses/courses.ex b/lib/cadet/courses/courses.ex index e17882aa1..56b1751d7 100644 --- a/lib/cadet/courses/courses.ex +++ b/lib/cadet/courses/courses.ex @@ -38,7 +38,7 @@ defmodule Cadet.Courses do course -> course - |> Course.changeset(%{source_chapter: chapter, source_variant: variant}) + |> Course.sublanguage_changeset(%{source_chapter: chapter, source_variant: variant}) |> Repo.update() end end diff --git a/test/cadet/courses/course_text.exs b/test/cadet/courses/course_text.exs new file mode 100644 index 000000000..78227a5b8 --- /dev/null +++ b/test/cadet/courses/course_text.exs @@ -0,0 +1,30 @@ +defmodule Cadet.Courses.CourseTest do + alias Cadet.Courses.Course + + use Cadet.ChangesetCase, entity: Course + + describe "Sublanguage Changesets" do + test "valid changesets" do + assert_changeset(%{source_chapter: 1, source_variant: "wasm"}, :valid, :sublanguage_changeset) + assert_changeset(%{source_chapter: 2, source_variant: "lazy"}, :valid, :sublanguage_changeset) + assert_changeset(%{source_chapter: 3, source_variant: "non-det"}, :valid, :sublanguage_changeset) + assert_changeset(%{source_chapter: 4, source_variant: "default"}, :valid, :sublanguage_changeset) + end + + test "invalid changeset missing required params" do + assert_changeset(%{source_chapter: 2}, :invalid, :sublanguage_changeset) + end + + test "invalid changeset with invalid chapter" do + assert_changeset(%{source_chapter: 5, source_variant: "default"}, :invalid, :sublanguage_changeset) + end + + test "invalid changeset with invalid variant" do + assert_changeset(%{source_chapter: Enum.random(1..4), source_variant: "error"}, :invalid, :sublanguage_changeset) + end + + test "invalid changeset with invalid chapter-variant combination" do + assert_changeset(%{source_chapter: 4, source_variant: "lazy"}, :invalid, :sublanguage_changeset) + end + end +end From 8091770563dfb8bab3f96d3c24cb65ed9dd3ad4c Mon Sep 17 00:00:00 2001 From: YaleChen299 Date: Thu, 3 Jun 2021 15:00:17 +0800 Subject: [PATCH 009/174] updated course_registration context function/getters --- lib/cadet/accounts/course_registration.ex | 56 +++++++++---------- lib/cadet/accounts/course_registrations.ex | 62 ++++++++++++++++++++++ lib/cadet/accounts/query.ex | 9 ++-- lib/cadet/accounts/user.ex | 3 ++ lib/cadet/assessments/assessments.ex | 5 +- lib/cadet/courses/group.ex | 7 +-- 6 files changed, 107 insertions(+), 35 deletions(-) create mode 100644 lib/cadet/accounts/course_registrations.ex diff --git a/lib/cadet/accounts/course_registration.ex b/lib/cadet/accounts/course_registration.ex index c229a7346..67b6f2c57 100644 --- a/lib/cadet/accounts/course_registration.ex +++ b/lib/cadet/accounts/course_registration.ex @@ -1,27 +1,29 @@ -defmodule Cadet.Accounts.CourseRegistration do - @moduledoc """ - The mapping table representing the registration of a user to a course. - """ - use Cadet, :model - - alias Cadet.Course.{Course, Group} - - schema "course_registrations" do - field(:role, Role) - field(:game_states, :map) - - belongs_to(:group, Group) - belongs_to(:user, User) - belongs_to(:course, Course) - - timestamps() - end - - # @optional_fields ~w(name leader_id mentor_id)a - - # def changeset(group, attrs \\ %{}) do - # group - # |> cast(attrs, @optional_fields) - # |> add_belongs_to_id_from_model([:leader, :mentor], attrs) - # end -end +defmodule Cadet.Accounts.CourseRegistration do + @moduledoc """ + The mapping table representing the registration of a user to a course. + """ + use Cadet, :model + + alias Cadet.Course.{Course, Group} + + schema "course_registrations" do + field(:role, Role) + field(:game_states, :map) + + belongs_to(:group, Group) + belongs_to(:user, User) + belongs_to(:course, Course) + + timestamps() + end + + @required_fields ~w(user_id course_id)a + @optional_fields ~w(role game_states group_id)a + + def changeset(course_registration, params \\ %{}) do + course_registration + |> cast(params, @optional_fields ++ @required_fields) + |> add_belongs_to_id_from_model([:user, :group, :course], params) + |> validate_required(@required_fields) + end +end diff --git a/lib/cadet/accounts/course_registrations.ex b/lib/cadet/accounts/course_registrations.ex new file mode 100644 index 000000000..8b3872c36 --- /dev/null +++ b/lib/cadet/accounts/course_registrations.ex @@ -0,0 +1,62 @@ +defmodule Cadet.Accounts.CourseRegistrations do + @moduledoc """ + Provides functions fetch, add, update course_registration + """ + use Cadet, :context + + import Ecto.Query + + alias Cadet.Repo + alias Cadet.Accounts.{User, CourseRegistration} + + # guide + # only join with User if need name or user name + # only join with Group if need leader/mentor/students in group + # only join with Course if need course info/config + # otherwise just use CourseRegistration + + def get_courses(%User{id: id}) do + CourseRegistration + |> where([cr], cr.user_id == ^id) + |> join(:inner, [cr], c in assoc(cr, :course)) + end + + def get_users(course_id) do + CourseRegistration + |> where([cr], cr.course_id == ^course_id) + |> join(:inner, [cr], u in assoc(cr, :user)) + end + + def get_users(course_id, group_id) do + get_users(course_id) + |> where([cr, u], cr.group_id == ^group_id) + + # |> join(:inner, [cr, u], g in assoc(cr, :group)) + # maybe not needed when we dont need group info + end + + def enroll_course(params = %{user_id: user_id, course_id: course_id, role: role}) + when is_ecto_id(user_id) && is_ecto_id(course_id) do + params |> insert_or_update_course_registration() + end + + @spec insert_or_update_course_registration(map()) :: + {:ok, Ecto.Schema.t()} | {:error, Ecto.Changeset.t()} + def insert_or_update_course_registration(params = %{user_id: user_id, course_id: course_id}) + when is_ecto_id(user_id) && is_ecto_id(course_id) do + CourseRegistration + |> where(user_id: ^user_id) + |> where(course_id: ^course_id) + |> Repo.one() + |> case do + nil -> + CourseRegistration.changeset(%CourseRegistration{}, params) + + cr -> + CourseRegistration.changeset(cr, params) + end + |> Repo.insert_or_update() + end + + +end diff --git a/lib/cadet/accounts/query.ex b/lib/cadet/accounts/query.ex index 023a9a528..1fb19d1f9 100644 --- a/lib/cadet/accounts/query.ex +++ b/lib/cadet/accounts/query.ex @@ -25,15 +25,16 @@ defmodule Cadet.Accounts.Query do # :TODO test @spec students_of(%CourseRegistration{}) :: Ecto.Query.t() def students_of(%CourseRegistration{user_id: id, role: :staff, course_id: course_id}) do - User - |> in_course(course_id) + CourseRegistration + |> where([cr], cr.course_id == ^course_id) |> join(:inner, [cr], g in Group, on: cr.group_id == g.id) |> where([cr, g], g.leader_id == ^id) end # :TODO test def avenger_of?(avenger_id, course_id, student_id) do - avengerInCourse = CourseRegistration + avengerInCourse = + CourseRegistration |> where([cr], cr.course_id = ^course_id) |> where([cr], cr.user_id = ^avenger_id) @@ -42,7 +43,7 @@ defmodule Cadet.Accounts.Query do students |> Repo.get_by(user_id: ^student_id) |> case do - nil -> false + nil -> false _ -> true end end diff --git a/lib/cadet/accounts/user.ex b/lib/cadet/accounts/user.ex index 57c56b9af..d6a1a61d9 100644 --- a/lib/cadet/accounts/user.ex +++ b/lib/cadet/accounts/user.ex @@ -15,7 +15,10 @@ defmodule Cadet.Accounts.User do # field(:role, Role) field(:username, :string) # field(:game_states, :map) + # belongs_to(:group, Group) + has_many(:course_registration, CourseRegistation) + timestamps() end diff --git a/lib/cadet/assessments/assessments.ex b/lib/cadet/assessments/assessments.ex index 1bc6439f0..34da093cc 100644 --- a/lib/cadet/assessments/assessments.ex +++ b/lib/cadet/assessments/assessments.ex @@ -710,7 +710,10 @@ defmodule Cadet.Assessments do # :TODO update avenger_of? call # def unsubmit_submission(submission_id, user = %User{id: user_id, role: role}) - def unsubmit_submission(submission_id, userCourse = %CourseRegistration{user_id: user_id, role: role, course_id: course_id}) + def unsubmit_submission( + submission_id, + userCourse = %CourseRegistration{user_id: user_id, role: role, course_id: course_id} + ) when is_ecto_id(submission_id) do submission = Submission diff --git a/lib/cadet/courses/group.ex b/lib/cadet/courses/group.ex index 5f9a75a8e..d953fea1c 100644 --- a/lib/cadet/courses/group.ex +++ b/lib/cadet/courses/group.ex @@ -9,9 +9,10 @@ defmodule Cadet.Courses.Group do schema "groups" do field(:name, :string) - belongs_to(:leader, User) - belongs_to(:mentor, User) - has_many(:students, User) + belongs_to(:leader, CourseRegistration) + belongs_to(:mentor, CourseRegistration) + + has_many(:students, CourseRegistration) end @optional_fields ~w(name leader_id mentor_id)a From 34f972e1bd270512cbaafaab369fa49915c275d3 Mon Sep 17 00:00:00 2001 From: Chow En Rong <53928333+chownces@users.noreply.github.com> Date: Thu, 3 Jun 2021 23:56:50 +0800 Subject: [PATCH 010/174] Updated course config functions and tests --- lib/cadet/courses/course.ex | 20 ++-- lib/cadet/courses/courses.ex | 19 ++-- .../admin_courses_controller.ex | 58 ++++++----- .../controllers/courses_controller.ex | 41 +++++--- lib/cadet_web/router.ex | 4 +- lib/cadet_web/views/courses_view.ex | 15 ++- test/cadet/courses/course_text.exs | 41 ++++++-- test/cadet/courses/courses_test.exs | 98 ++++++++++++++++--- .../admin_courses_controller_test.exs | 75 +++++++++++--- .../controllers/courses_controller_test.exs | 37 ++++--- 10 files changed, 301 insertions(+), 107 deletions(-) diff --git a/lib/cadet/courses/course.ex b/lib/cadet/courses/course.ex index 302177d08..57f4e0caa 100644 --- a/lib/cadet/courses/course.ex +++ b/lib/cadet/courses/course.ex @@ -18,17 +18,23 @@ defmodule Cadet.Courses.Course do timestamps() end - @required_fields_sublanguage ~w(source_chapter source_variant)a + @required_fields ~w(source_chapter source_variant)a + @optional_fields ~w(name module_code viewable enable_game enable_achievements enable_sourcecast module_help_text)a - def sublanguage_changeset(course, params) do - course - |> cast(params, @required_fields_sublanguage) - |> validate_required(@required_fields_sublanguage) - |> validate_allowed_combination() + def changeset(course, params) do + if Map.has_key?(params, :source_chapter) or Map.has_key?(params, :source_variant) do + course + |> cast(params, @required_fields ++ @optional_fields) + |> validate_required(@required_fields) + |> validate_sublanguage_combination() + else + course + |> cast(params, @optional_fields) + end end # Validates combination of Source chapter and variant - defp validate_allowed_combination(changeset) do + defp validate_sublanguage_combination(changeset) do case get_field(changeset, :source_chapter) do 1 -> validate_inclusion(changeset, :source_variant, ["default", "lazy", "wasm"]) 2 -> validate_inclusion(changeset, :source_variant, ["default", "lazy"]) diff --git a/lib/cadet/courses/courses.ex b/lib/cadet/courses/courses.ex index 56b1751d7..df71481ff 100644 --- a/lib/cadet/courses/courses.ex +++ b/lib/cadet/courses/courses.ex @@ -11,34 +11,33 @@ defmodule Cadet.Courses do alias Cadet.Courses.{Course, Group, Sourcecast, SourcecastUpload} @doc """ - Returns the default Source sublanguage of the Playground for the specified course. + Returns the course configuration for the specified course. """ - @spec get_sublanguage(integer) :: - {:ok, %{source_chapter: integer, source_variant: String.t()}} - | {:error, {:bad_request, String.t()}} - def get_sublanguage(course_id) when is_ecto_id(course_id) do + @spec get_course_config(integer) :: + {:ok, %Course{}} | {:error, {:bad_request, String.t()}} + def get_course_config(course_id) when is_ecto_id(course_id) do case retrieve_course(course_id) do nil -> {:error, {:bad_request, "Invalid course id"}} course -> - {:ok, %{source_chapter: course.source_chapter, source_variant: course.source_variant}} + {:ok, course} end end @doc """ - Updates the default Source sublanguage of the Playground for the specified course. + Updates the general course configuration for the specified course """ - @spec update_sublanguage(integer, integer, String.t()) :: + @spec update_course_config(integer, %{}) :: {:ok, %Course{}} | {:error, {:bad_request, String.t()} | {:error, Ecto.Changeset.t()}} - def update_sublanguage(course_id, chapter, variant) when is_ecto_id(course_id) do + def update_course_config(course_id, params) when is_ecto_id(course_id) do case retrieve_course(course_id) do nil -> {:error, {:bad_request, "Invalid course id"}} course -> course - |> Course.sublanguage_changeset(%{source_chapter: chapter, source_variant: variant}) + |> Course.changeset(params) |> Repo.update() end end diff --git a/lib/cadet_web/admin_controllers/admin_courses_controller.ex b/lib/cadet_web/admin_controllers/admin_courses_controller.ex index bcd1650fa..445a84fcb 100644 --- a/lib/cadet_web/admin_controllers/admin_courses_controller.ex +++ b/lib/cadet_web/admin_controllers/admin_courses_controller.ex @@ -5,34 +5,33 @@ defmodule CadetWeb.AdminCoursesController do alias Cadet.Courses - def update_sublanguage(conn, %{ - "courseid" => course_id, - "chapter" => chapter, - "variant" => variant - }) - when is_ecto_id(course_id) do - case Courses.update_sublanguage(course_id, chapter, variant) do - {:ok, _} -> - text(conn, "OK") - - {:error, {status, message}} -> - send_resp(conn, status, message) - - {:error, _} -> - conn - |> put_status(:bad_request) - |> text("Invalid parameter(s)") + def update(conn, params = %{"courseid" => course_id}) when is_ecto_id(course_id) do + params = for {key, val} <- params, into: %{}, do: {String.to_atom(key), val} + + if (Map.has_key?(params, :source_chapter) and Map.has_key?(params, :source_variant)) or + (not Map.has_key?(params, :source_chapter) and + not Map.has_key?(params, :source_variant)) do + case Courses.update_course_config(course_id, params) do + {:ok, _} -> + text(conn, "OK") + + {:error, {status, message}} -> + send_resp(conn, status, message) + + {:error, _} -> + conn + |> put_status(:bad_request) + |> text("Invalid parameter(s)") + end + else + send_resp(conn, :bad_request, "Missing parameter(s)") end end - def update_sublanguage(conn, _) do - send_resp(conn, :bad_request, "Missing parameter(s)") - end - - swagger_path :update_sublanguage do - put("/admin/courses/{courseId}/sublanguage") + swagger_path :update do + put("/admin/courses/{courseId}/config") - summary("Updates the default Source sublanguage of the Playground for the specified course") + summary("Updates the course configuration for the specified course") security([%{JWT: []}]) @@ -40,11 +39,20 @@ defmodule CadetWeb.AdminCoursesController do parameters do courseId(:path, :integer, "Course ID", required: true) - sublanguage(:body, Schema.ref(:AdminSublanguage), "sublanguage object", required: true) + name(:body, :string, "Course name") + module_code(:body, :string, "Course module code") + viewable(:body, :boolean, "Course viewability") + enable_game(:body, :boolean, "Enable game") + enable_achievements(:body, :boolean, "Enable achievements") + enable_sourcecast(:body, :boolean, "Enable sourcecast") + sublanguage(:body, Schema.ref(:AdminSublanguage), "sublanguage object") + module_help_text(:body, :string, "Module help text") end response(200, "OK") response(400, "Missing or invalid parameter(s)") + + # :TODO Check if this Forbidden comes from ensure_role. How about EnsureAuthenticated? response(403, "Forbidden") end diff --git a/lib/cadet_web/controllers/courses_controller.ex b/lib/cadet_web/controllers/courses_controller.ex index 564192534..bf646e598 100644 --- a/lib/cadet_web/controllers/courses_controller.ex +++ b/lib/cadet_web/controllers/courses_controller.ex @@ -5,17 +5,17 @@ defmodule CadetWeb.CoursesController do alias Cadet.Courses - def get_sublanguage(conn, %{"courseid" => course_id}) when is_ecto_id(course_id) do - case Courses.get_sublanguage(course_id) do - {:ok, sublanguage} -> render(conn, "sublanguage.json", sublanguage: sublanguage) + def index(conn, %{"courseid" => course_id}) when is_ecto_id(course_id) do + case Courses.get_course_config(course_id) do + {:ok, config} -> render(conn, "config.json", config: config) {:error, {status, message}} -> send_resp(conn, status, message) end end - swagger_path :get_sublanguage do - get("/courses/{courseId}/sublanguage") + swagger_path :get_course_config do + get("/courses/{courseId}/config") - summary("Retrieves the default Source sublanguage of the Playground for the specified course") + summary("Retrieves the course configuration of the specified course") security([%{JWT: []}]) @@ -25,25 +25,38 @@ defmodule CadetWeb.CoursesController do courseId(:path, :integer, "Course ID", required: true) end - response(200, "OK", Schema.ref(:Sublanguage)) + response(200, "OK", Schema.ref(:Config)) response(400, "Invalid courseId") end def swagger_definitions do %{ - Sublanguage: + Config: swagger_schema do - title("Sublanguage") + title("Course Configuration") properties do - chapter(:integer, "Chapter number from 1 to 4", required: true, minimum: 1, maximum: 4) - - variant(Schema.ref(:SourceVariant), "Variant name", required: true) + name(:string, "Course name", required: true) + module_code(:string, "Course module code", required: true) + viewable(:boolean, "Course viewability", required: true) + enable_game(:boolean, "Enable game", required: true) + enable_achievements(:boolean, "Enable achievements", required: true) + enable_sourcecast(:boolean, "Enable sourcecast", required: true) + source_chapter(:integer, "Source Chapter number from 1 to 4", required: true) + source_variant(Schema.ref(:SourceVariant), "Source Variant name", required: true) + module_help_text(:string, "Module help text", required: true) end example(%{ - chapter: 1, - variant: "default" + name: "Programming Methodology", + module_code: "CS1101S", + viewable: true, + enable_game: true, + enable_achievements: true, + enable_sourcecast: true, + source_chapter: 1, + source_variant: "default", + module_help_text: "Help text" }) end, SourceVariant: diff --git a/lib/cadet_web/router.ex b/lib/cadet_web/router.ex index 23b8d40a3..8d835c1ac 100644 --- a/lib/cadet_web/router.ex +++ b/lib/cadet_web/router.ex @@ -70,7 +70,7 @@ defmodule CadetWeb.Router do get("/user", UserController, :index) put("/user/game_states", UserController, :update_game_states) - get("/courses/:courseid/sublanguage", CoursesController, :get_sublanguage) + get("/courses/:courseid/config", CoursesController, :index) get("/devices", DevicesController, :index) post("/devices", DevicesController, :register) @@ -124,7 +124,7 @@ defmodule CadetWeb.Router do put("/goals/:uuid", AdminGoalsController, :update) delete("/goals/:uuid", AdminGoalsController, :delete) - put("/courses/:courseid/sublanguage", AdminCoursesController, :update_sublanguage) + put("/courses/:courseid/config", AdminCoursesController, :update) end # Other scopes may use custom stacks. diff --git a/lib/cadet_web/views/courses_view.ex b/lib/cadet_web/views/courses_view.ex index 233a2daa1..161de5ee8 100644 --- a/lib/cadet_web/views/courses_view.ex +++ b/lib/cadet_web/views/courses_view.ex @@ -1,9 +1,20 @@ defmodule CadetWeb.CoursesView do use CadetWeb, :view - def render("sublanguage.json", %{sublanguage: sublanguage}) do + def render("config.json", %{config: config}) do %{ - sublanguage: transform_map_for_view(sublanguage, [:source_chapter, :source_variant]) + config: + transform_map_for_view(config, [ + :name, + :module_code, + :viewable, + :enable_game, + :enable_achievements, + :enable_sourcecast, + :source_chapter, + :source_variant, + :module_help_text + ]) } end end diff --git a/test/cadet/courses/course_text.exs b/test/cadet/courses/course_text.exs index 78227a5b8..2e47d8699 100644 --- a/test/cadet/courses/course_text.exs +++ b/test/cadet/courses/course_text.exs @@ -3,28 +3,51 @@ defmodule Cadet.Courses.CourseTest do use Cadet.ChangesetCase, entity: Course - describe "Sublanguage Changesets" do + describe "Course Configuration Changesets" do test "valid changesets" do - assert_changeset(%{source_chapter: 1, source_variant: "wasm"}, :valid, :sublanguage_changeset) - assert_changeset(%{source_chapter: 2, source_variant: "lazy"}, :valid, :sublanguage_changeset) - assert_changeset(%{source_chapter: 3, source_variant: "non-det"}, :valid, :sublanguage_changeset) - assert_changeset(%{source_chapter: 4, source_variant: "default"}, :valid, :sublanguage_changeset) + assert_changeset(%{name: "Data Structures and Algorithms"}, :valid) + assert_changeset(%{module_code: "CS2040S"}, :valid) + assert_changeset(%{viewable: false}, :valid) + assert_changeset(%{enable_game: false}, :valid) + assert_changeset(%{enable_achievements: false}, :valid) + assert_changeset(%{enable_sourcecast: false}, :valid) + assert_changeset(%{module_help_text: ""}, :valid) + assert_changeset(%{module_help_text: "Module help text"}, :valid) + + assert_changeset( + %{ + enable_game: true, + enable_achievements: true, + enable_sourcecast: true + }, + :valid + ) + + assert_changeset(%{source_chapter: 1, source_variant: "wasm"}, :valid) + assert_changeset(%{source_chapter: 2, source_variant: "lazy"}, :valid) + assert_changeset(%{source_chapter: 3, source_variant: "non-det"}, :valid) + + assert_changeset( + %{source_chapter: 4, source_variant: "default", enable_achievements: true}, + :valid + ) end test "invalid changeset missing required params" do - assert_changeset(%{source_chapter: 2}, :invalid, :sublanguage_changeset) + assert_changeset(%{source_chapter: 2}, :invalid) + assert_changeset(%{source_variant: "default"}, :invalid) end test "invalid changeset with invalid chapter" do - assert_changeset(%{source_chapter: 5, source_variant: "default"}, :invalid, :sublanguage_changeset) + assert_changeset(%{source_chapter: 5, source_variant: "default"}, :invalid) end test "invalid changeset with invalid variant" do - assert_changeset(%{source_chapter: Enum.random(1..4), source_variant: "error"}, :invalid, :sublanguage_changeset) + assert_changeset(%{source_chapter: Enum.random(1..4), source_variant: "error"}, :invalid) end test "invalid changeset with invalid chapter-variant combination" do - assert_changeset(%{source_chapter: 4, source_variant: "lazy"}, :invalid, :sublanguage_changeset) + assert_changeset(%{source_chapter: 4, source_variant: "lazy"}, :invalid) end end end diff --git a/test/cadet/courses/courses_test.exs b/test/cadet/courses/courses_test.exs index c264ad316..a353097d0 100644 --- a/test/cadet/courses/courses_test.exs +++ b/test/cadet/courses/courses_test.exs @@ -4,41 +4,111 @@ defmodule Cadet.CourseTest do alias Cadet.{Courses, Repo} alias Cadet.Courses.{Group, Sourcecast, SourcecastUpload} - describe "get sublanguage" do + describe "get course config" do test "succeeds" do - course = insert(:course, %{source_chapter: 3, source_variant: "concurrent"}) - {:ok, sublanguage} = Courses.get_sublanguage(course.id) - assert sublanguage.source_chapter == 3 - assert sublanguage.source_variant == "concurrent" + course = insert(:course) + {:ok, course} = Courses.get_course_config(course.id) + assert course.name == "Programming Methodology" + assert course.module_code == "CS1101S" + assert course.viewable == true + assert course.enable_game == true + assert course.enable_achievements == true + assert course.enable_sourcecast == true + assert course.source_chapter == 1 + assert course.source_variant == "default" + assert course.module_help_text == "Help Text" end test "returns with error for invalid course id" do course = insert(:course) - assert {:error, _} = Courses.get_sublanguage(course.id + 1) + assert {:error, {status, message}} = Courses.get_course_config(course.id + 1) + assert status = :bad_request + assert message = "Invalid course id" end end - describe "update sublanguage" do - test "succeeds" do + describe "update course config" do + test "succeeds (without sublanguage update)" do + course = insert(:course) + + {:ok, updated_course} = + Courses.update_course_config(course.id, %{ + name: "Data Structures and Algorithms", + module_code: "CS2040S", + viewable: false, + enable_game: false, + enable_achievements: false, + enable_sourcecast: false, + module_help_text: "" + }) + + assert updated_course.name == "Data Structures and Algorithms" + assert updated_course.module_code == "CS2040S" + assert updated_course.viewable == false + assert updated_course.enable_game == false + assert updated_course.enable_achievements == false + assert updated_course.enable_sourcecast == false + assert updated_course.source_chapter == 1 + assert updated_course.source_variant == "default" + assert updated_course.module_help_text == nil + end + + test "succeeds (with sublanguage update)" do course = insert(:course) new_chapter = Enum.random(1..4) - {:ok, sublanguage} = Courses.update_sublanguage(course.id, new_chapter, "default") - assert sublanguage.source_chapter == new_chapter - assert sublanguage.source_variant == "default" + + {:ok, updated_course} = + Courses.update_course_config(course.id, %{ + name: "Data Structures and Algorithms", + module_code: "CS2040S", + viewable: false, + enable_game: false, + enable_achievements: false, + enable_sourcecast: false, + source_chapter: new_chapter, + source_variant: "default", + module_help_text: "help" + }) + + assert updated_course.name == "Data Structures and Algorithms" + assert updated_course.module_code == "CS2040S" + assert updated_course.viewable == false + assert updated_course.enable_game == false + assert updated_course.enable_achievements == false + assert updated_course.enable_sourcecast == false + assert updated_course.source_chapter == new_chapter + assert updated_course.source_variant == "default" + assert updated_course.module_help_text == "help" end test "returns with error for invalid course id" do course = insert(:course) new_chapter = Enum.random(1..4) - assert {:error, _} = Courses.update_sublanguage(course.id + 1, new_chapter, "default") + + assert {:error, {status, message}} = + Courses.update_course_config(course.id + 1, %{ + source_chapter: new_chapter, + source_variant: "default" + }) + + assert status = :bad_request + assert message = "Invalid course id" end test "returns with error for failed updates" do course = insert(:course) - assert {:error, changeset} = Courses.update_sublanguage(course.id, 0, "default") + + assert {:error, changeset} = + Courses.update_course_config(course.id, %{ + source_chapter: 0, + source_variant: "default" + }) + assert %{source_chapter: ["is invalid"]} = errors_on(changeset) - assert {:error, changeset} = Courses.update_sublanguage(course.id, 2, "gpu") + assert {:error, changeset} = + Courses.update_course_config(course.id, %{source_chapter: 2, source_variant: "gpu"}) + assert %{source_variant: ["is invalid"]} = errors_on(changeset) end end diff --git a/test/cadet_web/admin_controllers/admin_courses_controller_test.exs b/test/cadet_web/admin_controllers/admin_courses_controller_test.exs index fab877dca..6b3bc4bb9 100644 --- a/test/cadet_web/admin_controllers/admin_courses_controller_test.exs +++ b/test/cadet_web/admin_controllers/admin_courses_controller_test.exs @@ -5,18 +5,54 @@ defmodule CadetWeb.AdminCoursesControllerTest do test "swagger" do AdminCoursesController.swagger_definitions() - AdminCoursesController.swagger_path_update_sublanguage(nil) + AdminCoursesController.swagger_path_update(nil) end - describe "PUT /courses/{courseId}/sublanguage" do + describe "PUT /courses/{courseId}/config" do @tag authenticate: :admin - test "succeeds", %{conn: conn} do - course = insert(:course, %{source_chapter: 4, source_variant: "gpu"}) + test "succeeds 1", %{conn: conn} do + course = insert(:course) + + conn = + put(conn, build_url(course.id), %{ + "source_chapter" => Enum.random(1..4), + "source_variant" => "default" + }) + + assert response(conn, 200) == "OK" + end + + @tag authenticate: :admin + test "succeeds 2", %{conn: conn} do + course = insert(:course) conn = put(conn, build_url(course.id), %{ - "chapter" => Enum.random(1..4), - "variant" => "default" + "name" => "Data Structures and Algorithms", + "module_code" => "CS2040S", + "enable_game" => false, + "enable_achievements" => false, + "enable_sourcecast" => true, + "source_chapter" => Enum.random(1..4), + "source_variant" => "default", + "module_help_text" => "help" + }) + + assert response(conn, 200) == "OK" + end + + @tag authenticate: :admin + test "succeeds 3", %{conn: conn} do + course = insert(:course) + + conn = + put(conn, build_url(course.id), %{ + "name" => "Data Structures and Algorithms", + "module_code" => "CS2040S", + "enable_game" => false, + "enable_achievements" => false, + "enable_sourcecast" => true, + "module_help_text" => "help" }) assert response(conn, 200) == "OK" @@ -25,7 +61,9 @@ defmodule CadetWeb.AdminCoursesControllerTest do @tag authenticate: :student test "rejects forbidden request for non-staff users", %{conn: conn} do course = insert(:course) - conn = put(conn, build_url(course.id), %{"chapter" => 3, "variant" => "concurrent"}) + + conn = + put(conn, build_url(course.id), %{"source_chapter" => 3, "source_variant" => "concurrent"}) assert response(conn, 403) == "Forbidden" end @@ -33,13 +71,20 @@ defmodule CadetWeb.AdminCoursesControllerTest do @tag authenticate: :staff test "rejects requests with invalid course id", %{conn: conn} do course = insert(:course) - conn = put(conn, build_url(course.id + 1), %{"chapter" => 3, "variant" => "concurrent"}) + + conn = + put(conn, build_url(course.id + 1), %{ + "source_chapter" => 3, + "source_variant" => "concurrent" + }) + + assert response(conn, 400) == "Invalid course id" end @tag authenticate: :staff test "rejects requests with invalid params", %{conn: conn} do course = insert(:course) - conn = put(conn, build_url(course.id), %{"chapter" => 4, "variant" => "wasm"}) + conn = put(conn, build_url(course.id), %{"source_chapter" => 4, "source_variant" => "wasm"}) assert response(conn, 400) == "Invalid parameter(s)" end @@ -47,11 +92,19 @@ defmodule CadetWeb.AdminCoursesControllerTest do @tag authenticate: :staff test "rejects requests with missing params", %{conn: conn} do course = insert(:course) - conn = put(conn, build_url(course.id), %{"variant" => "default"}) + conn = put(conn, build_url(course.id), %{ + "name" => "Data Structures and Algorithms", + "module_code" => "CS2040S", + "enable_game" => false, + "enable_achievements" => false, + "enable_sourcecast" => true, + "module_help_text" => "help", + "source_variant" => "default" + }) assert response(conn, 400) == "Missing parameter(s)" end end - defp build_url(course_id), do: "/v2/admin/courses/#{course_id}/sublanguage" + defp build_url(course_id), do: "/v2/admin/courses/#{course_id}/config" end diff --git a/test/cadet_web/controllers/courses_controller_test.exs b/test/cadet_web/controllers/courses_controller_test.exs index 1629e4147..88b2c2640 100644 --- a/test/cadet_web/controllers/courses_controller_test.exs +++ b/test/cadet_web/controllers/courses_controller_test.exs @@ -5,25 +5,36 @@ defmodule CadetWeb.CoursesControllerTest do test "swagger" do CoursesController.swagger_definitions() - CoursesController.swagger_path_get_sublanguage(nil) + CoursesController.swagger_path_get_course_config(nil) end - describe "GET /, unauthenticated" do - test "unauthorized", %{conn: conn} do + describe "GET /courses/course_id/config, unauthenticated" do + test "unathorized", %{conn: conn} do course = insert(:course) - conn = get(conn, build_url(course.id)) - assert response(conn, 401) =~ "Unauthorised" + conn = get(conn, build_url_config(course.id)) + assert response(conn, 401) == "Unauthorised" end end - describe "GET /courses/course_id/sublanguage" do + describe "GET /courses/course_id/config" do @tag authenticate: :student test "succeeds", %{conn: conn} do - course = insert(:course, %{source_chapter: 2, source_variant: "lazy"}) - - resp = conn |> get(build_url(course.id)) |> json_response(200) - - assert %{"sublanguage" => %{"source_chapter" => 2, "source_variant" => "lazy"}} = resp + course = insert(:course) + resp = conn |> get(build_url_config(course.id)) |> json_response(200) + + assert %{ + "config" => %{ + "name" => "Programming Methodology", + "module_code" => "CS1101S", + "viewable" => true, + "enable_game" => true, + "enable_achievements" => true, + "enable_sourcecast" => true, + "source_chapter" => 1, + "source_variant" => "default", + "module_help_text" => "Help Text" + } + } = resp end @tag authenticate: :student @@ -32,11 +43,11 @@ defmodule CadetWeb.CoursesControllerTest do conn = conn - |> get(build_url(course.id + 1)) + |> get(build_url_config(course.id + 1)) assert response(conn, 400) == "Invalid course id" end end - defp build_url(course_id), do: "/v2/courses/#{course_id}/sublanguage" + defp build_url_config(course_id), do: "/v2/courses/#{course_id}/config" end From 064ea9923761021e5ddc4164291f4374d9ddab03 Mon Sep 17 00:00:00 2001 From: Chow En Rong <53928333+chownces@users.noreply.github.com> Date: Fri, 4 Jun 2021 13:22:41 +0800 Subject: [PATCH 011/174] Fix filename typo --- test/cadet/courses/{course_text.exs => course_test.exs} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename test/cadet/courses/{course_text.exs => course_test.exs} (100%) diff --git a/test/cadet/courses/course_text.exs b/test/cadet/courses/course_test.exs similarity index 100% rename from test/cadet/courses/course_text.exs rename to test/cadet/courses/course_test.exs From e9677d01e741c037b54bb765c6e63777864d80e7 Mon Sep 17 00:00:00 2001 From: Chow En Rong <53928333+chownces@users.noreply.github.com> Date: Fri, 4 Jun 2021 13:57:59 +0800 Subject: [PATCH 012/174] Updated endpoint url for course config update to prevent clash with assessment config and types updates --- .../admin_courses_controller.ex | 6 ++--- lib/cadet_web/router.ex | 2 +- .../admin_courses_controller_test.exs | 26 ++++++++++--------- 3 files changed, 18 insertions(+), 16 deletions(-) diff --git a/lib/cadet_web/admin_controllers/admin_courses_controller.ex b/lib/cadet_web/admin_controllers/admin_courses_controller.ex index 445a84fcb..54ffa790c 100644 --- a/lib/cadet_web/admin_controllers/admin_courses_controller.ex +++ b/lib/cadet_web/admin_controllers/admin_courses_controller.ex @@ -5,7 +5,7 @@ defmodule CadetWeb.AdminCoursesController do alias Cadet.Courses - def update(conn, params = %{"courseid" => course_id}) when is_ecto_id(course_id) do + def update_course_config(conn, params = %{"courseid" => course_id}) when is_ecto_id(course_id) do params = for {key, val} <- params, into: %{}, do: {String.to_atom(key), val} if (Map.has_key?(params, :source_chapter) and Map.has_key?(params, :source_variant)) or @@ -28,8 +28,8 @@ defmodule CadetWeb.AdminCoursesController do end end - swagger_path :update do - put("/admin/courses/{courseId}/config") + swagger_path :update_course_config do + put("/admin/courses/{courseId}/course_config") summary("Updates the course configuration for the specified course") diff --git a/lib/cadet_web/router.ex b/lib/cadet_web/router.ex index 8d835c1ac..177b8021e 100644 --- a/lib/cadet_web/router.ex +++ b/lib/cadet_web/router.ex @@ -124,7 +124,7 @@ defmodule CadetWeb.Router do put("/goals/:uuid", AdminGoalsController, :update) delete("/goals/:uuid", AdminGoalsController, :delete) - put("/courses/:courseid/config", AdminCoursesController, :update) + put("/courses/:courseid/course_config", AdminCoursesController, :update_course_config) end # Other scopes may use custom stacks. diff --git a/test/cadet_web/admin_controllers/admin_courses_controller_test.exs b/test/cadet_web/admin_controllers/admin_courses_controller_test.exs index 6b3bc4bb9..c61f47167 100644 --- a/test/cadet_web/admin_controllers/admin_courses_controller_test.exs +++ b/test/cadet_web/admin_controllers/admin_courses_controller_test.exs @@ -5,10 +5,10 @@ defmodule CadetWeb.AdminCoursesControllerTest do test "swagger" do AdminCoursesController.swagger_definitions() - AdminCoursesController.swagger_path_update(nil) + AdminCoursesController.swagger_path_update_course_config(nil) end - describe "PUT /courses/{courseId}/config" do + describe "PUT /courses/{courseId}/course_config" do @tag authenticate: :admin test "succeeds 1", %{conn: conn} do course = insert(:course) @@ -92,19 +92,21 @@ defmodule CadetWeb.AdminCoursesControllerTest do @tag authenticate: :staff test "rejects requests with missing params", %{conn: conn} do course = insert(:course) - conn = put(conn, build_url(course.id), %{ - "name" => "Data Structures and Algorithms", - "module_code" => "CS2040S", - "enable_game" => false, - "enable_achievements" => false, - "enable_sourcecast" => true, - "module_help_text" => "help", - "source_variant" => "default" - }) + + conn = + put(conn, build_url(course.id), %{ + "name" => "Data Structures and Algorithms", + "module_code" => "CS2040S", + "enable_game" => false, + "enable_achievements" => false, + "enable_sourcecast" => true, + "module_help_text" => "help", + "source_variant" => "default" + }) assert response(conn, 400) == "Missing parameter(s)" end end - defp build_url(course_id), do: "/v2/admin/courses/#{course_id}/config" + defp build_url(course_id), do: "/v2/admin/courses/#{course_id}/course_config" end From eaa84eeb178beaa5d6088c7c5442aaf208295c02 Mon Sep 17 00:00:00 2001 From: Chow En Rong <53928333+chownces@users.noreply.github.com> Date: Fri, 4 Jun 2021 18:26:25 +0800 Subject: [PATCH 013/174] Added assesment config routing, functions and tests --- lib/cadet/courses/assessment_config.ex | 23 +++- lib/cadet/courses/courses.ex | 19 +++- .../admin_courses_controller.ex | 46 +++++++- lib/cadet_web/router.ex | 1 + ...0210531155751_add_course_configuration.exs | 6 +- priv/repo/seeds.exs | 2 +- test/cadet/courses/assessment_config_test.exs | 104 ++++++++++++++++++ test/cadet/courses/courses_test.exs | 43 +++++++- .../admin_courses_controller_test.exs | 85 ++++++++++++-- .../courses/assessment_config_factory.ex | 20 ++++ test/factories/factory.ex | 2 +- 11 files changed, 327 insertions(+), 24 deletions(-) create mode 100644 test/cadet/courses/assessment_config_test.exs create mode 100644 test/factories/courses/assessment_config_factory.ex diff --git a/lib/cadet/courses/assessment_config.ex b/lib/cadet/courses/assessment_config.ex index 42527b176..ef4436c88 100644 --- a/lib/cadet/courses/assessment_config.ex +++ b/lib/cadet/courses/assessment_config.ex @@ -8,21 +8,32 @@ defmodule Cadet.Courses.AssessmentConfig do alias Cadet.Courses.Course schema "assessment_configs" do - field(:early_submission_xp, :integer, default: 200) - field(:days_before_early_xp_decay, :integer, default: 2) - field(:decay_rate_points_per_hour, :integer, default: 1) + field(:early_submission_xp, :integer) + field(:hours_before_early_xp_decay, :integer) + field(:decay_rate_points_per_hour, :integer) belongs_to(:course, Course) timestamps() end - @required_fields ~w(course)a - @optional_fields ~w(early_submission_xp days_before_early_xp_decay + @required_fields ~w(early_submission_xp hours_before_early_xp_decay decay_rate_points_per_hour)a def changeset(assessment_config, params) do assessment_config - |> cast(params, @required_fields ++ @optional_fields) + |> cast(params, @required_fields) |> validate_required(@required_fields) + |> foreign_key_constraint(:course_id) + |> validate_number(:early_submission_xp, greater_than_or_equal_to: 0) + |> validate_number(:hours_before_early_xp_decay, greater_than_or_equal_to: 0) + |> validate_number(:decay_rate_points_per_hour, greater_than_or_equal_to: 0) + |> validate_decay_rate() + end + + defp validate_decay_rate(changeset) do + changeset + |> validate_number(:decay_rate_points_per_hour, + less_than_or_equal_to: get_field(changeset, :early_submission_xp) + ) end end diff --git a/lib/cadet/courses/courses.ex b/lib/cadet/courses/courses.ex index df71481ff..78ade2147 100644 --- a/lib/cadet/courses/courses.ex +++ b/lib/cadet/courses/courses.ex @@ -8,7 +8,7 @@ defmodule Cadet.Courses do import Ecto.Query alias Cadet.Accounts.User - alias Cadet.Courses.{Course, Group, Sourcecast, SourcecastUpload} + alias Cadet.Courses.{AssessmentConfig, Course, Group, Sourcecast, SourcecastUpload} @doc """ Returns the course configuration for the specified course. @@ -48,6 +48,23 @@ defmodule Cadet.Courses do |> Repo.one() end + @doc """ + Updates the assessment configuration for the specified course + """ + @spec update_assessment_config(integer, integer, integer, integer) :: + {:ok, %AssessmentConfig{}} | {:error, Ecto.Changeset.t()} + def update_assessment_config(course_id, early_xp, hours_before_decay, decay_rate) do + AssessmentConfig + |> where(course_id: ^course_id) + |> Repo.one() + |> AssessmentConfig.changeset(%{ + early_submission_xp: early_xp, + hours_before_early_xp_decay: hours_before_decay, + decay_rate_points_per_hour: decay_rate + }) + |> Repo.update() + end + @doc """ Get a group based on the group name or create one if it doesn't exist """ diff --git a/lib/cadet_web/admin_controllers/admin_courses_controller.ex b/lib/cadet_web/admin_controllers/admin_courses_controller.ex index 54ffa790c..7ab9282aa 100644 --- a/lib/cadet_web/admin_controllers/admin_courses_controller.ex +++ b/lib/cadet_web/admin_controllers/admin_courses_controller.ex @@ -5,7 +5,8 @@ defmodule CadetWeb.AdminCoursesController do alias Cadet.Courses - def update_course_config(conn, params = %{"courseid" => course_id}) when is_ecto_id(course_id) do + def update_course_config(conn, params = %{"courseid" => course_id}) + when is_ecto_id(course_id) do params = for {key, val} <- params, into: %{}, do: {String.to_atom(key), val} if (Map.has_key?(params, :source_chapter) and Map.has_key?(params, :source_variant)) or @@ -28,6 +29,28 @@ defmodule CadetWeb.AdminCoursesController do end end + def update_assessment_config(conn, %{ + "courseid" => course_id, + "early_submission_xp" => early_xp, + "hours_before_early_xp_decay" => hours_before_decay, + "decay_rate_points_per_hour" => decay_rate + }) + when is_ecto_id(course_id) do + case Courses.update_assessment_config(course_id, early_xp, hours_before_decay, decay_rate) do + {:ok, _} -> + text(conn, "OK") + + {:error, _} -> + conn + |> put_status(:bad_request) + |> text("Invalid parameter(s)") + end + end + + def update_assessment_config(conn, _) do + send_resp(conn, :bad_request, "Missing parameter(s)") + end + swagger_path :update_course_config do put("/admin/courses/{courseId}/course_config") @@ -51,8 +74,27 @@ defmodule CadetWeb.AdminCoursesController do response(200, "OK") response(400, "Missing or invalid parameter(s)") + response(403, "Forbidden") + end + + swagger_path :update_assessment_config do + put("/admin/courses/{courseId}/assessment_config") - # :TODO Check if this Forbidden comes from ensure_role. How about EnsureAuthenticated? + summary("Updates the assessment configuration for the specified course") + + security([%{JWT: []}]) + + consumes("application/json") + + parameters do + courseId(:path, :integer, "Course ID", required: true) + early_submission_xp(:body, :integer, "Early submission xp") + hours_before_early_xp_decay(:body, :integer, "Hours before early submission xp decay") + decay_rate_points_per_hour(:body, :integer, "Decay rate in points per hour") + end + + response(200, "OK") + response(400, "Missing or invalid parameter(s)") response(403, "Forbidden") end diff --git a/lib/cadet_web/router.ex b/lib/cadet_web/router.ex index 177b8021e..5bdb7b5c0 100644 --- a/lib/cadet_web/router.ex +++ b/lib/cadet_web/router.ex @@ -125,6 +125,7 @@ defmodule CadetWeb.Router do delete("/goals/:uuid", AdminGoalsController, :delete) put("/courses/:courseid/course_config", AdminCoursesController, :update_course_config) + put("/courses/:courseid/assessment_config", AdminCoursesController, :update_assessment_config) end # Other scopes may use custom stacks. diff --git a/priv/repo/migrations/20210531155751_add_course_configuration.exs b/priv/repo/migrations/20210531155751_add_course_configuration.exs index 104dd760a..04f24655f 100644 --- a/priv/repo/migrations/20210531155751_add_course_configuration.exs +++ b/priv/repo/migrations/20210531155751_add_course_configuration.exs @@ -16,9 +16,9 @@ defmodule Cadet.Repo.Migrations.AddCourseConfiguration do end create table(:assessment_configs) do - add(:early_submission_xp, :integer, null: false, default: 200) - add(:days_before_early_xp_decay, :integer, null: false, default: 2) - add(:decay_rate_points_per_hour, :integer, null: false, default: 1) + add(:early_submission_xp, :integer, null: false) + add(:hours_before_early_xp_decay, :integer, null: false) + add(:decay_rate_points_per_hour, :integer, null: false) add(:course_id, references(:courses), null: false) timestamps() end diff --git a/priv/repo/seeds.exs b/priv/repo/seeds.exs index 71375a4ff..841ea3732 100644 --- a/priv/repo/seeds.exs +++ b/priv/repo/seeds.exs @@ -14,7 +14,7 @@ import Cadet.Factory alias Cadet.Assessments.SubmissionStatus # insert default source version -Cadet.Repo.insert!(%Cadet.Settings.Sublanguage{chapter: 1, variant: "default"}) +# Cadet.Repo.insert!(%Cadet.Settings.Sublanguage{chapter: 1, variant: "default"}) if Cadet.Env.env() == :dev do # User and Group diff --git a/test/cadet/courses/assessment_config_test.exs b/test/cadet/courses/assessment_config_test.exs new file mode 100644 index 000000000..6f2a698d5 --- /dev/null +++ b/test/cadet/courses/assessment_config_test.exs @@ -0,0 +1,104 @@ +defmodule Cadet.Courses.AssessmentConfigTest do + alias Cadet.Courses.AssessmentConfig + + use Cadet.ChangesetCase, entity: AssessmentConfig + + describe "Assessment Configuration Changesets" do + test "valid changesets" do + assert_changeset( + %{ + early_submission_xp: 200, + hours_before_early_xp_decay: 48, + decay_rate_points_per_hour: 1 + }, + :valid + ) + + assert_changeset( + %{ + early_submission_xp: 0, + hours_before_early_xp_decay: 0, + decay_rate_points_per_hour: 0 + }, + :valid + ) + + assert_changeset( + %{ + early_submission_xp: 200, + hours_before_early_xp_decay: 0, + decay_rate_points_per_hour: 10 + }, + :valid + ) + end + + test "invalid changeset missing required params" do + assert_changeset( + %{ + early_submission_xp: 0, + hours_before_early_xp_decay: 0 + }, + :invalid + ) + + assert_changeset( + %{ + early_submission_xp: 0 + }, + :invalid + ) + + assert_changeset( + %{ + decay_rate_points_per_hour: 1 + }, + :invalid + ) + end + + test "invalid changeset with invalid early xp" do + assert_changeset( + %{ + early_submission_xp: -1, + hours_before_early_xp_decay: 0, + decay_rate_points_per_hour: 10 + }, + :invalid + ) + end + + test "invalid changeset with invalid hours before decay" do + assert_changeset( + %{ + early_submission_xp: 200, + hours_before_early_xp_decay: -1, + decay_rate_points_per_hour: 10 + }, + :invalid + ) + end + + test "invalid changeset with invalid decay rate" do + assert_changeset( + %{ + early_submission_xp: 200, + hours_before_early_xp_decay: 0, + decay_rate_points_per_hour: -1 + }, + :invalid + ) + end + + test "invalid changeset with decay rate greater than early submission xp" do + assert_changeset( + %{ + early_submission_xp: 200, + hours_before_early_xp_decay: 48, + decay_rate_points_per_hour: 300 + }, + :invalid + ) + end + end +end diff --git a/test/cadet/courses/courses_test.exs b/test/cadet/courses/courses_test.exs index a353097d0..1f31f4b10 100644 --- a/test/cadet/courses/courses_test.exs +++ b/test/cadet/courses/courses_test.exs @@ -1,8 +1,7 @@ -defmodule Cadet.CourseTest do +defmodule Cadet.CoursesTest do use Cadet.DataCase alias Cadet.{Courses, Repo} - alias Cadet.Courses.{Group, Sourcecast, SourcecastUpload} describe "get course config" do test "succeeds" do @@ -113,6 +112,46 @@ defmodule Cadet.CourseTest do end end + describe "update assessment config" do + test "succeeds" do + assessment_config = insert(:assessment_config) + + {:ok, updated_config} = + Courses.update_assessment_config(assessment_config.course_id, 100, 24, 1) + + assert updated_config.early_submission_xp == 100 + assert updated_config.hours_before_early_xp_decay == 24 + assert updated_config.decay_rate_points_per_hour == 1 + end + + test "returns with error for failed updates" do + assessment_config = insert(:assessment_config) + + {:error, changeset} = + Courses.update_assessment_config(assessment_config.course_id, -1, 0, 0) + + assert %{early_submission_xp: ["must be greater than or equal to 0"]} = errors_on(changeset) + + {:error, changeset} = + Courses.update_assessment_config(assessment_config.course_id, 200, -1, 0) + + assert %{hours_before_early_xp_decay: ["must be greater than or equal to 0"]} = + errors_on(changeset) + + {:error, changeset} = + Courses.update_assessment_config(assessment_config.course_id, 200, 48, -1) + + assert %{decay_rate_points_per_hour: ["must be greater than or equal to 0"]} = + errors_on(changeset) + + {:error, changeset} = + Courses.update_assessment_config(assessment_config.course_id, 200, 48, 300) + + assert %{decay_rate_points_per_hour: ["must be less than or equal to 200"]} = + errors_on(changeset) + end + end + # describe "Sourcecast" do # setup do # on_exit(fn -> File.rm_rf!("uploads/test/sourcecasts") end) diff --git a/test/cadet_web/admin_controllers/admin_courses_controller_test.exs b/test/cadet_web/admin_controllers/admin_courses_controller_test.exs index c61f47167..93f378986 100644 --- a/test/cadet_web/admin_controllers/admin_courses_controller_test.exs +++ b/test/cadet_web/admin_controllers/admin_courses_controller_test.exs @@ -6,6 +6,7 @@ defmodule CadetWeb.AdminCoursesControllerTest do test "swagger" do AdminCoursesController.swagger_definitions() AdminCoursesController.swagger_path_update_course_config(nil) + AdminCoursesController.swagger_path_update_assessment_config(nil) end describe "PUT /courses/{courseId}/course_config" do @@ -14,7 +15,7 @@ defmodule CadetWeb.AdminCoursesControllerTest do course = insert(:course) conn = - put(conn, build_url(course.id), %{ + put(conn, build_url_course_config(course.id), %{ "source_chapter" => Enum.random(1..4), "source_variant" => "default" }) @@ -27,7 +28,7 @@ defmodule CadetWeb.AdminCoursesControllerTest do course = insert(:course) conn = - put(conn, build_url(course.id), %{ + put(conn, build_url_course_config(course.id), %{ "name" => "Data Structures and Algorithms", "module_code" => "CS2040S", "enable_game" => false, @@ -46,7 +47,7 @@ defmodule CadetWeb.AdminCoursesControllerTest do course = insert(:course) conn = - put(conn, build_url(course.id), %{ + put(conn, build_url_course_config(course.id), %{ "name" => "Data Structures and Algorithms", "module_code" => "CS2040S", "enable_game" => false, @@ -63,7 +64,10 @@ defmodule CadetWeb.AdminCoursesControllerTest do course = insert(:course) conn = - put(conn, build_url(course.id), %{"source_chapter" => 3, "source_variant" => "concurrent"}) + put(conn, build_url_course_config(course.id), %{ + "source_chapter" => 3, + "source_variant" => "concurrent" + }) assert response(conn, 403) == "Forbidden" end @@ -73,7 +77,7 @@ defmodule CadetWeb.AdminCoursesControllerTest do course = insert(:course) conn = - put(conn, build_url(course.id + 1), %{ + put(conn, build_url_course_config(course.id + 1), %{ "source_chapter" => 3, "source_variant" => "concurrent" }) @@ -84,7 +88,12 @@ defmodule CadetWeb.AdminCoursesControllerTest do @tag authenticate: :staff test "rejects requests with invalid params", %{conn: conn} do course = insert(:course) - conn = put(conn, build_url(course.id), %{"source_chapter" => 4, "source_variant" => "wasm"}) + + conn = + put(conn, build_url_course_config(course.id), %{ + "source_chapter" => 4, + "source_variant" => "wasm" + }) assert response(conn, 400) == "Invalid parameter(s)" end @@ -94,7 +103,7 @@ defmodule CadetWeb.AdminCoursesControllerTest do course = insert(:course) conn = - put(conn, build_url(course.id), %{ + put(conn, build_url_course_config(course.id), %{ "name" => "Data Structures and Algorithms", "module_code" => "CS2040S", "enable_game" => false, @@ -108,5 +117,65 @@ defmodule CadetWeb.AdminCoursesControllerTest do end end - defp build_url(course_id), do: "/v2/admin/courses/#{course_id}/course_config" + describe "PUT /courses/{courseId}/assessment_config" do + @tag authenticate: :admin + test "succeeds", %{conn: conn} do + assessment_config = insert(:assessment_config) + + conn = + put(conn, build_url_assessment_config(assessment_config.course_id), %{ + "early_submission_xp" => 100, + "hours_before_early_xp_decay" => 24, + "decay_rate_points_per_hour" => 2 + }) + + assert response(conn, 200) == "OK" + end + + @tag authenticate: :student + test "rejects forbidden request for non-staff users", %{conn: conn} do + assessment_config = insert(:assessment_config) + + conn = + put(conn, build_url_assessment_config(assessment_config.course_id), %{ + "early_submission_xp" => 100, + "hours_before_early_xp_decay" => 24, + "decay_rate_points_per_hour" => 2 + }) + + assert response(conn, 403) == "Forbidden" + end + + @tag authenticate: :staff + test "rejects requests with invalid params", %{conn: conn} do + assessment_config = insert(:assessment_config) + + conn = + put(conn, build_url_assessment_config(assessment_config.course_id), %{ + "early_submission_xp" => 100, + "hours_before_early_xp_decay" => -1, + "decay_rate_points_per_hour" => 200 + }) + + assert response(conn, 400) == "Invalid parameter(s)" + end + + @tag authenticate: :staff + test "rejects requests with missing params", %{conn: conn} do + assessment_config = insert(:assessment_config) + + conn = + put(conn, build_url_assessment_config(assessment_config.course_id), %{ + "hours_before_early_xp_decay" => 24, + "decay_rate_points_per_hour" => 2 + }) + + assert response(conn, 400) == "Missing parameter(s)" + end + end + + defp build_url_course_config(course_id), do: "/v2/admin/courses/#{course_id}/course_config" + + defp build_url_assessment_config(course_id), + do: "/v2/admin/courses/#{course_id}/assessment_config" end diff --git a/test/factories/courses/assessment_config_factory.ex b/test/factories/courses/assessment_config_factory.ex new file mode 100644 index 000000000..d4a4ffe04 --- /dev/null +++ b/test/factories/courses/assessment_config_factory.ex @@ -0,0 +1,20 @@ +defmodule Cadet.Courses.AssessmentConfigFactory do + @moduledoc """ + Factory for the AssessmentConfig entity + """ + + defmacro __using__(_opts) do + quote do + alias Cadet.Courses.AssessmentConfig + + def assessment_config_factory do + %AssessmentConfig{ + early_submission_xp: 200, + hours_before_early_xp_decay: 48, + decay_rate_points_per_hour: 1, + course: build(:course) + } + end + end + end +end diff --git a/test/factories/factory.ex b/test/factories/factory.ex index c419d28f3..3e74ee40b 100644 --- a/test/factories/factory.ex +++ b/test/factories/factory.ex @@ -22,7 +22,7 @@ defmodule Cadet.Factory do GoalFactory } - use Cadet.Courses.{CourseFactory, GroupFactory, SourcecastFactory} + use Cadet.Courses.{AssessmentConfigFactory, CourseFactory, GroupFactory, SourcecastFactory} use Cadet.Devices.DeviceFactory From 07c27d420f583e3bfc94b9a1fbbb2a72fed30564 Mon Sep 17 00:00:00 2001 From: Chow En Rong <53928333+chownces@users.noreply.github.com> Date: Fri, 4 Jun 2021 18:37:24 +0800 Subject: [PATCH 014/174] Temporarily fix references to Courses context in miscellaneous files --- lib/cadet_web/controllers/sourcecast_controller.ex | 6 +++--- lib/mix/tasks/users/import.ex | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/lib/cadet_web/controllers/sourcecast_controller.ex b/lib/cadet_web/controllers/sourcecast_controller.ex index dd08acd12..dc5cad9b9 100644 --- a/lib/cadet_web/controllers/sourcecast_controller.ex +++ b/lib/cadet_web/controllers/sourcecast_controller.ex @@ -2,7 +2,7 @@ defmodule CadetWeb.SourcecastController do use CadetWeb, :controller use PhoenixSwagger - alias Cadet.{Repo, Course} + alias Cadet.{Repo, Courses} alias Cadet.Courses.Sourcecast def index(conn, _params) do @@ -11,7 +11,7 @@ defmodule CadetWeb.SourcecastController do end def create(conn, %{"sourcecast" => sourcecast}) do - result = Course.upload_sourcecast_file(conn.assigns.current_user, sourcecast) + result = Courses.upload_sourcecast_file(conn.assigns.current_user, sourcecast) case result do {:ok, _nil} -> @@ -29,7 +29,7 @@ defmodule CadetWeb.SourcecastController do end def delete(conn, %{"id" => id}) do - result = Course.delete_sourcecast_file(conn.assigns.current_user, id) + result = Courses.delete_sourcecast_file(conn.assigns.current_user, id) case result do {:ok, _nil} -> diff --git a/lib/mix/tasks/users/import.ex b/lib/mix/tasks/users/import.ex index e49ed4c58..f2371e4eb 100644 --- a/lib/mix/tasks/users/import.ex +++ b/lib/mix/tasks/users/import.ex @@ -24,7 +24,7 @@ defmodule Mix.Tasks.Cadet.Users.Import do require Logger - alias Cadet.{Accounts, Course, Repo} + alias Cadet.{Accounts, Courses, Repo} alias Cadet.Courses.Group alias Cadet.Accounts.User @@ -48,7 +48,7 @@ defmodule Mix.Tasks.Cadet.Users.Import do csv_stream = path |> File.stream!() |> CSV.decode(strip_fields: true) for {:ok, [name, username, group_name]} <- csv_stream do - with {:ok, group = %Group{}} <- Course.get_or_create_group(group_name), + with {:ok, group = %Group{}} <- Courses.get_or_create_group(group_name), {:ok, %User{}} <- Accounts.insert_or_update_user(%{ username: username, From 9cabbafc15e7d8c95bdc8f69c74863e818bb4054 Mon Sep 17 00:00:00 2001 From: Chow En Rong <53928333+chownces@users.noreply.github.com> Date: Sat, 5 Jun 2021 14:18:29 +0800 Subject: [PATCH 015/174] Added context functions and tests for assessment types --- lib/cadet/courses/assessment_config.ex | 1 - lib/cadet/courses/assessment_types.ex | 11 +- lib/cadet/courses/courses.ex | 64 +++++- ...0210531155751_add_course_configuration.exs | 2 + test/cadet/courses/assessment_types_test.exs | 28 +++ test/cadet/courses/courses_test.exs | 196 +++++++++++++++++- .../courses/assessment_types_factory.ex | 19 ++ test/factories/factory.ex | 8 +- 8 files changed, 317 insertions(+), 12 deletions(-) create mode 100644 test/cadet/courses/assessment_types_test.exs create mode 100644 test/factories/courses/assessment_types_factory.ex diff --git a/lib/cadet/courses/assessment_config.ex b/lib/cadet/courses/assessment_config.ex index ef4436c88..223d9fc80 100644 --- a/lib/cadet/courses/assessment_config.ex +++ b/lib/cadet/courses/assessment_config.ex @@ -23,7 +23,6 @@ defmodule Cadet.Courses.AssessmentConfig do assessment_config |> cast(params, @required_fields) |> validate_required(@required_fields) - |> foreign_key_constraint(:course_id) |> validate_number(:early_submission_xp, greater_than_or_equal_to: 0) |> validate_number(:hours_before_early_xp_decay, greater_than_or_equal_to: 0) |> validate_number(:decay_rate_points_per_hour, greater_than_or_equal_to: 0) diff --git a/lib/cadet/courses/assessment_types.ex b/lib/cadet/courses/assessment_types.ex index 42dd43d09..95b1350d5 100644 --- a/lib/cadet/courses/assessment_types.ex +++ b/lib/cadet/courses/assessment_types.ex @@ -15,11 +15,20 @@ defmodule Cadet.Courses.AssessmentTypes do timestamps() end - @required_fields ~w(order type course)a + @required_fields ~w(order type course_id)a def changeset(assessment_type, params) do + params = capitalize(params, :type) + assessment_type |> cast(params, @required_fields) |> validate_required(@required_fields) + |> validate_number(:order, greater_than: 0) + |> validate_number(:order, less_than_or_equal_to: 5) + |> unique_constraint([:type, :course_id]) + end + + defp capitalize(params, field) do + Map.update(params, field, nil, &String.capitalize/1) end end diff --git a/lib/cadet/courses/courses.ex b/lib/cadet/courses/courses.ex index 78ade2147..115ff32c6 100644 --- a/lib/cadet/courses/courses.ex +++ b/lib/cadet/courses/courses.ex @@ -8,7 +8,15 @@ defmodule Cadet.Courses do import Ecto.Query alias Cadet.Accounts.User - alias Cadet.Courses.{AssessmentConfig, Course, Group, Sourcecast, SourcecastUpload} + + alias Cadet.Courses.{ + AssessmentConfig, + AssessmentTypes, + Course, + Group, + Sourcecast, + SourcecastUpload + } @doc """ Returns the course configuration for the specified course. @@ -21,7 +29,14 @@ defmodule Cadet.Courses do {:error, {:bad_request, "Invalid course id"}} course -> - {:ok, course} + assessment_types = + AssessmentTypes + |> where(course_id: ^course_id) + |> Repo.all() + |> Enum.sort(&(&1.order < &2.order)) + |> Enum.map(& &1.type) + + {:ok, Map.put_new(course, :assessment_types, assessment_types)} end end @@ -65,6 +80,51 @@ defmodule Cadet.Courses do |> Repo.update() end + @doc """ + Updates the Assessment Types for the specified course + """ + @spec update_assessment_types(integer, list()) :: :ok | {:error, {:bad_request, String.t()}} + def update_assessment_types(course_id, params) do + if not is_list(params) do + {:error, {:bad_request, "Invalid parameter(s)"}} + else + params_length = params |> length() + + with true <- params_length <= 5, + true <- params_length >= 1, + true <- params |> Enum.reduce(true, fn elem, acc -> acc and is_binary(elem) end), + true <- + params |> Enum.map(fn elem -> String.capitalize(elem) end) |> Enum.uniq() |> length() === + params_length do + (params ++ List.duplicate(nil, 5 - params_length)) + |> Enum.with_index(1) + |> Enum.each(fn {elem, idx} -> + case elem do + nil -> + AssessmentTypes + |> where(course_id: ^course_id) + |> where(order: ^idx) + |> Repo.delete_all() + + _ -> + AssessmentTypes.changeset(%AssessmentTypes{}, %{ + course_id: course_id, + order: idx, + type: elem + }) + |> Repo.insert( + on_conflict: {:replace, [:type]}, + conflict_target: [:course_id, :order] + ) + end + end) + else + false -> + {:error, {:bad_request, "Invalid parameter(s)"}} + end + end + end + @doc """ Get a group based on the group name or create one if it doesn't exist """ diff --git a/priv/repo/migrations/20210531155751_add_course_configuration.exs b/priv/repo/migrations/20210531155751_add_course_configuration.exs index 04f24655f..0c67986ef 100644 --- a/priv/repo/migrations/20210531155751_add_course_configuration.exs +++ b/priv/repo/migrations/20210531155751_add_course_configuration.exs @@ -30,6 +30,8 @@ defmodule Cadet.Repo.Migrations.AddCourseConfiguration do timestamps() end + create(unique_index(:assessment_types, [:course_id, :order])) + drop_if_exists(table(:sublanguages)) end end diff --git a/test/cadet/courses/assessment_types_test.exs b/test/cadet/courses/assessment_types_test.exs new file mode 100644 index 000000000..e7ea7b4f4 --- /dev/null +++ b/test/cadet/courses/assessment_types_test.exs @@ -0,0 +1,28 @@ +defmodule Cadet.Courses.AssessmentTypesTest do + alias Cadet.Courses.AssessmentTypes + + use Cadet.ChangesetCase, entity: AssessmentTypes + + describe "Assessment Types Changesets" do + test "valid changesets" do + assert_changeset(%{order: 1, type: "Missions", course_id: 1}, :valid) + assert_changeset(%{order: 2, type: "quests", course_id: 1}, :valid) + assert_changeset(%{order: 3, type: "Paths", course_id: 1}, :valid) + assert_changeset(%{order: 4, type: "contests", course_id: 1}, :valid) + assert_changeset(%{order: 5, type: "Others", course_id: 1}, :valid) + end + + test "invalid changeset missing required params" do + assert_changeset(%{order: 1}, :invalid) + assert_changeset(%{type: "Missions"}, :invalid) + assert_changeset(%{course_id: 1}, :invalid) + assert_changeset(%{order: 1, type: "Missions"}, :invalid) + assert_changeset(%{order: 1, course_id: 1}, :invalid) + end + + test "invalid changeset with invalid order" do + assert_changeset(%{order: 0, type: "Missions", course_id: 1}, :invalid) + assert_changeset(%{order: 6, type: "Missions", course_id: 1}, :invalid) + end + end +end diff --git a/test/cadet/courses/courses_test.exs b/test/cadet/courses/courses_test.exs index 1f31f4b10..815c49fd8 100644 --- a/test/cadet/courses/courses_test.exs +++ b/test/cadet/courses/courses_test.exs @@ -6,6 +6,9 @@ defmodule Cadet.CoursesTest do describe "get course config" do test "succeeds" do course = insert(:course) + insert(:assessment_types, %{order: 1, type: "Missions", course_id: course.id}) + insert(:assessment_types, %{order: 2, type: "Quests", course_id: course.id}) + {:ok, course} = Courses.get_course_config(course.id) assert course.name == "Programming Methodology" assert course.module_code == "CS1101S" @@ -16,13 +19,12 @@ defmodule Cadet.CoursesTest do assert course.source_chapter == 1 assert course.source_variant == "default" assert course.module_help_text == "Help Text" + assert course.assessment_types == ["Missions", "Quests"] end test "returns with error for invalid course id" do course = insert(:course) - assert {:error, {status, message}} = Courses.get_course_config(course.id + 1) - assert status = :bad_request - assert message = "Invalid course id" + assert {:error, {:bad_request, "Invalid course id"}} = Courses.get_course_config(course.id + 1) end end @@ -84,14 +86,11 @@ defmodule Cadet.CoursesTest do course = insert(:course) new_chapter = Enum.random(1..4) - assert {:error, {status, message}} = + assert {:error, {:bad_request, "Invalid course id"}} = Courses.update_course_config(course.id + 1, %{ source_chapter: new_chapter, source_variant: "default" }) - - assert status = :bad_request - assert message = "Invalid course id" end test "returns with error for failed updates" do @@ -152,6 +151,189 @@ defmodule Cadet.CoursesTest do end end + describe "update assessment types" do + test "succeeds" do + course = insert(:course) + course_id = course.id + + insert(:assessment_types, %{order: 1, type: "Missions", course_id: course_id}) + insert(:assessment_types, %{order: 2, type: "Quests", course_id: course_id}) + insert(:assessment_types, %{order: 3, type: "Paths", course_id: course_id}) + insert(:assessment_types, %{order: 4, type: "Contests", course_id: course_id}) + insert(:assessment_types, %{order: 5, type: "Others", course_id: course_id}) + + :ok = + Courses.update_assessment_types(course_id, [ + "Paths", + "Quests", + "Missions", + "Others", + "Contests" + ]) + + {:ok, updated_course_config} = Courses.get_course_config(course_id) + + assert updated_course_config.assessment_types == [ + "Paths", + "Quests", + "Missions", + "Others", + "Contests" + ] + end + + test "succeeds when database entries are not in order" do + course = insert(:course) + course_id = course.id + + insert(:assessment_types, %{order: 4, type: "Contests", course_id: course_id}) + insert(:assessment_types, %{order: 1, type: "Missions", course_id: course_id}) + insert(:assessment_types, %{order: 3, type: "Paths", course_id: course_id}) + insert(:assessment_types, %{order: 5, type: "Others", course_id: course_id}) + insert(:assessment_types, %{order: 2, type: "Quests", course_id: course_id}) + + :ok = + Courses.update_assessment_types(course_id, [ + "Paths", + "Quests", + "Missions", + "Others", + "Contests" + ]) + + {:ok, updated_course_config} = Courses.get_course_config(course_id) + + assert updated_course_config.assessment_types == [ + "Paths", + "Quests", + "Missions", + "Others", + "Contests" + ] + end + + test "succeeds and capitalizes the types during database insertion" do + course = insert(:course) + course_id = course.id + + insert(:assessment_types, %{order: 1, type: "Missions", course_id: course_id}) + insert(:assessment_types, %{order: 2, type: "Quests", course_id: course_id}) + insert(:assessment_types, %{order: 3, type: "Paths", course_id: course_id}) + insert(:assessment_types, %{order: 4, type: "Contests", course_id: course_id}) + insert(:assessment_types, %{order: 5, type: "Others", course_id: course_id}) + + :ok = + Courses.update_assessment_types(course_id, [ + "Paths", + "quests", + "Missions", + "Others", + "contests" + ]) + + {:ok, updated_course_config} = Courses.get_course_config(course_id) + + assert updated_course_config.assessment_types == [ + "Paths", + "Quests", + "Missions", + "Others", + "Contests" + ] + end + + test "succeeds when inserting more types than existing database entries" do + course = insert(:course) + course_id = course.id + + insert(:assessment_types, %{order: 1, type: "Missions", course_id: course_id}) + insert(:assessment_types, %{order: 2, type: "Quests", course_id: course_id}) + insert(:assessment_types, %{order: 3, type: "Paths", course_id: course_id}) + + :ok = + Courses.update_assessment_types(course_id, [ + "Paths", + "Quests", + "Missions", + "Others", + "Contests" + ]) + + {:ok, updated_course_config} = Courses.get_course_config(course_id) + + assert updated_course_config.assessment_types == [ + "Paths", + "Quests", + "Missions", + "Others", + "Contests" + ] + end + + test "succeeds when inserting less types than existing database entries" do + course = insert(:course) + course_id = course.id + + insert(:assessment_types, %{order: 1, type: "Missions", course_id: course_id}) + insert(:assessment_types, %{order: 2, type: "Quests", course_id: course_id}) + insert(:assessment_types, %{order: 3, type: "Paths", course_id: course_id}) + insert(:assessment_types, %{order: 4, type: "Contests", course_id: course_id}) + + :ok = Courses.update_assessment_types(course_id, ["Paths", "Quests", "Missions"]) + {:ok, updated_course_config} = Courses.get_course_config(course_id) + + assert updated_course_config.assessment_types == ["Paths", "Quests", "Missions"] + end + + test "returns with error for invalid parameters" do + course = insert(:course) + course_id = course.id + + insert(:assessment_types, %{order: 1, type: "Missions", course_id: course_id}) + insert(:assessment_types, %{order: 2, type: "Quests", course_id: course_id}) + insert(:assessment_types, %{order: 3, type: "Paths", course_id: course_id}) + + assert {:error, {:bad_request, "Invalid parameter(s)"}} = + Courses.update_assessment_types(course_id, [1, "Quests", "Missions"]) + end + + test "returns with error for duplicate parameters" do + course = insert(:course) + course_id = course.id + + insert(:assessment_types, %{order: 1, type: "Missions", course_id: course_id}) + insert(:assessment_types, %{order: 2, type: "Quests", course_id: course_id}) + insert(:assessment_types, %{order: 3, type: "Paths", course_id: course_id}) + + assert {:error, {:bad_request, "Invalid parameter(s)"}} = + Courses.update_assessment_types(course_id, ["Missions", "Quests", "Missions"]) + end + + test "returns with error for empty list parameter" do + course = insert(:course) + course_id = course.id + + insert(:assessment_types, %{order: 1, type: "Missions", course_id: course_id}) + insert(:assessment_types, %{order: 2, type: "Quests", course_id: course_id}) + insert(:assessment_types, %{order: 3, type: "Paths", course_id: course_id}) + + assert {:error, {:bad_request, "Invalid parameter(s)"}} = + Courses.update_assessment_types(course_id, []) + end + + test "returns with error for non-list parameter" do + course = insert(:course) + course_id = course.id + + insert(:assessment_types, %{order: 1, type: "Missions", course_id: course_id}) + insert(:assessment_types, %{order: 2, type: "Quests", course_id: course_id}) + insert(:assessment_types, %{order: 3, type: "Paths", course_id: course_id}) + + assert {:error, {:bad_request, "Invalid parameter(s)"}} = + Courses.update_assessment_types(course_id, "Missions") + end + end + # describe "Sourcecast" do # setup do # on_exit(fn -> File.rm_rf!("uploads/test/sourcecasts") end) diff --git a/test/factories/courses/assessment_types_factory.ex b/test/factories/courses/assessment_types_factory.ex new file mode 100644 index 000000000..ac15917dc --- /dev/null +++ b/test/factories/courses/assessment_types_factory.ex @@ -0,0 +1,19 @@ +defmodule Cadet.Courses.AssessmentTypesFactory do + @moduledoc """ + Factory for the AssessmentTypes entity + """ + + defmacro __using__(_opts) do + quote do + alias Cadet.Courses.AssessmentTypes + + def assessment_types_factory do + %AssessmentTypes{ + order: 1, + type: "Missions" + # course: build(:course) + } + end + end + end +end diff --git a/test/factories/factory.ex b/test/factories/factory.ex index 3e74ee40b..10a231f19 100644 --- a/test/factories/factory.ex +++ b/test/factories/factory.ex @@ -22,7 +22,13 @@ defmodule Cadet.Factory do GoalFactory } - use Cadet.Courses.{AssessmentConfigFactory, CourseFactory, GroupFactory, SourcecastFactory} + use Cadet.Courses.{ + AssessmentConfigFactory, + AssessmentTypesFactory, + CourseFactory, + GroupFactory, + SourcecastFactory + } use Cadet.Devices.DeviceFactory From 3609fa7ccb5921fdc20c227cf688351304384e0e Mon Sep 17 00:00:00 2001 From: YaleChen299 Date: Sat, 5 Jun 2021 14:32:13 +0800 Subject: [PATCH 016/174] adding course_registration test set --- lib/cadet/accounts/course_registration.ex | 4 +-- .../accounts/course_registration_test.exs | 21 +++++++++++ .../accounts/course_registration_factory.ex | 35 +++++++++++++++++++ test/factories/accounts/user_factory.ex | 11 +++--- 4 files changed, 64 insertions(+), 7 deletions(-) create mode 100644 test/cadet/accounts/course_registration_test.exs create mode 100644 test/factories/accounts/course_registration_factory.ex diff --git a/lib/cadet/accounts/course_registration.ex b/lib/cadet/accounts/course_registration.ex index 67b6f2c57..9a072318c 100644 --- a/lib/cadet/accounts/course_registration.ex +++ b/lib/cadet/accounts/course_registration.ex @@ -17,8 +17,8 @@ defmodule Cadet.Accounts.CourseRegistration do timestamps() end - @required_fields ~w(user_id course_id)a - @optional_fields ~w(role game_states group_id)a + @required_fields ~w(user_id course_id role)a + @optional_fields ~w(game_states group_id)a def changeset(course_registration, params \\ %{}) do course_registration diff --git a/test/cadet/accounts/course_registration_test.exs b/test/cadet/accounts/course_registration_test.exs new file mode 100644 index 000000000..546c531b1 --- /dev/null +++ b/test/cadet/accounts/course_registration_test.exs @@ -0,0 +1,21 @@ +defmodule Cadet.Accounts.CourseRegistrationTest do + alias Cadet.Accounts.CourseRegistration + + use Cadet.ChangesetCase, entity: CourseRegistration + + setup do + + end + + describe "Changesets" do + test "valid changeset" do + assert_changeset(%{user_id: , course_id: , role: :admin}, :valid) + assert_changeset(%{user_id: , course_id: , role: :student}, :valid) + end + + test "invalid changeset" do + assert_changeset(%{name: "people"}, :invalid) + assert_changeset(%{role: :avenger}, :invalid) + end + end +end diff --git a/test/factories/accounts/course_registration_factory.ex b/test/factories/accounts/course_registration_factory.ex new file mode 100644 index 000000000..0e42138e6 --- /dev/null +++ b/test/factories/accounts/course_registration_factory.ex @@ -0,0 +1,35 @@ +defmodule Cadet.Accounts.CouseRegistraionFactory do + @moduledoc """ + Factory(ies) for Cadet.Accounts.CourseRegistration entity + """ + + defmacro __using__(_opts) do + quote do + alias Cadet.Accounts.{Role, User, CourseRegistration} + alias Cadet.Courses.{Course, Group} + + def course_registration_factory do + %CourseRegstration{ + user: build(:user) + course: build(:course) + group: build(:group) + role: role: Enum.random(Role.__enum_map__()), + game_status: %{} + } + end + + # def student_factory do + # %User{ + # name: Faker.Person.En.name(), + # role: :student, + # username: + # sequence( + # :nusnet_id, + # &"E#{&1 |> Integer.to_string() |> String.pad_leading(7, "0")}" + # ), + # game_states: %{} + # } + # end + end + end +end diff --git a/test/factories/accounts/user_factory.ex b/test/factories/accounts/user_factory.ex index ebc2950ff..15e0c97a6 100644 --- a/test/factories/accounts/user_factory.ex +++ b/test/factories/accounts/user_factory.ex @@ -5,31 +5,32 @@ defmodule Cadet.Accounts.UserFactory do defmacro __using__(_opts) do quote do - alias Cadet.Accounts.{Role, User} + # alias Cadet.Accounts.{Role, User} + alias Cadet.Accounts.User def user_factory do %User{ name: Faker.Person.En.name(), - role: Enum.random(Role.__enum_map__()), + # role: Enum.random(Role.__enum_map__()), username: sequence( :nusnet_id, &"E#{&1 |> Integer.to_string() |> String.pad_leading(7, "0")}" ), - game_states: %{} + # game_states: %{} } end def student_factory do %User{ name: Faker.Person.En.name(), - role: :student, + # role: :student, username: sequence( :nusnet_id, &"E#{&1 |> Integer.to_string() |> String.pad_leading(7, "0")}" ), - game_states: %{} + # game_states: %{} } end end From d0d733e6dde34486bacc438b004e03d4ca87ee03 Mon Sep 17 00:00:00 2001 From: Chow En Rong <53928333+chownces@users.noreply.github.com> Date: Sat, 5 Jun 2021 14:40:22 +0800 Subject: [PATCH 017/174] Updated courses context tests --- test/cadet/courses/courses_test.exs | 23 ++++++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/test/cadet/courses/courses_test.exs b/test/cadet/courses/courses_test.exs index 815c49fd8..a8299c124 100644 --- a/test/cadet/courses/courses_test.exs +++ b/test/cadet/courses/courses_test.exs @@ -24,7 +24,9 @@ defmodule Cadet.CoursesTest do test "returns with error for invalid course id" do course = insert(:course) - assert {:error, {:bad_request, "Invalid course id"}} = Courses.get_course_config(course.id + 1) + + assert {:error, {:bad_request, "Invalid course id"}} = + Courses.get_course_config(course.id + 1) end end @@ -321,6 +323,25 @@ defmodule Cadet.CoursesTest do Courses.update_assessment_types(course_id, []) end + test "returns with error for list parameter of greater than length 5" do + course = insert(:course) + course_id = course.id + + insert(:assessment_types, %{order: 1, type: "Missions", course_id: course_id}) + insert(:assessment_types, %{order: 2, type: "Quests", course_id: course_id}) + insert(:assessment_types, %{order: 3, type: "Paths", course_id: course_id}) + + assert {:error, {:bad_request, "Invalid parameter(s)"}} = + Courses.update_assessment_types(course_id, [ + "Missions", + "Quests", + "Paths", + "Contests", + "Others", + "Assessments" + ]) + end + test "returns with error for non-list parameter" do course = insert(:course) course_id = course.id From 5b7ae637f4be491a86b428af0a84e0717571745f Mon Sep 17 00:00:00 2001 From: Chow En Rong <53928333+chownces@users.noreply.github.com> Date: Sat, 5 Jun 2021 15:24:41 +0800 Subject: [PATCH 018/174] Added update assessment types routing and routing tests --- .../admin_courses_controller.ex | 39 ++++++++++++ lib/cadet_web/router.ex | 1 + .../admin_courses_controller_test.exs | 63 +++++++++++++++++++ 3 files changed, 103 insertions(+) diff --git a/lib/cadet_web/admin_controllers/admin_courses_controller.ex b/lib/cadet_web/admin_controllers/admin_courses_controller.ex index 7ab9282aa..4ee558f3b 100644 --- a/lib/cadet_web/admin_controllers/admin_courses_controller.ex +++ b/lib/cadet_web/admin_controllers/admin_courses_controller.ex @@ -51,6 +51,26 @@ defmodule CadetWeb.AdminCoursesController do send_resp(conn, :bad_request, "Missing parameter(s)") end + def update_assessment_types(conn, %{ + "courseid" => course_id, + "assessment_types" => assessment_types + }) + when is_ecto_id(course_id) do + case Courses.update_assessment_types(course_id, assessment_types) do + :ok -> + text(conn, "OK") + + {:error, {status, message}} -> + conn + |> put_status(status) + |> text(message) + end + end + + def update_assessment_types(conn, _) do + send_resp(conn, :bad_request, "Missing parameter(s)") + end + swagger_path :update_course_config do put("/admin/courses/{courseId}/course_config") @@ -98,6 +118,25 @@ defmodule CadetWeb.AdminCoursesController do response(403, "Forbidden") end + swagger_path :update_assessment_types do + put("/admin/courses/{courseId}/assessment_types") + + summary("Updates the assessment types for the specified course") + + security([%{JWT: []}]) + + consumes("application/json") + + parameters do + courseId(:path, :integer, "Course ID", required: true) + assessment_types(:body, :list, "Assessment Types") + end + + response(200, "OK") + response(400, "Missing or invalid parameter(s)") + response(403, "Forbidden") + end + def swagger_definitions do %{ AdminSublanguage: diff --git a/lib/cadet_web/router.ex b/lib/cadet_web/router.ex index 5bdb7b5c0..eacc5071c 100644 --- a/lib/cadet_web/router.ex +++ b/lib/cadet_web/router.ex @@ -126,6 +126,7 @@ defmodule CadetWeb.Router do put("/courses/:courseid/course_config", AdminCoursesController, :update_course_config) put("/courses/:courseid/assessment_config", AdminCoursesController, :update_assessment_config) + put("/courses/:courseid/assessment_types", AdminCoursesController, :update_assessment_types) end # Other scopes may use custom stacks. diff --git a/test/cadet_web/admin_controllers/admin_courses_controller_test.exs b/test/cadet_web/admin_controllers/admin_courses_controller_test.exs index 93f378986..20ac968f2 100644 --- a/test/cadet_web/admin_controllers/admin_courses_controller_test.exs +++ b/test/cadet_web/admin_controllers/admin_courses_controller_test.exs @@ -7,6 +7,7 @@ defmodule CadetWeb.AdminCoursesControllerTest do AdminCoursesController.swagger_definitions() AdminCoursesController.swagger_path_update_course_config(nil) AdminCoursesController.swagger_path_update_assessment_config(nil) + AdminCoursesController.swagger_path_update_assessment_types(nil) end describe "PUT /courses/{courseId}/course_config" do @@ -174,8 +175,70 @@ defmodule CadetWeb.AdminCoursesControllerTest do end end + describe "PUT /courses/{courseId}/assessment_types" do + @tag authenticate: :admin + test "succeeds", %{conn: conn} do + course = insert(:course) + + conn = + put(conn, build_url_assessment_types(course.id), %{ + "assessment_types" => ["Missions", "Quests", "Contests"] + }) + + assert response(conn, 200) == "OK" + end + + @tag authenticate: :student + test "rejects forbidden request for non-staff users", %{conn: conn} do + course = insert(:course) + + conn = + put(conn, build_url_assessment_types(course.id), %{ + "assessment_types" => ["Missions", "Quests", "Contests"] + }) + + assert response(conn, 403) == "Forbidden" + end + + @tag authenticate: :staff + test "rejects requests with invalid params 1", %{conn: conn} do + course = insert(:course) + + conn = + put(conn, build_url_assessment_types(course.id), %{ + "assessment_types" => "Missions" + }) + + assert response(conn, 400) == "Invalid parameter(s)" + end + + @tag authenticate: :staff + test "rejects requests with invalid params 2", %{conn: conn} do + course = insert(:course) + + conn = + put(conn, build_url_assessment_types(course.id), %{ + "assessment_types" => [1, "Missions", "Quests"] + }) + + assert response(conn, 400) == "Invalid parameter(s)" + end + + @tag authenticate: :staff + test "rejects requests with missing params", %{conn: conn} do + course = insert(:course) + + conn = put(conn, build_url_assessment_types(course.id), %{}) + + assert response(conn, 400) == "Missing parameter(s)" + end + end + defp build_url_course_config(course_id), do: "/v2/admin/courses/#{course_id}/course_config" defp build_url_assessment_config(course_id), do: "/v2/admin/courses/#{course_id}/assessment_config" + + defp build_url_assessment_types(course_id), + do: "/v2/admin/courses/#{course_id}/assessment_types" end From 8bdb5e3e22c41640aa470e4ce907d368dd42c723 Mon Sep 17 00:00:00 2001 From: YaleChen299 Date: Sat, 5 Jun 2021 15:28:03 +0800 Subject: [PATCH 019/174] updated course_registration test --- lib/cadet/accounts/course_registrations.ex | 14 ++++++++++++++ test/cadet/accounts/course_registration_test.exs | 1 + .../accounts/course_registration_factory.ex | 13 ------------- 3 files changed, 15 insertions(+), 13 deletions(-) diff --git a/lib/cadet/accounts/course_registrations.ex b/lib/cadet/accounts/course_registrations.ex index 8b3872c36..6bb69c29a 100644 --- a/lib/cadet/accounts/course_registrations.ex +++ b/lib/cadet/accounts/course_registrations.ex @@ -58,5 +58,19 @@ defmodule Cadet.Accounts.CourseRegistrations do |> Repo.insert_or_update() end + # :TODO error handling + def delete_record(params = %{user_id: user_id, course_id: course_id}) + when is_ecto_id(user_id) && is_ecto_id(course_id) do + record = CourseRegistration + |> where(user_id: ^user_id) + |> where(course_id: ^course_id) + |> Repo.one() + + case Repo.delete(record) do + {:ok, struct} -> # Deleted with success + {:error, changeset} -> # Something went wrong + end + end + end diff --git a/test/cadet/accounts/course_registration_test.exs b/test/cadet/accounts/course_registration_test.exs index 546c531b1..7aedc2fd8 100644 --- a/test/cadet/accounts/course_registration_test.exs +++ b/test/cadet/accounts/course_registration_test.exs @@ -7,6 +7,7 @@ defmodule Cadet.Accounts.CourseRegistrationTest do end + # :TODO add context function test describe "Changesets" do test "valid changeset" do assert_changeset(%{user_id: , course_id: , role: :admin}, :valid) diff --git a/test/factories/accounts/course_registration_factory.ex b/test/factories/accounts/course_registration_factory.ex index 0e42138e6..3e9176fc2 100644 --- a/test/factories/accounts/course_registration_factory.ex +++ b/test/factories/accounts/course_registration_factory.ex @@ -17,19 +17,6 @@ defmodule Cadet.Accounts.CouseRegistraionFactory do game_status: %{} } end - - # def student_factory do - # %User{ - # name: Faker.Person.En.name(), - # role: :student, - # username: - # sequence( - # :nusnet_id, - # &"E#{&1 |> Integer.to_string() |> String.pad_leading(7, "0")}" - # ), - # game_states: %{} - # } - # end end end end From 2249d6ae310374c33bd673a46be11fdcc013b7b1 Mon Sep 17 00:00:00 2001 From: Chow En Rong <53928333+chownces@users.noreply.github.com> Date: Sat, 5 Jun 2021 15:36:23 +0800 Subject: [PATCH 020/174] Updated get course config tests to include assessment types for the specified course --- lib/cadet_web/controllers/courses_controller.ex | 4 +++- lib/cadet_web/views/courses_view.ex | 3 ++- .../cadet_web/controllers/courses_controller_test.exs | 11 ++++++++++- 3 files changed, 15 insertions(+), 3 deletions(-) diff --git a/lib/cadet_web/controllers/courses_controller.ex b/lib/cadet_web/controllers/courses_controller.ex index bf646e598..24f33495a 100644 --- a/lib/cadet_web/controllers/courses_controller.ex +++ b/lib/cadet_web/controllers/courses_controller.ex @@ -45,6 +45,7 @@ defmodule CadetWeb.CoursesController do source_chapter(:integer, "Source Chapter number from 1 to 4", required: true) source_variant(Schema.ref(:SourceVariant), "Source Variant name", required: true) module_help_text(:string, "Module help text", required: true) + assessment_types(:list, "Assessment Types", required: true) end example(%{ @@ -56,7 +57,8 @@ defmodule CadetWeb.CoursesController do enable_sourcecast: true, source_chapter: 1, source_variant: "default", - module_help_text: "Help text" + module_help_text: "Help text", + assessment_types: ["Missions", "Quests", "Paths", "Contests", "Others"] }) end, SourceVariant: diff --git a/lib/cadet_web/views/courses_view.ex b/lib/cadet_web/views/courses_view.ex index 161de5ee8..09cfb9a17 100644 --- a/lib/cadet_web/views/courses_view.ex +++ b/lib/cadet_web/views/courses_view.ex @@ -13,7 +13,8 @@ defmodule CadetWeb.CoursesView do :enable_sourcecast, :source_chapter, :source_variant, - :module_help_text + :module_help_text, + :assessment_types ]) } end diff --git a/test/cadet_web/controllers/courses_controller_test.exs b/test/cadet_web/controllers/courses_controller_test.exs index 88b2c2640..a4f9e29c2 100644 --- a/test/cadet_web/controllers/courses_controller_test.exs +++ b/test/cadet_web/controllers/courses_controller_test.exs @@ -3,6 +3,10 @@ defmodule CadetWeb.CoursesControllerTest do alias CadetWeb.CoursesController + # setup do + # Cadet.Test.Seeds. + # end + test "swagger" do CoursesController.swagger_definitions() CoursesController.swagger_path_get_course_config(nil) @@ -20,6 +24,10 @@ defmodule CadetWeb.CoursesControllerTest do @tag authenticate: :student test "succeeds", %{conn: conn} do course = insert(:course) + insert(:assessment_types, %{order: 1, type: "Missions", course_id: course.id}) + insert(:assessment_types, %{order: 2, type: "Quests", course_id: course.id}) + insert(:assessment_types, %{order: 3, type: "Paths", course_id: course.id}) + resp = conn |> get(build_url_config(course.id)) |> json_response(200) assert %{ @@ -32,7 +40,8 @@ defmodule CadetWeb.CoursesControllerTest do "enable_sourcecast" => true, "source_chapter" => 1, "source_variant" => "default", - "module_help_text" => "Help Text" + "module_help_text" => "Help Text", + "assessment_types" => ["Missions", "Quests", "Paths"] } } = resp end From e2935d99066e579bfe75eab96520b2c42d763c4b Mon Sep 17 00:00:00 2001 From: YaleChen299 Date: Sat, 5 Jun 2021 16:32:47 +0800 Subject: [PATCH 021/174] added pipeline for course_registration to assign course_reg in conn --- lib/cadet/accounts/course_registrations.ex | 35 ++++++++++----- lib/cadet_web/router.ex | 41 ++++++++++++++--- .../accounts/course_registration_test.exs | 43 +++++++++--------- test/cadet/courses/courses_test.exs | 4 +- .../accounts/course_registration_factory.ex | 44 +++++++++---------- test/factories/accounts/user_factory.ex | 4 +- test/factories/factory.ex | 2 +- 7 files changed, 108 insertions(+), 65 deletions(-) diff --git a/lib/cadet/accounts/course_registrations.ex b/lib/cadet/accounts/course_registrations.ex index 6bb69c29a..4472e9d87 100644 --- a/lib/cadet/accounts/course_registrations.ex +++ b/lib/cadet/accounts/course_registrations.ex @@ -15,6 +15,22 @@ defmodule Cadet.Accounts.CourseRegistrations do # only join with Course if need course info/config # otherwise just use CourseRegistration + def get_user_record(user_id, course_id) when is_ecto_id(user_id) && is_ecto_id(course_id) do + CourseRegistration + |> where([cr], cr.user_id == ^user_id) + |> where([cr], cr.course_id == ^course_id) + |> Repo.all() + |> case do + [cr = %CourseRegistration{}] -> {:ok, cr} + # when the user is not in the course + [] -> {:error, :no_record} + # when a user was added to a course twice + _ -> {:error, :backend_error} + end + + # |> Repo.get_by(%{user_id: ^user_id, course_id: ^course_id}) + end + def get_courses(%User{id: id}) do CourseRegistration |> where([cr], cr.user_id == ^id) @@ -61,16 +77,15 @@ defmodule Cadet.Accounts.CourseRegistrations do # :TODO error handling def delete_record(params = %{user_id: user_id, course_id: course_id}) when is_ecto_id(user_id) && is_ecto_id(course_id) do - record = CourseRegistration - |> where(user_id: ^user_id) - |> where(course_id: ^course_id) - |> Repo.one() + record = + CourseRegistration + |> where(user_id: ^user_id) + |> where(course_id: ^course_id) + |> Repo.one() - case Repo.delete(record) do - {:ok, struct} -> # Deleted with success - {:error, changeset} -> # Something went wrong - end + # case Repo.delete(record) do + # {:ok, struct} -> # Deleted with success + # {:error, changeset} -> # Something went wrong + # end end - - end diff --git a/lib/cadet_web/router.ex b/lib/cadet_web/router.ex index 5bdb7b5c0..0e88e3247 100644 --- a/lib/cadet_web/router.ex +++ b/lib/cadet_web/router.ex @@ -16,10 +16,20 @@ defmodule CadetWeb.Router do plug(Guardian.Plug.EnsureAuthenticated) end + pipeline :course do + plug(:assign_course) + end + pipeline :ensure_staff do plug(:ensure_role, [:staff, :admin]) end + # scope "/v2/course/:courseid", CadetWeb do + # pipe_through([:api, :auth, :ensure_auth, :course]) + + # # routes, more scopes, etc + # end + scope "/", CadetWeb do get("/.well-known/jwks.json", JWKSController, :index) end @@ -46,8 +56,8 @@ defmodule CadetWeb.Router do end # Authenticated Pages - scope "/v2", CadetWeb do - pipe_through([:api, :auth, :ensure_auth]) + scope "/v2/course/:courseid", CadetWeb do + pipe_through([:api, :auth, :ensure_auth, :course]) resources("/sourcecast", SourcecastController, only: [:create, :delete]) @@ -80,16 +90,16 @@ defmodule CadetWeb.Router do end # Authenticated Pages - scope "/v2/self", CadetWeb do - pipe_through([:api, :auth, :ensure_auth]) + scope "/v2/course/:courseid/self", CadetWeb do + pipe_through([:api, :auth, :ensure_auth, :course]) get("/goals", IncentivesController, :index_goals) post("/goals/:uuid/progress", IncentivesController, :update_progress) end # Admin pages - scope "/v2/admin", CadetWeb do - pipe_through([:api, :auth, :ensure_auth, :ensure_staff]) + scope "/v2/course/:courseid/admin", CadetWeb do + pipe_through([:api, :auth, :ensure_auth, :course, :ensure_staff]) get("/assets/:foldername", AdminAssetsController, :index) post("/assets/:foldername/*filename", AdminAssetsController, :upload) @@ -158,8 +168,25 @@ defmodule CadetWeb.Router do get("/", DefaultController, :index) end + defp assign_course(conn, opts) do + course_id = conn.path_params["courseid"] + + course_reg = + Cadet.Accounts.CourseRegistration.get_user_record(conn.assigns.current_user.id, course_id) + + case course_reg do + {:ok, cr} -> assign(conn, :course_reg, cr) + # user not in course + {:error, :no_record} -> send_resp(403, "Forbidden") |> halt() + # :TODO not sure what to put yet + {:error, :backend_error} -> send_resp(403, "Forbidden") |> halt() + end + + conn + end + defp ensure_role(conn, opts) do - if not is_nil(conn.assigns.current_user) and conn.assigns.current_user.role in opts do + if not is_nil(conn.assigns.current_user) and conn.assigns.course_reg.role in opts do conn else conn diff --git a/test/cadet/accounts/course_registration_test.exs b/test/cadet/accounts/course_registration_test.exs index 7aedc2fd8..b5a2d2d23 100644 --- a/test/cadet/accounts/course_registration_test.exs +++ b/test/cadet/accounts/course_registration_test.exs @@ -1,22 +1,21 @@ -defmodule Cadet.Accounts.CourseRegistrationTest do - alias Cadet.Accounts.CourseRegistration - - use Cadet.ChangesetCase, entity: CourseRegistration - - setup do - - end - - # :TODO add context function test - describe "Changesets" do - test "valid changeset" do - assert_changeset(%{user_id: , course_id: , role: :admin}, :valid) - assert_changeset(%{user_id: , course_id: , role: :student}, :valid) - end - - test "invalid changeset" do - assert_changeset(%{name: "people"}, :invalid) - assert_changeset(%{role: :avenger}, :invalid) - end - end -end +defmodule Cadet.Accounts.CourseRegistrationTest do + alias Cadet.Accounts.CourseRegistration + + use Cadet.ChangesetCase, entity: CourseRegistration + + setup do + end + + # :TODO add context function test + describe "Changesets" do + # test "valid changeset" do + # assert_changeset(%{user_id: , course_id: , role: :admin}, :valid) + # assert_changeset(%{user_id: , course_id: , role: :student}, :valid) + # end + + # test "invalid changeset" do + # assert_changeset(%{name: "people"}, :invalid) + # assert_changeset(%{role: :avenger}, :invalid) + # end + end +end diff --git a/test/cadet/courses/courses_test.exs b/test/cadet/courses/courses_test.exs index 815c49fd8..205dc9dca 100644 --- a/test/cadet/courses/courses_test.exs +++ b/test/cadet/courses/courses_test.exs @@ -24,7 +24,9 @@ defmodule Cadet.CoursesTest do test "returns with error for invalid course id" do course = insert(:course) - assert {:error, {:bad_request, "Invalid course id"}} = Courses.get_course_config(course.id + 1) + + assert {:error, {:bad_request, "Invalid course id"}} = + Courses.get_course_config(course.id + 1) end end diff --git a/test/factories/accounts/course_registration_factory.ex b/test/factories/accounts/course_registration_factory.ex index 3e9176fc2..d983a15d2 100644 --- a/test/factories/accounts/course_registration_factory.ex +++ b/test/factories/accounts/course_registration_factory.ex @@ -1,22 +1,22 @@ -defmodule Cadet.Accounts.CouseRegistraionFactory do - @moduledoc """ - Factory(ies) for Cadet.Accounts.CourseRegistration entity - """ - - defmacro __using__(_opts) do - quote do - alias Cadet.Accounts.{Role, User, CourseRegistration} - alias Cadet.Courses.{Course, Group} - - def course_registration_factory do - %CourseRegstration{ - user: build(:user) - course: build(:course) - group: build(:group) - role: role: Enum.random(Role.__enum_map__()), - game_status: %{} - } - end - end - end -end +defmodule Cadet.Accounts.CouseRegistraionFactory do + @moduledoc """ + Factory(ies) for Cadet.Accounts.CourseRegistration entity + """ + + defmacro __using__(_opts) do + quote do + alias Cadet.Accounts.{Role, User, CourseRegistration} + # alias Cadet.Courses.{Course, Group} + + def course_registration_factory do + %CourseRegstration{ + user: build(:user), + course: build(:course), + group: build(:group), + role: Enum.random(Role.__enum_map__()), + game_status: %{} + } + end + end + end +end diff --git a/test/factories/accounts/user_factory.ex b/test/factories/accounts/user_factory.ex index 15e0c97a6..aa5221236 100644 --- a/test/factories/accounts/user_factory.ex +++ b/test/factories/accounts/user_factory.ex @@ -16,7 +16,7 @@ defmodule Cadet.Accounts.UserFactory do sequence( :nusnet_id, &"E#{&1 |> Integer.to_string() |> String.pad_leading(7, "0")}" - ), + ) # game_states: %{} } end @@ -29,7 +29,7 @@ defmodule Cadet.Accounts.UserFactory do sequence( :nusnet_id, &"E#{&1 |> Integer.to_string() |> String.pad_leading(7, "0")}" - ), + ) # game_states: %{} } end diff --git a/test/factories/factory.ex b/test/factories/factory.ex index 10a231f19..8f54e14a1 100644 --- a/test/factories/factory.ex +++ b/test/factories/factory.ex @@ -4,7 +4,7 @@ defmodule Cadet.Factory do """ use ExMachina.Ecto, repo: Cadet.Repo - use Cadet.Accounts.{NotificationFactory, UserFactory} + use Cadet.Accounts.{NotificationFactory, UserFactory, CouseRegistraionFactory} use Cadet.Assessments.{ AnswerFactory, From 598c5131f2442d776551c8b4b2a508974b2b66d9 Mon Sep 17 00:00:00 2001 From: Chow En Rong <53928333+chownces@users.noreply.github.com> Date: Sat, 5 Jun 2021 18:51:31 +0800 Subject: [PATCH 022/174] Temporary edits to make tests compilable --- lib/cadet/accounts/course_registration.ex | 1 + lib/cadet/accounts/course_registrations.ex | 8 +- lib/cadet/accounts/query.ex | 6 +- lib/cadet/assessments/assessments.ex | 60 ++++++------ lib/cadet/courses/courses.ex | 54 +++++------ lib/cadet/stories/stories.ex | 94 +++++++++---------- lib/cadet_web/router.ex | 6 +- .../accounts/course_registration_factory.ex | 6 +- test/factories/factory.ex | 2 +- 9 files changed, 119 insertions(+), 118 deletions(-) diff --git a/lib/cadet/accounts/course_registration.ex b/lib/cadet/accounts/course_registration.ex index 9a072318c..7656d93c7 100644 --- a/lib/cadet/accounts/course_registration.ex +++ b/lib/cadet/accounts/course_registration.ex @@ -4,6 +4,7 @@ defmodule Cadet.Accounts.CourseRegistration do """ use Cadet, :model + alias Cadet.Accounts.Role alias Cadet.Course.{Course, Group} schema "course_registrations" do diff --git a/lib/cadet/accounts/course_registrations.ex b/lib/cadet/accounts/course_registrations.ex index 4472e9d87..1e386b7f5 100644 --- a/lib/cadet/accounts/course_registrations.ex +++ b/lib/cadet/accounts/course_registrations.ex @@ -15,7 +15,7 @@ defmodule Cadet.Accounts.CourseRegistrations do # only join with Course if need course info/config # otherwise just use CourseRegistration - def get_user_record(user_id, course_id) when is_ecto_id(user_id) && is_ecto_id(course_id) do + def get_user_record(user_id, course_id) when is_ecto_id(user_id) and is_ecto_id(course_id) do CourseRegistration |> where([cr], cr.user_id == ^user_id) |> where([cr], cr.course_id == ^course_id) @@ -52,14 +52,14 @@ defmodule Cadet.Accounts.CourseRegistrations do end def enroll_course(params = %{user_id: user_id, course_id: course_id, role: role}) - when is_ecto_id(user_id) && is_ecto_id(course_id) do + when is_ecto_id(user_id) and is_ecto_id(course_id) do params |> insert_or_update_course_registration() end @spec insert_or_update_course_registration(map()) :: {:ok, Ecto.Schema.t()} | {:error, Ecto.Changeset.t()} def insert_or_update_course_registration(params = %{user_id: user_id, course_id: course_id}) - when is_ecto_id(user_id) && is_ecto_id(course_id) do + when is_ecto_id(user_id) and is_ecto_id(course_id) do CourseRegistration |> where(user_id: ^user_id) |> where(course_id: ^course_id) @@ -76,7 +76,7 @@ defmodule Cadet.Accounts.CourseRegistrations do # :TODO error handling def delete_record(params = %{user_id: user_id, course_id: course_id}) - when is_ecto_id(user_id) && is_ecto_id(course_id) do + when is_ecto_id(user_id) and is_ecto_id(course_id) do record = CourseRegistration |> where(user_id: ^user_id) diff --git a/lib/cadet/accounts/query.ex b/lib/cadet/accounts/query.ex index 1fb19d1f9..eeb83890c 100644 --- a/lib/cadet/accounts/query.ex +++ b/lib/cadet/accounts/query.ex @@ -35,13 +35,13 @@ defmodule Cadet.Accounts.Query do def avenger_of?(avenger_id, course_id, student_id) do avengerInCourse = CourseRegistration - |> where([cr], cr.course_id = ^course_id) - |> where([cr], cr.user_id = ^avenger_id) + |> where([cr], cr.course_id == ^course_id) + |> where([cr], cr.user_id == ^avenger_id) students = students_of(avengerInCourse) students - |> Repo.get_by(user_id: ^student_id) + |> Repo.get_by(user_id: student_id) |> case do nil -> false _ -> true diff --git a/lib/cadet/assessments/assessments.ex b/lib/cadet/assessments/assessments.ex index 34da093cc..9055f0ec1 100644 --- a/lib/cadet/assessments/assessments.ex +++ b/lib/cadet/assessments/assessments.ex @@ -226,36 +226,36 @@ defmodule Cadet.Assessments do end end - def assessment_with_questions_and_answers( - assessment = %Assessment{id: id}, - user = %User{role: role} - ) do - if Timex.compare(Timex.now(), assessment.open_at) >= 0 or role in @open_all_assessment_roles 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), on: q.id == a.question_id) - |> join(:left, [_, a], g in assoc(a, :grader)) - |> select([q, a, g], {q, a, g}) - |> order_by(:display_order) - |> Repo.all() - |> Enum.map(fn - {q, nil, _} -> %{q | answer: %Answer{grader: nil}} - {q, a, g} -> %{q | answer: %Answer{a | grader: g}} - end) - |> load_contest_voting_entries(user.id) - - assessment = Map.put(assessment, :questions, questions) - {:ok, assessment} - else - {:error, {:unauthorized, "Assessment not open"}} - end - end + # def assessment_with_questions_and_answers( + # assessment = %Assessment{id: id}, + # user = %User{role: role} + # ) do + # if Timex.compare(Timex.now(), assessment.open_at) >= 0 or role in @open_all_assessment_roles 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), on: q.id == a.question_id) + # |> join(:left, [_, a], g in assoc(a, :grader)) + # |> select([q, a, g], {q, a, g}) + # |> order_by(:display_order) + # |> Repo.all() + # |> Enum.map(fn + # {q, nil, _} -> %{q | answer: %Answer{grader: nil}} + # {q, a, g} -> %{q | answer: %Answer{a | grader: g}} + # end) + # |> load_contest_voting_entries(user.id) + + # assessment = Map.put(assessment, :questions, questions) + # {:ok, assessment} + # else + # {:error, {:unauthorized, "Assessment not open"}} + # end + # end def assessment_with_questions_and_answers(id, user = %User{}) do assessment_with_questions_and_answers(id, user, nil) diff --git a/lib/cadet/courses/courses.ex b/lib/cadet/courses/courses.ex index 115ff32c6..eb235c398 100644 --- a/lib/cadet/courses/courses.ex +++ b/lib/cadet/courses/courses.ex @@ -218,35 +218,35 @@ defmodule Cadet.Courses do @doc """ Upload a sourcecast file """ - def upload_sourcecast_file(uploader = %User{role: role}, attrs = %{}) do - if role in @upload_file_roles do - changeset = - %Sourcecast{} - |> Sourcecast.changeset(attrs) - |> put_assoc(:uploader, uploader) - - case Repo.insert(changeset) do - {:ok, sourcecast} -> - {:ok, sourcecast} - - {:error, changeset} -> - {:error, {:bad_request, full_error_messages(changeset)}} - end - else - {:error, {:forbidden, "User is not permitted to upload"}} - end - end + # def upload_sourcecast_file(uploader = %User{role: role}, attrs = %{}) do + # if role in @upload_file_roles do + # changeset = + # %Sourcecast{} + # |> Sourcecast.changeset(attrs) + # |> put_assoc(:uploader, uploader) + + # case Repo.insert(changeset) do + # {:ok, sourcecast} -> + # {:ok, sourcecast} + + # {:error, changeset} -> + # {:error, {:bad_request, full_error_messages(changeset)}} + # end + # else + # {:error, {:forbidden, "User is not permitted to upload"}} + # end + # end @doc """ Delete a sourcecast file """ - def delete_sourcecast_file(_deleter = %User{role: role}, id) do - if role in @upload_file_roles do - sourcecast = Repo.get(Sourcecast, id) - SourcecastUpload.delete({sourcecast.audio, sourcecast}) - Repo.delete(sourcecast) - else - {:error, {:forbidden, "User is not permitted to delete"}} - end - end + # def delete_sourcecast_file(_deleter = %User{role: role}, id) do + # if role in @upload_file_roles do + # sourcecast = Repo.get(Sourcecast, id) + # SourcecastUpload.delete({sourcecast.audio, sourcecast}) + # Repo.delete(sourcecast) + # else + # {:error, {:forbidden, "User is not permitted to delete"}} + # end + # end end diff --git a/lib/cadet/stories/stories.ex b/lib/cadet/stories/stories.ex index b619c98cf..f664e3adf 100644 --- a/lib/cadet/stories/stories.ex +++ b/lib/cadet/stories/stories.ex @@ -11,51 +11,51 @@ defmodule Cadet.Stories.Stories do @manage_stories_role ~w(staff admin)a - def list_stories(_user = %User{role: role}) do - if role in @manage_stories_role do - Repo.all(Story) - else - Story - |> where(is_published: ^true) - |> where([s], s.open_at <= ^Timex.now()) - |> Repo.all() - end - end - - def create_story(attrs = %{}, _user = %User{role: role}) do - if role in @manage_stories_role do - %Story{} - |> Story.changeset(attrs) - |> Repo.insert() - else - {:error, {:forbidden, "User not allowed to manage stories"}} - end - end - - def update_story(attrs = %{}, id, _user = %User{role: role}) do - if role in @manage_stories_role do - case Repo.get(Story, id) do - nil -> - {:error, {:not_found, "Story not found"}} - - story -> - story - |> Story.changeset(attrs) - |> Repo.update() - end - else - {:error, {:forbidden, "User not allowed to manage stories"}} - end - end - - def delete_story(id, _user = %User{role: role}) do - if role in @manage_stories_role do - case Repo.get(Story, id) do - nil -> {:error, {:not_found, "Story not found"}} - story -> Repo.delete(story) - end - else - {:error, {:forbidden, "User not allowed to manage stories"}} - end - end + # def list_stories(_user = %User{role: role}) do + # if role in @manage_stories_role do + # Repo.all(Story) + # else + # Story + # |> where(is_published: ^true) + # |> where([s], s.open_at <= ^Timex.now()) + # |> Repo.all() + # end + # end + + # def create_story(attrs = %{}, _user = %User{role: role}) do + # if role in @manage_stories_role do + # %Story{} + # |> Story.changeset(attrs) + # |> Repo.insert() + # else + # {:error, {:forbidden, "User not allowed to manage stories"}} + # end + # end + + # def update_story(attrs = %{}, id, _user = %User{role: role}) do + # if role in @manage_stories_role do + # case Repo.get(Story, id) do + # nil -> + # {:error, {:not_found, "Story not found"}} + + # story -> + # story + # |> Story.changeset(attrs) + # |> Repo.update() + # end + # else + # {:error, {:forbidden, "User not allowed to manage stories"}} + # end + # end + + # def delete_story(id, _user = %User{role: role}) do + # if role in @manage_stories_role do + # case Repo.get(Story, id) do + # nil -> {:error, {:not_found, "Story not found"}} + # story -> Repo.delete(story) + # end + # else + # {:error, {:forbidden, "User not allowed to manage stories"}} + # end + # end end diff --git a/lib/cadet_web/router.ex b/lib/cadet_web/router.ex index 0e88e3247..79d825bac 100644 --- a/lib/cadet_web/router.ex +++ b/lib/cadet_web/router.ex @@ -168,7 +168,7 @@ defmodule CadetWeb.Router do get("/", DefaultController, :index) end - defp assign_course(conn, opts) do + defp assign_course(conn, _opts) do course_id = conn.path_params["courseid"] course_reg = @@ -177,9 +177,9 @@ defmodule CadetWeb.Router do case course_reg do {:ok, cr} -> assign(conn, :course_reg, cr) # user not in course - {:error, :no_record} -> send_resp(403, "Forbidden") |> halt() + {:error, :no_record} -> send_resp(conn, 403, "Forbidden") |> halt() # :TODO not sure what to put yet - {:error, :backend_error} -> send_resp(403, "Forbidden") |> halt() + {:error, :backend_error} -> send_resp(conn, 403, "Forbidden") |> halt() end conn diff --git a/test/factories/accounts/course_registration_factory.ex b/test/factories/accounts/course_registration_factory.ex index d983a15d2..15cba58c0 100644 --- a/test/factories/accounts/course_registration_factory.ex +++ b/test/factories/accounts/course_registration_factory.ex @@ -1,4 +1,4 @@ -defmodule Cadet.Accounts.CouseRegistraionFactory do +defmodule Cadet.Accounts.CourseRegistrationFactory do @moduledoc """ Factory(ies) for Cadet.Accounts.CourseRegistration entity """ @@ -9,12 +9,12 @@ defmodule Cadet.Accounts.CouseRegistraionFactory do # alias Cadet.Courses.{Course, Group} def course_registration_factory do - %CourseRegstration{ + %CourseRegistration{ user: build(:user), course: build(:course), group: build(:group), role: Enum.random(Role.__enum_map__()), - game_status: %{} + game_states: %{} } end end diff --git a/test/factories/factory.ex b/test/factories/factory.ex index 8f54e14a1..5257cdd2c 100644 --- a/test/factories/factory.ex +++ b/test/factories/factory.ex @@ -4,7 +4,7 @@ defmodule Cadet.Factory do """ use ExMachina.Ecto, repo: Cadet.Repo - use Cadet.Accounts.{NotificationFactory, UserFactory, CouseRegistraionFactory} + use Cadet.Accounts.{NotificationFactory, UserFactory, CourseRegistrationFactory} use Cadet.Assessments.{ AnswerFactory, From 9032a0681335a8ace2cb09bdb8f3b568a957bb55 Mon Sep 17 00:00:00 2001 From: Chow En Rong <53928333+chownces@users.noreply.github.com> Date: Sat, 5 Jun 2021 19:02:22 +0800 Subject: [PATCH 023/174] Updated admin courses controller routes and test urls --- .../admin_controllers/admin_courses_controller.ex | 4 ++-- lib/cadet_web/router.ex | 6 +++--- .../admin_courses_controller_test.exs | 12 ++++++------ 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/lib/cadet_web/admin_controllers/admin_courses_controller.ex b/lib/cadet_web/admin_controllers/admin_courses_controller.ex index 4ee558f3b..cd4907a8f 100644 --- a/lib/cadet_web/admin_controllers/admin_courses_controller.ex +++ b/lib/cadet_web/admin_controllers/admin_courses_controller.ex @@ -72,7 +72,7 @@ defmodule CadetWeb.AdminCoursesController do end swagger_path :update_course_config do - put("/admin/courses/{courseId}/course_config") + put("/v2/course/{courseId}/admin/course_config") summary("Updates the course configuration for the specified course") @@ -98,7 +98,7 @@ defmodule CadetWeb.AdminCoursesController do end swagger_path :update_assessment_config do - put("/admin/courses/{courseId}/assessment_config") + put("/v2/course/{courseId}/admin/assessment_config") summary("Updates the assessment configuration for the specified course") diff --git a/lib/cadet_web/router.ex b/lib/cadet_web/router.ex index fd34b7aac..f23330fca 100644 --- a/lib/cadet_web/router.ex +++ b/lib/cadet_web/router.ex @@ -134,9 +134,9 @@ defmodule CadetWeb.Router do put("/goals/:uuid", AdminGoalsController, :update) delete("/goals/:uuid", AdminGoalsController, :delete) - put("/courses/:courseid/course_config", AdminCoursesController, :update_course_config) - put("/courses/:courseid/assessment_config", AdminCoursesController, :update_assessment_config) - put("/courses/:courseid/assessment_types", AdminCoursesController, :update_assessment_types) + put("/course_config", AdminCoursesController, :update_course_config) + put("/assessment_config", AdminCoursesController, :update_assessment_config) + put("/assessment_types", AdminCoursesController, :update_assessment_types) end # Other scopes may use custom stacks. diff --git a/test/cadet_web/admin_controllers/admin_courses_controller_test.exs b/test/cadet_web/admin_controllers/admin_courses_controller_test.exs index 20ac968f2..6e35919c1 100644 --- a/test/cadet_web/admin_controllers/admin_courses_controller_test.exs +++ b/test/cadet_web/admin_controllers/admin_courses_controller_test.exs @@ -10,7 +10,7 @@ defmodule CadetWeb.AdminCoursesControllerTest do AdminCoursesController.swagger_path_update_assessment_types(nil) end - describe "PUT /courses/{courseId}/course_config" do + describe "PUT /v2/course/{courseId}/admin/course_config" do @tag authenticate: :admin test "succeeds 1", %{conn: conn} do course = insert(:course) @@ -118,7 +118,7 @@ defmodule CadetWeb.AdminCoursesControllerTest do end end - describe "PUT /courses/{courseId}/assessment_config" do + describe "PUT /v2/course/{courseId}/admin/assessment_config" do @tag authenticate: :admin test "succeeds", %{conn: conn} do assessment_config = insert(:assessment_config) @@ -175,7 +175,7 @@ defmodule CadetWeb.AdminCoursesControllerTest do end end - describe "PUT /courses/{courseId}/assessment_types" do + describe "PUT /v2/course/{courseId}/admin/assessment_types" do @tag authenticate: :admin test "succeeds", %{conn: conn} do course = insert(:course) @@ -234,11 +234,11 @@ defmodule CadetWeb.AdminCoursesControllerTest do end end - defp build_url_course_config(course_id), do: "/v2/admin/courses/#{course_id}/course_config" + defp build_url_course_config(course_id), do: "/v2/course/#{course_id}/admin/course_config" defp build_url_assessment_config(course_id), - do: "/v2/admin/courses/#{course_id}/assessment_config" + do: "/v2/course/#{course_id}/admin/assessment_config" defp build_url_assessment_types(course_id), - do: "/v2/admin/courses/#{course_id}/assessment_types" + do: "/v2/courses/#{course_id}/admin/assessment_types" end From 053bf637b9e72a9f2f9555d253f7e33cf4adfc98 Mon Sep 17 00:00:00 2001 From: Chow En Rong <53928333+chownces@users.noreply.github.com> Date: Sat, 5 Jun 2021 19:04:15 +0800 Subject: [PATCH 024/174] Updated courses controller routes and test urls --- lib/cadet_web/controllers/courses_controller.ex | 2 +- lib/cadet_web/router.ex | 2 +- test/cadet_web/controllers/courses_controller_test.exs | 6 +++--- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/lib/cadet_web/controllers/courses_controller.ex b/lib/cadet_web/controllers/courses_controller.ex index 24f33495a..ee277a362 100644 --- a/lib/cadet_web/controllers/courses_controller.ex +++ b/lib/cadet_web/controllers/courses_controller.ex @@ -13,7 +13,7 @@ defmodule CadetWeb.CoursesController do end swagger_path :get_course_config do - get("/courses/{courseId}/config") + get("/v2/course/{courseId}/config") summary("Retrieves the course configuration of the specified course") diff --git a/lib/cadet_web/router.ex b/lib/cadet_web/router.ex index f23330fca..f3fcda925 100644 --- a/lib/cadet_web/router.ex +++ b/lib/cadet_web/router.ex @@ -80,7 +80,7 @@ defmodule CadetWeb.Router do get("/user", UserController, :index) put("/user/game_states", UserController, :update_game_states) - get("/courses/:courseid/config", CoursesController, :index) + get("/config", CoursesController, :index) get("/devices", DevicesController, :index) post("/devices", DevicesController, :register) diff --git a/test/cadet_web/controllers/courses_controller_test.exs b/test/cadet_web/controllers/courses_controller_test.exs index a4f9e29c2..4bdd921b2 100644 --- a/test/cadet_web/controllers/courses_controller_test.exs +++ b/test/cadet_web/controllers/courses_controller_test.exs @@ -12,7 +12,7 @@ defmodule CadetWeb.CoursesControllerTest do CoursesController.swagger_path_get_course_config(nil) end - describe "GET /courses/course_id/config, unauthenticated" do + describe "GET /v2/course/course_id/config, unauthenticated" do test "unathorized", %{conn: conn} do course = insert(:course) conn = get(conn, build_url_config(course.id)) @@ -20,7 +20,7 @@ defmodule CadetWeb.CoursesControllerTest do end end - describe "GET /courses/course_id/config" do + describe "GET /v2/course/course_id/config" do @tag authenticate: :student test "succeeds", %{conn: conn} do course = insert(:course) @@ -58,5 +58,5 @@ defmodule CadetWeb.CoursesControllerTest do end end - defp build_url_config(course_id), do: "/v2/courses/#{course_id}/config" + defp build_url_config(course_id), do: "/v2/course/#{course_id}/config" end From 90ed3c96ba06528b76b5ce0883a276346353d775 Mon Sep 17 00:00:00 2001 From: Chow En Rong <53928333+chownces@users.noreply.github.com> Date: Sat, 5 Jun 2021 22:47:53 +0800 Subject: [PATCH 025/174] Update admin courses controller tests --- lib/cadet/accounts/course_registration.ex | 4 +- lib/cadet/accounts/user.ex | 10 +- lib/cadet/courses/courses.ex | 1 + lib/cadet_web/router.ex | 16 +-- ...0210531155751_add_course_configuration.exs | 18 ++++ .../admin_courses_controller_test.exs | 101 ++++++++++++------ .../controllers/courses_controller_test.exs | 25 ++--- .../accounts/course_registration_factory.ex | 3 +- .../courses/assessment_config_factory.ex | 4 +- test/support/conn_case.ex | 13 ++- 10 files changed, 123 insertions(+), 72 deletions(-) diff --git a/lib/cadet/accounts/course_registration.ex b/lib/cadet/accounts/course_registration.ex index 7656d93c7..1cb31825f 100644 --- a/lib/cadet/accounts/course_registration.ex +++ b/lib/cadet/accounts/course_registration.ex @@ -4,8 +4,8 @@ defmodule Cadet.Accounts.CourseRegistration do """ use Cadet, :model - alias Cadet.Accounts.Role - alias Cadet.Course.{Course, Group} + alias Cadet.Accounts.{Role, User} + alias Cadet.Courses.{Course, Group} schema "course_registrations" do field(:role, Role) diff --git a/lib/cadet/accounts/user.ex b/lib/cadet/accounts/user.ex index d6a1a61d9..458869ba3 100644 --- a/lib/cadet/accounts/user.ex +++ b/lib/cadet/accounts/user.ex @@ -7,24 +7,18 @@ defmodule Cadet.Accounts.User do """ use Cadet, :model - # alias Cadet.Accounts.Role - # alias Cadet.Course.Group + alias Cadet.Accounts.CourseRegistration schema "users" do field(:name, :string) - # field(:role, Role) field(:username, :string) - # field(:game_states, :map) - # belongs_to(:group, Group) - has_many(:course_registration, CourseRegistation) + has_many(:course_registration, CourseRegistration) timestamps() end - # @required_fields ~w(name role)a @required_fields ~w(name)a - # @optional_fields ~w(username group_id game_states)a @optional_fields ~w(username)a def changeset(user, params \\ %{}) do diff --git a/lib/cadet/courses/courses.ex b/lib/cadet/courses/courses.ex index eb235c398..f06e11013 100644 --- a/lib/cadet/courses/courses.ex +++ b/lib/cadet/courses/courses.ex @@ -218,6 +218,7 @@ defmodule Cadet.Courses do @doc """ Upload a sourcecast file """ + # def upload_sourcecast_file(uploader = %User{role: role}, attrs = %{}) do # if role in @upload_file_roles do # changeset = diff --git a/lib/cadet_web/router.ex b/lib/cadet_web/router.ex index f3fcda925..6945043d2 100644 --- a/lib/cadet_web/router.ex +++ b/lib/cadet_web/router.ex @@ -173,17 +173,19 @@ defmodule CadetWeb.Router do course_id = conn.path_params["courseid"] course_reg = - Cadet.Accounts.CourseRegistration.get_user_record(conn.assigns.current_user.id, course_id) + Cadet.Accounts.CourseRegistrations.get_user_record(conn.assigns.current_user.id, course_id) case course_reg do - {:ok, cr} -> assign(conn, :course_reg, cr) - # user not in course - {:error, :no_record} -> send_resp(conn, 403, "Forbidden") |> halt() + {:ok, cr} -> + assign(conn, :course_reg, cr) + + {:error, :no_record} -> + send_resp(conn, 403, "Forbidden") |> halt() + # :TODO not sure what to put yet - {:error, :backend_error} -> send_resp(conn, 403, "Forbidden") |> halt() + {:error, :backend_error} -> + send_resp(conn, 403, "Forbidden") |> halt() end - - conn end defp ensure_role(conn, opts) do diff --git a/priv/repo/migrations/20210531155751_add_course_configuration.exs b/priv/repo/migrations/20210531155751_add_course_configuration.exs index 0c67986ef..945b07c56 100644 --- a/priv/repo/migrations/20210531155751_add_course_configuration.exs +++ b/priv/repo/migrations/20210531155751_add_course_configuration.exs @@ -1,7 +1,15 @@ defmodule Cadet.Repo.Migrations.AddCourseConfiguration do use Ecto.Migration + alias Cadet.Accounts.Role + def change do + alter table(:users) do + remove(:role) + remove(:group_id) + remove(:game_states) + end + create table(:courses) do add(:name, :string, null: false) add(:module_code, :string) @@ -32,6 +40,16 @@ defmodule Cadet.Repo.Migrations.AddCourseConfiguration do create(unique_index(:assessment_types, [:course_id, :order])) + # :TODO Consider adding a unique constraint on user_id and course_id + create table(:course_registrations) do + add(:role, :role, null: false) + add(:game_states, :map, default: %{}) + add(:group_id, references(:groups)) + add(:user_id, references(:users), null: false) + add(:course_id, references(:courses), null: false) + timestamps() + end + drop_if_exists(table(:sublanguages)) end end diff --git a/test/cadet_web/admin_controllers/admin_courses_controller_test.exs b/test/cadet_web/admin_controllers/admin_courses_controller_test.exs index 6e35919c1..18f60ee05 100644 --- a/test/cadet_web/admin_controllers/admin_courses_controller_test.exs +++ b/test/cadet_web/admin_controllers/admin_courses_controller_test.exs @@ -13,10 +13,10 @@ defmodule CadetWeb.AdminCoursesControllerTest do describe "PUT /v2/course/{courseId}/admin/course_config" do @tag authenticate: :admin test "succeeds 1", %{conn: conn} do - course = insert(:course) + course_id = conn.assigns[:course_id] conn = - put(conn, build_url_course_config(course.id), %{ + put(conn, build_url_course_config(course_id), %{ "source_chapter" => Enum.random(1..4), "source_variant" => "default" }) @@ -26,10 +26,10 @@ defmodule CadetWeb.AdminCoursesControllerTest do @tag authenticate: :admin test "succeeds 2", %{conn: conn} do - course = insert(:course) + course_id = conn.assigns[:course_id] conn = - put(conn, build_url_course_config(course.id), %{ + put(conn, build_url_course_config(course_id), %{ "name" => "Data Structures and Algorithms", "module_code" => "CS2040S", "enable_game" => false, @@ -45,10 +45,10 @@ defmodule CadetWeb.AdminCoursesControllerTest do @tag authenticate: :admin test "succeeds 3", %{conn: conn} do - course = insert(:course) + course_id = conn.assigns[:course_id] conn = - put(conn, build_url_course_config(course.id), %{ + put(conn, build_url_course_config(course_id), %{ "name" => "Data Structures and Algorithms", "module_code" => "CS2040S", "enable_game" => false, @@ -62,10 +62,10 @@ defmodule CadetWeb.AdminCoursesControllerTest do @tag authenticate: :student test "rejects forbidden request for non-staff users", %{conn: conn} do - course = insert(:course) + course_id = conn.assigns[:course_id] conn = - put(conn, build_url_course_config(course.id), %{ + put(conn, build_url_course_config(course_id), %{ "source_chapter" => 3, "source_variant" => "concurrent" }) @@ -74,24 +74,24 @@ defmodule CadetWeb.AdminCoursesControllerTest do end @tag authenticate: :staff - test "rejects requests with invalid course id", %{conn: conn} do - course = insert(:course) + test "rejects requests if user does not belong to the specified course", %{conn: conn} do + course_id = conn.assigns[:course_id] conn = - put(conn, build_url_course_config(course.id + 1), %{ + put(conn, build_url_course_config(course_id + 1), %{ "source_chapter" => 3, "source_variant" => "concurrent" }) - assert response(conn, 400) == "Invalid course id" + assert response(conn, 403) == "Forbidden" end @tag authenticate: :staff test "rejects requests with invalid params", %{conn: conn} do - course = insert(:course) + course_id = conn.assigns[:course_id] conn = - put(conn, build_url_course_config(course.id), %{ + put(conn, build_url_course_config(course_id), %{ "source_chapter" => 4, "source_variant" => "wasm" }) @@ -101,10 +101,10 @@ defmodule CadetWeb.AdminCoursesControllerTest do @tag authenticate: :staff test "rejects requests with missing params", %{conn: conn} do - course = insert(:course) + course_id = conn.assigns[:course_id] conn = - put(conn, build_url_course_config(course.id), %{ + put(conn, build_url_course_config(course_id), %{ "name" => "Data Structures and Algorithms", "module_code" => "CS2040S", "enable_game" => false, @@ -121,10 +121,11 @@ defmodule CadetWeb.AdminCoursesControllerTest do describe "PUT /v2/course/{courseId}/admin/assessment_config" do @tag authenticate: :admin test "succeeds", %{conn: conn} do - assessment_config = insert(:assessment_config) + course_id = conn.assigns[:course_id] + insert(:assessment_config, %{course_id: course_id}) conn = - put(conn, build_url_assessment_config(assessment_config.course_id), %{ + put(conn, build_url_assessment_config(course_id), %{ "early_submission_xp" => 100, "hours_before_early_xp_decay" => 24, "decay_rate_points_per_hour" => 2 @@ -135,10 +136,26 @@ defmodule CadetWeb.AdminCoursesControllerTest do @tag authenticate: :student test "rejects forbidden request for non-staff users", %{conn: conn} do - assessment_config = insert(:assessment_config) + course_id = conn.assigns[:course_id] + insert(:assessment_config, %{course_id: course_id}) conn = - put(conn, build_url_assessment_config(assessment_config.course_id), %{ + put(conn, build_url_assessment_config(course_id), %{ + "early_submission_xp" => 100, + "hours_before_early_xp_decay" => 24, + "decay_rate_points_per_hour" => 2 + }) + + assert response(conn, 403) == "Forbidden" + end + + @tag authenticate: :staff + test "rejects request if user does not belong to specified course", %{conn: conn} do + course_id = conn.assigns[:course_id] + insert(:assessment_config, %{course_id: course_id}) + + conn = + put(conn, build_url_assessment_config(course_id + 1), %{ "early_submission_xp" => 100, "hours_before_early_xp_decay" => 24, "decay_rate_points_per_hour" => 2 @@ -149,10 +166,11 @@ defmodule CadetWeb.AdminCoursesControllerTest do @tag authenticate: :staff test "rejects requests with invalid params", %{conn: conn} do - assessment_config = insert(:assessment_config) + course_id = conn.assigns[:course_id] + insert(:assessment_config, %{course_id: course_id}) conn = - put(conn, build_url_assessment_config(assessment_config.course_id), %{ + put(conn, build_url_assessment_config(course_id), %{ "early_submission_xp" => 100, "hours_before_early_xp_decay" => -1, "decay_rate_points_per_hour" => 200 @@ -163,10 +181,11 @@ defmodule CadetWeb.AdminCoursesControllerTest do @tag authenticate: :staff test "rejects requests with missing params", %{conn: conn} do - assessment_config = insert(:assessment_config) + course_id = conn.assigns[:course_id] + insert(:assessment_config, %{course_id: course_id}) conn = - put(conn, build_url_assessment_config(assessment_config.course_id), %{ + put(conn, build_url_assessment_config(course_id), %{ "hours_before_early_xp_decay" => 24, "decay_rate_points_per_hour" => 2 }) @@ -178,10 +197,10 @@ defmodule CadetWeb.AdminCoursesControllerTest do describe "PUT /v2/course/{courseId}/admin/assessment_types" do @tag authenticate: :admin test "succeeds", %{conn: conn} do - course = insert(:course) + course_id = conn.assigns[:course_id] conn = - put(conn, build_url_assessment_types(course.id), %{ + put(conn, build_url_assessment_types(course_id), %{ "assessment_types" => ["Missions", "Quests", "Contests"] }) @@ -190,10 +209,22 @@ defmodule CadetWeb.AdminCoursesControllerTest do @tag authenticate: :student test "rejects forbidden request for non-staff users", %{conn: conn} do - course = insert(:course) + course_id = conn.assigns[:course_id] + + conn = + put(conn, build_url_assessment_types(course_id), %{ + "assessment_types" => ["Missions", "Quests", "Contests"] + }) + + assert response(conn, 403) == "Forbidden" + end + + @tag authenticate: :staff + test "rejects request if user is not in specified course", %{conn: conn} do + course_id = conn.assigns[:course_id] conn = - put(conn, build_url_assessment_types(course.id), %{ + put(conn, build_url_assessment_types(course_id + 1), %{ "assessment_types" => ["Missions", "Quests", "Contests"] }) @@ -202,10 +233,10 @@ defmodule CadetWeb.AdminCoursesControllerTest do @tag authenticate: :staff test "rejects requests with invalid params 1", %{conn: conn} do - course = insert(:course) + course_id = conn.assigns[:course_id] conn = - put(conn, build_url_assessment_types(course.id), %{ + put(conn, build_url_assessment_types(course_id), %{ "assessment_types" => "Missions" }) @@ -214,10 +245,10 @@ defmodule CadetWeb.AdminCoursesControllerTest do @tag authenticate: :staff test "rejects requests with invalid params 2", %{conn: conn} do - course = insert(:course) + course_id = conn.assigns[:course_id] conn = - put(conn, build_url_assessment_types(course.id), %{ + put(conn, build_url_assessment_types(course_id), %{ "assessment_types" => [1, "Missions", "Quests"] }) @@ -226,9 +257,9 @@ defmodule CadetWeb.AdminCoursesControllerTest do @tag authenticate: :staff test "rejects requests with missing params", %{conn: conn} do - course = insert(:course) + course_id = conn.assigns[:course_id] - conn = put(conn, build_url_assessment_types(course.id), %{}) + conn = put(conn, build_url_assessment_types(course_id), %{}) assert response(conn, 400) == "Missing parameter(s)" end @@ -240,5 +271,5 @@ defmodule CadetWeb.AdminCoursesControllerTest do do: "/v2/course/#{course_id}/admin/assessment_config" defp build_url_assessment_types(course_id), - do: "/v2/courses/#{course_id}/admin/assessment_types" + do: "/v2/course/#{course_id}/admin/assessment_types" end diff --git a/test/cadet_web/controllers/courses_controller_test.exs b/test/cadet_web/controllers/courses_controller_test.exs index 4bdd921b2..6f15bd7a0 100644 --- a/test/cadet_web/controllers/courses_controller_test.exs +++ b/test/cadet_web/controllers/courses_controller_test.exs @@ -3,17 +3,13 @@ defmodule CadetWeb.CoursesControllerTest do alias CadetWeb.CoursesController - # setup do - # Cadet.Test.Seeds. - # end - test "swagger" do CoursesController.swagger_definitions() CoursesController.swagger_path_get_course_config(nil) end describe "GET /v2/course/course_id/config, unauthenticated" do - test "unathorized", %{conn: conn} do + test "unauthorized", %{conn: conn} do course = insert(:course) conn = get(conn, build_url_config(course.id)) assert response(conn, 401) == "Unauthorised" @@ -23,12 +19,13 @@ defmodule CadetWeb.CoursesControllerTest do describe "GET /v2/course/course_id/config" do @tag authenticate: :student test "succeeds", %{conn: conn} do - course = insert(:course) - insert(:assessment_types, %{order: 1, type: "Missions", course_id: course.id}) - insert(:assessment_types, %{order: 2, type: "Quests", course_id: course.id}) - insert(:assessment_types, %{order: 3, type: "Paths", course_id: course.id}) + course_id = conn.assigns[:course_id] - resp = conn |> get(build_url_config(course.id)) |> json_response(200) + insert(:assessment_types, %{order: 1, type: "Missions", course_id: course_id}) + insert(:assessment_types, %{order: 2, type: "Quests", course_id: course_id}) + insert(:assessment_types, %{order: 3, type: "Paths", course_id: course_id}) + + resp = conn |> get(build_url_config(course_id)) |> json_response(200) assert %{ "config" => %{ @@ -47,14 +44,14 @@ defmodule CadetWeb.CoursesControllerTest do end @tag authenticate: :student - test "returns with error for invalid course id", %{conn: conn} do - course = insert(:course) + test "returns with error for user not belonging to the specified course", %{conn: conn} do + course_id = conn.assigns[:course_id] conn = conn - |> get(build_url_config(course.id + 1)) + |> get(build_url_config(course_id + 1)) - assert response(conn, 400) == "Invalid course id" + assert response(conn, 403) == "Forbidden" end end diff --git a/test/factories/accounts/course_registration_factory.ex b/test/factories/accounts/course_registration_factory.ex index 15cba58c0..7a0470bbc 100644 --- a/test/factories/accounts/course_registration_factory.ex +++ b/test/factories/accounts/course_registration_factory.ex @@ -12,7 +12,8 @@ defmodule Cadet.Accounts.CourseRegistrationFactory do %CourseRegistration{ user: build(:user), course: build(:course), - group: build(:group), + # :TODO Group factory is currently wrongly configured + # group: build(:group), role: Enum.random(Role.__enum_map__()), game_states: %{} } diff --git a/test/factories/courses/assessment_config_factory.ex b/test/factories/courses/assessment_config_factory.ex index d4a4ffe04..15aac4b91 100644 --- a/test/factories/courses/assessment_config_factory.ex +++ b/test/factories/courses/assessment_config_factory.ex @@ -11,8 +11,8 @@ defmodule Cadet.Courses.AssessmentConfigFactory do %AssessmentConfig{ early_submission_xp: 200, hours_before_early_xp_decay: 48, - decay_rate_points_per_hour: 1, - course: build(:course) + decay_rate_points_per_hour: 1 + # course: build(:course) } end end diff --git a/test/support/conn_case.ex b/test/support/conn_case.ex index eb6c06858..a9a951d28 100644 --- a/test/support/conn_case.ex +++ b/test/support/conn_case.ex @@ -50,11 +50,12 @@ defmodule CadetWeb.ConnCase do conn = Phoenix.ConnTest.build_conn() if tags[:authenticate] do - user = + course_registration = cond do is_atom(tags[:authenticate]) -> - Cadet.Factory.insert(:user, %{role: tags[:authenticate]}) + Cadet.Factory.insert(:course_registration, %{role: tags[:authenticate]}) + # :TODO: This is_map case has not been handled. To recheck in the future. is_map(tags[:authenticate]) -> tags[:authenticate] @@ -62,7 +63,13 @@ defmodule CadetWeb.ConnCase do nil end - conn = sign_in(conn, user) + # We assign course_id to the conn during testing, so that we can generate the correct + # course URL for the user created during the test. The course_id is assigned here instead + # of the course_registration since we want the router plug to assign the course_registration + # when actually accessing the endpoint during the test. + conn = + sign_in(conn, course_registration.user) + |> assign(:course_id, course_registration.course_id) {:ok, conn: conn} else From ed1f1a6452d6bc87511c696c60e953e71db9bf32 Mon Sep 17 00:00:00 2001 From: Chow En Rong <53928333+chownces@users.noreply.github.com> Date: Sat, 5 Jun 2021 22:53:00 +0800 Subject: [PATCH 026/174] Updated courses context tests --- test/cadet/courses/courses_test.exs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/cadet/courses/courses_test.exs b/test/cadet/courses/courses_test.exs index a8299c124..64d414259 100644 --- a/test/cadet/courses/courses_test.exs +++ b/test/cadet/courses/courses_test.exs @@ -115,7 +115,7 @@ defmodule Cadet.CoursesTest do describe "update assessment config" do test "succeeds" do - assessment_config = insert(:assessment_config) + assessment_config = insert(:assessment_config, %{course: insert(:course)}) {:ok, updated_config} = Courses.update_assessment_config(assessment_config.course_id, 100, 24, 1) @@ -126,7 +126,7 @@ defmodule Cadet.CoursesTest do end test "returns with error for failed updates" do - assessment_config = insert(:assessment_config) + assessment_config = insert(:assessment_config, %{course: insert(:course)}) {:error, changeset} = Courses.update_assessment_config(assessment_config.course_id, -1, 0, 0) From b81306e81c3427c8a919f04ba72e5d55cda646cc Mon Sep 17 00:00:00 2001 From: Chow En Rong <53928333+chownces@users.noreply.github.com> Date: Sat, 5 Jun 2021 23:07:55 +0800 Subject: [PATCH 027/174] Updated course_registrations function --- lib/cadet/accounts/course_registrations.ex | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/lib/cadet/accounts/course_registrations.ex b/lib/cadet/accounts/course_registrations.ex index 1e386b7f5..f8d9142f6 100644 --- a/lib/cadet/accounts/course_registrations.ex +++ b/lib/cadet/accounts/course_registrations.ex @@ -19,13 +19,13 @@ defmodule Cadet.Accounts.CourseRegistrations do CourseRegistration |> where([cr], cr.user_id == ^user_id) |> where([cr], cr.course_id == ^course_id) - |> Repo.all() + |> Repo.one() |> case do - [cr = %CourseRegistration{}] -> {:ok, cr} - # when the user is not in the course - [] -> {:error, :no_record} - # when a user was added to a course twice - _ -> {:error, :backend_error} + nil -> + {:error, :no_record} + + cr -> + {:ok, cr} end # |> Repo.get_by(%{user_id: ^user_id, course_id: ^course_id}) From 7d599597cfa78e8197a6463455dbf62120a49bae Mon Sep 17 00:00:00 2001 From: Chow En Rong <53928333+chownces@users.noreply.github.com> Date: Sat, 5 Jun 2021 23:30:38 +0800 Subject: [PATCH 028/174] Follow up on router assign_course --- lib/cadet_web/router.ex | 4 ---- 1 file changed, 4 deletions(-) diff --git a/lib/cadet_web/router.ex b/lib/cadet_web/router.ex index 6945043d2..291990eef 100644 --- a/lib/cadet_web/router.ex +++ b/lib/cadet_web/router.ex @@ -181,10 +181,6 @@ defmodule CadetWeb.Router do {:error, :no_record} -> send_resp(conn, 403, "Forbidden") |> halt() - - # :TODO not sure what to put yet - {:error, :backend_error} -> - send_resp(conn, 403, "Forbidden") |> halt() end end From 52119bde3ef6f101dd1b8481451aceaf085bcef2 Mon Sep 17 00:00:00 2001 From: YaleChen299 Date: Sun, 6 Jun 2021 14:54:37 +0800 Subject: [PATCH 029/174] addded course_registration test for changeset --- lib/cadet/accounts/course_registration.ex | 1 + lib/cadet/accounts/course_registrations.ex | 21 +++--- ...0210531155751_add_course_configuration.exs | 7 +- .../accounts/course_registration_test.exs | 69 ++++++++++++++++--- 4 files changed, 75 insertions(+), 23 deletions(-) diff --git a/lib/cadet/accounts/course_registration.ex b/lib/cadet/accounts/course_registration.ex index 1cb31825f..d2f7420e2 100644 --- a/lib/cadet/accounts/course_registration.ex +++ b/lib/cadet/accounts/course_registration.ex @@ -26,5 +26,6 @@ defmodule Cadet.Accounts.CourseRegistration do |> cast(params, @optional_fields ++ @required_fields) |> add_belongs_to_id_from_model([:user, :group, :course], params) |> validate_required(@required_fields) + |> unique_constraint(:user_id, name: :course_registrations_user_id_course_id_index) end end diff --git a/lib/cadet/accounts/course_registrations.ex b/lib/cadet/accounts/course_registrations.ex index f8d9142f6..c39420fce 100644 --- a/lib/cadet/accounts/course_registrations.ex +++ b/lib/cadet/accounts/course_registrations.ex @@ -51,7 +51,7 @@ defmodule Cadet.Accounts.CourseRegistrations do # maybe not needed when we dont need group info end - def enroll_course(params = %{user_id: user_id, course_id: course_id, role: role}) + def enroll_course(params = %{user_id: user_id, course_id: course_id, role: _role}) when is_ecto_id(user_id) and is_ecto_id(course_id) do params |> insert_or_update_course_registration() end @@ -74,18 +74,13 @@ defmodule Cadet.Accounts.CourseRegistrations do |> Repo.insert_or_update() end - # :TODO error handling - def delete_record(params = %{user_id: user_id, course_id: course_id}) + @spec delete_record(map()) :: {:ok, Ecto.Schema.t()} | {:error, Ecto.Changeset.t()} + def delete_record(%{user_id: user_id, course_id: course_id}) when is_ecto_id(user_id) and is_ecto_id(course_id) do - record = - CourseRegistration - |> where(user_id: ^user_id) - |> where(course_id: ^course_id) - |> Repo.one() - - # case Repo.delete(record) do - # {:ok, struct} -> # Deleted with success - # {:error, changeset} -> # Something went wrong - # end + CourseRegistration + |> where(user_id: ^user_id) + |> where(course_id: ^course_id) + |> Repo.one() + |> Repo.delete() end end diff --git a/priv/repo/migrations/20210531155751_add_course_configuration.exs b/priv/repo/migrations/20210531155751_add_course_configuration.exs index 945b07c56..6c0d15760 100644 --- a/priv/repo/migrations/20210531155751_add_course_configuration.exs +++ b/priv/repo/migrations/20210531155751_add_course_configuration.exs @@ -40,7 +40,6 @@ defmodule Cadet.Repo.Migrations.AddCourseConfiguration do create(unique_index(:assessment_types, [:course_id, :order])) - # :TODO Consider adding a unique constraint on user_id and course_id create table(:course_registrations) do add(:role, :role, null: false) add(:game_states, :map, default: %{}) @@ -50,6 +49,12 @@ defmodule Cadet.Repo.Migrations.AddCourseConfiguration do timestamps() end + create( + unique_index(:course_registrations, [:user_id, :course_id], + name: :course_registrations_user_id_course_id_index + ) + ) + drop_if_exists(table(:sublanguages)) end end diff --git a/test/cadet/accounts/course_registration_test.exs b/test/cadet/accounts/course_registration_test.exs index b5a2d2d23..34af244d4 100644 --- a/test/cadet/accounts/course_registration_test.exs +++ b/test/cadet/accounts/course_registration_test.exs @@ -3,19 +3,70 @@ defmodule Cadet.Accounts.CourseRegistrationTest do use Cadet.ChangesetCase, entity: CourseRegistration + alias Cadet.Repo + setup do + user1 = insert(:user, %{name: "test 1"}) + user2 = insert(:user, %{name: "test 2"}) + # group1 = insert(:group) + course1 = insert(:course, %{module_code: "CS1101S"}) + course2 = insert(:course, %{module_code: "CS2040S"}) + + {:ok, %{user1: user1, user2: user2, course1: course1, course2: course2}} end # :TODO add context function test describe "Changesets" do - # test "valid changeset" do - # assert_changeset(%{user_id: , course_id: , role: :admin}, :valid) - # assert_changeset(%{user_id: , course_id: , role: :student}, :valid) - # end - - # test "invalid changeset" do - # assert_changeset(%{name: "people"}, :invalid) - # assert_changeset(%{role: :avenger}, :invalid) - # end + test "valid changeset", %{ + user1: user1, + user2: user2, + course1: course1, + course2: course2 + } do + assert_changeset(%{user_id: user1.id, course_id: course2.id, role: :admin}, :valid) + assert_changeset(%{user_id: user2.id, course_id: course1.id, role: :student}, :valid) + + # assert_changeset(%{user_id: user2.id, course_id: course2.id, role: :staff, group_id: group.id}, :valid) + end + + test "invalid changeset missing required params", %{user1: user1, course2: course2} do + assert_changeset(%{user_id: user1.id, course_id: course2.id}, :invalid) + assert_changeset(%{user_id: user1.id, role: :avenger}, :invalid) + assert_changeset(%{course_id: course2.id, role: :avenger}, :invalid) + end + + test "invalid changeset bad params", %{ + user1: user1, + course2: course2 + } do + assert_changeset(%{user_id: user1.id, course_id: course2.id, role: :avenger}, :invalid) + end + + test "invalid changeset repeated records", %{ + user1: user1, + course1: course1 + } do + changeset = + CourseRegistration.changeset(%CourseRegistration{}, %{ + course_id: course1.id, + user_id: user1.id, + role: :student + }) + + {:ok, course_reg} = Repo.insert(changeset) + + {:error, changeset} = Repo.insert(changeset) + + assert changeset.errors == [ + user_id: + {"has already been taken", + [ + {:constraint, :unique}, + {:constraint_name, "course_registrations_user_id_course_id_index"} + ]} + ] + + refute changeset.valid? + end end end From 185be5be5d894ffb01607563977d0c676ae60558 Mon Sep 17 00:00:00 2001 From: YaleChen299 Date: Sun, 6 Jun 2021 16:03:25 +0800 Subject: [PATCH 030/174] fix group factory, partial fix for the ecto.setup flow --- lib/cadet/courses/group.ex | 2 +- lib/mix/tasks/users/import.ex | 15 ++++----- priv/repo/seeds.exs | 31 +++++++++++++++---- .../accounts/course_registration_factory.ex | 2 +- test/factories/courses/group_factory.ex | 3 +- 5 files changed, 37 insertions(+), 16 deletions(-) diff --git a/lib/cadet/courses/group.ex b/lib/cadet/courses/group.ex index d953fea1c..b78e318f4 100644 --- a/lib/cadet/courses/group.ex +++ b/lib/cadet/courses/group.ex @@ -5,7 +5,7 @@ defmodule Cadet.Courses.Group do """ use Cadet, :model - alias Cadet.Accounts.User + alias Cadet.Accounts.CourseRegistration schema "groups" do field(:name, :string) diff --git a/lib/mix/tasks/users/import.ex b/lib/mix/tasks/users/import.ex index f2371e4eb..4774da54e 100644 --- a/lib/mix/tasks/users/import.ex +++ b/lib/mix/tasks/users/import.ex @@ -48,13 +48,13 @@ defmodule Mix.Tasks.Cadet.Users.Import do csv_stream = path |> File.stream!() |> CSV.decode(strip_fields: true) for {:ok, [name, username, group_name]} <- csv_stream do - with {:ok, group = %Group{}} <- Courses.get_or_create_group(group_name), + with {:ok, _group = %Group{}} <- Courses.get_or_create_group(group_name), {:ok, %User{}} <- Accounts.insert_or_update_user(%{ username: username, name: name, - role: :student, - group: group + # role: :student, + # group: group }) do :ok else @@ -82,9 +82,9 @@ defmodule Mix.Tasks.Cadet.Users.Import do for {:ok, [name, username, group_name]} <- csv_stream do with {:ok, leader = %User{}} <- - Accounts.insert_or_update_user(%{username: username, name: name, role: :staff}), + Accounts.insert_or_update_user(%{username: username, name: name}), {:ok, %Group{}} <- - Course.insert_or_update_group(%{name: group_name, leader: leader}) do + Courses.insert_or_update_group(%{name: group_name, leader: leader}) do :ok else error -> @@ -105,15 +105,16 @@ defmodule Mix.Tasks.Cadet.Users.Import do end end + # :TODO check mentor is staff before update group and add enroll course logit defp process_mentors_csv(path) when is_binary(path) do if File.exists?(path) do csv_stream = path |> File.stream!() |> CSV.decode(strip_fields: true) for {:ok, [name, username, group_name]} <- csv_stream do with {:ok, mentor = %User{}} <- - Accounts.insert_or_update_user(%{username: username, name: name, role: :staff}), + Accounts.insert_or_update_user(%{username: username, name: name}), {:ok, %Group{}} <- - Course.insert_or_update_group(%{name: group_name, mentor: mentor}) do + Courses.insert_or_update_group(%{name: group_name, mentor: mentor}) do :ok else error -> diff --git a/priv/repo/seeds.exs b/priv/repo/seeds.exs index 841ea3732..97f83f26c 100644 --- a/priv/repo/seeds.exs +++ b/priv/repo/seeds.exs @@ -17,12 +17,31 @@ alias Cadet.Assessments.SubmissionStatus # Cadet.Repo.insert!(%Cadet.Settings.Sublanguage{chapter: 1, variant: "default"}) if Cadet.Env.env() == :dev do - # User and Group - avenger = insert(:user, %{name: "avenger", role: :staff}) - mentor = insert(:user, %{name: "mentor", role: :staff}) - group = insert(:group, %{leader: avenger, mentor: mentor}) - students = insert_list(5, :student, %{group: group}) - admin = insert(:user, %{name: "admin", role: :admin}) + # Course + course1 = insert(:course) + # Users + avenger = insert(:user, %{name: "avenger", username: "E1234561"}) + mentor = insert(:user, %{name: "mentor", username: "E1234562"}) + admin = insert(:user, %{name: "admin", username: "E1234563"}) + studenta = insert(:user, %{username: "E1234564"}) + studentb = insert(:user, %{username: "E1234564"}) + studentc = insert(:user, %{username: "E1234564"}) + # CourseRegistration and Group + avenger1 = insert(:course_registration, %{user: avenger, course: course1, role: :staff}) + mentor1 = insert(:course_registration, %{user: mentor, course: course1, role: :staff}) + admin1 = insert(:course_registration, %{user: admin, course: course1, role: :admin}) + group = insert(:group, %{leader: avenger1, mentor: mentor1}) + studenta1 = insert(:course_registration, %{user: studenta, course: course1, role: :student, group: group}) + studentb1 = insert(:course_registration, %{user: studentb, course: course1, role: :student, group: group}) + studentc1 = insert(:course_registration, %{user: studentc, course: course1, role: :student, group: group}) + students = [studenta1, studentb1, studentc1] + # :TODO fix assessment and notification then come back + + # avenger = insert(:user, %{name: "avenger", role: :staff}) + # mentor = insert(:user, %{name: "mentor", role: :staff}) + # group = insert(:group, %{leader: avenger, mentor: mentor}) + # students = insert_list(5, :student, %{group: group}) + # admin = insert(:user, %{name: "admin", role: :admin}) # Assessments for _ <- 1..5 do diff --git a/test/factories/accounts/course_registration_factory.ex b/test/factories/accounts/course_registration_factory.ex index 7a0470bbc..dde296493 100644 --- a/test/factories/accounts/course_registration_factory.ex +++ b/test/factories/accounts/course_registration_factory.ex @@ -13,7 +13,7 @@ defmodule Cadet.Accounts.CourseRegistrationFactory do user: build(:user), course: build(:course), # :TODO Group factory is currently wrongly configured - # group: build(:group), + group: build(:group), role: Enum.random(Role.__enum_map__()), game_states: %{} } diff --git a/test/factories/courses/group_factory.ex b/test/factories/courses/group_factory.ex index b4acf3e6b..c90522b5a 100644 --- a/test/factories/courses/group_factory.ex +++ b/test/factories/courses/group_factory.ex @@ -10,7 +10,8 @@ defmodule Cadet.Courses.GroupFactory do def group_factory do %Group{ name: sequence("group"), - leader: build(:user, role: :staff) + # leader: build(:course_registration) + # leader: build(:user, role: :staff) } end end From cda1cf3fc6d4bf2cda557baf60358d7596e39a73 Mon Sep 17 00:00:00 2001 From: YaleChen299 Date: Sun, 6 Jun 2021 17:28:00 +0800 Subject: [PATCH 031/174] updated course_registration context function test getters --- lib/cadet/accounts/course_registrations.ex | 3 + lib/cadet/accounts/query.ex | 2 +- .../accounts/course_registration_test.exs | 95 +++++++++++++++---- 3 files changed, 78 insertions(+), 22 deletions(-) diff --git a/lib/cadet/accounts/course_registrations.ex b/lib/cadet/accounts/course_registrations.ex index c39420fce..654bfedb2 100644 --- a/lib/cadet/accounts/course_registrations.ex +++ b/lib/cadet/accounts/course_registrations.ex @@ -35,17 +35,20 @@ defmodule Cadet.Accounts.CourseRegistrations do CourseRegistration |> where([cr], cr.user_id == ^id) |> join(:inner, [cr], c in assoc(cr, :course)) + |> Repo.all() end def get_users(course_id) do CourseRegistration |> where([cr], cr.course_id == ^course_id) |> join(:inner, [cr], u in assoc(cr, :user)) + |> Repo.all() end def get_users(course_id, group_id) do get_users(course_id) |> where([cr, u], cr.group_id == ^group_id) + |> Repo.all() # |> join(:inner, [cr, u], g in assoc(cr, :group)) # maybe not needed when we dont need group info diff --git a/lib/cadet/accounts/query.ex b/lib/cadet/accounts/query.ex index eeb83890c..1f5879067 100644 --- a/lib/cadet/accounts/query.ex +++ b/lib/cadet/accounts/query.ex @@ -13,7 +13,6 @@ defmodule Cadet.Accounts.Query do User |> in_course(course_id) |> where([u, cr], cr.role == "student") - |> preload(:group) |> Repo.all() end @@ -37,6 +36,7 @@ defmodule Cadet.Accounts.Query do CourseRegistration |> where([cr], cr.course_id == ^course_id) |> where([cr], cr.user_id == ^avenger_id) + |> Repo.one() students = students_of(avengerInCourse) diff --git a/test/cadet/accounts/course_registration_test.exs b/test/cadet/accounts/course_registration_test.exs index 34af244d4..eba0e7004 100644 --- a/test/cadet/accounts/course_registration_test.exs +++ b/test/cadet/accounts/course_registration_test.exs @@ -1,22 +1,29 @@ defmodule Cadet.Accounts.CourseRegistrationTest do - alias Cadet.Accounts.CourseRegistration + alias Cadet.Accounts.{CourseRegistration, CourseRegistrations} use Cadet.ChangesetCase, entity: CourseRegistration alias Cadet.Repo setup do - user1 = insert(:user, %{name: "test 1"}) - user2 = insert(:user, %{name: "test 2"}) + user1 = insert(:user, %{name: "user 1"}) + user2 = insert(:user, %{name: "user 2"}) # group1 = insert(:group) - course1 = insert(:course, %{module_code: "CS1101S"}) - course2 = insert(:course, %{module_code: "CS2040S"}) + course1 = insert(:course, %{module_code: "course 1"}) + course2 = insert(:course, %{module_code: "course 2"}) + changeset = + CourseRegistration.changeset(%CourseRegistration{}, %{ + course_id: course1.id, + user_id: user1.id, + role: :student + }) + + {:ok, _course_reg} = Repo.insert(changeset) - {:ok, %{user1: user1, user2: user2, course1: course1, course2: course2}} + {:ok, %{user1: user1, user2: user2, course1: course1, course2: course2, changeset: changeset}} end - # :TODO add context function test - describe "Changesets" do + describe "Changesets:" do test "valid changeset", %{ user1: user1, user2: user2, @@ -42,19 +49,7 @@ defmodule Cadet.Accounts.CourseRegistrationTest do assert_changeset(%{user_id: user1.id, course_id: course2.id, role: :avenger}, :invalid) end - test "invalid changeset repeated records", %{ - user1: user1, - course1: course1 - } do - changeset = - CourseRegistration.changeset(%CourseRegistration{}, %{ - course_id: course1.id, - user_id: user1.id, - role: :student - }) - - {:ok, course_reg} = Repo.insert(changeset) - + test "invalid changeset repeated records", %{changeset: changeset} do {:error, changeset} = Repo.insert(changeset) assert changeset.errors == [ @@ -69,4 +64,62 @@ defmodule Cadet.Accounts.CourseRegistrationTest do refute changeset.valid? end end + + describe "get course_registrations" do + test "of a user succeeds", %{user1: user1, course1: course1, course2: course2} do + changeset2 = + CourseRegistration.changeset(%CourseRegistration{}, %{ + course_id: course2.id, + user_id: user1.id, + role: :student + }) + + {:ok, _course_reg} = Repo.insert(changeset2) + + course_reg_user1 = CourseRegistrations.get_courses(user1) + course_reg_user1_course1 = hd(course_reg_user1) + course_reg_user1_course2 = hd(tl(course_reg_user1)) + assert user1.id == course_reg_user1_course1.user_id + assert course1.id == course_reg_user1_course1.course_id + assert user1.id == course_reg_user1_course2.user_id + assert course2.id == course_reg_user1_course2.course_id + end + + test "of a user failed due to invalid id", %{user2: user2} do + assert [] == CourseRegistrations.get_courses(user2) + end + + test "of a course succeeds", %{user1: user1, user2: user2, course1: course1} do + changeset2 = + CourseRegistration.changeset(%CourseRegistration{}, %{ + course_id: course1.id, + user_id: user2.id, + role: :student + }) + + {:ok, _course_reg} = Repo.insert(changeset2) + + course_reg_course1 = CourseRegistrations.get_users(course1.id) + course_reg_course1_user1 = hd(course_reg_course1) + course_reg_course1_user2 = hd(tl(course_reg_course1)) + assert user1.id == course_reg_course1_user1.user_id + assert course1.id == course_reg_course1_user1.course_id + assert user2.id == course_reg_course1_user2.user_id + assert course1.id == course_reg_course1_user2.course_id + end + + test "of a course failed due to invalid id", %{course2: course2} do + assert [] == CourseRegistrations.get_users(course2.id) + end + + + + # test "" do + + # end + + # test "" do + + # end + end end From eec0983adc901b6a53fb26f2c4d887970ba06d96 Mon Sep 17 00:00:00 2001 From: Chow En Rong <53928333+chownces@users.noreply.github.com> Date: Sun, 6 Jun 2021 22:10:44 +0800 Subject: [PATCH 032/174] Updated sourcecast context functions and tests --- lib/cadet/courses/courses.ex | 118 +++++++++--- lib/cadet/courses/sourcecast.ex | 4 +- .../controllers/sourcecast_controller.ex | 39 +++- lib/cadet_web/router.ex | 1 + ...0210531155751_add_course_configuration.exs | 4 + test/cadet/courses/courses_test.exs | 63 +++---- .../sourcecast_controller_test.exs | 171 +++++++++++++++--- test/factories/courses/sourcecast_factory.ex | 2 +- 8 files changed, 309 insertions(+), 93 deletions(-) diff --git a/lib/cadet/courses/courses.ex b/lib/cadet/courses/courses.ex index f06e11013..66117e777 100644 --- a/lib/cadet/courses/courses.ex +++ b/lib/cadet/courses/courses.ex @@ -7,7 +7,7 @@ defmodule Cadet.Courses do import Ecto.Query - alias Cadet.Accounts.User + alias Cadet.Accounts.CourseRegistration alias Cadet.Courses.{ AssessmentConfig, @@ -216,38 +216,102 @@ defmodule Cadet.Courses do @upload_file_roles ~w(admin staff)a @doc """ - Upload a sourcecast file + Upload a sourcecast file. + + Note that there are no checks for whether the user belongs to the course, as this has been checked + inside a plug in the router. """ + def upload_sourcecast_file( + _inserter = %CourseRegistration{user_id: user_id, course_id: course_id, role: role}, + attrs = %{} + ) do + if role in @upload_file_roles do + course_reg = + CourseRegistration + |> where(user_id: ^user_id) + |> where(course_id: ^course_id) + |> preload(:course) + |> preload(:user) + |> Repo.one() - # def upload_sourcecast_file(uploader = %User{role: role}, attrs = %{}) do - # if role in @upload_file_roles do - # changeset = - # %Sourcecast{} - # |> Sourcecast.changeset(attrs) - # |> put_assoc(:uploader, uploader) + changeset = + %Sourcecast{} + |> Sourcecast.changeset(attrs) + |> put_assoc(:uploader, course_reg.user) + |> put_assoc(:course, course_reg.course) - # case Repo.insert(changeset) do - # {:ok, sourcecast} -> - # {:ok, sourcecast} + case Repo.insert(changeset) do + {:ok, sourcecast} -> + {:ok, sourcecast} - # {:error, changeset} -> - # {:error, {:bad_request, full_error_messages(changeset)}} - # end - # else - # {:error, {:forbidden, "User is not permitted to upload"}} - # end - # end + {:error, changeset} -> + {:error, {:bad_request, full_error_messages(changeset)}} + end + else + {:error, {:forbidden, "User is not permitted to upload"}} + end + end + + @doc """ + Upload a public sourcecast file. + + Note that there are no checks for whether the user belongs to the course, as this has been checked + inside a plug in the router. + """ + def upload_sourcecast_file_public( + inserter, + _inserter_course_reg = %CourseRegistration{role: role}, + attrs = %{} + ) do + if role in @upload_file_roles do + changeset = + %Sourcecast{} + |> Sourcecast.changeset(attrs) + |> put_assoc(:uploader, inserter) + + case Repo.insert(changeset) do + {:ok, sourcecast} -> + {:ok, sourcecast} + + {:error, changeset} -> + {:error, {:bad_request, full_error_messages(changeset)}} + end + else + {:error, {:forbidden, "User is not permitted to upload"}} + end + end @doc """ Delete a sourcecast file + + Note that there are no checks for whether the user belongs to the course, as this has been checked + inside a plug in the router. """ - # def delete_sourcecast_file(_deleter = %User{role: role}, id) do - # if role in @upload_file_roles do - # sourcecast = Repo.get(Sourcecast, id) - # SourcecastUpload.delete({sourcecast.audio, sourcecast}) - # Repo.delete(sourcecast) - # else - # {:error, {:forbidden, "User is not permitted to delete"}} - # end - # end + def delete_sourcecast_file(_deleter = %CourseRegistration{role: role}, sourcecast_id) do + if role in @upload_file_roles do + sourcecast = Repo.get(Sourcecast, sourcecast_id) + SourcecastUpload.delete({sourcecast.audio, sourcecast}) + Repo.delete(sourcecast) + else + {:error, {:forbidden, "User is not permitted to delete"}} + end + end + + @doc """ + Get sourcecast files + """ + def get_sourcecast_files(course_id) when is_ecto_id(course_id) do + Sourcecast + |> where(course_id: ^course_id) + |> Repo.all() + |> Repo.preload(:uploader) + end + + def get_sourcecast_files do + Sourcecast + # Public sourcecasts are those without course_id + |> where([s], is_nil(s.course_id)) + |> Repo.all() + |> Repo.preload(:uploader) + end end diff --git a/lib/cadet/courses/sourcecast.ex b/lib/cadet/courses/sourcecast.ex index b8b01a529..a46f68fea 100644 --- a/lib/cadet/courses/sourcecast.ex +++ b/lib/cadet/courses/sourcecast.ex @@ -6,7 +6,7 @@ defmodule Cadet.Courses.Sourcecast do use Arc.Ecto.Schema alias Cadet.Accounts.User - alias Cadet.Courses.SourcecastUpload + alias Cadet.Courses.{Course, SourcecastUpload} schema "sourcecasts" do field(:title, :string) @@ -16,6 +16,7 @@ defmodule Cadet.Courses.Sourcecast do field(:audio, SourcecastUpload.Type) belongs_to(:uploader, User) + belongs_to(:course, Course) timestamps() end @@ -46,5 +47,6 @@ defmodule Cadet.Courses.Sourcecast do |> validate_required(@required_fields ++ @required_file_fields) |> validate_format(:uid, @regex) |> foreign_key_constraint(:uploader_id) + |> foreign_key_constraint(:course_id) end end diff --git a/lib/cadet_web/controllers/sourcecast_controller.ex b/lib/cadet_web/controllers/sourcecast_controller.ex index dc5cad9b9..b08560cd9 100644 --- a/lib/cadet_web/controllers/sourcecast_controller.ex +++ b/lib/cadet_web/controllers/sourcecast_controller.ex @@ -2,16 +2,39 @@ defmodule CadetWeb.SourcecastController do use CadetWeb, :controller use PhoenixSwagger - alias Cadet.{Repo, Courses} - alias Cadet.Courses.Sourcecast + alias Cadet.Courses + + def index(conn, %{"courseid" => course_id}) do + sourcecasts = Courses.get_sourcecast_files(course_id) + render(conn, "index.json", sourcecasts: sourcecasts) + end def index(conn, _params) do - sourcecasts = Sourcecast |> Repo.all() |> Repo.preload(:uploader) + sourcecasts = Courses.get_sourcecast_files() render(conn, "index.json", sourcecasts: sourcecasts) end + def create(conn, %{"sourcecast" => sourcecast, "public" => _public}) do + result = + Courses.upload_sourcecast_file_public( + conn.assigns.current_user, + conn.assigns.course_reg, + sourcecast + ) + + case result do + {:ok, _nil} -> + send_resp(conn, 200, "OK") + + {:error, {status, message}} -> + conn + |> put_status(status) + |> text(message) + end + end + def create(conn, %{"sourcecast" => sourcecast}) do - result = Courses.upload_sourcecast_file(conn.assigns.current_user, sourcecast) + result = Courses.upload_sourcecast_file(conn.assigns.course_reg, sourcecast) case result do {:ok, _nil} -> @@ -29,7 +52,7 @@ defmodule CadetWeb.SourcecastController do end def delete(conn, %{"id" => id}) do - result = Courses.delete_sourcecast_file(conn.assigns.current_user, id) + result = Courses.delete_sourcecast_file(conn.assigns.course_reg, id) case result do {:ok, _nil} -> @@ -62,6 +85,12 @@ defmodule CadetWeb.SourcecastController do security([%{JWT: []}]) parameters do + public( + :body, + :boolean, + "Uploads as public sourcecast when 'public' is specified regardless of truthy or falsy" + ) + sourcecast(:body, Schema.ref(:Sourcecast), "sourcecast object", required: true) end diff --git a/lib/cadet_web/router.ex b/lib/cadet_web/router.ex index 291990eef..d46259f7a 100644 --- a/lib/cadet_web/router.ex +++ b/lib/cadet_web/router.ex @@ -59,6 +59,7 @@ defmodule CadetWeb.Router do scope "/v2/course/:courseid", CadetWeb do pipe_through([:api, :auth, :ensure_auth, :course]) + get("/sourcecast", SourcecastController, :index) resources("/sourcecast", SourcecastController, only: [:create, :delete]) get("/assessments", AssessmentsController, :index) diff --git a/priv/repo/migrations/20210531155751_add_course_configuration.exs b/priv/repo/migrations/20210531155751_add_course_configuration.exs index 945b07c56..483bad5af 100644 --- a/priv/repo/migrations/20210531155751_add_course_configuration.exs +++ b/priv/repo/migrations/20210531155751_add_course_configuration.exs @@ -51,5 +51,9 @@ defmodule Cadet.Repo.Migrations.AddCourseConfiguration do end drop_if_exists(table(:sublanguages)) + + alter table(:sourcecasts) do + add(:course_id, references(:courses)) + end end end diff --git a/test/cadet/courses/courses_test.exs b/test/cadet/courses/courses_test.exs index 64d414259..a344a89c9 100644 --- a/test/cadet/courses/courses_test.exs +++ b/test/cadet/courses/courses_test.exs @@ -2,6 +2,7 @@ defmodule Cadet.CoursesTest do use Cadet.DataCase alias Cadet.{Courses, Repo} + alias Cadet.Courses.{Sourcecast, SourcecastUpload} describe "get course config" do test "succeeds" do @@ -355,38 +356,38 @@ defmodule Cadet.CoursesTest do end end - # describe "Sourcecast" do - # setup do - # on_exit(fn -> File.rm_rf!("uploads/test/sourcecasts") end) - # end + describe "Sourcecast" do + setup do + on_exit(fn -> File.rm_rf!("uploads/test/sourcecasts") end) + end - # test "upload file to folder then delete it" do - # uploader = insert(:user, %{role: :staff}) - - # upload = %Plug.Upload{ - # content_type: "audio/wav", - # filename: "upload.wav", - # path: "test/fixtures/upload.wav" - # } - - # result = - # Courses.upload_sourcecast_file(uploader, %{ - # title: "Test Upload", - # audio: upload, - # playbackData: - # "{\"init\":{\"editorValue\":\"// Type your program in here!\"},\"inputs\":[]}" - # }) - - # assert {:ok, sourcecast} = result - # path = SourcecastUpload.url({sourcecast.audio, sourcecast}) - # assert path =~ "/uploads/test/sourcecasts/upload.wav" - - # deleter = insert(:user, %{role: :staff}) - # assert {:ok, _} = Courses.delete_sourcecast_file(deleter, sourcecast.id) - # assert Repo.get(Sourcecast, sourcecast.id) == nil - # refute File.exists?("uploads/test/sourcecasts/upload.wav") - # end - # end + test "upload file to folder then delete it" do + inserter_course_registration = insert(:course_registration, %{role: :staff}) + + upload = %Plug.Upload{ + content_type: "audio/wav", + filename: "upload.wav", + path: "test/fixtures/upload.wav" + } + + result = + Courses.upload_sourcecast_file(inserter_course_registration, %{ + title: "Test Upload", + audio: upload, + playbackData: + "{\"init\":{\"editorValue\":\"// Type your program in here!\"},\"inputs\":[]}" + }) + + assert {:ok, sourcecast} = result + path = SourcecastUpload.url({sourcecast.audio, sourcecast}) + assert path =~ "/uploads/test/sourcecasts/upload.wav" + + deleter_course_registration = insert(:course_registration, %{role: :staff}) + assert {:ok, _} = Courses.delete_sourcecast_file(deleter_course_registration, sourcecast.id) + assert Repo.get(Sourcecast, sourcecast.id) == nil + refute File.exists?("uploads/test/sourcecasts/upload.wav") + end + end # describe "get_or_create_group" do # test "existing group" do diff --git a/test/cadet_web/controllers/sourcecast_controller_test.exs b/test/cadet_web/controllers/sourcecast_controller_test.exs index 7603a612e..ebb1d990d 100644 --- a/test/cadet_web/controllers/sourcecast_controller_test.exs +++ b/test/cadet_web/controllers/sourcecast_controller_test.exs @@ -10,11 +10,13 @@ defmodule CadetWeb.SourcecastControllerTest do SourcecastController.swagger_path_delete(nil) end - describe "GET /sourcecast, unauthenticated" do + describe "GET /v2/sourcecast, unauthenticated" do test "renders a list of all sourcecast entries for public", %{ conn: conn } do %{sourcecasts: sourcecasts} = seed_db() + course = insert(:course) + seed_db(course.id) expected = sourcecasts @@ -45,25 +47,30 @@ defmodule CadetWeb.SourcecastControllerTest do end end - describe "POST /sourcecast, unauthenticated" do + describe "POST /course/{courseId}/sourcecast, unauthenticated" do test "unauthorized", %{conn: conn} do - conn = post(conn, build_url(), %{}) + course = insert(:course) + conn = post(conn, build_url(course.id), %{}) assert response(conn, 401) =~ "Unauthorised" end end - describe "DELETE /sourcecast, unauthenticated" do + describe "DELETE /course/{courseId}/sourcecast, unauthenticated" do test "unauthorized", %{conn: conn} do - conn = delete(conn, build_url(1), %{}) + course = insert(:course) + seed_db(course.id) + conn = delete(conn, build_url(course.id, 1), %{}) assert response(conn, 401) =~ "Unauthorised" end end - describe "GET /sourcecast, all roles" do - test "renders a list of all sourcecast entries", %{ + describe "GET /sourcecast, returns public sourcecasts (those without course_id)" do + test "renders a list of all public sourcecast entries", %{ conn: conn } do + course = insert(:course) %{sourcecasts: sourcecasts} = seed_db() + seed_db(course.id) expected = sourcecasts @@ -94,11 +101,13 @@ defmodule CadetWeb.SourcecastControllerTest do end end - describe "POST /sourcecast, student" do + describe "POST /course/{courseId}/sourcecast, student" do @tag authenticate: :student test "prohibited", %{conn: conn} do + course_id = conn.assigns[:course_id] + conn = - post(conn, build_url(), %{ + post(conn, build_url(course_id), %{ "sourcecast" => %{ "title" => "Title", "description" => "Description", @@ -116,20 +125,47 @@ defmodule CadetWeb.SourcecastControllerTest do end end - describe "DELETE /sourcecast, student" do + describe "DELETE /course/{courseId}/sourcecast, student" do @tag authenticate: :student test "prohibited", %{conn: conn} do - conn = delete(conn, build_url(1), %{}) + course_id = conn.assigns[:course_id] + + conn = delete(conn, build_url(course_id, 1), %{}) assert response(conn, 403) =~ "User is not permitted to delete" end end - describe "POST /sourcecast, staff" do + describe "POST /course/{courseId}/sourcecast, staff" do + @tag authenticate: :staff + test "successful for public sourcecast", %{conn: conn} do + course_id = conn.assigns[:course_id] + + conn = + post(conn, build_url(course_id), %{ + "sourcecast" => %{ + "title" => "Title", + "description" => "Description", + "playbackData" => + "{\"init\":{\"editorValue\":\"// Type your program in here!\"},\"inputs\":[]}", + "audio" => %Plug.Upload{ + content_type: "audio/wav", + filename: "upload.wav", + path: "test/fixtures/upload.wav" + } + }, + "public" => true + }) + + assert response(conn, 200) == "OK" + end + @tag authenticate: :staff - test "successful", %{conn: conn} do + test "successful for course sourcecast", %{conn: conn} do + course_id = conn.assigns[:course_id] + conn = - post(conn, build_url(), %{ + post(conn, build_url(course_id), %{ "sourcecast" => %{ "title" => "Title", "description" => "Description", @@ -148,28 +184,69 @@ defmodule CadetWeb.SourcecastControllerTest do @tag authenticate: :staff test "missing parameter", %{conn: conn} do - conn = post(conn, build_url(), %{}) + course_id = conn.assigns[:course_id] + + conn = post(conn, build_url(course_id), %{}) assert response(conn, 400) =~ "Missing or invalid parameter(s)" end end - describe "DELETE /sourcecast, staff" do + describe "DELETE /course/{courseId}/sourcecast, staff" do @tag authenticate: :staff - test "successful", %{conn: conn} do + test "successful for public sourcecast", %{conn: conn} do + course_id = conn.assigns[:course_id] + %{sourcecasts: sourcecasts} = seed_db() sourcecast = List.first(sourcecasts) - conn = delete(conn, build_url(sourcecast.id), %{}) + conn = delete(conn, build_url(course_id, sourcecast.id), %{}) + + assert response(conn, 200) =~ "OK" + end + + @tag authenticate: :staff + test "successful for course sourcecast", %{conn: conn} do + course_id = conn.assigns[:course_id] + + %{sourcecasts: sourcecasts} = seed_db(course_id) + sourcecast = List.first(sourcecasts) + + conn = delete(conn, build_url(course_id, sourcecast.id), %{}) assert response(conn, 200) =~ "OK" end end - describe "POST /sourcecast, admin" do + describe "POST /course/{courseId}/sourcecast, admin" do @tag authenticate: :admin - test "successful", %{conn: conn} do + test "successful for public sourcecast", %{conn: conn} do + course_id = conn.assigns[:course_id] + conn = - post(conn, build_url(), %{ + post(conn, build_url(course_id), %{ + "sourcecast" => %{ + "title" => "Title", + "description" => "Description", + "playbackData" => + "{\"init\":{\"editorValue\":\"// Type your program in here!\"},\"inputs\":[]}", + "audio" => %Plug.Upload{ + content_type: "audio/wav", + filename: "upload.wav", + path: "test/fixtures/upload.wav" + } + }, + "public" => true + }) + + assert response(conn, 200) == "OK" + end + + @tag authenticate: :admin + test "successful for course sourcecast", %{conn: conn} do + course_id = conn.assigns[:course_id] + + conn = + post(conn, build_url(course_id), %{ "sourcecast" => %{ "title" => "Title", "description" => "Description", @@ -188,29 +265,67 @@ defmodule CadetWeb.SourcecastControllerTest do @tag authenticate: :admin test "missing parameter", %{conn: conn} do - conn = post(conn, build_url(), %{}) + course_id = conn.assigns[:course_id] + + conn = post(conn, build_url(course_id), %{}) assert response(conn, 400) =~ "Missing or invalid parameter(s)" end end - describe "DELETE /sourcecast, admin" do + describe "DELETE /course/{courseId}/sourcecast, admin" do @tag authenticate: :admin - test "successful", %{conn: conn} do + test "successful for public sourcecast", %{conn: conn} do + course_id = conn.assigns[:course_id] + %{sourcecasts: sourcecasts} = seed_db() sourcecast = List.first(sourcecasts) - conn = delete(conn, build_url(sourcecast.id), %{}) + conn = delete(conn, build_url(course_id, sourcecast.id), %{}) + + assert response(conn, 200) =~ "OK" + end + + @tag authenticate: :admin + test "successful for course sourcecast", %{conn: conn} do + course_id = conn.assigns[:course_id] + + %{sourcecasts: sourcecasts} = seed_db(course_id) + sourcecast = List.first(sourcecasts) + + conn = delete(conn, build_url(course_id, sourcecast.id), %{}) assert response(conn, 200) =~ "OK" end end - defp build_url, do: "v2/sourcecast/" - defp build_url(sourcecast_id), do: "#{build_url()}#{sourcecast_id}/" + defp build_url(), do: "/v2/sourcecast/" + defp build_url(course_id), do: "/v2/course/#{course_id}/sourcecast/" + defp build_url(course_id, sourcecast_id), do: "#{build_url(course_id)}#{sourcecast_id}/" - defp seed_db do + defp seed_db(course_id) do sourcecasts = for i <- 0..4 do + insert(:sourcecast, %{ + title: "Title#{i}", + description: "Description#{i}", + uid: "unique_id#{i}", + playbackData: + "{\"init\":{\"editorValue\":\"// Type your program in here!\"},\"inputs\":[]}", + audio: %Plug.Upload{ + content_type: "audio/wav", + filename: "upload#{i}.wav", + path: "test/fixtures/upload.wav" + }, + course_id: course_id + }) + end + + %{sourcecasts: sourcecasts} + end + + defp seed_db do + sourcecasts = + for i <- 5..9 do insert(:sourcecast, %{ title: "Title#{i}", description: "Description#{i}", diff --git a/test/factories/courses/sourcecast_factory.ex b/test/factories/courses/sourcecast_factory.ex index 048371dbe..ada1ff7fe 100644 --- a/test/factories/courses/sourcecast_factory.ex +++ b/test/factories/courses/sourcecast_factory.ex @@ -13,7 +13,7 @@ defmodule Cadet.Courses.SourcecastFactory do description: Faker.StarWars.planet(), audio: build(:upload), playbackData: Faker.StarWars.planet(), - uploader: build(:user, %{role: :staff}) + uploader: build(:user) } end end From 7e29acf87d4aac62b0594e82ec6af7e66f09ad06 Mon Sep 17 00:00:00 2001 From: YaleChen299 Date: Sun, 6 Jun 2021 22:22:12 +0800 Subject: [PATCH 033/174] updated test for course_registration insert and update --- lib/cadet/accounts/course_registrations.ex | 11 ++- .../accounts/course_registration_test.exs | 72 +++++++++++++++++-- 2 files changed, 75 insertions(+), 8 deletions(-) diff --git a/lib/cadet/accounts/course_registrations.ex b/lib/cadet/accounts/course_registrations.ex index 654bfedb2..6f4bea50a 100644 --- a/lib/cadet/accounts/course_registrations.ex +++ b/lib/cadet/accounts/course_registrations.ex @@ -35,6 +35,7 @@ defmodule Cadet.Accounts.CourseRegistrations do CourseRegistration |> where([cr], cr.user_id == ^id) |> join(:inner, [cr], c in assoc(cr, :course)) + |> preload(:course) |> Repo.all() end @@ -42,12 +43,18 @@ defmodule Cadet.Accounts.CourseRegistrations do CourseRegistration |> where([cr], cr.course_id == ^course_id) |> join(:inner, [cr], u in assoc(cr, :user)) + |> preload(:user) |> Repo.all() end def get_users(course_id, group_id) do - get_users(course_id) - |> where([cr, u], cr.group_id == ^group_id) + CourseRegistration + |> where([cr], cr.course_id == ^course_id) + |> where([cr], cr.group_id == ^group_id) + |> join(:inner, [cr], u in assoc(cr, :user)) + |> join(:inner, [cr, u], g in assoc(cr, :group)) + |> preload(:user) + |> preload(:group) |> Repo.all() # |> join(:inner, [cr, u], g in assoc(cr, :group)) diff --git a/test/cadet/accounts/course_registration_test.exs b/test/cadet/accounts/course_registration_test.exs index eba0e7004..4a48c21f2 100644 --- a/test/cadet/accounts/course_registration_test.exs +++ b/test/cadet/accounts/course_registration_test.exs @@ -8,19 +8,21 @@ defmodule Cadet.Accounts.CourseRegistrationTest do setup do user1 = insert(:user, %{name: "user 1"}) user2 = insert(:user, %{name: "user 2"}) - # group1 = insert(:group) + group1 = insert(:group, %{name: "group 1"}) + group2 = insert(:group, %{name: "group 2"}) course1 = insert(:course, %{module_code: "course 1"}) course2 = insert(:course, %{module_code: "course 2"}) changeset = CourseRegistration.changeset(%CourseRegistration{}, %{ course_id: course1.id, user_id: user1.id, + group_id: group1.id, role: :student }) {:ok, _course_reg} = Repo.insert(changeset) - {:ok, %{user1: user1, user2: user2, course1: course1, course2: course2, changeset: changeset}} + {:ok, %{user1: user1, user2: user2, group1: group1, group2: group2, course1: course1, course2: course2, changeset: changeset}} end describe "Changesets:" do @@ -112,14 +114,72 @@ defmodule Cadet.Accounts.CourseRegistrationTest do assert [] == CourseRegistrations.get_users(course2.id) end + test "of a group in a course succeeds", %{user1: user1, user2: user2, group1: group1, group2: group2, course1: course1} do + changeset2 = + CourseRegistration.changeset(%CourseRegistration{}, %{ + course_id: course1.id, + user_id: user2.id, + group_id: group2.id, + role: :student + }) + + {:ok, _course_reg} = Repo.insert(changeset2) + course_reg_course1_group1 = CourseRegistrations.get_users(course1.id, group1.id) + assert length(course_reg_course1_group1) == 1 + [hd|_] = course_reg_course1_group1 + assert user1.id == hd.user_id + assert group1.id == hd.group_id + assert course1.id == hd.course_id + end + test "of a group in a course failed due to invalid id", %{course1: course1}do + group2 = insert(:group, %{name: "group2"}) + assert [] == CourseRegistrations.get_users(course1.id, group2.id) + end + end - # test "" do + describe "update course_registration" do + test "successful insert", %{course1: course1, user2: user2} do + assert length(CourseRegistrations.get_users(course1.id)) == 1 + {:ok, course_reg} = CourseRegistrations.insert_or_update_course_registration(%{user_id: user2.id, course_id: course1.id, role: :student}) + assert length(CourseRegistrations.get_users(course1.id)) == 2 + assert course_reg.user_id == user2.id + assert course_reg.course_id == course1.id + end - # end + test "successful insert wil enroll", %{course1: course1, user2: user2} do + assert length(CourseRegistrations.get_users(course1.id)) == 1 + {:ok, course_reg} = CourseRegistrations.enroll_course(%{user_id: user2.id, course_id: course1.id, role: :student}) + assert length(CourseRegistrations.get_users(course1.id)) == 2 + assert course_reg.user_id == user2.id + assert course_reg.course_id == course1.id + end - # test "" do + test "successfully update role", %{course1: course1, user1: user1, group1: group1} do + assert length(CourseRegistrations.get_users(course1.id)) == 1 + {:ok, course_reg} = CourseRegistrations.insert_or_update_course_registration(%{user_id: user1.id, course_id: course1.id, role: :staff}) + assert length(CourseRegistrations.get_users(course1.id)) == 1 + assert course_reg.user_id == user1.id + assert course_reg.course_id == course1.id + assert course_reg.role == :staff + assert course_reg.group_id == group1.id + end - # end + test "successfully update group", %{course1: course1, user1: user1, group2: group2} do + assert length(CourseRegistrations.get_users(course1.id)) == 1 + {:ok, course_reg} = CourseRegistrations.insert_or_update_course_registration(%{user_id: user1.id, course_id: course1.id, role: :student, group_id: group2.id}) + assert length(CourseRegistrations.get_users(course1.id)) == 1 + assert course_reg.user_id == user1.id + assert course_reg.course_id == course1.id + assert course_reg.role == :student + assert course_reg.group_id == group2.id + end + + test "failed due to incomplete changeset", %{course1: course1, user2: user2} do + assert length(CourseRegistrations.get_users(course1.id)) == 1 + {:error, changeset} = CourseRegistrations.insert_or_update_course_registration(%{user_id: user2.id, course_id: course1.id}) + assert length(CourseRegistrations.get_users(course1.id)) == 1 + refute changeset.valid? + end end end From 7f2f6efed17094ccfb9812f4022c5df64be19abe Mon Sep 17 00:00:00 2001 From: YaleChen299 Date: Sun, 6 Jun 2021 23:04:50 +0800 Subject: [PATCH 034/174] updated course_registration test --- lib/cadet/accounts/course_registrations.ex | 12 +++--- .../accounts/course_registration_test.exs | 41 +++++++++++++++++-- 2 files changed, 44 insertions(+), 9 deletions(-) diff --git a/lib/cadet/accounts/course_registrations.ex b/lib/cadet/accounts/course_registrations.ex index 6f4bea50a..58293d577 100644 --- a/lib/cadet/accounts/course_registrations.ex +++ b/lib/cadet/accounts/course_registrations.ex @@ -75,22 +75,24 @@ defmodule Cadet.Accounts.CourseRegistrations do |> where(course_id: ^course_id) |> Repo.one() |> case do - nil -> - CourseRegistration.changeset(%CourseRegistration{}, params) + nil ->CourseRegistration.changeset(%CourseRegistration{}, params) - cr -> - CourseRegistration.changeset(cr, params) + cr -> CourseRegistration.changeset(cr, params) end |> Repo.insert_or_update() end @spec delete_record(map()) :: {:ok, Ecto.Schema.t()} | {:error, Ecto.Changeset.t()} - def delete_record(%{user_id: user_id, course_id: course_id}) + def delete_record(params = %{user_id: user_id, course_id: course_id}) when is_ecto_id(user_id) and is_ecto_id(course_id) do CourseRegistration |> where(user_id: ^user_id) |> where(course_id: ^course_id) |> Repo.one() + |> case do + nil -> CourseRegistration.changeset(%CourseRegistration{}, params) + cr -> CourseRegistration.changeset(cr, params) + end |> Repo.delete() end end diff --git a/test/cadet/accounts/course_registration_test.exs b/test/cadet/accounts/course_registration_test.exs index 4a48c21f2..e235206d4 100644 --- a/test/cadet/accounts/course_registration_test.exs +++ b/test/cadet/accounts/course_registration_test.exs @@ -88,7 +88,7 @@ defmodule Cadet.Accounts.CourseRegistrationTest do end test "of a user failed due to invalid id", %{user2: user2} do - assert [] == CourseRegistrations.get_courses(user2) + assert CourseRegistrations.get_courses(user2) == [] end test "of a course succeeds", %{user1: user1, user2: user2, course1: course1} do @@ -111,7 +111,7 @@ defmodule Cadet.Accounts.CourseRegistrationTest do end test "of a course failed due to invalid id", %{course2: course2} do - assert [] == CourseRegistrations.get_users(course2.id) + assert CourseRegistrations.get_users(course2.id) == [] end test "of a group in a course succeeds", %{user1: user1, user2: user2, group1: group1, group2: group2, course1: course1} do @@ -134,7 +134,7 @@ defmodule Cadet.Accounts.CourseRegistrationTest do test "of a group in a course failed due to invalid id", %{course1: course1}do group2 = insert(:group, %{name: "group2"}) - assert [] == CourseRegistrations.get_users(course1.id, group2.id) + assert CourseRegistrations.get_users(course1.id, group2.id) == [] end end @@ -147,7 +147,7 @@ defmodule Cadet.Accounts.CourseRegistrationTest do assert course_reg.course_id == course1.id end - test "successful insert wil enroll", %{course1: course1, user2: user2} do + test "successful insert through enroll_course", %{course1: course1, user2: user2} do assert length(CourseRegistrations.get_users(course1.id)) == 1 {:ok, course_reg} = CourseRegistrations.enroll_course(%{user_id: user2.id, course_id: course1.id, role: :student}) assert length(CourseRegistrations.get_users(course1.id)) == 2 @@ -182,4 +182,37 @@ defmodule Cadet.Accounts.CourseRegistrationTest do refute changeset.valid? end end + + describe "delete course_registration" do + test "succeeds", %{course1: course1, user1: user1} do + assert length(CourseRegistrations.get_users(course1.id)) == 1 + {:ok, _course_reg} = CourseRegistrations.delete_record(%{user_id: user1.id, course_id: course1.id, role: :student}) + assert CourseRegistrations.get_users(course1.id) == [] + end + + test "failed due to repeated removal", %{course1: course1, user1: user1} do + assert length(CourseRegistrations.get_users(course1.id)) == 1 + {:ok, _course_reg} = CourseRegistrations.delete_record(%{user_id: user1.id, course_id: course1.id, role: :student}) + assert CourseRegistrations.get_users(course1.id) == [] + assert_raise Ecto.NoPrimaryKeyValueError, fn -> + CourseRegistrations.delete_record(%{user_id: user1.id, course_id: course1.id, role: :student}) + end + end + + test "failed due to non existing entry", %{course1: course1, user2: user2} do + assert length(CourseRegistrations.get_users(course1.id)) == 1 + assert_raise Ecto.NoPrimaryKeyValueError, fn -> + CourseRegistrations.delete_record(%{user_id: user2.id, course_id: course1.id, role: :student}) + end + end + + test "failed due to invalid changeset", %{course1: course1, user2: user2} do + assert length(CourseRegistrations.get_users(course1.id)) == 1 + {:error, changeset} = CourseRegistrations.delete_record(%{user_id: user2.id, course_id: course1.id}) + assert length(CourseRegistrations.get_users(course1.id)) == 1 + refute changeset.valid? + end + + + end end From c653241ac81533852dec7a3a46d404d0d59eedc8 Mon Sep 17 00:00:00 2001 From: YaleChen299 Date: Sun, 6 Jun 2021 23:04:50 +0800 Subject: [PATCH 035/174] updated course_registration test --- lib/cadet/accounts/course_registrations.ex | 14 +++--- .../accounts/course_registration_test.exs | 44 ++++++++++++++++--- 2 files changed, 47 insertions(+), 11 deletions(-) diff --git a/lib/cadet/accounts/course_registrations.ex b/lib/cadet/accounts/course_registrations.ex index 6f4bea50a..8434701b8 100644 --- a/lib/cadet/accounts/course_registrations.ex +++ b/lib/cadet/accounts/course_registrations.ex @@ -68,29 +68,31 @@ defmodule Cadet.Accounts.CourseRegistrations do @spec insert_or_update_course_registration(map()) :: {:ok, Ecto.Schema.t()} | {:error, Ecto.Changeset.t()} - def insert_or_update_course_registration(params = %{user_id: user_id, course_id: course_id}) + def insert_or_update_course_registration(params = %{user_id: user_id, course_id: course_id, role: _role}) when is_ecto_id(user_id) and is_ecto_id(course_id) do CourseRegistration |> where(user_id: ^user_id) |> where(course_id: ^course_id) |> Repo.one() |> case do - nil -> - CourseRegistration.changeset(%CourseRegistration{}, params) + nil ->CourseRegistration.changeset(%CourseRegistration{}, params) - cr -> - CourseRegistration.changeset(cr, params) + cr -> CourseRegistration.changeset(cr, params) end |> Repo.insert_or_update() end @spec delete_record(map()) :: {:ok, Ecto.Schema.t()} | {:error, Ecto.Changeset.t()} - def delete_record(%{user_id: user_id, course_id: course_id}) + def delete_record(params = %{user_id: user_id, course_id: course_id}) when is_ecto_id(user_id) and is_ecto_id(course_id) do CourseRegistration |> where(user_id: ^user_id) |> where(course_id: ^course_id) |> Repo.one() + |> case do + nil -> CourseRegistration.changeset(%CourseRegistration{}, params) + cr -> CourseRegistration.changeset(cr, params) + end |> Repo.delete() end end diff --git a/test/cadet/accounts/course_registration_test.exs b/test/cadet/accounts/course_registration_test.exs index 4a48c21f2..537468f70 100644 --- a/test/cadet/accounts/course_registration_test.exs +++ b/test/cadet/accounts/course_registration_test.exs @@ -88,7 +88,7 @@ defmodule Cadet.Accounts.CourseRegistrationTest do end test "of a user failed due to invalid id", %{user2: user2} do - assert [] == CourseRegistrations.get_courses(user2) + assert CourseRegistrations.get_courses(user2) == [] end test "of a course succeeds", %{user1: user1, user2: user2, course1: course1} do @@ -111,7 +111,7 @@ defmodule Cadet.Accounts.CourseRegistrationTest do end test "of a course failed due to invalid id", %{course2: course2} do - assert [] == CourseRegistrations.get_users(course2.id) + assert CourseRegistrations.get_users(course2.id) == [] end test "of a group in a course succeeds", %{user1: user1, user2: user2, group1: group1, group2: group2, course1: course1} do @@ -134,7 +134,7 @@ defmodule Cadet.Accounts.CourseRegistrationTest do test "of a group in a course failed due to invalid id", %{course1: course1}do group2 = insert(:group, %{name: "group2"}) - assert [] == CourseRegistrations.get_users(course1.id, group2.id) + assert CourseRegistrations.get_users(course1.id, group2.id) == [] end end @@ -147,7 +147,7 @@ defmodule Cadet.Accounts.CourseRegistrationTest do assert course_reg.course_id == course1.id end - test "successful insert wil enroll", %{course1: course1, user2: user2} do + test "successful insert through enroll_course", %{course1: course1, user2: user2} do assert length(CourseRegistrations.get_users(course1.id)) == 1 {:ok, course_reg} = CourseRegistrations.enroll_course(%{user_id: user2.id, course_id: course1.id, role: :student}) assert length(CourseRegistrations.get_users(course1.id)) == 2 @@ -177,9 +177,43 @@ defmodule Cadet.Accounts.CourseRegistrationTest do test "failed due to incomplete changeset", %{course1: course1, user2: user2} do assert length(CourseRegistrations.get_users(course1.id)) == 1 - {:error, changeset} = CourseRegistrations.insert_or_update_course_registration(%{user_id: user2.id, course_id: course1.id}) + assert_raise FunctionClauseError, fn -> + CourseRegistrations.insert_or_update_course_registration(%{user_id: user2.id, course_id: course1.id}) + end + assert length(CourseRegistrations.get_users(course1.id)) == 1 + end + end + + describe "delete course_registration" do + test "succeeds", %{course1: course1, user1: user1} do + assert length(CourseRegistrations.get_users(course1.id)) == 1 + {:ok, _course_reg} = CourseRegistrations.delete_record(%{user_id: user1.id, course_id: course1.id, role: :student}) + assert CourseRegistrations.get_users(course1.id) == [] + end + + test "failed due to repeated removal", %{course1: course1, user1: user1} do + assert length(CourseRegistrations.get_users(course1.id)) == 1 + {:ok, _course_reg} = CourseRegistrations.delete_record(%{user_id: user1.id, course_id: course1.id, role: :student}) + assert CourseRegistrations.get_users(course1.id) == [] + assert_raise Ecto.NoPrimaryKeyValueError, fn -> + CourseRegistrations.delete_record(%{user_id: user1.id, course_id: course1.id, role: :student}) + end + end + + test "failed due to non existing entry", %{course1: course1, user2: user2} do + assert length(CourseRegistrations.get_users(course1.id)) == 1 + assert_raise Ecto.NoPrimaryKeyValueError, fn -> + CourseRegistrations.delete_record(%{user_id: user2.id, course_id: course1.id, role: :student}) + end + end + + test "failed due to invalid changeset", %{course1: course1, user2: user2} do + assert length(CourseRegistrations.get_users(course1.id)) == 1 + {:error, changeset} = CourseRegistrations.delete_record(%{user_id: user2.id, course_id: course1.id}) assert length(CourseRegistrations.get_users(course1.id)) == 1 refute changeset.valid? end + + end end From 268ae424d0cb458e7943964beb2aca730839a44f Mon Sep 17 00:00:00 2001 From: Chow En Rong <53928333+chownces@users.noreply.github.com> Date: Mon, 7 Jun 2021 00:32:30 +0800 Subject: [PATCH 036/174] Updated sourcecast tests --- lib/cadet_web/views/sourcecast_view.ex | 3 +- .../sourcecast_controller_test.exs | 107 ++++++++++++++---- 2 files changed, 87 insertions(+), 23 deletions(-) diff --git a/lib/cadet_web/views/sourcecast_view.ex b/lib/cadet_web/views/sourcecast_view.ex index 767d00633..0c9ca2f75 100644 --- a/lib/cadet_web/views/sourcecast_view.ex +++ b/lib/cadet_web/views/sourcecast_view.ex @@ -16,7 +16,8 @@ defmodule CadetWeb.SourcecastView do audio: :audio, playbackData: :playbackData, uploader: &transform_map_for_view(&1.uploader, [:name, :id]), - url: &Cadet.Courses.SourcecastUpload.url({&1.audio, &1}) + url: &Cadet.Courses.SourcecastUpload.url({&1.audio, &1}), + course_id: :course_id }) end end diff --git a/test/cadet_web/controllers/sourcecast_controller_test.exs b/test/cadet_web/controllers/sourcecast_controller_test.exs index ebb1d990d..e90582274 100644 --- a/test/cadet_web/controllers/sourcecast_controller_test.exs +++ b/test/cadet_web/controllers/sourcecast_controller_test.exs @@ -1,6 +1,10 @@ defmodule CadetWeb.SourcecastControllerTest do use CadetWeb.ConnCase + import Ecto.Query + + alias Cadet.Repo + alias Cadet.Courses.Course alias CadetWeb.SourcecastController test "swagger" do @@ -11,7 +15,7 @@ defmodule CadetWeb.SourcecastControllerTest do end describe "GET /v2/sourcecast, unauthenticated" do - test "renders a list of all sourcecast entries for public", %{ + test "renders a list of all sourcecast entries for public (those without course_id)", %{ conn: conn } do %{sourcecasts: sourcecasts} = seed_db() @@ -31,7 +35,8 @@ defmodule CadetWeb.SourcecastControllerTest do "name" => &1.uploader.name, "id" => &1.uploader.id }, - "url" => Cadet.Courses.SourcecastUpload.url({&1.audio, &1}) + "url" => Cadet.Courses.SourcecastUpload.url({&1.audio, &1}), + "course_id" => nil } ) @@ -47,7 +52,7 @@ defmodule CadetWeb.SourcecastControllerTest do end end - describe "POST /course/{courseId}/sourcecast, unauthenticated" do + describe "POST /v2/course/{courseId}/sourcecast, unauthenticated" do test "unauthorized", %{conn: conn} do course = insert(:course) conn = post(conn, build_url(course.id), %{}) @@ -55,7 +60,7 @@ defmodule CadetWeb.SourcecastControllerTest do end end - describe "DELETE /course/{courseId}/sourcecast, unauthenticated" do + describe "DELETE /v2/course/{courseId}/sourcecast, unauthenticated" do test "unauthorized", %{conn: conn} do course = insert(:course) seed_db(course.id) @@ -64,13 +69,14 @@ defmodule CadetWeb.SourcecastControllerTest do end end - describe "GET /sourcecast, returns public sourcecasts (those without course_id)" do - test "renders a list of all public sourcecast entries", %{ + describe "GET /v2/course/{courseId}/sourcecast, returns course sourcecasts" do + @tag authenticate: :student + test "renders a list of all course sourcecast entries", %{ conn: conn } do - course = insert(:course) - %{sourcecasts: sourcecasts} = seed_db() - seed_db(course.id) + course_id = conn.assigns[:course_id] + seed_db() + %{sourcecasts: sourcecasts} = seed_db(course_id) expected = sourcecasts @@ -85,13 +91,14 @@ defmodule CadetWeb.SourcecastControllerTest do "name" => &1.uploader.name, "id" => &1.uploader.id }, - "url" => Cadet.Courses.SourcecastUpload.url({&1.audio, &1}) + "url" => Cadet.Courses.SourcecastUpload.url({&1.audio, &1}), + "course_id" => course_id } ) res = conn - |> get(build_url()) + |> get(build_url(course_id)) |> json_response(200) |> Enum.map(&Map.delete(&1, "audio")) |> Enum.map(&Map.delete(&1, "inserted_at")) @@ -101,7 +108,7 @@ defmodule CadetWeb.SourcecastControllerTest do end end - describe "POST /course/{courseId}/sourcecast, student" do + describe "POST /v2/course/{courseId}/sourcecast, student" do @tag authenticate: :student test "prohibited", %{conn: conn} do course_id = conn.assigns[:course_id] @@ -125,7 +132,7 @@ defmodule CadetWeb.SourcecastControllerTest do end end - describe "DELETE /course/{courseId}/sourcecast, student" do + describe "DELETE /v2/course/{courseId}/sourcecast, student" do @tag authenticate: :student test "prohibited", %{conn: conn} do course_id = conn.assigns[:course_id] @@ -136,12 +143,12 @@ defmodule CadetWeb.SourcecastControllerTest do end end - describe "POST /course/{courseId}/sourcecast, staff" do + describe "POST /v2/course/{courseId}/sourcecast, staff" do @tag authenticate: :staff test "successful for public sourcecast", %{conn: conn} do course_id = conn.assigns[:course_id] - conn = + post_conn = post(conn, build_url(course_id), %{ "sourcecast" => %{ "title" => "Title", @@ -157,14 +164,41 @@ defmodule CadetWeb.SourcecastControllerTest do "public" => true }) - assert response(conn, 200) == "OK" + assert response(post_conn, 200) == "OK" + + expected = [ + %{ + "title" => "Title", + "description" => "Description", + "playbackData" => + "{\"init\":{\"editorValue\":\"// Type your program in here!\"},\"inputs\":[]}", + "uploader" => %{ + "id" => conn.assigns[:current_user].id, + "name" => conn.assigns[:current_user].name + }, + "course_id" => nil + } + ] + + res = + conn + |> get(build_url()) + |> json_response(200) + |> Enum.map(&Map.delete(&1, "audio")) + |> Enum.map(&Map.delete(&1, "inserted_at")) + |> Enum.map(&Map.delete(&1, "updated_at")) + |> Enum.map(&Map.delete(&1, "id")) + |> Enum.map(&Map.delete(&1, "uid")) + |> Enum.map(&Map.delete(&1, "url")) + + assert expected == res end @tag authenticate: :staff test "successful for course sourcecast", %{conn: conn} do course_id = conn.assigns[:course_id] - conn = + post_conn = post(conn, build_url(course_id), %{ "sourcecast" => %{ "title" => "Title", @@ -179,7 +213,34 @@ defmodule CadetWeb.SourcecastControllerTest do } }) - assert response(conn, 200) == "OK" + assert response(post_conn, 200) == "OK" + + expected = [ + %{ + "title" => "Title", + "description" => "Description", + "playbackData" => + "{\"init\":{\"editorValue\":\"// Type your program in here!\"},\"inputs\":[]}", + "uploader" => %{ + "id" => conn.assigns[:current_user].id, + "name" => conn.assigns[:current_user].name + }, + "course_id" => course_id + } + ] + + res = + conn + |> get(build_url(course_id)) + |> json_response(200) + |> Enum.map(&Map.delete(&1, "audio")) + |> Enum.map(&Map.delete(&1, "inserted_at")) + |> Enum.map(&Map.delete(&1, "updated_at")) + |> Enum.map(&Map.delete(&1, "id")) + |> Enum.map(&Map.delete(&1, "uid")) + |> Enum.map(&Map.delete(&1, "url")) + + assert expected == res end @tag authenticate: :staff @@ -191,7 +252,7 @@ defmodule CadetWeb.SourcecastControllerTest do end end - describe "DELETE /course/{courseId}/sourcecast, staff" do + describe "DELETE /v2/course/{courseId}/sourcecast, staff" do @tag authenticate: :staff test "successful for public sourcecast", %{conn: conn} do course_id = conn.assigns[:course_id] @@ -217,7 +278,7 @@ defmodule CadetWeb.SourcecastControllerTest do end end - describe "POST /course/{courseId}/sourcecast, admin" do + describe "POST /v2/course/{courseId}/sourcecast, admin" do @tag authenticate: :admin test "successful for public sourcecast", %{conn: conn} do course_id = conn.assigns[:course_id] @@ -272,7 +333,7 @@ defmodule CadetWeb.SourcecastControllerTest do end end - describe "DELETE /course/{courseId}/sourcecast, admin" do + describe "DELETE /v2/course/{courseId}/sourcecast, admin" do @tag authenticate: :admin test "successful for public sourcecast", %{conn: conn} do course_id = conn.assigns[:course_id] @@ -303,6 +364,8 @@ defmodule CadetWeb.SourcecastControllerTest do defp build_url(course_id, sourcecast_id), do: "#{build_url(course_id)}#{sourcecast_id}/" defp seed_db(course_id) do + course = Course |> where(id: ^course_id) |> Repo.one() + sourcecasts = for i <- 0..4 do insert(:sourcecast, %{ @@ -316,7 +379,7 @@ defmodule CadetWeb.SourcecastControllerTest do filename: "upload#{i}.wav", path: "test/fixtures/upload.wav" }, - course_id: course_id + course: course }) end From a7ab26178352a1a20d9555c1b9b1fd35ed55e2e3 Mon Sep 17 00:00:00 2001 From: Chow En Rong <53928333+chownces@users.noreply.github.com> Date: Mon, 7 Jun 2021 00:42:26 +0800 Subject: [PATCH 037/174] Updated assessment types tests --- lib/cadet/courses/assessment_types.ex | 1 + test/cadet/courses/courses_test.exs | 74 +++++++++---------- .../courses/assessment_types_factory.ex | 1 - 3 files changed, 38 insertions(+), 38 deletions(-) diff --git a/lib/cadet/courses/assessment_types.ex b/lib/cadet/courses/assessment_types.ex index 95b1350d5..424923762 100644 --- a/lib/cadet/courses/assessment_types.ex +++ b/lib/cadet/courses/assessment_types.ex @@ -26,6 +26,7 @@ defmodule Cadet.Courses.AssessmentTypes do |> validate_number(:order, greater_than: 0) |> validate_number(:order, less_than_or_equal_to: 5) |> unique_constraint([:type, :course_id]) + |> unique_constraint([:order, :course_id]) end defp capitalize(params, field) do diff --git a/test/cadet/courses/courses_test.exs b/test/cadet/courses/courses_test.exs index a344a89c9..0788edb78 100644 --- a/test/cadet/courses/courses_test.exs +++ b/test/cadet/courses/courses_test.exs @@ -159,11 +159,11 @@ defmodule Cadet.CoursesTest do course = insert(:course) course_id = course.id - insert(:assessment_types, %{order: 1, type: "Missions", course_id: course_id}) - insert(:assessment_types, %{order: 2, type: "Quests", course_id: course_id}) - insert(:assessment_types, %{order: 3, type: "Paths", course_id: course_id}) - insert(:assessment_types, %{order: 4, type: "Contests", course_id: course_id}) - insert(:assessment_types, %{order: 5, type: "Others", course_id: course_id}) + insert(:assessment_types, %{order: 1, type: "Missions", course: course}) + insert(:assessment_types, %{order: 2, type: "Quests", course: course}) + insert(:assessment_types, %{order: 3, type: "Paths", course: course}) + insert(:assessment_types, %{order: 4, type: "Contests", course: course}) + insert(:assessment_types, %{order: 5, type: "Others", course: course}) :ok = Courses.update_assessment_types(course_id, [ @@ -189,11 +189,11 @@ defmodule Cadet.CoursesTest do course = insert(:course) course_id = course.id - insert(:assessment_types, %{order: 4, type: "Contests", course_id: course_id}) - insert(:assessment_types, %{order: 1, type: "Missions", course_id: course_id}) - insert(:assessment_types, %{order: 3, type: "Paths", course_id: course_id}) - insert(:assessment_types, %{order: 5, type: "Others", course_id: course_id}) - insert(:assessment_types, %{order: 2, type: "Quests", course_id: course_id}) + insert(:assessment_types, %{order: 4, type: "Contests", course: course}) + insert(:assessment_types, %{order: 1, type: "Missions", course: course}) + insert(:assessment_types, %{order: 3, type: "Paths", course: course}) + insert(:assessment_types, %{order: 5, type: "Others", course: course}) + insert(:assessment_types, %{order: 2, type: "Quests", course: course}) :ok = Courses.update_assessment_types(course_id, [ @@ -219,11 +219,11 @@ defmodule Cadet.CoursesTest do course = insert(:course) course_id = course.id - insert(:assessment_types, %{order: 1, type: "Missions", course_id: course_id}) - insert(:assessment_types, %{order: 2, type: "Quests", course_id: course_id}) - insert(:assessment_types, %{order: 3, type: "Paths", course_id: course_id}) - insert(:assessment_types, %{order: 4, type: "Contests", course_id: course_id}) - insert(:assessment_types, %{order: 5, type: "Others", course_id: course_id}) + insert(:assessment_types, %{order: 1, type: "Missions", course: course}) + insert(:assessment_types, %{order: 2, type: "Quests", course: course}) + insert(:assessment_types, %{order: 3, type: "Paths", course: course}) + insert(:assessment_types, %{order: 4, type: "Contests", course: course}) + insert(:assessment_types, %{order: 5, type: "Others", course: course}) :ok = Courses.update_assessment_types(course_id, [ @@ -249,9 +249,9 @@ defmodule Cadet.CoursesTest do course = insert(:course) course_id = course.id - insert(:assessment_types, %{order: 1, type: "Missions", course_id: course_id}) - insert(:assessment_types, %{order: 2, type: "Quests", course_id: course_id}) - insert(:assessment_types, %{order: 3, type: "Paths", course_id: course_id}) + insert(:assessment_types, %{order: 1, type: "Missions", course: course}) + insert(:assessment_types, %{order: 2, type: "Quests", course: course}) + insert(:assessment_types, %{order: 3, type: "Paths", course: course}) :ok = Courses.update_assessment_types(course_id, [ @@ -277,10 +277,10 @@ defmodule Cadet.CoursesTest do course = insert(:course) course_id = course.id - insert(:assessment_types, %{order: 1, type: "Missions", course_id: course_id}) - insert(:assessment_types, %{order: 2, type: "Quests", course_id: course_id}) - insert(:assessment_types, %{order: 3, type: "Paths", course_id: course_id}) - insert(:assessment_types, %{order: 4, type: "Contests", course_id: course_id}) + insert(:assessment_types, %{order: 1, type: "Missions", course: course}) + insert(:assessment_types, %{order: 2, type: "Quests", course: course}) + insert(:assessment_types, %{order: 3, type: "Paths", course: course}) + insert(:assessment_types, %{order: 4, type: "Contests", course: course}) :ok = Courses.update_assessment_types(course_id, ["Paths", "Quests", "Missions"]) {:ok, updated_course_config} = Courses.get_course_config(course_id) @@ -292,9 +292,9 @@ defmodule Cadet.CoursesTest do course = insert(:course) course_id = course.id - insert(:assessment_types, %{order: 1, type: "Missions", course_id: course_id}) - insert(:assessment_types, %{order: 2, type: "Quests", course_id: course_id}) - insert(:assessment_types, %{order: 3, type: "Paths", course_id: course_id}) + insert(:assessment_types, %{order: 1, type: "Missions", course: course}) + insert(:assessment_types, %{order: 2, type: "Quests", course: course}) + insert(:assessment_types, %{order: 3, type: "Paths", course: course}) assert {:error, {:bad_request, "Invalid parameter(s)"}} = Courses.update_assessment_types(course_id, [1, "Quests", "Missions"]) @@ -304,9 +304,9 @@ defmodule Cadet.CoursesTest do course = insert(:course) course_id = course.id - insert(:assessment_types, %{order: 1, type: "Missions", course_id: course_id}) - insert(:assessment_types, %{order: 2, type: "Quests", course_id: course_id}) - insert(:assessment_types, %{order: 3, type: "Paths", course_id: course_id}) + insert(:assessment_types, %{order: 1, type: "Missions", course: course}) + insert(:assessment_types, %{order: 2, type: "Quests", course: course}) + insert(:assessment_types, %{order: 3, type: "Paths", course: course}) assert {:error, {:bad_request, "Invalid parameter(s)"}} = Courses.update_assessment_types(course_id, ["Missions", "Quests", "Missions"]) @@ -316,9 +316,9 @@ defmodule Cadet.CoursesTest do course = insert(:course) course_id = course.id - insert(:assessment_types, %{order: 1, type: "Missions", course_id: course_id}) - insert(:assessment_types, %{order: 2, type: "Quests", course_id: course_id}) - insert(:assessment_types, %{order: 3, type: "Paths", course_id: course_id}) + insert(:assessment_types, %{order: 1, type: "Missions", course: course}) + insert(:assessment_types, %{order: 2, type: "Quests", course: course}) + insert(:assessment_types, %{order: 3, type: "Paths", course: course}) assert {:error, {:bad_request, "Invalid parameter(s)"}} = Courses.update_assessment_types(course_id, []) @@ -328,9 +328,9 @@ defmodule Cadet.CoursesTest do course = insert(:course) course_id = course.id - insert(:assessment_types, %{order: 1, type: "Missions", course_id: course_id}) - insert(:assessment_types, %{order: 2, type: "Quests", course_id: course_id}) - insert(:assessment_types, %{order: 3, type: "Paths", course_id: course_id}) + insert(:assessment_types, %{order: 1, type: "Missions", course: course}) + insert(:assessment_types, %{order: 2, type: "Quests", course: course}) + insert(:assessment_types, %{order: 3, type: "Paths", course: course}) assert {:error, {:bad_request, "Invalid parameter(s)"}} = Courses.update_assessment_types(course_id, [ @@ -347,9 +347,9 @@ defmodule Cadet.CoursesTest do course = insert(:course) course_id = course.id - insert(:assessment_types, %{order: 1, type: "Missions", course_id: course_id}) - insert(:assessment_types, %{order: 2, type: "Quests", course_id: course_id}) - insert(:assessment_types, %{order: 3, type: "Paths", course_id: course_id}) + insert(:assessment_types, %{order: 1, type: "Missions", course: course}) + insert(:assessment_types, %{order: 2, type: "Quests", course: course}) + insert(:assessment_types, %{order: 3, type: "Paths", course: course}) assert {:error, {:bad_request, "Invalid parameter(s)"}} = Courses.update_assessment_types(course_id, "Missions") diff --git a/test/factories/courses/assessment_types_factory.ex b/test/factories/courses/assessment_types_factory.ex index ac15917dc..d11cc455e 100644 --- a/test/factories/courses/assessment_types_factory.ex +++ b/test/factories/courses/assessment_types_factory.ex @@ -11,7 +11,6 @@ defmodule Cadet.Courses.AssessmentTypesFactory do %AssessmentTypes{ order: 1, type: "Missions" - # course: build(:course) } end end From 9c09f16860c74e0ba941ae23d22b4de736691e93 Mon Sep 17 00:00:00 2001 From: YaleChen299 Date: Mon, 7 Jun 2021 11:24:06 +0800 Subject: [PATCH 038/174] fix course_registration_test.exs --- .../accounts/course_registration_test.exs | 35 ------------------- 1 file changed, 35 deletions(-) diff --git a/test/cadet/accounts/course_registration_test.exs b/test/cadet/accounts/course_registration_test.exs index 4d6837e05..e4228f437 100644 --- a/test/cadet/accounts/course_registration_test.exs +++ b/test/cadet/accounts/course_registration_test.exs @@ -213,40 +213,5 @@ defmodule Cadet.Accounts.CourseRegistrationTest do assert length(CourseRegistrations.get_users(course1.id)) == 1 refute changeset.valid? end - - - end - - describe "delete course_registration" do - test "succeeds", %{course1: course1, user1: user1} do - assert length(CourseRegistrations.get_users(course1.id)) == 1 - {:ok, _course_reg} = CourseRegistrations.delete_record(%{user_id: user1.id, course_id: course1.id, role: :student}) - assert CourseRegistrations.get_users(course1.id) == [] - end - - test "failed due to repeated removal", %{course1: course1, user1: user1} do - assert length(CourseRegistrations.get_users(course1.id)) == 1 - {:ok, _course_reg} = CourseRegistrations.delete_record(%{user_id: user1.id, course_id: course1.id, role: :student}) - assert CourseRegistrations.get_users(course1.id) == [] - assert_raise Ecto.NoPrimaryKeyValueError, fn -> - CourseRegistrations.delete_record(%{user_id: user1.id, course_id: course1.id, role: :student}) - end - end - - test "failed due to non existing entry", %{course1: course1, user2: user2} do - assert length(CourseRegistrations.get_users(course1.id)) == 1 - assert_raise Ecto.NoPrimaryKeyValueError, fn -> - CourseRegistrations.delete_record(%{user_id: user2.id, course_id: course1.id, role: :student}) - end - end - - test "failed due to invalid changeset", %{course1: course1, user2: user2} do - assert length(CourseRegistrations.get_users(course1.id)) == 1 - {:error, changeset} = CourseRegistrations.delete_record(%{user_id: user2.id, course_id: course1.id}) - assert length(CourseRegistrations.get_users(course1.id)) == 1 - refute changeset.valid? - end - - end end From c408f8521607c1d30f343d1551fd65b667da3f76 Mon Sep 17 00:00:00 2001 From: Chow En Rong <53928333+chownces@users.noreply.github.com> Date: Mon, 7 Jun 2021 17:05:18 +0800 Subject: [PATCH 039/174] Updated sourcecast changeset --- lib/cadet/courses/sourcecast.ex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/cadet/courses/sourcecast.ex b/lib/cadet/courses/sourcecast.ex index a46f68fea..330663ab8 100644 --- a/lib/cadet/courses/sourcecast.ex +++ b/lib/cadet/courses/sourcecast.ex @@ -22,7 +22,7 @@ defmodule Cadet.Courses.Sourcecast do end @required_fields ~w(title playbackData uid)a - @optional_fields ~w(description)a + @optional_fields ~w(description course_id)a @required_file_fields ~w(audio)a @regex Regex.compile!("^[a-zA-Z0-9_-]*$") From f991d77d2f76560054d7cbe8838fa3c0f9ef2314 Mon Sep 17 00:00:00 2001 From: Chow En Rong <53928333+chownces@users.noreply.github.com> Date: Mon, 7 Jun 2021 17:16:50 +0800 Subject: [PATCH 040/174] Updated stories schema and tests --- lib/cadet/stories/stories.ex | 129 ++++++++------ lib/cadet/stories/story.ex | 6 +- .../controllers/stories_controller.ex | 27 +-- lib/cadet_web/views/stories_view.ex | 3 +- ...0210531155751_add_course_configuration.exs | 4 + test/cadet/stories/stories_test.exs | 132 +++++++++++--- .../controllers/stories_controller_test.exs | 162 +++++++++++++----- test/factories/stories/story_factory.ex | 3 +- 8 files changed, 341 insertions(+), 125 deletions(-) diff --git a/lib/cadet/stories/stories.ex b/lib/cadet/stories/stories.ex index f664e3adf..d996c5961 100644 --- a/lib/cadet/stories/stories.ex +++ b/lib/cadet/stories/stories.ex @@ -2,60 +2,93 @@ defmodule Cadet.Stories.Stories do @moduledoc """ Manages stories for the Source Academy game """ + use Cadet, [:context] import Ecto.Query alias Cadet.Repo - alias Cadet.Accounts.User + alias Cadet.Accounts.CourseRegistration alias Cadet.Stories.Story + alias Cadet.Courses.Course @manage_stories_role ~w(staff admin)a - # def list_stories(_user = %User{role: role}) do - # if role in @manage_stories_role do - # Repo.all(Story) - # else - # Story - # |> where(is_published: ^true) - # |> where([s], s.open_at <= ^Timex.now()) - # |> Repo.all() - # end - # end - - # def create_story(attrs = %{}, _user = %User{role: role}) do - # if role in @manage_stories_role do - # %Story{} - # |> Story.changeset(attrs) - # |> Repo.insert() - # else - # {:error, {:forbidden, "User not allowed to manage stories"}} - # end - # end - - # def update_story(attrs = %{}, id, _user = %User{role: role}) do - # if role in @manage_stories_role do - # case Repo.get(Story, id) do - # nil -> - # {:error, {:not_found, "Story not found"}} - - # story -> - # story - # |> Story.changeset(attrs) - # |> Repo.update() - # end - # else - # {:error, {:forbidden, "User not allowed to manage stories"}} - # end - # end - - # def delete_story(id, _user = %User{role: role}) do - # if role in @manage_stories_role do - # case Repo.get(Story, id) do - # nil -> {:error, {:not_found, "Story not found"}} - # story -> Repo.delete(story) - # end - # else - # {:error, {:forbidden, "User not allowed to manage stories"}} - # end - # end + def list_stories( + _user_course_registration = %CourseRegistration{course_id: course_id, role: role} + ) do + if role in @manage_stories_role do + Story + |> where(course_id: ^course_id) + |> Repo.all() + else + Story + |> where(course_id: ^course_id) + |> where(is_published: ^true) + |> where([s], s.open_at <= ^Timex.now()) + |> Repo.all() + end + end + + def create_story( + attrs = %{}, + _user_course_registration = %CourseRegistration{course_id: course_id, role: role} + ) do + if role in @manage_stories_role do + course = + Course + |> where(id: ^course_id) + |> Repo.one() + + %Story{} + |> Story.changeset(Map.put(attrs, :course_id, course.id)) + |> Repo.insert() + else + {:error, {:forbidden, "User not allowed to manage stories"}} + end + end + + def update_story( + attrs = %{}, + id, + _user_course_registration = %CourseRegistration{course_id: course_id, role: role} + ) do + if role in @manage_stories_role do + case Repo.get(Story, id) do + nil -> + {:error, {:not_found, "Story not found"}} + + story -> + if story.course_id == course_id do + story + |> Story.changeset(attrs) + |> Repo.update() + else + {:error, {:forbidden, "User not allowed to manage stories from another course"}} + end + end + else + {:error, {:forbidden, "User not allowed to manage stories"}} + end + end + + def delete_story( + id, + _user_course_registration = %CourseRegistration{course_id: course_id, role: role} + ) do + if role in @manage_stories_role do + case Repo.get(Story, id) do + nil -> + {:error, {:not_found, "Story not found"}} + + story -> + if story.course_id == course_id do + Repo.delete(story) + else + {:error, {:forbidden, "User not allowed to manage stories from another course"}} + end + end + else + {:error, {:forbidden, "User not allowed to manage stories"}} + end + end end diff --git a/lib/cadet/stories/story.ex b/lib/cadet/stories/story.ex index a789f9788..0d7ca679b 100644 --- a/lib/cadet/stories/story.ex +++ b/lib/cadet/stories/story.ex @@ -4,6 +4,8 @@ defmodule Cadet.Stories.Story do """ use Cadet, :model + alias Cadet.Courses.Course + schema "stories" do field(:open_at, :utc_datetime_usec) field(:close_at, :utc_datetime_usec) @@ -12,10 +14,12 @@ defmodule Cadet.Stories.Story do field(:image_url, :string) field(:filenames, {:array, :string}) + belongs_to(:course, Course) + timestamps() end - @required_fields ~w(open_at close_at title filenames)a + @required_fields ~w(open_at close_at title filenames course_id)a @optional_fields ~w(is_published image_url)a def changeset(story, attrs \\ %{}) do diff --git a/lib/cadet_web/controllers/stories_controller.ex b/lib/cadet_web/controllers/stories_controller.ex index 71cb846f5..4c169b013 100644 --- a/lib/cadet_web/controllers/stories_controller.ex +++ b/lib/cadet_web/controllers/stories_controller.ex @@ -5,7 +5,7 @@ defmodule CadetWeb.StoriesController do alias Cadet.Stories.Stories def index(conn, _) do - stories = Stories.list_stories(conn.assigns.current_user) + stories = Stories.list_stories(conn.assigns.course_reg) render(conn, "index.json", stories: stories) end @@ -13,7 +13,8 @@ defmodule CadetWeb.StoriesController do result = story |> snake_casify_string_keys() - |> Stories.create_story(conn.assigns.current_user) + |> string_to_atom_map_keys() + |> Stories.create_story(conn.assigns.course_reg) case result do {:ok, _story} -> @@ -30,7 +31,7 @@ defmodule CadetWeb.StoriesController do result = story |> snake_casify_string_keys() - |> Stories.update_story(id, conn.assigns.current_user) + |> Stories.update_story(id, conn.assigns.course_reg) case result do {:ok, _story} -> @@ -44,7 +45,7 @@ defmodule CadetWeb.StoriesController do end def delete(conn, _params = %{"storyid" => id}) do - result = Stories.delete_story(id, conn.assigns.current_user) + result = Stories.delete_story(id, conn.assigns.course_reg) case result do {:ok, _nil} -> @@ -57,19 +58,22 @@ defmodule CadetWeb.StoriesController do end end + defp string_to_atom_map_keys(map) do + for {key, value} <- map, into: %{}, do: {key |> String.to_atom(), value} + end + swagger_path :index do - get("/stories") + get("/v2/course/{courseId}/stories") summary("Get a list of all stories") security([%{JWT: []}]) response(200, "OK", Schema.array(:Story)) - response(403, "User not allowed to manage stories") end swagger_path :create do - post("/stories") + post("/v2/course/{courseId}/stories") summary("Creates a new story") @@ -81,7 +85,7 @@ defmodule CadetWeb.StoriesController do end swagger_path :delete do - PhoenixSwagger.Path.delete("/stories/{storyId}") + PhoenixSwagger.Path.delete("/v2/course/{courseId}/stories/{storyId}") summary("Delete a story from database by id") @@ -92,12 +96,12 @@ defmodule CadetWeb.StoriesController do security([%{JWT: []}]) response(204, "OK") - response(403, "User not allowed to manage stories") + response(403, "User not allowed to manage stories or stories from another course") response(404, "Story not found") end swagger_path :update do - post("/stories/{storyId}") + post("/v2/course/{courseId}/stories/{storyId}") summary("Update details regarding a story") @@ -110,7 +114,7 @@ defmodule CadetWeb.StoriesController do produces("application/json") response(200, "OK", :Story) - response(403, "User not allowed to manage stories") + response(403, "User not allowed to manage stories or stories from another course") response(404, "Story not found") end @@ -126,6 +130,7 @@ defmodule CadetWeb.StoriesController do openAt(:string, "The opening date", format: "date-time", required: true) closeAt(:string, "The closing date", format: "date-time", required: true) isPublished(:boolean, "Whether or not is published", required: false) + courseId(:integer, "The id of the course that this story belongs to", required: true) end end } diff --git a/lib/cadet_web/views/stories_view.ex b/lib/cadet_web/views/stories_view.ex index cd3a02da4..00508e6c7 100644 --- a/lib/cadet_web/views/stories_view.ex +++ b/lib/cadet_web/views/stories_view.ex @@ -13,7 +13,8 @@ defmodule CadetWeb.StoriesView do imageUrl: :image_url, isPublished: :is_published, openAt: &format_datetime(&1.open_at), - closeAt: &format_datetime(&1.close_at) + closeAt: &format_datetime(&1.close_at), + courseId: :course_id }) end end diff --git a/priv/repo/migrations/20210531155751_add_course_configuration.exs b/priv/repo/migrations/20210531155751_add_course_configuration.exs index 483bad5af..87a9041f8 100644 --- a/priv/repo/migrations/20210531155751_add_course_configuration.exs +++ b/priv/repo/migrations/20210531155751_add_course_configuration.exs @@ -55,5 +55,9 @@ defmodule Cadet.Repo.Migrations.AddCourseConfiguration do alter table(:sourcecasts) do add(:course_id, references(:courses)) end + + alter table(:stories) do + add(:course_id, references(:courses), null: false) + end end end diff --git a/test/cadet/stories/stories_test.exs b/test/cadet/stories/stories_test.exs index 2d763b76c..36390838b 100644 --- a/test/cadet/stories/stories_test.exs +++ b/test/cadet/stories/stories_test.exs @@ -1,6 +1,5 @@ defmodule Cadet.StoriesTest do alias Cadet.Stories.{Story, Stories} - alias Cadet.Accounts.User use Cadet.ChangesetCase, entity: Story @@ -24,7 +23,8 @@ defmodule Cadet.StoriesTest do describe "Changesets" do test "valid params", %{valid_params: params} do - assert_changeset_db(params, :valid) + course = insert(:course) + assert_changeset_db(Map.put(params, :course_id, course.id), :valid) end test "invalid params", %{valid_params: params} do @@ -34,48 +34,138 @@ defmodule Cadet.StoriesTest do end describe "List stories" do - test "All stories" do - story1 = insert(:story) - story2 = insert(:story) - assert Stories.list_stories(%User{role: :staff}) == [story1, story2] + test "All stories from own course" do + course = insert(:course) + story1 = insert(:story, %{course: course}) |> remove_course_assoc() + story2 = insert(:story, %{course: course}) |> remove_course_assoc() + + assert Stories.list_stories(insert(:course_registration, %{course: course, role: :staff})) == + [story1, story2] + end + + test "Does not list stories from other courses" do + course = insert(:course) + insert(:story) + story2 = insert(:story, %{course: course}) |> remove_course_assoc() + + assert Stories.list_stories(insert(:course_registration, %{course: course, role: :staff})) == + [story2] end test "Only show published and open stories", %{valid_params: params} do one_week_ago = Timex.shift(Timex.now(), weeks: -1) - insert(:story) - insert(:story, %{params | :is_published => true}) - insert(:story, %{params | :open_at => one_week_ago}) + course = insert(:course) + insert(:story, %{course: course}) + insert(:story, %{Map.put(params, :course, course) | :is_published => true}) + insert(:story, %{Map.put(params, :course, course) | :open_at => one_week_ago}) published_open_story = - insert(:story, %{params | :is_published => true, :open_at => one_week_ago}) - - assert Stories.list_stories(%User{role: :student}) == [published_open_story] + insert( + :story, + %{Map.put(params, :course, course) | :is_published => true, :open_at => one_week_ago} + ) + |> remove_course_assoc() + + assert Stories.list_stories(insert(:course_registration, %{course: course, role: :student})) == + [published_open_story] end end describe "Create story" do - test "create story", %{valid_params: params} do - {:ok, story} = Stories.create_story(params, %User{role: :staff}) + test "create course story as staff", %{valid_params: params} do + course_registration = insert(:course_registration, %{role: :staff}) + {:ok, story} = Stories.create_story(params, course_registration) + params = Map.put(params, :course_id, course_registration.course_id) + assert story |> Map.take(params |> Map.keys()) == params end + + test "students not allowed to create story", %{valid_params: params} do + course_registration = insert(:course_registration, %{role: :student}) + + assert {:error, {:forbidden, "User not allowed to manage stories"}} = + Stories.create_story(params, course_registration) + end end describe "Update story" do - test "update story", %{updated_params: updated_params} do - story = insert(:story) - {:ok, story} = Stories.update_story(updated_params, story.id, %User{role: :staff}) + test "updating story as staff in own course", %{updated_params: updated_params} do + course_registration = insert(:course_registration, %{role: :staff}) + story = insert(:story, %{course: course_registration.course}) + {:ok, updated_story} = Stories.update_story(updated_params, story.id, course_registration) + updated_params = Map.put(updated_params, :course_id, course_registration.course_id) + + assert updated_story |> Map.take(updated_params |> Map.keys()) == updated_params + end + + test "updating story that does not exist as staff", %{updated_params: updated_params} do + course_registration = insert(:course_registration, %{role: :staff}) + story = insert(:story, %{course: course_registration.course}) + + {:error, {:not_found, "Story not found"}} = + Stories.update_story(updated_params, story.id + 1, course_registration) + end - assert story |> Map.take(updated_params |> Map.keys()) == updated_params + test "staff fails to update story of another course", %{updated_params: updated_params} do + course_registration = insert(:course_registration, %{role: :staff}) + story = insert(:story, %{course: build(:course)}) + + assert {:error, {:forbidden, "User not allowed to manage stories from another course"}} = + Stories.update_story(updated_params, story.id, course_registration) + end + + test "student fails to update story of own course", %{updated_params: updated_params} do + course_registration = insert(:course_registration, %{role: :student}) + story = insert(:story, %{course: course_registration.course}) + + assert {:error, {:forbidden, "User not allowed to manage stories"}} = + Stories.update_story(updated_params, story.id, course_registration) end end describe "Delete story" do - test "delete story" do - story = insert(:story) - {:ok, story} = Stories.delete_story(story.id, %User{role: :staff}) + test "staff deleting course story from own course" do + course_registration = insert(:course_registration, %{role: :staff}) + story = insert(:story, %{course: course_registration.course}) + {:ok, story} = Stories.delete_story(story.id, course_registration) assert Repo.get(Story, story.id) == nil end + + test "staff deleting course story that does not exist" do + course_registration = insert(:course_registration, %{role: :staff}) + story = insert(:story, %{course: course_registration.course}) + + assert {:error, {:not_found, "Story not found"}} = + Stories.delete_story(story.id + 1, course_registration) + end + + test "staff fails to delete story from another course" do + course_registration = insert(:course_registration, %{role: :staff}) + story = insert(:story, %{course: build(:course)}) + + assert {:error, {:forbidden, "User not allowed to manage stories from another course"}} = + Stories.delete_story(story.id, course_registration) + end + + test "student fails to delete story from own course" do + course_registration = insert(:course_registration, %{role: :student}) + story = insert(:story, %{course: course_registration.course}) + + assert {:error, {:forbidden, "User not allowed to manage stories"}} = + Stories.delete_story(story.id, course_registration) + end + end + + defp remove_course_assoc(story) do + %{ + story + | :course => %Ecto.Association.NotLoaded{ + __field__: :course, + __owner__: story.__struct__, + __cardinality__: :one + } + } end end diff --git a/test/cadet_web/controllers/stories_controller_test.exs b/test/cadet_web/controllers/stories_controller_test.exs index df81f1b98..954cb892d 100644 --- a/test/cadet_web/controllers/stories_controller_test.exs +++ b/test/cadet_web/controllers/stories_controller_test.exs @@ -4,6 +4,7 @@ defmodule CadetWeb.StoriesControllerTest do import Ecto.Query + alias Cadet.Courses.Course alias Cadet.Repo alias Cadet.Stories.Story alias CadetWeb.StoriesController @@ -34,44 +35,61 @@ defmodule CadetWeb.StoriesControllerTest do StoriesController.swagger_path_update(nil) end - describe "public access, unauthenticated" do - test "GET /stories/", %{conn: conn} do - conn = get(conn, build_url(), %{}) + describe "unauthenticated" do + test "GET /v2/course/{courseId}/stories/", %{conn: conn} do + course = insert(:course) + conn = get(conn, build_url(course.id), %{}) assert response(conn, 401) =~ "Unauthorised" end - test "POST /stories/new", %{conn: conn} do - conn = post(conn, build_url("new"), %{}) + test "POST /v2/course/{courseId}/stories/", %{conn: conn} do + course = insert(:course) + conn = post(conn, build_url(course.id), %{}) assert response(conn, 401) =~ "Unauthorised" end - test "DELETE /stories/:storyid", %{conn: conn} do - conn = delete(conn, build_url("storyid"), %{}) + test "DELETE /v2/course/{courseId}/stories/:storyid", %{conn: conn} do + course = insert(:course) + conn = delete(conn, build_url(course.id, "storyid"), %{}) assert response(conn, 401) =~ "Unauthorised" end - test "POST /stories/:storyid", %{conn: conn} do - conn = post(conn, build_url("storyid"), %{}) + test "POST /v2/course/{courseId}/stories/:storyid", %{conn: conn} do + course = insert(:course) + conn = post(conn, build_url(course.id, "storyid"), %{}) assert response(conn, 401) =~ "Unauthorised" end end - describe "GET /stories" do + describe "GET /v2/course/{courseId}/stories" do @tag authenticate: :student - test "student permission, only obtain published open stories", %{ + test "student permission, only obtain published open stories from own course", %{ conn: conn, valid_params: params } do + course_id = conn.assigns[:course_id] + course = Course |> where(id: ^course_id) |> Repo.one() one_week_ago = Timex.shift(Timex.now(), weeks: -1) - insert(:story) - insert(:story, %{params | :is_published => true}) - insert(:story, %{params | :open_at => one_week_ago}) - insert(:story, %{params | :is_published => true, :open_at => one_week_ago}) + insert(:story, %{course: course}) + insert(:story, %{Map.put(params, :course, course) | :is_published => true}) + insert(:story, %{Map.put(params, :course, course) | :open_at => one_week_ago}) + + insert(:story, %{ + Map.put(params, :course, course) + | :is_published => true, + :open_at => one_week_ago + }) + + insert(:story, %{ + Map.put(params, :course, build(:course)) + | :is_published => true, + :open_at => one_week_ago + }) {:ok, resp} = conn - |> get(build_url()) + |> get(build_url(course_id)) |> response(200) |> Jason.decode() @@ -79,17 +97,26 @@ defmodule CadetWeb.StoriesControllerTest do end @tag authenticate: :staff - test "obtain all stories", %{conn: conn, valid_params: params} do + test "obtain all stories from own course", %{conn: conn, valid_params: params} do + course_id = conn.assigns[:course_id] + course = Course |> where(id: ^course_id) |> Repo.one() one_week_ago = Timex.shift(Timex.now(), weeks: -1) - insert(:story) - insert(:story, %{params | :is_published => true}) - insert(:story, %{params | :open_at => one_week_ago}) - insert(:story, %{params | :is_published => true, :open_at => one_week_ago}) + insert(:story, %{course: course}) + insert(:story, %{Map.put(params, :course, course) | :is_published => true}) + insert(:story, %{Map.put(params, :course, course) | :open_at => one_week_ago}) + + insert(:story, %{ + Map.put(params, :course, course) + | :is_published => true, + :open_at => one_week_ago + }) + + insert(:story, %{course: build(:course)}) {:ok, resp} = conn - |> get(build_url()) + |> get(build_url(course_id)) |> response(200) |> Jason.decode() @@ -98,15 +125,18 @@ defmodule CadetWeb.StoriesControllerTest do @tag authenticate: :staff test "All fields are present and in the right format", %{conn: conn} do - insert(:story) + course_id = conn.assigns[:course_id] + course = Course |> where(id: ^course_id) |> Repo.one() + + insert(:story, %{course: course}) {:ok, [resp]} = conn - |> get(build_url()) + |> get(build_url(course_id)) |> response(200) |> Jason.decode() - required_fields = ~w(openAt closeAt isPublished id title filenames imageUrl) + required_fields = ~w(openAt closeAt isPublished id title filenames imageUrl courseId) Enum.each(required_fields, fn required_field -> value = resp[required_field] @@ -116,78 +146,126 @@ defmodule CadetWeb.StoriesControllerTest do "id" -> assert is_integer(value) "filenames" -> assert is_list(value) "isPublished" -> assert is_boolean(value) + "courseId" -> assert is_integer(value) _ -> assert is_binary(value) end end) end end - describe "DELETE /stories/:storyid" do + describe "DELETE /v2/course/{courseId}/stories/:storyid" do @tag authenticate: :student test "student permission, forbidden", %{conn: conn} do - conn = delete(conn, build_url(1), %{}) + course_id = conn.assigns[:course_id] + course = Course |> where(id: ^course_id) |> Repo.one() + story = insert(:story, %{course: course}) + + conn = delete(conn, build_url(course_id, story.id), %{}) assert response(conn, 403) =~ "User not allowed to manage stories" end @tag authenticate: :staff - test "deletes story", %{conn: conn} do - to_be_deleted = insert(:story) - resp = delete(conn, build_url(to_be_deleted.id), %{}) + test "staff successfully deletes story from own course", %{conn: conn} do + course_id = conn.assigns[:course_id] + course = Course |> where(id: ^course_id) |> Repo.one() + story = insert(:story, %{course: course}) + + resp = delete(conn, build_url(course_id, story.id), %{}) assert Story - |> where(id: ^to_be_deleted.id) + |> where(id: ^story.id) |> Repo.one() == nil assert response(resp, 204) == "" end + + @tag authenticate: :staff + test "staff fails to delete story from another course", %{conn: conn} do + course_id = conn.assigns[:course_id] + story = insert(:story, %{course: build(:course)}) + + resp = delete(conn, build_url(course_id, story.id), %{}) + + assert response(resp, 403) == "User not allowed to manage stories from another course" + end end - describe "POST /stories/new" do + describe "POST /v2/course/{courseId}/stories/" do @tag authenticate: :student test "student permission, forbidden", %{conn: conn, valid_params: params} do - conn = post(conn, build_url(), params) + course_id = conn.assigns[:course_id] + + conn = post(conn, build_url(course_id), params) assert response(conn, 403) =~ "User not allowed to manage stories" end @tag authenticate: :staff test "creates a new story", %{conn: conn, valid_params: params} do - conn = post(conn, build_url(), stringify_camelise_keys(params)) + course_id = conn.assigns[:course_id] + + conn = post(conn, build_url(course_id), stringify_camelise_keys(params)) inserted_story = Story |> where(title: ^params.title) |> Repo.one() + params = Map.put(params, :course_id, course_id) assert inserted_story |> Map.take(Map.keys(params)) == params - assert response(conn, 200) == "" end end - describe "POST /stories/:storyid" do + describe "POST /v2/course/{courseId}/stories/:storyid" do @tag authenticate: :student test "student permission, forbidden", %{conn: conn, valid_params: params} do - conn = post(conn, build_url(1), %{"story" => params}) + course_id = conn.assigns[:course_id] + + conn = post(conn, build_url(course_id), %{"story" => params}) assert response(conn, 403) =~ "User not allowed to manage stories" end @tag authenticate: :staff - test "updates a story", %{conn: conn, updated_params: updated_params} do - story = insert(:story) + test "staff successfully updates a story from own course", %{ + conn: conn, + updated_params: updated_params + } do + course_id = conn.assigns[:course_id] + course = Course |> where(id: ^course_id) |> Repo.one() + story = insert(:story, %{course: course}) conn = - post(conn, build_url(story.id), %{"story" => stringify_camelise_keys(updated_params)}) + post(conn, build_url(course_id, story.id), %{ + "story" => stringify_camelise_keys(updated_params) + }) updated_story = Repo.get(Story, story.id) + updated_params = Map.put(updated_params, :course_id, course_id) assert updated_story |> Map.take(Map.keys(updated_params)) == updated_params assert response(conn, 200) == "" end + + @tag authenticate: :staff + test "staff fails to update a story from another course", %{ + conn: conn, + updated_params: updated_params + } do + course_id = conn.assigns[:course_id] + story = insert(:story, %{course: build(:course)}) + + resp = + post(conn, build_url(course_id, story.id), %{ + "story" => stringify_camelise_keys(updated_params) + }) + + assert response(resp, 403) == "User not allowed to manage stories from another course" + end end - defp build_url, do: "/v2/stories" - defp build_url(url), do: "#{build_url()}/#{url}" + defp build_url(course_id), do: "/v2/course/#{course_id}/stories" + defp build_url(course_id, story_id), do: "#{build_url(course_id)}/#{story_id}" defp stringify_camelise_keys(map) do for {key, value} <- map, into: %{}, do: {key |> Atom.to_string() |> Recase.to_camel(), value} diff --git a/test/factories/stories/story_factory.ex b/test/factories/stories/story_factory.ex index 48d985721..fc4c9fa6e 100644 --- a/test/factories/stories/story_factory.ex +++ b/test/factories/stories/story_factory.ex @@ -14,7 +14,8 @@ defmodule Cadet.Stories.StoryFactory do is_published: false, filenames: ["mission-1.txt"], title: "Mission1", - image_url: "http://example.com" + image_url: "http://example.com", + course: build(:course) } end end From e119fb3b6b30c8bcfe2cdbf914ed8bbecf08b786 Mon Sep 17 00:00:00 2001 From: YaleChen299 Date: Mon, 7 Jun 2021 17:29:43 +0800 Subject: [PATCH 041/174] upadted assessment schema and relevant test --- lib/cadet/assessments/assessment.ex | 31 +++- priv/repo/seeds.exs | 4 +- test/cadet/assessments/assessment_test.exs | 142 +++++++++++++++--- .../accounts/course_registration_factory.ex | 1 - .../courses/assessment_types_factory.ex | 4 +- 5 files changed, 154 insertions(+), 28 deletions(-) diff --git a/lib/cadet/assessments/assessment.ex b/lib/cadet/assessments/assessment.ex index 3165c0676..f179fdc8f 100644 --- a/lib/cadet/assessments/assessment.ex +++ b/lib/cadet/assessments/assessment.ex @@ -6,10 +6,12 @@ defmodule Cadet.Assessments.Assessment do use Cadet, :model use Arc.Ecto.Schema + alias Cadet.Repo alias Cadet.Assessments.{AssessmentAccess, Question, SubmissionStatus, Upload} + alias Cadet.Courses.{Course, AssessmentTypes} - @assessment_types ~w(contest mission path practical sidequest) - def assessment_types, do: @assessment_types + # @assessment_types ~w(contest mission path practical sidequest) + # def assessment_types, do: @assessment_types schema "assessments" do field(:access, AssessmentAccess, virtual: true, default: :public) @@ -23,7 +25,6 @@ defmodule Cadet.Assessments.Assessment do field(:graded_count, :integer, virtual: true) field(:title, :string) field(:is_published, :boolean, default: false) - field(:type, :string) field(:summary_short, :string) field(:summary_long, :string) field(:open_at, :utc_datetime_usec) @@ -35,11 +36,15 @@ defmodule Cadet.Assessments.Assessment do field(:reading, :string) field(:password, :string, default: nil) + # field(:type, :string) + belongs_to(:type, AssessmentTypes) + belongs_to(:course, Course) + has_many(:questions, Question, on_delete: :delete_all) timestamps() end - @required_fields ~w(type title open_at close_at number)a + @required_fields ~w(title open_at close_at number course_id type_id)a @optional_fields ~w(reading summary_short summary_long is_published story cover_picture access password)a @optional_file_fields ~w(mission_pdf)a @@ -54,10 +59,26 @@ defmodule Cadet.Assessments.Assessment do |> cast_attachments(params, @optional_file_fields) |> cast(params, @required_fields ++ @optional_fields) |> validate_required(@required_fields) - |> validate_inclusion(:type, @assessment_types) + # |> validate_inclusion(:type, @assessment_types) + |> validate_type_course |> validate_open_close_date end + defp validate_type_course(changeset) do + type_id = get_field(changeset, :type_id) + course_id = get_field(changeset, :course_id) + + case Repo.get(AssessmentTypes, type_id) do + nil -> add_error(changeset, :type, "does not exist") + + type -> if type.course_id == course_id do + changeset + else + add_error(changeset, :type, "does not belong to the same course as this assessment") + end + end + end + defp validate_open_close_date(changeset) do validate_change(changeset, :open_at, fn :open_at, open_at -> if Timex.before?(open_at, get_field(changeset, :close_at)) do diff --git a/priv/repo/seeds.exs b/priv/repo/seeds.exs index 97f83f26c..f1883f41a 100644 --- a/priv/repo/seeds.exs +++ b/priv/repo/seeds.exs @@ -24,8 +24,8 @@ if Cadet.Env.env() == :dev do mentor = insert(:user, %{name: "mentor", username: "E1234562"}) admin = insert(:user, %{name: "admin", username: "E1234563"}) studenta = insert(:user, %{username: "E1234564"}) - studentb = insert(:user, %{username: "E1234564"}) - studentc = insert(:user, %{username: "E1234564"}) + studentb = insert(:user, %{username: "E1234565"}) + studentc = insert(:user, %{username: "E1234566"}) # CourseRegistration and Group avenger1 = insert(:course_registration, %{user: avenger, course: course1, role: :staff}) mentor1 = insert(:course_registration, %{user: mentor, course: course1, role: :staff}) diff --git a/test/cadet/assessments/assessment_test.exs b/test/cadet/assessments/assessment_test.exs index fdf62e3e9..2c780624f 100644 --- a/test/cadet/assessments/assessment_test.exs +++ b/test/cadet/assessments/assessment_test.exs @@ -3,11 +3,21 @@ defmodule Cadet.Assessments.AssessmentTest do use Cadet.ChangesetCase, entity: Assessment + setup do + course1 = insert(:course, %{module_code: "course 1"}) + course2 = insert(:course, %{module_code: "course 2"}) + type1 = insert(:assessment_types, %{course: course1}) + type2 = insert(:assessment_types, %{course: course2}) + + {:ok, %{course1: course1, course2: course2, type1: type1, type2: type2}} + end + describe "Changesets" do - test "valid changesets" do + test "valid changesets", %{course1: course1, course2: course2, type1: type1, type2: type2} do assert_changeset( %{ - type: "mission", + type_id: type1.id, + course_id: course1.id, title: "mission", number: "M#{Enum.random(0..10)}", open_at: Timex.now() |> Timex.to_unix() |> Integer.to_string(), @@ -18,7 +28,8 @@ defmodule Cadet.Assessments.AssessmentTest do assert_changeset( %{ - type: Enum.random(Assessment.assessment_types()), + type_id: type2.id, + course_id: course2.id, title: "mission", number: "M#{Enum.random(0..10)}", open_at: Timex.now() |> Timex.to_unix() |> Integer.to_string(), @@ -30,33 +41,128 @@ defmodule Cadet.Assessments.AssessmentTest do ) end - test "invalid changesets" do - assert_changeset(%{type: "mission", title: "mission", max_grade: 100}, :invalid) - + test "invalid changesets missing required params", %{course1: course1, type1: type1} do assert_changeset( %{ + type_id: type1.id, + course_id: course1.id, title: "mission", - open_at: Timex.now(), - close_at: Timex.shift(Timex.now(), days: 7), - max_grade: 100 + number: "M#{Enum.random(0..10)}", }, :invalid ) - assert_changeset( %{ - type: "misc", - title: "invalid type", - number: "M#{Enum.random(1..10)}", + type_id: type1.id, + course_id: course1.id, + title: "mission", open_at: Timex.now() |> Timex.to_unix() |> Integer.to_string(), - close_at: - Timex.now() - |> Timex.shift(days: Enum.random(1..7)) - |> Timex.to_unix() - |> Integer.to_string() + close_at: Timex.now() |> Timex.shift(days: 7) |> Timex.to_unix() |> Integer.to_string() }, :invalid ) end + + test "invalid changesets due to type_course", %{course1: course1, type1: type1, type2: type2} do + type_not_in_course = Assessment.changeset(%Assessment{}, %{ + type_id: type2.id, + course_id: course1.id, + title: "mission", + number: "4", + open_at: Timex.now(), + close_at: Timex.shift(Timex.now(), days: 7), + }) + {:error, changeset} = Repo.insert(type_not_in_course) + assert changeset.errors == [{:type, {"does not belong to the same course as this assessment", []}}] + refute changeset.valid? + + + type_not_exist = Assessment.changeset(%Assessment{}, %{ + type_id: type1.id + type2.id, + course_id: course1.id, + title: "invalid type", + number: "M#{Enum.random(1..10)}", + open_at: Timex.now() |> Timex.to_unix() |> Integer.to_string(), + close_at: + Timex.now() + |> Timex.shift(days: Enum.random(1..7)) + |> Timex.to_unix() + |> Integer.to_string() + }) + {:error, changeset2} = Repo.insert(type_not_exist) + assert changeset2.errors == [{:type, {"does not exist", []}}] + refute changeset2.valid? + end + + test "invalid changesets due to invalid dates", %{course1: course1, type1: type1} do + invalid_date = Assessment.changeset(%Assessment{}, %{ + type_id: type1.id, + course_id: course1.id, + title: "mission", + number: "4", + open_at: Timex.shift(Timex.now(), days: 7), + close_at: Timex.now(), + }) + {:error, changeset} = Repo.insert(invalid_date) + assert changeset.errors == [{:open_at, {"Open date must be before close date", []}}] + refute changeset.valid? + end end + # describe "Changesets" do + # test "valid changesets" do + # assert_changeset( + # %{ + # type_id: "mission", + # course_id: + # title: "mission", + # number: "M#{Enum.random(0..10)}", + # open_at: Timex.now() |> Timex.to_unix() |> Integer.to_string(), + # close_at: Timex.now() |> Timex.shift(days: 7) |> Timex.to_unix() |> Integer.to_string() + # }, + # :valid + # ) + + # assert_changeset( + # %{ + # type: Enum.random(Assessment.assessment_types()), + # title: "mission", + # number: "M#{Enum.random(0..10)}", + # open_at: Timex.now() |> Timex.to_unix() |> Integer.to_string(), + # close_at: Timex.now() |> Timex.shift(days: 7) |> Timex.to_unix() |> Integer.to_string(), + # cover_picture: Faker.Avatar.image_url(), + # mission_pdf: build_upload("test/fixtures/upload.pdf", "application/pdf") + # }, + # :valid + # ) + # end + + # test "invalid changesets" do + # assert_changeset(%{type: "mission", title: "mission", max_grade: 100}, :invalid) + + # assert_changeset( + # %{ + # title: "mission", + # open_at: Timex.now(), + # close_at: Timex.shift(Timex.now(), days: 7), + # max_grade: 100 + # }, + # :invalid + # ) + + # assert_changeset( + # %{ + # type: "misc", + # title: "invalid type", + # number: "M#{Enum.random(1..10)}", + # open_at: Timex.now() |> Timex.to_unix() |> Integer.to_string(), + # close_at: + # Timex.now() + # |> Timex.shift(days: Enum.random(1..7)) + # |> Timex.to_unix() + # |> Integer.to_string() + # }, + # :invalid + # ) + # end + # end end diff --git a/test/factories/accounts/course_registration_factory.ex b/test/factories/accounts/course_registration_factory.ex index dde296493..15cba58c0 100644 --- a/test/factories/accounts/course_registration_factory.ex +++ b/test/factories/accounts/course_registration_factory.ex @@ -12,7 +12,6 @@ defmodule Cadet.Accounts.CourseRegistrationFactory do %CourseRegistration{ user: build(:user), course: build(:course), - # :TODO Group factory is currently wrongly configured group: build(:group), role: Enum.random(Role.__enum_map__()), game_states: %{} diff --git a/test/factories/courses/assessment_types_factory.ex b/test/factories/courses/assessment_types_factory.ex index ac15917dc..51a74c521 100644 --- a/test/factories/courses/assessment_types_factory.ex +++ b/test/factories/courses/assessment_types_factory.ex @@ -10,8 +10,8 @@ defmodule Cadet.Courses.AssessmentTypesFactory do def assessment_types_factory do %AssessmentTypes{ order: 1, - type: "Missions" - # course: build(:course) + type: "Missions", + course: build(:course) } end end From 58ee16dc1e023e31e64b28041333ce721c464921 Mon Sep 17 00:00:00 2001 From: YaleChen299 Date: Tue, 8 Jun 2021 15:24:59 +0800 Subject: [PATCH 042/174] update assessment factory --- lib/cadet/assessments/assessment.ex | 2 - test/cadet/assessments/assessment_test.exs | 57 ------------------- .../assessments/assessment_factory.ex | 7 ++- 3 files changed, 5 insertions(+), 61 deletions(-) diff --git a/lib/cadet/assessments/assessment.ex b/lib/cadet/assessments/assessment.ex index f179fdc8f..3d5eb18c7 100644 --- a/lib/cadet/assessments/assessment.ex +++ b/lib/cadet/assessments/assessment.ex @@ -36,7 +36,6 @@ defmodule Cadet.Assessments.Assessment do field(:reading, :string) field(:password, :string, default: nil) - # field(:type, :string) belongs_to(:type, AssessmentTypes) belongs_to(:course, Course) @@ -59,7 +58,6 @@ defmodule Cadet.Assessments.Assessment do |> cast_attachments(params, @optional_file_fields) |> cast(params, @required_fields ++ @optional_fields) |> validate_required(@required_fields) - # |> validate_inclusion(:type, @assessment_types) |> validate_type_course |> validate_open_close_date end diff --git a/test/cadet/assessments/assessment_test.exs b/test/cadet/assessments/assessment_test.exs index 2c780624f..a53f32836 100644 --- a/test/cadet/assessments/assessment_test.exs +++ b/test/cadet/assessments/assessment_test.exs @@ -108,61 +108,4 @@ defmodule Cadet.Assessments.AssessmentTest do refute changeset.valid? end end - # describe "Changesets" do - # test "valid changesets" do - # assert_changeset( - # %{ - # type_id: "mission", - # course_id: - # title: "mission", - # number: "M#{Enum.random(0..10)}", - # open_at: Timex.now() |> Timex.to_unix() |> Integer.to_string(), - # close_at: Timex.now() |> Timex.shift(days: 7) |> Timex.to_unix() |> Integer.to_string() - # }, - # :valid - # ) - - # assert_changeset( - # %{ - # type: Enum.random(Assessment.assessment_types()), - # title: "mission", - # number: "M#{Enum.random(0..10)}", - # open_at: Timex.now() |> Timex.to_unix() |> Integer.to_string(), - # close_at: Timex.now() |> Timex.shift(days: 7) |> Timex.to_unix() |> Integer.to_string(), - # cover_picture: Faker.Avatar.image_url(), - # mission_pdf: build_upload("test/fixtures/upload.pdf", "application/pdf") - # }, - # :valid - # ) - # end - - # test "invalid changesets" do - # assert_changeset(%{type: "mission", title: "mission", max_grade: 100}, :invalid) - - # assert_changeset( - # %{ - # title: "mission", - # open_at: Timex.now(), - # close_at: Timex.shift(Timex.now(), days: 7), - # max_grade: 100 - # }, - # :invalid - # ) - - # assert_changeset( - # %{ - # type: "misc", - # title: "invalid type", - # number: "M#{Enum.random(1..10)}", - # open_at: Timex.now() |> Timex.to_unix() |> Integer.to_string(), - # close_at: - # Timex.now() - # |> Timex.shift(days: Enum.random(1..7)) - # |> Timex.to_unix() - # |> Integer.to_string() - # }, - # :invalid - # ) - # end - # end end diff --git a/test/factories/assessments/assessment_factory.ex b/test/factories/assessments/assessment_factory.ex index b756ce94b..794c19b2b 100644 --- a/test/factories/assessments/assessment_factory.ex +++ b/test/factories/assessments/assessment_factory.ex @@ -8,7 +8,9 @@ defmodule Cadet.Assessments.AssessmentFactory do alias Cadet.Assessments.Assessment def assessment_factory do - type = Enum.random(Assessment.assessment_types() -- ["practical"]) + # type = Enum.random(Assessment.assessment_types() -- ["practical"]) + type = build(:assessment_types) + type_title = type.title # These are actual story identifiers so front-end can use seeds to test more effectively valid_stories = [ @@ -28,11 +30,12 @@ defmodule Cadet.Assessments.AssessmentFactory do number: sequence( :number, - &"#{type |> String.first() |> String.upcase()}#{&1}" + &"#{type_title |> String.first() |> String.upcase()}#{&1}" ), story: Enum.random(valid_stories), reading: Faker.Lorem.sentence(), type: type, + course: build(:course), open_at: Timex.now(), close_at: Timex.shift(Timex.now(), days: Enum.random(1..30)), is_published: false From 3370defcb5000f5d8f300e7d300086d87cc5a309 Mon Sep 17 00:00:00 2001 From: YaleChen299 Date: Tue, 8 Jun 2021 17:36:11 +0800 Subject: [PATCH 043/174] updated submissions schema with test, and added db migration --- lib/cadet/accounts/course_registrations.ex | 7 +- lib/cadet/assessments/assessment.ex | 17 ++- lib/cadet/assessments/submission.ex | 6 +- lib/cadet_web/router.ex | 6 - lib/mix/tasks/users/import.ex | 6 +- .../20210608085548_update_assessments.exs | 21 ++++ priv/repo/seeds.exs | 13 +- .../accounts/course_registration_test.exs | 117 +++++++++++++++--- test/cadet/assessments/assessment_test.exs | 70 ++++++----- test/cadet/assessments/submission_test.exs | 8 +- .../assessments/assessment_factory.ex | 7 +- .../assessments/submission_factory.ex | 2 +- test/factories/courses/group_factory.ex | 2 +- 13 files changed, 200 insertions(+), 82 deletions(-) create mode 100644 priv/repo/migrations/20210608085548_update_assessments.exs diff --git a/lib/cadet/accounts/course_registrations.ex b/lib/cadet/accounts/course_registrations.ex index 8434701b8..a695fb966 100644 --- a/lib/cadet/accounts/course_registrations.ex +++ b/lib/cadet/accounts/course_registrations.ex @@ -68,15 +68,16 @@ defmodule Cadet.Accounts.CourseRegistrations do @spec insert_or_update_course_registration(map()) :: {:ok, Ecto.Schema.t()} | {:error, Ecto.Changeset.t()} - def insert_or_update_course_registration(params = %{user_id: user_id, course_id: course_id, role: _role}) + def insert_or_update_course_registration( + params = %{user_id: user_id, course_id: course_id, role: _role} + ) when is_ecto_id(user_id) and is_ecto_id(course_id) do CourseRegistration |> where(user_id: ^user_id) |> where(course_id: ^course_id) |> Repo.one() |> case do - nil ->CourseRegistration.changeset(%CourseRegistration{}, params) - + nil -> CourseRegistration.changeset(%CourseRegistration{}, params) cr -> CourseRegistration.changeset(cr, params) end |> Repo.insert_or_update() diff --git a/lib/cadet/assessments/assessment.ex b/lib/cadet/assessments/assessment.ex index 3d5eb18c7..81b0631c8 100644 --- a/lib/cadet/assessments/assessment.ex +++ b/lib/cadet/assessments/assessment.ex @@ -58,6 +58,9 @@ defmodule Cadet.Assessments.Assessment do |> cast_attachments(params, @optional_file_fields) |> cast(params, @required_fields ++ @optional_fields) |> validate_required(@required_fields) + |> add_belongs_to_id_from_model([:type, :course], params) + |> foreign_key_constraint(:type_id) + |> foreign_key_constraint(:course_id) |> validate_type_course |> validate_open_close_date end @@ -67,13 +70,15 @@ defmodule Cadet.Assessments.Assessment do course_id = get_field(changeset, :course_id) case Repo.get(AssessmentTypes, type_id) do - nil -> add_error(changeset, :type, "does not exist") + nil -> + add_error(changeset, :type, "does not exist") - type -> if type.course_id == course_id do - changeset - else - add_error(changeset, :type, "does not belong to the same course as this assessment") - end + type -> + if type.course_id == course_id do + changeset + else + add_error(changeset, :type, "does not belong to the same course as this assessment") + end end end diff --git a/lib/cadet/assessments/submission.ex b/lib/cadet/assessments/submission.ex index 299ffb58e..31e5be223 100644 --- a/lib/cadet/assessments/submission.ex +++ b/lib/cadet/assessments/submission.ex @@ -2,7 +2,7 @@ defmodule Cadet.Assessments.Submission do @moduledoc false use Cadet, :model - alias Cadet.Accounts.User + alias Cadet.Accounts.CourseRegistration alias Cadet.Assessments.{Answer, Assessment, SubmissionStatus} schema "submissions" do @@ -19,8 +19,8 @@ defmodule Cadet.Assessments.Submission do field(:unsubmitted_at, :utc_datetime_usec) belongs_to(:assessment, Assessment) - belongs_to(:student, User) - belongs_to(:unsubmitted_by, User) + belongs_to(:student, CourseRegistration) + belongs_to(:unsubmitted_by, CourseRegistration) has_many(:answers, Answer) timestamps() diff --git a/lib/cadet_web/router.ex b/lib/cadet_web/router.ex index d46259f7a..b42fe9eb4 100644 --- a/lib/cadet_web/router.ex +++ b/lib/cadet_web/router.ex @@ -24,12 +24,6 @@ defmodule CadetWeb.Router do plug(:ensure_role, [:staff, :admin]) end - # scope "/v2/course/:courseid", CadetWeb do - # pipe_through([:api, :auth, :ensure_auth, :course]) - - # # routes, more scopes, etc - # end - scope "/", CadetWeb do get("/.well-known/jwks.json", JWKSController, :index) end diff --git a/lib/mix/tasks/users/import.ex b/lib/mix/tasks/users/import.ex index 4774da54e..c05cdf4c9 100644 --- a/lib/mix/tasks/users/import.ex +++ b/lib/mix/tasks/users/import.ex @@ -52,9 +52,9 @@ defmodule Mix.Tasks.Cadet.Users.Import do {:ok, %User{}} <- Accounts.insert_or_update_user(%{ username: username, - name: name, - # role: :student, - # group: group + name: name + # role: :student, + # group: group }) do :ok else diff --git a/priv/repo/migrations/20210608085548_update_assessments.exs b/priv/repo/migrations/20210608085548_update_assessments.exs new file mode 100644 index 000000000..ffb162b4d --- /dev/null +++ b/priv/repo/migrations/20210608085548_update_assessments.exs @@ -0,0 +1,21 @@ +defmodule Cadet.Repo.Migrations.UpdateAssessments do + use Ecto.Migration + + def change do + alter table(:assessments) do + remove(:type) + add(:type_id, references(:assessment_types), null: false) + add(:course_id, references(:courses), null: false) + end + + alter table(:submissions) do + remove(:student_id) + add(:student_id, references(:course_registrations), null: false) + remove(:unsubmitted_by_id) + add(:unsubmitted_by_id, references(:course_registrations), null: true) + end + + create(index(:submissions, :student_id)) + create(unique_index(:submissions, [:assessment_id, :student_id])) + end +end diff --git a/priv/repo/seeds.exs b/priv/repo/seeds.exs index f1883f41a..b1e087e28 100644 --- a/priv/repo/seeds.exs +++ b/priv/repo/seeds.exs @@ -31,9 +31,16 @@ if Cadet.Env.env() == :dev do mentor1 = insert(:course_registration, %{user: mentor, course: course1, role: :staff}) admin1 = insert(:course_registration, %{user: admin, course: course1, role: :admin}) group = insert(:group, %{leader: avenger1, mentor: mentor1}) - studenta1 = insert(:course_registration, %{user: studenta, course: course1, role: :student, group: group}) - studentb1 = insert(:course_registration, %{user: studentb, course: course1, role: :student, group: group}) - studentc1 = insert(:course_registration, %{user: studentc, course: course1, role: :student, group: group}) + + studenta1 = + insert(:course_registration, %{user: studenta, course: course1, role: :student, group: group}) + + studentb1 = + insert(:course_registration, %{user: studentb, course: course1, role: :student, group: group}) + + studentc1 = + insert(:course_registration, %{user: studentc, course: course1, role: :student, group: group}) + students = [studenta1, studentb1, studentc1] # :TODO fix assessment and notification then come back diff --git a/test/cadet/accounts/course_registration_test.exs b/test/cadet/accounts/course_registration_test.exs index e4228f437..7d4845940 100644 --- a/test/cadet/accounts/course_registration_test.exs +++ b/test/cadet/accounts/course_registration_test.exs @@ -12,17 +12,27 @@ defmodule Cadet.Accounts.CourseRegistrationTest do group2 = insert(:group, %{name: "group 2"}) course1 = insert(:course, %{module_code: "course 1"}) course2 = insert(:course, %{module_code: "course 2"}) + changeset = - CourseRegistration.changeset(%CourseRegistration{}, %{ - course_id: course1.id, - user_id: user1.id, - group_id: group1.id, - role: :student - }) + CourseRegistration.changeset(%CourseRegistration{}, %{ + course_id: course1.id, + user_id: user1.id, + group_id: group1.id, + role: :student + }) {:ok, _course_reg} = Repo.insert(changeset) - {:ok, %{user1: user1, user2: user2, group1: group1, group2: group2, course1: course1, course2: course2, changeset: changeset}} + {:ok, + %{ + user1: user1, + user2: user2, + group1: group1, + group2: group2, + course1: course1, + course2: course2, + changeset: changeset + }} end describe "Changesets:" do @@ -114,7 +124,13 @@ defmodule Cadet.Accounts.CourseRegistrationTest do assert CourseRegistrations.get_users(course2.id) == [] end - test "of a group in a course succeeds", %{user1: user1, user2: user2, group1: group1, group2: group2, course1: course1} do + test "of a group in a course succeeds", %{ + user1: user1, + user2: user2, + group1: group1, + group2: group2, + course1: course1 + } do changeset2 = CourseRegistration.changeset(%CourseRegistration{}, %{ course_id: course1.id, @@ -126,13 +142,13 @@ defmodule Cadet.Accounts.CourseRegistrationTest do {:ok, _course_reg} = Repo.insert(changeset2) course_reg_course1_group1 = CourseRegistrations.get_users(course1.id, group1.id) assert length(course_reg_course1_group1) == 1 - [hd|_] = course_reg_course1_group1 + [hd | _] = course_reg_course1_group1 assert user1.id == hd.user_id assert group1.id == hd.group_id assert course1.id == hd.course_id end - test "of a group in a course failed due to invalid id", %{course1: course1}do + test "of a group in a course failed due to invalid id", %{course1: course1} do group2 = insert(:group, %{name: "group2"}) assert CourseRegistrations.get_users(course1.id, group2.id) == [] end @@ -141,7 +157,14 @@ defmodule Cadet.Accounts.CourseRegistrationTest do describe "update course_registration" do test "successful insert", %{course1: course1, user2: user2} do assert length(CourseRegistrations.get_users(course1.id)) == 1 - {:ok, course_reg} = CourseRegistrations.insert_or_update_course_registration(%{user_id: user2.id, course_id: course1.id, role: :student}) + + {:ok, course_reg} = + CourseRegistrations.insert_or_update_course_registration(%{ + user_id: user2.id, + course_id: course1.id, + role: :student + }) + assert length(CourseRegistrations.get_users(course1.id)) == 2 assert course_reg.user_id == user2.id assert course_reg.course_id == course1.id @@ -149,7 +172,14 @@ defmodule Cadet.Accounts.CourseRegistrationTest do test "successful insert through enroll_course", %{course1: course1, user2: user2} do assert length(CourseRegistrations.get_users(course1.id)) == 1 - {:ok, course_reg} = CourseRegistrations.enroll_course(%{user_id: user2.id, course_id: course1.id, role: :student}) + + {:ok, course_reg} = + CourseRegistrations.enroll_course(%{ + user_id: user2.id, + course_id: course1.id, + role: :student + }) + assert length(CourseRegistrations.get_users(course1.id)) == 2 assert course_reg.user_id == user2.id assert course_reg.course_id == course1.id @@ -157,7 +187,14 @@ defmodule Cadet.Accounts.CourseRegistrationTest do test "successfully update role", %{course1: course1, user1: user1, group1: group1} do assert length(CourseRegistrations.get_users(course1.id)) == 1 - {:ok, course_reg} = CourseRegistrations.insert_or_update_course_registration(%{user_id: user1.id, course_id: course1.id, role: :staff}) + + {:ok, course_reg} = + CourseRegistrations.insert_or_update_course_registration(%{ + user_id: user1.id, + course_id: course1.id, + role: :staff + }) + assert length(CourseRegistrations.get_users(course1.id)) == 1 assert course_reg.user_id == user1.id assert course_reg.course_id == course1.id @@ -167,7 +204,15 @@ defmodule Cadet.Accounts.CourseRegistrationTest do test "successfully update group", %{course1: course1, user1: user1, group2: group2} do assert length(CourseRegistrations.get_users(course1.id)) == 1 - {:ok, course_reg} = CourseRegistrations.insert_or_update_course_registration(%{user_id: user1.id, course_id: course1.id, role: :student, group_id: group2.id}) + + {:ok, course_reg} = + CourseRegistrations.insert_or_update_course_registration(%{ + user_id: user1.id, + course_id: course1.id, + role: :student, + group_id: group2.id + }) + assert length(CourseRegistrations.get_users(course1.id)) == 1 assert course_reg.user_id == user1.id assert course_reg.course_id == course1.id @@ -177,9 +222,14 @@ defmodule Cadet.Accounts.CourseRegistrationTest do test "failed due to incomplete changeset", %{course1: course1, user2: user2} do assert length(CourseRegistrations.get_users(course1.id)) == 1 + assert_raise FunctionClauseError, fn -> - CourseRegistrations.insert_or_update_course_registration(%{user_id: user2.id, course_id: course1.id}) + CourseRegistrations.insert_or_update_course_registration(%{ + user_id: user2.id, + course_id: course1.id + }) end + assert length(CourseRegistrations.get_users(course1.id)) == 1 end end @@ -187,29 +237,56 @@ defmodule Cadet.Accounts.CourseRegistrationTest do describe "delete course_registration" do test "succeeds", %{course1: course1, user1: user1} do assert length(CourseRegistrations.get_users(course1.id)) == 1 - {:ok, _course_reg} = CourseRegistrations.delete_record(%{user_id: user1.id, course_id: course1.id, role: :student}) + + {:ok, _course_reg} = + CourseRegistrations.delete_record(%{ + user_id: user1.id, + course_id: course1.id, + role: :student + }) + assert CourseRegistrations.get_users(course1.id) == [] end test "failed due to repeated removal", %{course1: course1, user1: user1} do assert length(CourseRegistrations.get_users(course1.id)) == 1 - {:ok, _course_reg} = CourseRegistrations.delete_record(%{user_id: user1.id, course_id: course1.id, role: :student}) + + {:ok, _course_reg} = + CourseRegistrations.delete_record(%{ + user_id: user1.id, + course_id: course1.id, + role: :student + }) + assert CourseRegistrations.get_users(course1.id) == [] + assert_raise Ecto.NoPrimaryKeyValueError, fn -> - CourseRegistrations.delete_record(%{user_id: user1.id, course_id: course1.id, role: :student}) + CourseRegistrations.delete_record(%{ + user_id: user1.id, + course_id: course1.id, + role: :student + }) end end test "failed due to non existing entry", %{course1: course1, user2: user2} do assert length(CourseRegistrations.get_users(course1.id)) == 1 + assert_raise Ecto.NoPrimaryKeyValueError, fn -> - CourseRegistrations.delete_record(%{user_id: user2.id, course_id: course1.id, role: :student}) + CourseRegistrations.delete_record(%{ + user_id: user2.id, + course_id: course1.id, + role: :student + }) end end test "failed due to invalid changeset", %{course1: course1, user2: user2} do assert length(CourseRegistrations.get_users(course1.id)) == 1 - {:error, changeset} = CourseRegistrations.delete_record(%{user_id: user2.id, course_id: course1.id}) + + {:error, changeset} = + CourseRegistrations.delete_record(%{user_id: user2.id, course_id: course1.id}) + assert length(CourseRegistrations.get_users(course1.id)) == 1 refute changeset.valid? end diff --git a/test/cadet/assessments/assessment_test.exs b/test/cadet/assessments/assessment_test.exs index a53f32836..04ba58239 100644 --- a/test/cadet/assessments/assessment_test.exs +++ b/test/cadet/assessments/assessment_test.exs @@ -47,10 +47,11 @@ defmodule Cadet.Assessments.AssessmentTest do type_id: type1.id, course_id: course1.id, title: "mission", - number: "M#{Enum.random(0..10)}", + number: "M#{Enum.random(0..10)}" }, :invalid ) + assert_changeset( %{ type_id: type1.id, @@ -64,45 +65,54 @@ defmodule Cadet.Assessments.AssessmentTest do end test "invalid changesets due to type_course", %{course1: course1, type1: type1, type2: type2} do - type_not_in_course = Assessment.changeset(%Assessment{}, %{ - type_id: type2.id, - course_id: course1.id, - title: "mission", - number: "4", - open_at: Timex.now(), - close_at: Timex.shift(Timex.now(), days: 7), - }) + type_not_in_course = + Assessment.changeset(%Assessment{}, %{ + type_id: type2.id, + course_id: course1.id, + title: "mission", + number: "4", + open_at: Timex.now(), + close_at: Timex.shift(Timex.now(), days: 7) + }) + {:error, changeset} = Repo.insert(type_not_in_course) - assert changeset.errors == [{:type, {"does not belong to the same course as this assessment", []}}] + + assert changeset.errors == [ + {:type, {"does not belong to the same course as this assessment", []}} + ] + refute changeset.valid? + type_not_exist = + Assessment.changeset(%Assessment{}, %{ + type_id: type1.id + type2.id, + course_id: course1.id, + title: "invalid type", + number: "M#{Enum.random(1..10)}", + open_at: Timex.now() |> Timex.to_unix() |> Integer.to_string(), + close_at: + Timex.now() + |> Timex.shift(days: Enum.random(1..7)) + |> Timex.to_unix() + |> Integer.to_string() + }) - type_not_exist = Assessment.changeset(%Assessment{}, %{ - type_id: type1.id + type2.id, - course_id: course1.id, - title: "invalid type", - number: "M#{Enum.random(1..10)}", - open_at: Timex.now() |> Timex.to_unix() |> Integer.to_string(), - close_at: - Timex.now() - |> Timex.shift(days: Enum.random(1..7)) - |> Timex.to_unix() - |> Integer.to_string() - }) {:error, changeset2} = Repo.insert(type_not_exist) assert changeset2.errors == [{:type, {"does not exist", []}}] refute changeset2.valid? end test "invalid changesets due to invalid dates", %{course1: course1, type1: type1} do - invalid_date = Assessment.changeset(%Assessment{}, %{ - type_id: type1.id, - course_id: course1.id, - title: "mission", - number: "4", - open_at: Timex.shift(Timex.now(), days: 7), - close_at: Timex.now(), - }) + invalid_date = + Assessment.changeset(%Assessment{}, %{ + type_id: type1.id, + course_id: course1.id, + title: "mission", + number: "4", + open_at: Timex.shift(Timex.now(), days: 7), + close_at: Timex.now() + }) + {:error, changeset} = Repo.insert(invalid_date) assert changeset.errors == [{:open_at, {"Open date must be before close date", []}}] refute changeset.valid? diff --git a/test/cadet/assessments/submission_test.exs b/test/cadet/assessments/submission_test.exs index f06d834b1..bd1f579d9 100644 --- a/test/cadet/assessments/submission_test.exs +++ b/test/cadet/assessments/submission_test.exs @@ -6,8 +6,10 @@ defmodule Cadet.Assessments.SubmissionTest do @required_fields ~w(student_id assessment_id)a setup do - assessment = insert(:assessment) - student = insert(:user, %{role: :student}) + course = insert(:course) + type = insert(:assessment_types, %{course: course}) + assessment = insert(:assessment, %{type: type, course: course}) + student = insert(:course_registration, %{course: course, role: :student}) valid_params = %{student_id: student.id, assessment_id: assessment.id} @@ -40,7 +42,7 @@ defmodule Cadet.Assessments.SubmissionTest do assert_changeset_db(params, :invalid) - new_student = insert(:user, %{role: :student}) + new_student = insert(:course_registration, %{role: :student}) {:ok, _} = Repo.delete(assessment) params diff --git a/test/factories/assessments/assessment_factory.ex b/test/factories/assessments/assessment_factory.ex index 794c19b2b..ca2b6f648 100644 --- a/test/factories/assessments/assessment_factory.ex +++ b/test/factories/assessments/assessment_factory.ex @@ -9,8 +9,9 @@ defmodule Cadet.Assessments.AssessmentFactory do def assessment_factory do # type = Enum.random(Assessment.assessment_types() -- ["practical"]) - type = build(:assessment_types) - type_title = type.title + course = build(:course) + type = build(:assessment_types, %{course: course}) + type_title = type.type # These are actual story identifiers so front-end can use seeds to test more effectively valid_stories = [ @@ -35,7 +36,7 @@ defmodule Cadet.Assessments.AssessmentFactory do story: Enum.random(valid_stories), reading: Faker.Lorem.sentence(), type: type, - course: build(:course), + course: course, open_at: Timex.now(), close_at: Timex.shift(Timex.now(), days: Enum.random(1..30)), is_published: false diff --git a/test/factories/assessments/submission_factory.ex b/test/factories/assessments/submission_factory.ex index 9e9ec1581..971345a93 100644 --- a/test/factories/assessments/submission_factory.ex +++ b/test/factories/assessments/submission_factory.ex @@ -9,7 +9,7 @@ defmodule Cadet.Assessments.SubmissionFactory do def submission_factory do %Submission{ - student: build(:user, %{role: :student}), + student: build(:course_registration, %{role: :student}), assessment: build(:assessment) } end diff --git a/test/factories/courses/group_factory.ex b/test/factories/courses/group_factory.ex index c90522b5a..c46c0769c 100644 --- a/test/factories/courses/group_factory.ex +++ b/test/factories/courses/group_factory.ex @@ -9,7 +9,7 @@ defmodule Cadet.Courses.GroupFactory do def group_factory do %Group{ - name: sequence("group"), + name: sequence("group") # leader: build(:course_registration) # leader: build(:user, role: :staff) } From 40f8eae71ba7606223ba62f17f7219085921c8a6 Mon Sep 17 00:00:00 2001 From: YaleChen299 Date: Tue, 8 Jun 2021 23:45:08 +0800 Subject: [PATCH 044/174] added latest_viewed_course in user and course fk to group --- lib/cadet/accounts/accounts.ex | 7 +++---- lib/cadet/accounts/course_registrations.ex | 11 ++++++++++- lib/cadet/accounts/user.ex | 6 ++++-- lib/cadet/courses/courses.ex | 2 +- lib/cadet/courses/group.ex | 10 +++++++--- .../migrations/20210608085548_update_assessments.exs | 11 +++++++++++ 6 files changed, 36 insertions(+), 11 deletions(-) diff --git a/lib/cadet/accounts/accounts.ex b/lib/cadet/accounts/accounts.ex index d5e14ccb1..c505b574f 100644 --- a/lib/cadet/accounts/accounts.ex +++ b/lib/cadet/accounts/accounts.ex @@ -109,11 +109,10 @@ defmodule Cadet.Accounts do end end - # :TODO Pipe through module - def update_game_states(user = %User{}, new_game_state = %{}) do + def update_latest_viewed(user = %User{}, latest_viewed_id) when is_ecto_id(latest_viewed_id) do case user - |> User.changeset(%{game_states: new_game_state}) - |> Repo.update() do + |> User.changeset(%{latest_viewed_id: latest_viewed_id}) + |> Repo.update() do result = {:ok, _} -> result {:error, changeset} -> {:error, {:internal_server_error, full_error_messages(changeset)}} end diff --git a/lib/cadet/accounts/course_registrations.ex b/lib/cadet/accounts/course_registrations.ex index a695fb966..bbdb61b10 100644 --- a/lib/cadet/accounts/course_registrations.ex +++ b/lib/cadet/accounts/course_registrations.ex @@ -2,7 +2,7 @@ defmodule Cadet.Accounts.CourseRegistrations do @moduledoc """ Provides functions fetch, add, update course_registration """ - use Cadet, :context + use Cadet, [:context, :display] import Ecto.Query @@ -96,4 +96,13 @@ defmodule Cadet.Accounts.CourseRegistrations do end |> Repo.delete() end + + def update_game_states(cr = %CourseRegistration{}, new_game_state = %{}) do + case cr + |> CourseRegistration.changeset(%{game_states: new_game_state}) + |> Repo.update() do + result = {:ok, _} -> result + {:error, changeset} -> {:error, {:internal_server_error, full_error_messages(changeset)}} + end + end end diff --git a/lib/cadet/accounts/user.ex b/lib/cadet/accounts/user.ex index 458869ba3..bd9ffce60 100644 --- a/lib/cadet/accounts/user.ex +++ b/lib/cadet/accounts/user.ex @@ -8,23 +8,25 @@ defmodule Cadet.Accounts.User do use Cadet, :model alias Cadet.Accounts.CourseRegistration + alias Cadet.Courses.Course schema "users" do field(:name, :string) field(:username, :string) + belongs_to(:latest_viewed, Course) has_many(:course_registration, CourseRegistration) timestamps() end @required_fields ~w(name)a - @optional_fields ~w(username)a + @optional_fields ~w(username latest_viewed_id)a def changeset(user, params \\ %{}) do user |> cast(params, @required_fields ++ @optional_fields) - # |> add_belongs_to_id_from_model(:group, params) |> validate_required(@required_fields) + |> foreign_key_constraint(:latest_viewed_id) end end diff --git a/lib/cadet/courses/courses.ex b/lib/cadet/courses/courses.ex index 66117e777..19d6caff3 100644 --- a/lib/cadet/courses/courses.ex +++ b/lib/cadet/courses/courses.ex @@ -204,7 +204,7 @@ defmodule Cadet.Courses do # @doc """ # Get list of students under staff discussion group # """ - # def list_students_by_leader(staff = %User{}) do + # def list_students_by_leader(staff = %CourseRegistration{}) do # import Cadet.Course.Query, only: [group_members: 1] # staff diff --git a/lib/cadet/courses/group.ex b/lib/cadet/courses/group.ex index b78e318f4..40ca660cb 100644 --- a/lib/cadet/courses/group.ex +++ b/lib/cadet/courses/group.ex @@ -6,20 +6,24 @@ defmodule Cadet.Courses.Group do use Cadet, :model alias Cadet.Accounts.CourseRegistration + alias Cadet.Courses.Course schema "groups" do field(:name, :string) belongs_to(:leader, CourseRegistration) belongs_to(:mentor, CourseRegistration) + belongs_to(:course, Course) has_many(:students, CourseRegistration) end - @optional_fields ~w(name leader_id mentor_id)a + @required_fields ~w(name course_id)a + @optional_fields ~w(leader_id mentor_id)a def changeset(group, attrs \\ %{}) do group - |> cast(attrs, @optional_fields) - |> add_belongs_to_id_from_model([:leader, :mentor], attrs) + |> cast(attrs, @required_fields ++ @optional_fields) + |> validate_required(@required_fields) + |> add_belongs_to_id_from_model([:leader, :mentor, :course], attrs) end end diff --git a/priv/repo/migrations/20210608085548_update_assessments.exs b/priv/repo/migrations/20210608085548_update_assessments.exs index ffb162b4d..025c48bdd 100644 --- a/priv/repo/migrations/20210608085548_update_assessments.exs +++ b/priv/repo/migrations/20210608085548_update_assessments.exs @@ -17,5 +17,16 @@ defmodule Cadet.Repo.Migrations.UpdateAssessments do create(index(:submissions, :student_id)) create(unique_index(:submissions, [:assessment_id, :student_id])) + + alter table(:groups) do + remove(:leader_id) + add(:leader_id, references(:course_registrations), null: false) + remove(:mentor_id) + add(:mentor_id, references(:course_registrations), null: true) + add(:course_id, references(:courses), null: false) + end + + create(unique_index(:groups, [:name, :course_id])) + end end From fecbea4b1c7c626c813608fe915c84093a7a6bab Mon Sep 17 00:00:00 2001 From: YaleChen299 Date: Wed, 9 Jun 2021 19:01:25 +0800 Subject: [PATCH 045/174] fix snake case --- lib/cadet/accounts/accounts.ex | 37 +++----- lib/cadet/accounts/course_registrations.ex | 9 +- lib/cadet/courses/group.ex | 13 +++ .../admin_courses_controller.ex | 18 ++-- .../admin_user_controller.ex | 13 +-- lib/cadet_web/admin_views/admin_user_view.ex | 15 ++-- .../controllers/courses_controller.ex | 8 +- .../controllers/sourcecast_controller.ex | 2 +- .../controllers/stories_controller.ex | 10 +-- lib/cadet_web/router.ex | 8 +- lib/cadet_web/views/stories_view.ex | 2 +- .../20210608085548_update_assessments.exs | 8 +- test/cadet/accounts/accounts_test.exs | 86 +++++++++---------- .../admin_courses_controller_test.exs | 6 +- .../admin_user_controller_test.exs | 78 +++++++++-------- .../sourcecast_controller_test.exs | 18 ++-- .../controllers/stories_controller_test.exs | 20 ++--- test/factories/accounts/user_factory.ex | 4 - test/factories/courses/group_factory.ex | 6 +- 19 files changed, 183 insertions(+), 178 deletions(-) diff --git a/lib/cadet/accounts/accounts.ex b/lib/cadet/accounts/accounts.ex index c505b574f..efa05fb91 100644 --- a/lib/cadet/accounts/accounts.ex +++ b/lib/cadet/accounts/accounts.ex @@ -6,7 +6,7 @@ defmodule Cadet.Accounts do import Ecto.Query - alias Cadet.Accounts.{Query, User} + alias Cadet.Accounts.{Query, User, CourseRegistration} alias Cadet.Auth.Provider @doc """ @@ -14,23 +14,10 @@ defmodule Cadet.Accounts do Returns {:ok, user} on success, otherwise {:error, changeset} """ - # def register(attrs = %{username: username}, role) when is_binary(username) do - # attrs |> Map.put(:role, role) |> insert_or_update_user() - # end def register(attrs = %{username: username}) when is_binary(username) do attrs |> insert_or_update_user() end - @doc """ - Creates User entity with specified attributes. - """ - # :TODO recheck if deprecated - def create_user(attrs \\ %{}) do - %User{} - |> User.changeset(attrs) - |> Repo.insert() - end - @doc """ Updates User entity with specified attributes. If the User does not exist yet, create one. @@ -60,20 +47,22 @@ defmodule Cadet.Accounts do @doc """ Returns users matching a given set of criteria. """ - # :TODO to pipe thru some join functon with mapping table so van get group id in a course - def get_users(filter \\ []) do - User - |> join(:left, [u], g in assoc(u, :group)) - |> preload([u, g], group: g) - |> get_users(filter) + def get_users_by(filter \\ [], %CourseRegistration{course_id: course_id}) do + CourseRegistration + |> where([cr], cr.course_id == ^course_id) + |> join(:inner, [cr], u in assoc(cr, :user)) + |> preload([cr, u], user: u) + |> join(:inner, [cr, u], g in assoc(cr, :group)) + |> preload([cr, u, g], group: g) + |> get_users_helper(filter) end - defp get_users(query, []), do: Repo.all(query) + defp get_users_helper(query, []), do: Repo.all(query) - defp get_users(query, [{:group, group} | filters]), - do: query |> where([u, g], g.name == ^group) |> get_users(filters) + defp get_users_helper(query, [{:group, group} | filters]), + do: query |> where([cr, u, g], g.name == ^group) |> get_users_helper(filters) - defp get_users(query, [filter | filters]), do: query |> where(^[filter]) |> get_users(filters) + defp get_users_helper(query, [filter | filters]), do: query |> where(^[filter]) |> get_users_helper(filters) @spec sign_in(String.t(), Provider.token(), Provider.provider_instance()) :: {:error, :bad_request | :forbidden | :internal_server_error, String.t()} | {:ok, any} diff --git a/lib/cadet/accounts/course_registrations.ex b/lib/cadet/accounts/course_registrations.ex index bbdb61b10..ed9ab8091 100644 --- a/lib/cadet/accounts/course_registrations.ex +++ b/lib/cadet/accounts/course_registrations.ex @@ -27,8 +27,6 @@ defmodule Cadet.Accounts.CourseRegistrations do cr -> {:ok, cr} end - - # |> Repo.get_by(%{user_id: ^user_id, course_id: ^course_id}) end def get_courses(%User{id: id}) do @@ -39,7 +37,7 @@ defmodule Cadet.Accounts.CourseRegistrations do |> Repo.all() end - def get_users(course_id) do + def get_users(course_id) when is_ecto_id(course_id) do CourseRegistration |> where([cr], cr.course_id == ^course_id) |> join(:inner, [cr], u in assoc(cr, :user)) @@ -47,7 +45,7 @@ defmodule Cadet.Accounts.CourseRegistrations do |> Repo.all() end - def get_users(course_id, group_id) do + def get_users(course_id, group_id) when is_ecto_id(group_id) and is_ecto_id(course_id) do CourseRegistration |> where([cr], cr.course_id == ^course_id) |> where([cr], cr.group_id == ^group_id) @@ -56,9 +54,6 @@ defmodule Cadet.Accounts.CourseRegistrations do |> preload(:user) |> preload(:group) |> Repo.all() - - # |> join(:inner, [cr, u], g in assoc(cr, :group)) - # maybe not needed when we dont need group info end def enroll_course(params = %{user_id: user_id, course_id: course_id, role: _role}) diff --git a/lib/cadet/courses/group.ex b/lib/cadet/courses/group.ex index 40ca660cb..a3138c866 100644 --- a/lib/cadet/courses/group.ex +++ b/lib/cadet/courses/group.ex @@ -5,6 +5,7 @@ defmodule Cadet.Courses.Group do """ use Cadet, :model + # alias Cadet.Repo alias Cadet.Accounts.CourseRegistration alias Cadet.Courses.Course @@ -25,5 +26,17 @@ defmodule Cadet.Courses.Group do |> cast(attrs, @required_fields ++ @optional_fields) |> validate_required(@required_fields) |> add_belongs_to_id_from_model([:leader, :mentor, :course], attrs) + # |> validate_course end + + # defp validate_course(changeset) do + # course_id = get_field(changeset, :course_id) + # leader_id = get_field(changeset, :leader_id) + # mentor_id = get_field(changeset, :mentor_id) + + # if leader_id != nil && Repo.get(CourseRegistration, leader_id).course_id != course_id do + # add_error(changeset, :leader, "does not belong to the same course ") + # end + + # end end diff --git a/lib/cadet_web/admin_controllers/admin_courses_controller.ex b/lib/cadet_web/admin_controllers/admin_courses_controller.ex index cd4907a8f..001beb2d9 100644 --- a/lib/cadet_web/admin_controllers/admin_courses_controller.ex +++ b/lib/cadet_web/admin_controllers/admin_courses_controller.ex @@ -5,7 +5,7 @@ defmodule CadetWeb.AdminCoursesController do alias Cadet.Courses - def update_course_config(conn, params = %{"courseid" => course_id}) + def update_course_config(conn, params = %{"course_id" => course_id}) when is_ecto_id(course_id) do params = for {key, val} <- params, into: %{}, do: {String.to_atom(key), val} @@ -30,7 +30,7 @@ defmodule CadetWeb.AdminCoursesController do end def update_assessment_config(conn, %{ - "courseid" => course_id, + "course_id" => course_id, "early_submission_xp" => early_xp, "hours_before_early_xp_decay" => hours_before_decay, "decay_rate_points_per_hour" => decay_rate @@ -52,7 +52,7 @@ defmodule CadetWeb.AdminCoursesController do end def update_assessment_types(conn, %{ - "courseid" => course_id, + "course_id" => course_id, "assessment_types" => assessment_types }) when is_ecto_id(course_id) do @@ -72,7 +72,7 @@ defmodule CadetWeb.AdminCoursesController do end swagger_path :update_course_config do - put("/v2/course/{courseId}/admin/course_config") + put("/v2/course/{course_id}/admin/course_config") summary("Updates the course configuration for the specified course") @@ -81,7 +81,7 @@ defmodule CadetWeb.AdminCoursesController do consumes("application/json") parameters do - courseId(:path, :integer, "Course ID", required: true) + course_id(:path, :integer, "Course ID", required: true) name(:body, :string, "Course name") module_code(:body, :string, "Course module code") viewable(:body, :boolean, "Course viewability") @@ -98,7 +98,7 @@ defmodule CadetWeb.AdminCoursesController do end swagger_path :update_assessment_config do - put("/v2/course/{courseId}/admin/assessment_config") + put("/v2/course/{course_id}/admin/assessment_config") summary("Updates the assessment configuration for the specified course") @@ -107,7 +107,7 @@ defmodule CadetWeb.AdminCoursesController do consumes("application/json") parameters do - courseId(:path, :integer, "Course ID", required: true) + course_id(:path, :integer, "Course ID", required: true) early_submission_xp(:body, :integer, "Early submission xp") hours_before_early_xp_decay(:body, :integer, "Hours before early submission xp decay") decay_rate_points_per_hour(:body, :integer, "Decay rate in points per hour") @@ -119,7 +119,7 @@ defmodule CadetWeb.AdminCoursesController do end swagger_path :update_assessment_types do - put("/admin/courses/{courseId}/assessment_types") + put("/admin/courses/{course_id}/assessment_types") summary("Updates the assessment types for the specified course") @@ -128,7 +128,7 @@ defmodule CadetWeb.AdminCoursesController do consumes("application/json") parameters do - courseId(:path, :integer, "Course ID", required: true) + course_id(:path, :integer, "Course ID", required: true) assessment_types(:body, :list, "Assessment Types") end diff --git a/lib/cadet_web/admin_controllers/admin_user_controller.ex b/lib/cadet_web/admin_controllers/admin_user_controller.ex index fa20d0d58..ec783a36e 100644 --- a/lib/cadet_web/admin_controllers/admin_user_controller.ex +++ b/lib/cadet_web/admin_controllers/admin_user_controller.ex @@ -3,9 +3,12 @@ defmodule CadetWeb.AdminUserController do use PhoenixSwagger alias Cadet.Accounts + # alias Cadet.Accounts.CourseRegistrations + + # :TODO this controller seems to be obsolte in the current version we will use the course_reg controller to find all users of a course def index(conn, filter) do - users = filter |> try_keywordise_string_keys() |> Accounts.get_users() + users = filter |> try_keywordise_string_keys() |> Accounts.get_users_by(conn.assigns.course_reg) render(conn, "users.json", users: users) end @@ -13,7 +16,7 @@ defmodule CadetWeb.AdminUserController do swagger_path :index do get("/admin/users") - summary("Returns a list of users") + summary("Returns a list of users in the course owned by the admin") security([%{JWT: []}]) produces("application/json") @@ -26,16 +29,16 @@ defmodule CadetWeb.AdminUserController do AdminUserInfo: swagger_schema do title("User") - description("Basic information about the user") + description("Basic information about the users in this course") properties do userId(:integer, "User's ID") name(:string, "Full name of the user") - role(:string, "Role of the user. Can be 'student', 'staff', or 'admin'") + role(:string, "Role of the user in this course. Can be 'student', 'staff', or 'admin'") group( :string, - "Group the user belongs to. May be null if the user does not belong to any group" + "Group the user belongs to in this course. May be null if the user does not belong to any group" ) end end diff --git a/lib/cadet_web/admin_views/admin_user_view.ex b/lib/cadet_web/admin_views/admin_user_view.ex index 3bf82ac3c..4b48af13d 100644 --- a/lib/cadet_web/admin_views/admin_user_view.ex +++ b/lib/cadet_web/admin_views/admin_user_view.ex @@ -2,18 +2,19 @@ defmodule CadetWeb.AdminUserView do use CadetWeb, :view def render("users.json", %{users: users}) do - render_many(users, CadetWeb.AdminUserView, "user.json", as: :user) + render_many(users, CadetWeb.AdminUserView, "cr.json", as: :cr) end - def render("user.json", %{user: user}) do + def render("cr.json", %{cr: cr}) do %{ - userId: user.id, - name: user.name, - role: user.role, + crId: cr.id, + course_id: cr.course_id, + name: cr.user.name, + role: cr.role, group: - case user.group do + case cr.group do nil -> nil - _ -> user.group.name + _ -> cr.group.name end } end diff --git a/lib/cadet_web/controllers/courses_controller.ex b/lib/cadet_web/controllers/courses_controller.ex index ee277a362..c33f04f38 100644 --- a/lib/cadet_web/controllers/courses_controller.ex +++ b/lib/cadet_web/controllers/courses_controller.ex @@ -5,7 +5,7 @@ defmodule CadetWeb.CoursesController do alias Cadet.Courses - def index(conn, %{"courseid" => course_id}) when is_ecto_id(course_id) do + def index(conn, %{"course_id" => course_id}) when is_ecto_id(course_id) do case Courses.get_course_config(course_id) do {:ok, config} -> render(conn, "config.json", config: config) {:error, {status, message}} -> send_resp(conn, status, message) @@ -13,7 +13,7 @@ defmodule CadetWeb.CoursesController do end swagger_path :get_course_config do - get("/v2/course/{courseId}/config") + get("/v2/course/{course_id}/config") summary("Retrieves the course configuration of the specified course") @@ -22,11 +22,11 @@ defmodule CadetWeb.CoursesController do produces("application/json") parameters do - courseId(:path, :integer, "Course ID", required: true) + course_id(:path, :integer, "Course ID", required: true) end response(200, "OK", Schema.ref(:Config)) - response(400, "Invalid courseId") + response(400, "Invalid course_id") end def swagger_definitions do diff --git a/lib/cadet_web/controllers/sourcecast_controller.ex b/lib/cadet_web/controllers/sourcecast_controller.ex index b08560cd9..c16704735 100644 --- a/lib/cadet_web/controllers/sourcecast_controller.ex +++ b/lib/cadet_web/controllers/sourcecast_controller.ex @@ -4,7 +4,7 @@ defmodule CadetWeb.SourcecastController do alias Cadet.Courses - def index(conn, %{"courseid" => course_id}) do + def index(conn, %{"course_id" => course_id}) do sourcecasts = Courses.get_sourcecast_files(course_id) render(conn, "index.json", sourcecasts: sourcecasts) end diff --git a/lib/cadet_web/controllers/stories_controller.ex b/lib/cadet_web/controllers/stories_controller.ex index 4c169b013..1a69eb639 100644 --- a/lib/cadet_web/controllers/stories_controller.ex +++ b/lib/cadet_web/controllers/stories_controller.ex @@ -63,7 +63,7 @@ defmodule CadetWeb.StoriesController do end swagger_path :index do - get("/v2/course/{courseId}/stories") + get("/v2/course/{course_id}/stories") summary("Get a list of all stories") @@ -73,7 +73,7 @@ defmodule CadetWeb.StoriesController do end swagger_path :create do - post("/v2/course/{courseId}/stories") + post("/v2/course/{course_id}/stories") summary("Creates a new story") @@ -85,7 +85,7 @@ defmodule CadetWeb.StoriesController do end swagger_path :delete do - PhoenixSwagger.Path.delete("/v2/course/{courseId}/stories/{storyId}") + PhoenixSwagger.Path.delete("/v2/course/{course_id}/stories/{storyId}") summary("Delete a story from database by id") @@ -101,7 +101,7 @@ defmodule CadetWeb.StoriesController do end swagger_path :update do - post("/v2/course/{courseId}/stories/{storyId}") + post("/v2/course/{course_id}/stories/{storyId}") summary("Update details regarding a story") @@ -130,7 +130,7 @@ defmodule CadetWeb.StoriesController do openAt(:string, "The opening date", format: "date-time", required: true) closeAt(:string, "The closing date", format: "date-time", required: true) isPublished(:boolean, "Whether or not is published", required: false) - courseId(:integer, "The id of the course that this story belongs to", required: true) + course_id(:integer, "The id of the course that this story belongs to", required: true) end end } diff --git a/lib/cadet_web/router.ex b/lib/cadet_web/router.ex index b42fe9eb4..bb3edb556 100644 --- a/lib/cadet_web/router.ex +++ b/lib/cadet_web/router.ex @@ -50,7 +50,7 @@ defmodule CadetWeb.Router do end # Authenticated Pages - scope "/v2/course/:courseid", CadetWeb do + scope "/v2/course/:course_id", CadetWeb do pipe_through([:api, :auth, :ensure_auth, :course]) get("/sourcecast", SourcecastController, :index) @@ -85,7 +85,7 @@ defmodule CadetWeb.Router do end # Authenticated Pages - scope "/v2/course/:courseid/self", CadetWeb do + scope "/v2/course/:course_id/self", CadetWeb do pipe_through([:api, :auth, :ensure_auth, :course]) get("/goals", IncentivesController, :index_goals) @@ -93,7 +93,7 @@ defmodule CadetWeb.Router do end # Admin pages - scope "/v2/course/:courseid/admin", CadetWeb do + scope "/v2/course/:course_id/admin", CadetWeb do pipe_through([:api, :auth, :ensure_auth, :course, :ensure_staff]) get("/assets/:foldername", AdminAssetsController, :index) @@ -165,7 +165,7 @@ defmodule CadetWeb.Router do end defp assign_course(conn, _opts) do - course_id = conn.path_params["courseid"] + course_id = conn.path_params["course_id"] course_reg = Cadet.Accounts.CourseRegistrations.get_user_record(conn.assigns.current_user.id, course_id) diff --git a/lib/cadet_web/views/stories_view.ex b/lib/cadet_web/views/stories_view.ex index 00508e6c7..18a9de43a 100644 --- a/lib/cadet_web/views/stories_view.ex +++ b/lib/cadet_web/views/stories_view.ex @@ -14,7 +14,7 @@ defmodule CadetWeb.StoriesView do isPublished: :is_published, openAt: &format_datetime(&1.open_at), closeAt: &format_datetime(&1.close_at), - courseId: :course_id + course_id: :course_id }) end end diff --git a/priv/repo/migrations/20210608085548_update_assessments.exs b/priv/repo/migrations/20210608085548_update_assessments.exs index 025c48bdd..19efcaedb 100644 --- a/priv/repo/migrations/20210608085548_update_assessments.exs +++ b/priv/repo/migrations/20210608085548_update_assessments.exs @@ -20,13 +20,17 @@ defmodule Cadet.Repo.Migrations.UpdateAssessments do alter table(:groups) do remove(:leader_id) - add(:leader_id, references(:course_registrations), null: false) + add(:leader_id, references(:course_registrations), null: true) remove(:mentor_id) add(:mentor_id, references(:course_registrations), null: true) - add(:course_id, references(:courses), null: false) + add(:course_id, references(:courses), null: true) end create(unique_index(:groups, [:name, :course_id])) + alter table(:users) do + add(:latest_viewed_id, references(:courses), null: true) + end + end end diff --git a/test/cadet/accounts/accounts_test.exs b/test/cadet/accounts/accounts_test.exs index 956ea40c0..edb4d1aa2 100644 --- a/test/cadet/accounts/accounts_test.exs +++ b/test/cadet/accounts/accounts_test.exs @@ -15,30 +15,10 @@ defmodule Cadet.AccountsTest do HTTPoison.start() end - test "create user" do - {:ok, user} = - Accounts.create_user(%{ - name: "happy user", - role: :student - }) - - assert %{name: "happy user", role: :student} = user - end - - test "invalid create user" do - {:error, changeset} = - Accounts.create_user(%{ - name: "happy user", - role: :unknown - }) - - assert %{role: ["is invalid"]} = errors_on(changeset) - end - test "get existing user" do - user = insert(:user, name: "Teddy", role: :student) + user = insert(:user, name: "Teddy") result = Accounts.get_user(user.id) - assert %{name: "Teddy", role: :student} = result + assert %{name: "Teddy"} = result end test "get unknown user" do @@ -51,8 +31,8 @@ defmodule Cadet.AccountsTest do username: "e0123456" } - assert {:ok, user} = Accounts.register(attrs, :student) - assert %{name: "Test Name", role: :student} = user + assert {:ok, user} = Accounts.register(attrs) + assert %{name: "Test Name"} = user end describe "sign in using auth provider" do @@ -75,29 +55,29 @@ defmodule Cadet.AccountsTest do Accounts.sign_in("student", "invalid_token", "test") end - test_with_mock "upstream error", Cadet.Auth.Provider, - get_role: fn _, _ -> {:error, :upstream, "Upstream error"} end do - assert {:error, :bad_request, "Upstream error"} == - Accounts.sign_in("student", "student_token", "test") - end + # test_with_mock "upstream error", Cadet.Auth.Provider, + # get_role: fn _, _ -> {:error, :upstream, "Upstream error"} end do + # assert {:error, :bad_request, "Upstream error"} == + # Accounts.sign_in("student", "student_token", "test") + # end end - describe "sign in with unregistered user gets the right roles" do - test ~s(user has admin access) do - assert {:ok, user} = Accounts.sign_in("admin", "admin_token", "test") - assert %{role: :admin} = user - end + # describe "sign in with unregistered user gets the right roles" do + # test ~s(user has admin access) do + # assert {:ok, user} = Accounts.sign_in("admin", "admin_token", "test") + # assert %{role: :admin} = user + # end - test ~s(user has staff access) do - assert {:ok, user} = Accounts.sign_in("staff", "staff_token", "test") - assert %{role: :staff} = user - end + # test ~s(user has staff access) do + # assert {:ok, user} = Accounts.sign_in("staff", "staff_token", "test") + # assert %{role: :staff} = user + # end - test ~s(user has student access) do - assert {:ok, user} = Accounts.sign_in("student", "student_token", "test") - assert %{role: :student} = user - end - end + # test ~s(user has student access) do + # assert {:ok, user} = Accounts.sign_in("student", "student_token", "test") + # assert %{role: :student} = user + # end + # end describe "insert_or_update_user" do test "existing user" do @@ -112,7 +92,6 @@ defmodule Cadet.AccountsTest do assert updated_user.id == user.id assert updated_user.name == user_params.name - assert updated_user.role == user_params.role end test "non-existing user" do @@ -125,7 +104,24 @@ defmodule Cadet.AccountsTest do |> Repo.one() assert updated_user.name == user_params.name - assert updated_user.role == user_params.role + end + end + + describe "get_users_by" do + test "get all users in a course" do + + end + + test "get all students in a course" do + + end + + test "get all users in a group in a course" do + + end + + test "get all students in a group in a course" do + end end end diff --git a/test/cadet_web/admin_controllers/admin_courses_controller_test.exs b/test/cadet_web/admin_controllers/admin_courses_controller_test.exs index 18f60ee05..00b1c3fd9 100644 --- a/test/cadet_web/admin_controllers/admin_courses_controller_test.exs +++ b/test/cadet_web/admin_controllers/admin_courses_controller_test.exs @@ -10,7 +10,7 @@ defmodule CadetWeb.AdminCoursesControllerTest do AdminCoursesController.swagger_path_update_assessment_types(nil) end - describe "PUT /v2/course/{courseId}/admin/course_config" do + describe "PUT /v2/course/{course_id}/admin/course_config" do @tag authenticate: :admin test "succeeds 1", %{conn: conn} do course_id = conn.assigns[:course_id] @@ -118,7 +118,7 @@ defmodule CadetWeb.AdminCoursesControllerTest do end end - describe "PUT /v2/course/{courseId}/admin/assessment_config" do + describe "PUT /v2/course/{course_id}/admin/assessment_config" do @tag authenticate: :admin test "succeeds", %{conn: conn} do course_id = conn.assigns[:course_id] @@ -194,7 +194,7 @@ defmodule CadetWeb.AdminCoursesControllerTest do end end - describe "PUT /v2/course/{courseId}/admin/assessment_types" do + describe "PUT /v2/course/{course_id}/admin/assessment_types" do @tag authenticate: :admin test "succeeds", %{conn: conn} do course_id = conn.assigns[:course_id] diff --git a/test/cadet_web/admin_controllers/admin_user_controller_test.exs b/test/cadet_web/admin_controllers/admin_user_controller_test.exs index 1ea56aae6..702835450 100644 --- a/test/cadet_web/admin_controllers/admin_user_controller_test.exs +++ b/test/cadet_web/admin_controllers/admin_user_controller_test.exs @@ -4,63 +4,71 @@ defmodule CadetWeb.AdminUserControllerTest do import Cadet.Factory alias CadetWeb.AdminUserController + alias Cadet.Repo + alias Cadet.Courses.Course + alias Cadet.Accounts.CourseRegistrations + alias Cadet.Accounts test "swagger" do assert is_map(AdminUserController.swagger_definitions()) assert is_map(AdminUserController.swagger_path_index(nil)) end - describe "GET /admin/users" do + describe "GET /v2/course/{course_id}/admin/users" do @tag authenticate: :staff test "success, when staff retrieves users", %{conn: conn} do - insert(:student) + course_id = conn.assigns[:course_id] + course = Repo.get(Course, course_id) + stu = insert(:course_registration, %{role: :student, course: course}) resp = conn - |> get("/v2/admin/users") + |> get(build_url(course_id)) |> json_response(200) assert 2 == Enum.count(resp) end - @tag authenticate: :staff - test "can filter by role", %{conn: conn} do - insert(:student) + # @tag authenticate: :staff + # test "can filter by role", %{conn: conn} do + # insert(:student) - resp = - conn - |> get("/v2/admin/users?role=student") - |> json_response(200) + # resp = + # conn + # |> get("/v2/admin/users?role=student") + # |> json_response(200) - assert 1 == Enum.count(resp) - assert "student" == List.first(resp)["role"] - end + # assert 1 == Enum.count(resp) + # assert "student" == List.first(resp)["role"] + # end - @tag authenticate: :staff - test "can filter by group", %{conn: conn} do - group = insert(:group) - insert(:student, group: group) + # @tag authenticate: :staff + # test "can filter by group", %{conn: conn} do + # group = insert(:group) + # insert(:student, group: group) - resp = - conn - |> get("/v2/admin/users?group=#{group.name}") - |> json_response(200) + # resp = + # conn + # |> get("/v2/admin/users?group=#{group.name}") + # |> json_response(200) - assert 1 == Enum.count(resp) - assert group.name == List.first(resp)["group"] - end + # assert 1 == Enum.count(resp) + # assert group.name == List.first(resp)["group"] + # end - @tag authenticate: :student - test "forbidden, when student retrieves users", %{conn: conn} do - assert conn - |> get("/v2/admin/users") - |> response(403) - end + # @tag authenticate: :student + # test "forbidden, when student retrieves users", %{conn: conn} do + # assert conn + # |> get("/v2/admin/users") + # |> response(403) + # end - test "401 when not logged in", %{conn: conn} do - assert conn - |> get("/v2/admin/users") - |> response(401) - end + # test "401 when not logged in", %{conn: conn} do + # assert conn + # |> get("/v2/admin/users") + # |> response(401) + # end end + + defp build_url(course_id), do: "/v2/course/#{course_id}/admin/users" end diff --git a/test/cadet_web/controllers/sourcecast_controller_test.exs b/test/cadet_web/controllers/sourcecast_controller_test.exs index e90582274..40a26aa5b 100644 --- a/test/cadet_web/controllers/sourcecast_controller_test.exs +++ b/test/cadet_web/controllers/sourcecast_controller_test.exs @@ -52,7 +52,7 @@ defmodule CadetWeb.SourcecastControllerTest do end end - describe "POST /v2/course/{courseId}/sourcecast, unauthenticated" do + describe "POST /v2/course/{course_id}/sourcecast, unauthenticated" do test "unauthorized", %{conn: conn} do course = insert(:course) conn = post(conn, build_url(course.id), %{}) @@ -60,7 +60,7 @@ defmodule CadetWeb.SourcecastControllerTest do end end - describe "DELETE /v2/course/{courseId}/sourcecast, unauthenticated" do + describe "DELETE /v2/course/{course_id}/sourcecast, unauthenticated" do test "unauthorized", %{conn: conn} do course = insert(:course) seed_db(course.id) @@ -69,7 +69,7 @@ defmodule CadetWeb.SourcecastControllerTest do end end - describe "GET /v2/course/{courseId}/sourcecast, returns course sourcecasts" do + describe "GET /v2/course/{course_id}/sourcecast, returns course sourcecasts" do @tag authenticate: :student test "renders a list of all course sourcecast entries", %{ conn: conn @@ -108,7 +108,7 @@ defmodule CadetWeb.SourcecastControllerTest do end end - describe "POST /v2/course/{courseId}/sourcecast, student" do + describe "POST /v2/course/{course_id}/sourcecast, student" do @tag authenticate: :student test "prohibited", %{conn: conn} do course_id = conn.assigns[:course_id] @@ -132,7 +132,7 @@ defmodule CadetWeb.SourcecastControllerTest do end end - describe "DELETE /v2/course/{courseId}/sourcecast, student" do + describe "DELETE /v2/course/{course_id}/sourcecast, student" do @tag authenticate: :student test "prohibited", %{conn: conn} do course_id = conn.assigns[:course_id] @@ -143,7 +143,7 @@ defmodule CadetWeb.SourcecastControllerTest do end end - describe "POST /v2/course/{courseId}/sourcecast, staff" do + describe "POST /v2/course/{course_id}/sourcecast, staff" do @tag authenticate: :staff test "successful for public sourcecast", %{conn: conn} do course_id = conn.assigns[:course_id] @@ -252,7 +252,7 @@ defmodule CadetWeb.SourcecastControllerTest do end end - describe "DELETE /v2/course/{courseId}/sourcecast, staff" do + describe "DELETE /v2/course/{course_id}/sourcecast, staff" do @tag authenticate: :staff test "successful for public sourcecast", %{conn: conn} do course_id = conn.assigns[:course_id] @@ -278,7 +278,7 @@ defmodule CadetWeb.SourcecastControllerTest do end end - describe "POST /v2/course/{courseId}/sourcecast, admin" do + describe "POST /v2/course/{course_id}/sourcecast, admin" do @tag authenticate: :admin test "successful for public sourcecast", %{conn: conn} do course_id = conn.assigns[:course_id] @@ -333,7 +333,7 @@ defmodule CadetWeb.SourcecastControllerTest do end end - describe "DELETE /v2/course/{courseId}/sourcecast, admin" do + describe "DELETE /v2/course/{course_id}/sourcecast, admin" do @tag authenticate: :admin test "successful for public sourcecast", %{conn: conn} do course_id = conn.assigns[:course_id] diff --git a/test/cadet_web/controllers/stories_controller_test.exs b/test/cadet_web/controllers/stories_controller_test.exs index 954cb892d..8c9809936 100644 --- a/test/cadet_web/controllers/stories_controller_test.exs +++ b/test/cadet_web/controllers/stories_controller_test.exs @@ -36,32 +36,32 @@ defmodule CadetWeb.StoriesControllerTest do end describe "unauthenticated" do - test "GET /v2/course/{courseId}/stories/", %{conn: conn} do + test "GET /v2/course/{course_id}/stories/", %{conn: conn} do course = insert(:course) conn = get(conn, build_url(course.id), %{}) assert response(conn, 401) =~ "Unauthorised" end - test "POST /v2/course/{courseId}/stories/", %{conn: conn} do + test "POST /v2/course/{course_id}/stories/", %{conn: conn} do course = insert(:course) conn = post(conn, build_url(course.id), %{}) assert response(conn, 401) =~ "Unauthorised" end - test "DELETE /v2/course/{courseId}/stories/:storyid", %{conn: conn} do + test "DELETE /v2/course/{course_id}/stories/:storyid", %{conn: conn} do course = insert(:course) conn = delete(conn, build_url(course.id, "storyid"), %{}) assert response(conn, 401) =~ "Unauthorised" end - test "POST /v2/course/{courseId}/stories/:storyid", %{conn: conn} do + test "POST /v2/course/{course_id}/stories/:storyid", %{conn: conn} do course = insert(:course) conn = post(conn, build_url(course.id, "storyid"), %{}) assert response(conn, 401) =~ "Unauthorised" end end - describe "GET /v2/course/{courseId}/stories" do + describe "GET /v2/course/{course_id}/stories" do @tag authenticate: :student test "student permission, only obtain published open stories from own course", %{ conn: conn, @@ -136,7 +136,7 @@ defmodule CadetWeb.StoriesControllerTest do |> response(200) |> Jason.decode() - required_fields = ~w(openAt closeAt isPublished id title filenames imageUrl courseId) + required_fields = ~w(openAt closeAt isPublished id title filenames imageUrl course_id) Enum.each(required_fields, fn required_field -> value = resp[required_field] @@ -146,14 +146,14 @@ defmodule CadetWeb.StoriesControllerTest do "id" -> assert is_integer(value) "filenames" -> assert is_list(value) "isPublished" -> assert is_boolean(value) - "courseId" -> assert is_integer(value) + "course_id" -> assert is_integer(value) _ -> assert is_binary(value) end end) end end - describe "DELETE /v2/course/{courseId}/stories/:storyid" do + describe "DELETE /v2/course/{course_id}/stories/:storyid" do @tag authenticate: :student test "student permission, forbidden", %{conn: conn} do course_id = conn.assigns[:course_id] @@ -190,7 +190,7 @@ defmodule CadetWeb.StoriesControllerTest do end end - describe "POST /v2/course/{courseId}/stories/" do + describe "POST /v2/course/{course_id}/stories/" do @tag authenticate: :student test "student permission, forbidden", %{conn: conn, valid_params: params} do course_id = conn.assigns[:course_id] @@ -216,7 +216,7 @@ defmodule CadetWeb.StoriesControllerTest do end end - describe "POST /v2/course/{courseId}/stories/:storyid" do + describe "POST /v2/course/{course_id}/stories/:storyid" do @tag authenticate: :student test "student permission, forbidden", %{conn: conn, valid_params: params} do course_id = conn.assigns[:course_id] diff --git a/test/factories/accounts/user_factory.ex b/test/factories/accounts/user_factory.ex index aa5221236..165fec74b 100644 --- a/test/factories/accounts/user_factory.ex +++ b/test/factories/accounts/user_factory.ex @@ -11,26 +11,22 @@ defmodule Cadet.Accounts.UserFactory do def user_factory do %User{ name: Faker.Person.En.name(), - # role: Enum.random(Role.__enum_map__()), username: sequence( :nusnet_id, &"E#{&1 |> Integer.to_string() |> String.pad_leading(7, "0")}" ) - # game_states: %{} } end def student_factory do %User{ name: Faker.Person.En.name(), - # role: :student, username: sequence( :nusnet_id, &"E#{&1 |> Integer.to_string() |> String.pad_leading(7, "0")}" ) - # game_states: %{} } end end diff --git a/test/factories/courses/group_factory.ex b/test/factories/courses/group_factory.ex index c46c0769c..9d7f51ec5 100644 --- a/test/factories/courses/group_factory.ex +++ b/test/factories/courses/group_factory.ex @@ -9,9 +9,9 @@ defmodule Cadet.Courses.GroupFactory do def group_factory do %Group{ - name: sequence("group") - # leader: build(:course_registration) - # leader: build(:user, role: :staff) + name: sequence("group"), + # leader: build(:course_registration), + # course: build(:course) } end end From dd1b5206ae970f39a800ea362f1d6929cfc75137 Mon Sep 17 00:00:00 2001 From: YaleChen299 Date: Wed, 9 Jun 2021 20:34:45 +0800 Subject: [PATCH 046/174] updated accounts context function and adminUserController with test --- .../20210608085548_update_assessments.exs | 4 +- .../admin_user_controller_test.exs | 69 +++++++++++-------- .../accounts/course_registration_factory.ex | 2 +- test/factories/courses/group_factory.ex | 4 +- 4 files changed, 45 insertions(+), 34 deletions(-) diff --git a/priv/repo/migrations/20210608085548_update_assessments.exs b/priv/repo/migrations/20210608085548_update_assessments.exs index 19efcaedb..e6d660c6f 100644 --- a/priv/repo/migrations/20210608085548_update_assessments.exs +++ b/priv/repo/migrations/20210608085548_update_assessments.exs @@ -20,10 +20,10 @@ defmodule Cadet.Repo.Migrations.UpdateAssessments do alter table(:groups) do remove(:leader_id) - add(:leader_id, references(:course_registrations), null: true) + add(:leader_id, references(:course_registrations), null: false) remove(:mentor_id) add(:mentor_id, references(:course_registrations), null: true) - add(:course_id, references(:courses), null: true) + add(:course_id, references(:courses), null: false) end create(unique_index(:groups, [:name, :course_id])) diff --git a/test/cadet_web/admin_controllers/admin_user_controller_test.exs b/test/cadet_web/admin_controllers/admin_user_controller_test.exs index 702835450..00565bab7 100644 --- a/test/cadet_web/admin_controllers/admin_user_controller_test.exs +++ b/test/cadet_web/admin_controllers/admin_user_controller_test.exs @@ -19,7 +19,9 @@ defmodule CadetWeb.AdminUserControllerTest do test "success, when staff retrieves users", %{conn: conn} do course_id = conn.assigns[:course_id] course = Repo.get(Course, course_id) - stu = insert(:course_registration, %{role: :student, course: course}) + group = insert(:group, %{course: course}) + insert(:course_registration, %{role: :student, course: course, group: group}) + insert(:course_registration, %{role: :staff, course: course, group: group}) resp = conn @@ -29,43 +31,52 @@ defmodule CadetWeb.AdminUserControllerTest do assert 2 == Enum.count(resp) end - # @tag authenticate: :staff - # test "can filter by role", %{conn: conn} do - # insert(:student) + @tag authenticate: :staff + test "can filter by role", %{conn: conn} do + course_id = conn.assigns[:course_id] + course = Repo.get(Course, course_id) + group = insert(:group, %{course: course}) + insert(:course_registration, %{role: :student, course: course, group: group}) + insert(:course_registration, %{role: :staff, course: course, group: group}) - # resp = - # conn - # |> get("/v2/admin/users?role=student") - # |> json_response(200) + resp = + conn + |> get(build_url(course_id) <> "?role=student") + |> json_response(200) - # assert 1 == Enum.count(resp) - # assert "student" == List.first(resp)["role"] - # end + assert 1 == Enum.count(resp) + assert "student" == List.first(resp)["role"] + end - # @tag authenticate: :staff - # test "can filter by group", %{conn: conn} do - # group = insert(:group) - # insert(:student, group: group) + @tag authenticate: :staff + test "can filter by group", %{conn: conn} do + course_id = conn.assigns[:course_id] + course = Repo.get(Course, course_id) + group = insert(:group, %{course: course}) + insert(:course_registration, %{role: :student, course: course, group: group}) + insert(:course_registration, %{role: :staff, course: course, group: group}) - # resp = - # conn - # |> get("/v2/admin/users?group=#{group.name}") - # |> json_response(200) + resp = + conn + |> get(build_url(course_id) <> "?group=#{group.name}") + |> json_response(200) - # assert 1 == Enum.count(resp) - # assert group.name == List.first(resp)["group"] - # end + assert 2 == Enum.count(resp) + assert group.name == List.first(resp)["group"] + end - # @tag authenticate: :student - # test "forbidden, when student retrieves users", %{conn: conn} do - # assert conn - # |> get("/v2/admin/users") - # |> response(403) - # end + @tag authenticate: :student + test "forbidden, when student retrieves users", %{conn: conn} do + course_id = conn.assigns[:course_id] + assert conn + |> get(build_url(course_id)) + |> response(403) + end # test "401 when not logged in", %{conn: conn} do + # course_id = conn.assigns[:course_id] # assert conn - # |> get("/v2/admin/users") + # |> get(build_url(course_id)) # |> response(401) # end end diff --git a/test/factories/accounts/course_registration_factory.ex b/test/factories/accounts/course_registration_factory.ex index 15cba58c0..9e3a1a0f6 100644 --- a/test/factories/accounts/course_registration_factory.ex +++ b/test/factories/accounts/course_registration_factory.ex @@ -12,7 +12,7 @@ defmodule Cadet.Accounts.CourseRegistrationFactory do %CourseRegistration{ user: build(:user), course: build(:course), - group: build(:group), + # group: build(:group), role: Enum.random(Role.__enum_map__()), game_states: %{} } diff --git a/test/factories/courses/group_factory.ex b/test/factories/courses/group_factory.ex index 9d7f51ec5..92e6a5c99 100644 --- a/test/factories/courses/group_factory.ex +++ b/test/factories/courses/group_factory.ex @@ -10,8 +10,8 @@ defmodule Cadet.Courses.GroupFactory do def group_factory do %Group{ name: sequence("group"), - # leader: build(:course_registration), - # course: build(:course) + leader: build(:course_registration), + course: build(:course) } end end From 2026d6aa04ad3a6616a53a637e9efb647dc2ef1d Mon Sep 17 00:00:00 2001 From: YaleChen299 Date: Thu, 10 Jun 2021 11:39:06 +0800 Subject: [PATCH 047/174] update accounts_test with get_users_by and fix queries with no groups --- lib/cadet/accounts/accounts.ex | 5 +- test/cadet/accounts/accounts_test.exs | 48 +++++++++++++++---- .../admin_user_controller_test.exs | 6 +-- 3 files changed, 45 insertions(+), 14 deletions(-) diff --git a/lib/cadet/accounts/accounts.ex b/lib/cadet/accounts/accounts.ex index efa05fb91..eeeb41b72 100644 --- a/lib/cadet/accounts/accounts.ex +++ b/lib/cadet/accounts/accounts.ex @@ -44,15 +44,16 @@ defmodule Cadet.Accounts do Repo.get(User, id) end + @get_all_role ~w(admin staff)a @doc """ Returns users matching a given set of criteria. """ - def get_users_by(filter \\ [], %CourseRegistration{course_id: course_id}) do + def get_users_by(filter \\ [], %CourseRegistration{course_id: course_id, role: role}) when role in @get_all_role do CourseRegistration |> where([cr], cr.course_id == ^course_id) |> join(:inner, [cr], u in assoc(cr, :user)) |> preload([cr, u], user: u) - |> join(:inner, [cr, u], g in assoc(cr, :group)) + |> join(:left, [cr, u], g in assoc(cr, :group)) |> preload([cr, u, g], group: g) |> get_users_helper(filter) end diff --git a/test/cadet/accounts/accounts_test.exs b/test/cadet/accounts/accounts_test.exs index edb4d1aa2..1b421751e 100644 --- a/test/cadet/accounts/accounts_test.exs +++ b/test/cadet/accounts/accounts_test.exs @@ -9,7 +9,7 @@ defmodule Cadet.AccountsTest do alias Cadet.{Accounts, Repo} alias Cadet.Accounts.{Query, User} - import Mock + # import Mock setup_all do HTTPoison.start() @@ -107,21 +107,53 @@ defmodule Cadet.AccountsTest do end end - describe "get_users_by" do - test "get all users in a course" do + describe "get_users_by" do + setup do + c1 = insert(:course, %{name: "c1"}) + c2 = insert(:course, %{name: "c2"}) + admin1 = insert(:course_registration, %{course: c1, role: :admin}) + admin2 = insert(:course_registration, %{course: c2, role: :admin}) + g1 = insert(:group, %{course: c1}) + g2 = insert(:group, %{course: c1}) + insert(:course_registration, %{course: c1, group: g1, role: :student}) + insert(:course_registration, %{course: c1, group: g1, role: :student}) + + {:ok, %{c1: c1, c2: c2, a1: admin1, a2: admin2, g1: g1, g2: g2}} end - test "get all students in a course" do - + test "get all users in a course", %{a1: admin1, a2: admin2} do + all_in_c1 = Accounts.get_users_by([], admin1) + assert length(all_in_c1) == 3 + all_in_c2 = Accounts.get_users_by([], admin2) + assert length(all_in_c2) == 1 end - test "get all users in a group in a course" do - + test "get all students in a course", %{a1: admin1, a2: admin2} do + all_stu_in_c1 = Accounts.get_users_by([role: :student], admin1) + assert length(all_stu_in_c1) == 2 + all_stu_in_c2 = Accounts.get_users_by([role: :student], admin2) + assert length(all_stu_in_c2) == 0 end - test "get all students in a group in a course" do + test "get all users in a group in a course", %{a1: admin1, g1: g1, g2: g2} do + all_in_c1g1 = Accounts.get_users_by([group: g1.name], admin1) + assert length(all_in_c1g1) == 2 + all_in_c1g2 = Accounts.get_users_by([group: g2.name], admin1) + assert length(all_in_c1g2) == 0 + end + test "get all students in a group in a course", %{c1: c1, a1: admin1, g1: g1, g2: g2} do + insert(:course_registration, %{course: c1, group: g1, role: :staff}) + insert(:course_registration, %{course: c1, group: g2, role: :staff}) + all_in_c1g1 = Accounts.get_users_by([group: g1.name], admin1) + assert length(all_in_c1g1) == 3 + all_in_c1g2 = Accounts.get_users_by([group: g2.name], admin1) + assert length(all_in_c1g2) == 1 + all_stu_in_c1g1 = Accounts.get_users_by([group: g1.name, role: :student], admin1) + assert length(all_stu_in_c1g1) == 2 + all_stu_in_c1g2 = Accounts.get_users_by([group: g2.name, role: :student], admin1) + assert length(all_stu_in_c1g2) == 0 end end end diff --git a/test/cadet_web/admin_controllers/admin_user_controller_test.exs b/test/cadet_web/admin_controllers/admin_user_controller_test.exs index 00565bab7..fed1fde87 100644 --- a/test/cadet_web/admin_controllers/admin_user_controller_test.exs +++ b/test/cadet_web/admin_controllers/admin_user_controller_test.exs @@ -6,8 +6,6 @@ defmodule CadetWeb.AdminUserControllerTest do alias CadetWeb.AdminUserController alias Cadet.Repo alias Cadet.Courses.Course - alias Cadet.Accounts.CourseRegistrations - alias Cadet.Accounts test "swagger" do assert is_map(AdminUserController.swagger_definitions()) @@ -16,7 +14,7 @@ defmodule CadetWeb.AdminUserControllerTest do describe "GET /v2/course/{course_id}/admin/users" do @tag authenticate: :staff - test "success, when staff retrieves users", %{conn: conn} do + test "success, when staff retrieves all users", %{conn: conn} do course_id = conn.assigns[:course_id] course = Repo.get(Course, course_id) group = insert(:group, %{course: course}) @@ -28,7 +26,7 @@ defmodule CadetWeb.AdminUserControllerTest do |> get(build_url(course_id)) |> json_response(200) - assert 2 == Enum.count(resp) + assert 3 == Enum.count(resp) end @tag authenticate: :staff From 6f32c378d202c9f96efb10977af7dd9fd3de004d Mon Sep 17 00:00:00 2001 From: YaleChen299 Date: Sat, 12 Jun 2021 22:32:18 +0800 Subject: [PATCH 048/174] update userController with test(except for stories) --- lib/cadet/accounts/course_registrations.ex | 14 +- lib/cadet/assessments/assessments.ex | 38 +- .../admin_user_controller.ex | 7 +- lib/cadet_web/controllers/user_controller.ex | 39 +- lib/cadet_web/router.ex | 20 +- lib/cadet_web/views/user_view.ex | 117 ++++- .../controllers/user_controller_test.exs | 443 +++++++++--------- test/factories/accounts/user_factory.ex | 6 +- test/support/conn_case.ex | 4 +- 9 files changed, 409 insertions(+), 279 deletions(-) diff --git a/lib/cadet/accounts/course_registrations.ex b/lib/cadet/accounts/course_registrations.ex index ed9ab8091..968e6391d 100644 --- a/lib/cadet/accounts/course_registrations.ex +++ b/lib/cadet/accounts/course_registrations.ex @@ -20,13 +20,15 @@ defmodule Cadet.Accounts.CourseRegistrations do |> where([cr], cr.user_id == ^user_id) |> where([cr], cr.course_id == ^course_id) |> Repo.one() - |> case do - nil -> - {:error, :no_record} + end - cr -> - {:ok, cr} - end + def get_user_course(user_id, course_id) when is_ecto_id(user_id) and is_ecto_id(course_id) do + CourseRegistration + |> where([cr], cr.user_id == ^user_id) + |> where([cr], cr.course_id == ^course_id) + |> preload(:course) + |> preload(:group) + |> Repo.one() end def get_courses(%User{id: id}) do diff --git a/lib/cadet/assessments/assessments.ex b/lib/cadet/assessments/assessments.ex index 9055f0ec1..f9c907756 100644 --- a/lib/cadet/assessments/assessments.ex +++ b/lib/cadet/assessments/assessments.ex @@ -63,11 +63,11 @@ defmodule Cadet.Assessments do Repo.delete_all(submissions) end - @spec user_max_grade(%User{}) :: integer() - def user_max_grade(%User{id: user_id}) when is_ecto_id(user_id) do + @spec user_max_grade(%CourseRegistration{}) :: integer() + def user_max_grade(%CourseRegistration{id: cr_id}) do Submission |> where(status: ^:submitted) - |> where(student_id: ^user_id) + |> where(student_id: ^cr_id) |> join( :inner, [s], @@ -79,10 +79,10 @@ defmodule Cadet.Assessments do |> decimal_to_integer() end - def user_total_grade_xp(%User{id: user_id}) do + def user_total_grade_xp(%CourseRegistration{id: cr_id}) do submission_grade_xp = Submission - |> where(student_id: ^user_id) + |> where(student_id: ^cr_id) |> join(:inner, [s], a in Answer, on: s.id == a.submission_id) |> group_by([s], s.id) |> select([s, a], %{ @@ -112,23 +112,18 @@ defmodule Cadet.Assessments do end end - def user_with_group(%User{id: id}) do - User - |> preload(:group) - |> Repo.get(id) - end - - def user_current_story(user = %User{}) do + # :TODO to check how this story works + def user_current_story(cr = %CourseRegistration{}) do {:ok, %{result: story}} = Multi.new() |> Multi.run(:unattempted, fn _repo, _ -> - {:ok, get_user_story_by_type(user, :unattempted)} + {:ok, get_user_story_by_type(cr, :unattempted)} end) |> Multi.run(:result, fn _repo, %{unattempted: unattempted_story} -> if unattempted_story do {:ok, %{play_story?: true, story: unattempted_story}} else - {:ok, %{play_story?: false, story: get_user_story_by_type(user, :attempted)}} + {:ok, %{play_story?: false, story: get_user_story_by_type(cr, :attempted)}} end end) |> Repo.transaction() @@ -136,8 +131,8 @@ defmodule Cadet.Assessments do story end - @spec get_user_story_by_type(%User{}, :unattempted | :attempted) :: String.t() | nil - def get_user_story_by_type(%User{id: user_id}, type) + @spec get_user_story_by_type(%CourseRegistration{}, :unattempted | :attempted) :: String.t() | nil + def get_user_story_by_type(%CourseRegistration{id: cr_id}, type) when is_atom(type) do filter_and_sort = fn query -> case type do @@ -155,9 +150,9 @@ defmodule Cadet.Assessments do |> where(is_published: true) |> where([a], not is_nil(a.story)) |> where([a], a.open_at <= from_now(0, "second") and a.close_at >= from_now(0, "second")) - |> join(:left, [a], s in Submission, on: s.assessment_id == a.id and s.student_id == ^user_id) + |> join(:left, [a], s in Submission, on: s.assessment_id == a.id and s.student_id == ^cr_id) |> filter_and_sort.() - |> order_by([a], a.type) + |> order_by([a], a.type_id) |> select([a], a.story) |> first() |> Repo.one() @@ -203,9 +198,10 @@ defmodule Cadet.Assessments do end end - def assessment_with_questions_and_answers(id, user = %User{}, password) + # def assessment_with_questions_and_answers(id, user = %User{}, password) + def assessment_with_questions_and_answers(id, cr = %CourseRegistration{}, password) when is_ecto_id(id) do - role = user.role + role = cr.role assessment = if role in @open_all_assessment_roles do @@ -220,7 +216,7 @@ defmodule Cadet.Assessments do end if assessment do - assessment_with_questions_and_answers(assessment, user, password) + assessment_with_questions_and_answers(assessment, cr, password) else {:error, {:bad_request, "Assessment not found"}} end diff --git a/lib/cadet_web/admin_controllers/admin_user_controller.ex b/lib/cadet_web/admin_controllers/admin_user_controller.ex index ec783a36e..23a77116f 100644 --- a/lib/cadet_web/admin_controllers/admin_user_controller.ex +++ b/lib/cadet_web/admin_controllers/admin_user_controller.ex @@ -3,9 +3,8 @@ defmodule CadetWeb.AdminUserController do use PhoenixSwagger alias Cadet.Accounts - # alias Cadet.Accounts.CourseRegistrations - # :TODO this controller seems to be obsolte in the current version we will use the course_reg controller to find all users of a course + # This controller is used to find all users of a course def index(conn, filter) do users = filter |> try_keywordise_string_keys() |> Accounts.get_users_by(conn.assigns.course_reg) @@ -14,7 +13,7 @@ defmodule CadetWeb.AdminUserController do end swagger_path :index do - get("/admin/users") + get("/v2/course/{course_id}/admin/users") summary("Returns a list of users in the course owned by the admin") @@ -29,7 +28,7 @@ defmodule CadetWeb.AdminUserController do AdminUserInfo: swagger_schema do title("User") - description("Basic information about the users in this course") + description("Basic information about the user in this course") properties do userId(:integer, "User's ID") diff --git a/lib/cadet_web/controllers/user_controller.ex b/lib/cadet_web/controllers/user_controller.ex index f8ac61f26..659899571 100644 --- a/lib/cadet_web/controllers/user_controller.ex +++ b/lib/cadet_web/controllers/user_controller.ex @@ -7,29 +7,50 @@ defmodule CadetWeb.UserController do use PhoenixSwagger import Cadet.Assessments alias Cadet.Accounts + alias Cadet.Accounts.CourseRegistrations def index(conn, _) do - user = user_with_group(conn.assigns.current_user) - %{total_grade: grade, total_xp: xp} = user_total_grade_xp(user) - max_grade = user_max_grade(user) - story = user_current_story(user) + user = Accounts.get_user(conn.assigns.current_user.id) + user_courses = CourseRegistrations.get_courses(conn.assigns.current_user) + latest = CourseRegistrations.get_user_course(user.id, user.latest_viewed_id) + + %{total_grade: grade, total_xp: xp} = user_total_grade_xp(latest) + max_grade = user_max_grade(latest) + story = user_current_story(latest) render( conn, "index.json", user: user, + courses: user_courses, + latest: latest, grade: grade, max_grade: max_grade, story: story, - xp: xp, - game_states: user.game_states + xp: xp ) end + # def index(conn, _) do + # user = user_with_group(conn.assigns.current_user) + # %{total_grade: grade, total_xp: xp} = user_total_grade_xp(user) + # max_grade = user_max_grade(user) + # story = user_current_story(user) + + # render( + # conn, + # "index.json", + # user: user, + # grade: grade, + # max_grade: max_grade, + # story: story, + # xp: xp + # ) + # end def update_game_states(conn, %{"gameStates" => new_game_states}) do user = conn.assigns[:current_user] - case Accounts.update_game_states(user, new_game_states) do + case CourseRegistrations.update_game_states(user, new_game_states) do {:ok, %{}} -> text(conn, "OK") @@ -41,9 +62,9 @@ defmodule CadetWeb.UserController do end swagger_path :index do - get("/user") + get("/v2/user") - summary("Get the name, role and group of a user") + summary("Get the name, and latest_viewed_course of a user") security([%{JWT: []}]) produces("application/json") diff --git a/lib/cadet_web/router.ex b/lib/cadet_web/router.ex index bb3edb556..8d3c1acb2 100644 --- a/lib/cadet_web/router.ex +++ b/lib/cadet_web/router.ex @@ -49,7 +49,16 @@ defmodule CadetWeb.Router do get("/devices/:secret/mqtt_endpoint", DevicesController, :get_mqtt_endpoint) end - # Authenticated Pages + # Authenticated Pages without course + scope "/v2", CadetWeb do + pipe_through([:api, :auth, :ensure_auth]) + + get("/user", UserController, :index) + get("/user/latest_viewed", UserController, :get_latest_viewed) + post("/user/latest_viewed", UserController, :update_latest_viewed) + end + + # Authenticated Pages with course scope "/v2/course/:course_id", CadetWeb do pipe_through([:api, :auth, :ensure_auth, :course]) @@ -72,7 +81,7 @@ defmodule CadetWeb.Router do get("/notifications", NotificationsController, :index) post("/notifications/acknowledge", NotificationsController, :acknowledge) - get("/user", UserController, :index) + get("/user", UserController, :get_course_reg) put("/user/game_states", UserController, :update_game_states) get("/config", CoursesController, :index) @@ -171,11 +180,8 @@ defmodule CadetWeb.Router do Cadet.Accounts.CourseRegistrations.get_user_record(conn.assigns.current_user.id, course_id) case course_reg do - {:ok, cr} -> - assign(conn, :course_reg, cr) - - {:error, :no_record} -> - send_resp(conn, 403, "Forbidden") |> halt() + nil -> send_resp(conn, 403, "Forbidden") |> halt() + cr -> assign(conn, :course_reg, cr) end end diff --git a/lib/cadet_web/views/user_view.ex b/lib/cadet_web/views/user_view.ex index 553f00603..382d7321f 100644 --- a/lib/cadet_web/views/user_view.ex +++ b/lib/cadet_web/views/user_view.ex @@ -1,32 +1,111 @@ defmodule CadetWeb.UserView do use CadetWeb, :view + # def render("index.json", %{ + # user: user, + # cr: cr, + # grade: grade, + # max_grade: max_grade, + # xp: xp, + # story: story + # }) do + # %{ + # userId: user.id, + # name: user.name, + # role: cr.role, + # group: + # case cr.group do + # nil -> nil + # _ -> cr.group.name + # end, + # grade: grade, + # xp: xp, + # maxGrade: max_grade, + # story: + # transform_map_for_view(story, %{ + # story: :story, + # playStory: :play_story? + # }), + # gameStates: cr.game_states + # } + # end + def render("index.json", %{ user: user, + courses: courses, + latest: latest, grade: grade, max_grade: max_grade, xp: xp, - story: story, - game_states: game_states + story: story }) do %{ - userId: user.id, - name: user.name, - role: user.role, - group: - case user.group do - nil -> nil - _ -> user.group.name - end, - grade: grade, - xp: xp, - maxGrade: max_grade, - story: - transform_map_for_view(story, %{ - story: :story, - playStory: :play_story? - }), - gameStates: game_states + user: %{ + userId: user.id, + name: user.name, + courses: render_many(courses, CadetWeb.UserView, "course.json", as: :cr) + }, + latestViewedCourse: render_latest(%{ + latest: latest, + grade: grade, + max_grade: max_grade, + xp: xp, + story: story + }) + } + end + + def render("course.json", %{cr: cr}) do + %{ + course_id: cr.course_id, + name: cr.course.name, + moduleCode: cr.course.module_code, + viewable: cr.course.viewable } end + + defp render_latest(%{ + latest: latest, + grade: grade, + max_grade: max_grade, + xp: xp, + story: story + }) do + + case latest do + nil -> nil + + _ ->%{ + course: transform_map_for_view(latest.course, [ + :name, + :module_code, + :viewable, + :enable_game, + :enable_achievements, + :enable_sourcecast, + :source_chapter, + :source_variant, + :module_help_text, + :assessment_types + ]), + role: latest.role, + group: + case latest.group do + nil -> nil + _ -> latest.group.name + end, + grade: grade, + xp: xp, + maxGrade: max_grade, + story: + transform_map_for_view(story, %{ + story: :story, + playStory: :play_story? + }), + gameStates: latest.game_states + } + end + end + + end diff --git a/test/cadet_web/controllers/user_controller_test.exs b/test/cadet_web/controllers/user_controller_test.exs index e1bd36ccf..afd70b13b 100644 --- a/test/cadet_web/controllers/user_controller_test.exs +++ b/test/cadet_web/controllers/user_controller_test.exs @@ -6,24 +6,26 @@ defmodule CadetWeb.UserControllerTest do alias Cadet.Repo alias CadetWeb.UserController alias Cadet.Assessments.{Assessment, Submission} - alias Cadet.Accounts.User + alias Cadet.Accounts.{User, CourseRegistration} test "swagger" do assert is_map(UserController.swagger_definitions()) assert is_map(UserController.swagger_path_index(nil)) end - describe "GET /user" do + describe "GET v2/user" do @tag authenticate: :student test "success, student non-story fields", %{conn: conn} do user = conn.assigns.current_user - assessment = insert(:assessment, %{is_published: true}) + course = user.latest_viewed + cr = Repo.get_by(CourseRegistration, course_id: course.id, user_id: user.id) + assessment = insert(:assessment, %{is_published: true, course: course}) question = insert(:question, %{assessment: assessment}) submission = insert(:submission, %{ assessment: assessment, - student: user, + student: cr, status: :submitted, xp_bonus: 100 }) @@ -37,11 +39,11 @@ defmodule CadetWeb.UserControllerTest do xp_adjustment: -10 }) - not_submitted_assessment = insert(:assessment, is_published: true) + not_submitted_assessment = insert(:assessment, %{is_published: true, course: course}) not_submitted_question = insert(:question, assessment: not_submitted_assessment) not_submitted_submission = - insert(:submission, assessment: not_submitted_assessment, student: user) + insert(:submission, %{assessment: not_submitted_assessment, student: cr}) insert( :answer, @@ -55,229 +57,250 @@ defmodule CadetWeb.UserControllerTest do conn |> get("/v2/user") |> json_response(200) - |> Map.delete("story") + |> put_in(["latestViewedCourse", "story"], nil) + |> IO.inspect() expected = %{ - "name" => user.name, - "role" => "#{user.role}", - "group" => nil, - "xp" => 110, - "grade" => 40, - "maxGrade" => question.max_grade, - "gameStates" => %{}, - "userId" => user.id - } - - assert expected == resp - end - - # This also tests for the case where assessment has no submission - @tag authenticate: :student - test "success, student story ordering", %{conn: conn} do - early_assessments = build_assessments_starting_at(Timex.shift(Timex.now(), days: -3)) - late_assessments = build_assessments_starting_at(Timex.shift(Timex.now(), days: -1)) - - for assessment <- early_assessments ++ late_assessments do - resp_story = - conn - |> get("/v2/user") - |> json_response(200) - |> Map.get("story") - - expected_story = %{ - "story" => assessment.story, - "playStory" => true + "user" => %{ + "userId" => user.id, + "name" => user.name, + "courses" => [ + %{"course_id" => user.latest_viewed_id, "moduleCode" => "CS1101S", "name" => "Programming Methodology", "viewable" => true} + ] + }, + "latestViewedCourse" => %{ + "course" => %{ + "assessment_types" => nil, + "enable_achievements" => true, + "enable_game" => true, + "enable_sourcecast" => true, + "module_code" => "CS1101S", + "module_help_text" => "Help Text", + "name" => "Programming Methodology", + "source_chapter" => 1, + "source_variant" => "default", + "viewable" => true + }, + "role" => "#{cr.role}", + "group" => nil, + "xp" => 110, + "grade" => 40, + "maxGrade" => question.max_grade, + "gameStates" => %{}, + "story" => nil } - - assert resp_story == expected_story - - {:ok, _} = Repo.delete(assessment) - end - end - - @tag authenticate: :student - test "success, student story skips assessment without story", %{conn: conn} do - assessments = build_assessments_starting_at(Timex.shift(Timex.now(), days: -1)) - - assessments - |> List.first() - |> Assessment.changeset(%{story: nil}) - |> Repo.update() - - resp_story = - conn - |> get("/v2/user") - |> json_response(200) - |> Map.get("story") - - expected_story = %{ - "story" => Enum.fetch!(assessments, 1).story, - "playStory" => true - } - - assert resp_story == expected_story - end - - @tag authenticate: :student - test "success, student story skips unopen assessments", %{conn: conn} do - build_assessments_starting_at(Timex.shift(Timex.now(), days: 1)) - build_assessments_starting_at(Timex.shift(Timex.now(), months: -1)) - - valid_assessments = build_assessments_starting_at(Timex.shift(Timex.now(), days: -1)) - - for assessment <- valid_assessments do - assessment - |> Assessment.changeset(%{is_published: false}) - |> Repo.update!() - end - - resp_story = - conn - |> get("/v2/user") - |> json_response(200) - |> Map.get("story") - - expected_story = %{ - "story" => nil, - "playStory" => false - } - - assert resp_story == expected_story - end - - @tag authenticate: :student - test "success, student story skips attempting/attempted/submitted", %{conn: conn} do - user = conn.assigns.current_user - - early_assessments = build_assessments_starting_at(Timex.shift(Timex.now(), days: -3)) - late_assessments = build_assessments_starting_at(Timex.shift(Timex.now(), days: -1)) - - # Submit for i-th assessment, expect (i+1)th story to be returned - for status <- [:attempting, :attempted, :submitted] do - for [tester, checker] <- - Enum.chunk_every(early_assessments ++ late_assessments, 2, 1, :discard) do - insert(:submission, %{student: user, assessment: tester, status: status}) - - resp_story = - conn - |> get("/v2/user") - |> json_response(200) - |> Map.get("story") - - expected_story = %{ - "story" => checker.story, - "playStory" => true - } - - assert resp_story == expected_story - end - - Repo.delete_all(Submission) - end - end - - @tag authenticate: :student - test "success, return most recent assessment when all are attempted", %{conn: conn} do - user = conn.assigns.current_user - - early_assessments = build_assessments_starting_at(Timex.shift(Timex.now(), days: -3)) - late_assessments = build_assessments_starting_at(Timex.shift(Timex.now(), days: -1)) - - for assessment <- early_assessments ++ late_assessments do - insert(:submission, %{student: user, assessment: assessment, status: :attempted}) - end - - resp_story = - conn - |> get("/v2/user") - |> json_response(200) - |> Map.get("story") - - expected_story = %{ - "story" => late_assessments |> List.first() |> Map.get(:story), - "playStory" => false - } - - assert resp_story == expected_story - end - - @tag authenticate: :staff - test "success, staff", %{conn: conn} do - user = conn.assigns.current_user - - resp = - conn - |> get("/v2/user") - |> json_response(200) - |> Map.delete("story") - - expected = %{ - "name" => user.name, - "role" => "#{user.role}", - "group" => nil, - "grade" => 0, - "maxGrade" => 0, - "xp" => 0, - "gameStates" => %{}, - "userId" => user.id } assert expected == resp end + # # This also tests for the case where assessment has no submission + # @tag authenticate: :student + # test "success, student story ordering", %{conn: conn} do + # early_assessments = build_assessments_starting_at(Timex.shift(Timex.now(), days: -3)) + # late_assessments = build_assessments_starting_at(Timex.shift(Timex.now(), days: -1)) + + # for assessment <- early_assessments ++ late_assessments do + # resp_story = + # conn + # |> get("/v2/user") + # |> json_response(200) + # |> Map.get("latestViewedCourse").story + + # expected_story = %{ + # "story" => assessment.story, + # "playStory" => true + # } + + # assert resp_story == expected_story + + # {:ok, _} = Repo.delete(assessment) + # end + # end + + # @tag authenticate: :student + # test "success, student story skips assessment without story", %{conn: conn} do + # assessments = build_assessments_starting_at(Timex.shift(Timex.now(), days: -1)) + + # assessments + # |> List.first() + # |> Assessment.changeset(%{story: nil}) + # |> Repo.update() + + # resp_story = + # conn + # |> get("/v2/user") + # |> json_response(200) + # |> Map.get("story") + + # expected_story = %{ + # "story" => Enum.fetch!(assessments, 1).story, + # "playStory" => true + # } + + # assert resp_story == expected_story + # end + + # @tag authenticate: :student + # test "success, student story skips unopen assessments", %{conn: conn} do + # build_assessments_starting_at(Timex.shift(Timex.now(), days: 1)) + # build_assessments_starting_at(Timex.shift(Timex.now(), months: -1)) + + # valid_assessments = build_assessments_starting_at(Timex.shift(Timex.now(), days: -1)) + + # for assessment <- valid_assessments do + # assessment + # |> Assessment.changeset(%{is_published: false}) + # |> Repo.update!() + # end + + # resp_story = + # conn + # |> get("/v2/user") + # |> json_response(200) + # |> Map.get("story") + + # expected_story = %{ + # "story" => nil, + # "playStory" => false + # } + + # assert resp_story == expected_story + # end + + # @tag authenticate: :student + # test "success, student story skips attempting/attempted/submitted", %{conn: conn} do + # user = conn.assigns.current_user + + # early_assessments = build_assessments_starting_at(Timex.shift(Timex.now(), days: -3)) + # late_assessments = build_assessments_starting_at(Timex.shift(Timex.now(), days: -1)) + + # # Submit for i-th assessment, expect (i+1)th story to be returned + # for status <- [:attempting, :attempted, :submitted] do + # for [tester, checker] <- + # Enum.chunk_every(early_assessments ++ late_assessments, 2, 1, :discard) do + # insert(:submission, %{student: user, assessment: tester, status: status}) + + # resp_story = + # conn + # |> get("/v2/user") + # |> json_response(200) + # |> Map.get("story") + + # expected_story = %{ + # "story" => checker.story, + # "playStory" => true + # } + + # assert resp_story == expected_story + # end + + # Repo.delete_all(Submission) + # end + # end + + # @tag authenticate: :student + # test "success, return most recent assessment when all are attempted", %{conn: conn} do + # user = conn.assigns.current_user + + # early_assessments = build_assessments_starting_at(Timex.shift(Timex.now(), days: -3)) + # late_assessments = build_assessments_starting_at(Timex.shift(Timex.now(), days: -1)) + + # for assessment <- early_assessments ++ late_assessments do + # insert(:submission, %{student: user, assessment: assessment, status: :attempted}) + # end + + # resp_story = + # conn + # |> get("/v2/user") + # |> json_response(200) + # |> Map.get("story") + + # expected_story = %{ + # "story" => late_assessments |> List.first() |> Map.get(:story), + # "playStory" => false + # } + + # assert resp_story == expected_story + # end + + # @tag authenticate: :staff + # test "success, staff", %{conn: conn} do + # user = conn.assigns.current_user + + # resp = + # conn + # |> get("/v2/user") + # |> json_response(200) + # |> Map.delete("story") + + # expected = %{ + # "name" => user.name, + # "role" => "#{user.role}", + # "group" => nil, + # "grade" => 0, + # "maxGrade" => 0, + # "xp" => 0, + # "gameStates" => %{}, + # "userId" => user.id + # } + + # assert expected == resp + # end + test "unauthorized", %{conn: conn} do conn = get(conn, "/v2/user", nil) assert response(conn, 401) =~ "Unauthorised" end - defp build_assessments_starting_at(time) do - type_order_map = - Assessment.assessment_types() - |> Enum.with_index() - |> Enum.reduce(%{}, fn {type, idx}, acc -> Map.put(acc, type, idx) end) - - Assessment.assessment_types() - |> Enum.map( - &build(:assessment, %{ - type: &1, - is_published: true, - open_at: time, - close_at: Timex.shift(time, days: 10) - }) - ) - |> Enum.shuffle() - |> Enum.map(&insert(&1)) - |> Enum.sort(&(type_order_map[&1.type] < type_order_map[&2.type])) - end + # defp build_assessments_starting_at(time) do + # type_order_map = + # Assessment.assessment_types() + # |> Enum.with_index() + # |> Enum.reduce(%{}, fn {type, idx}, acc -> Map.put(acc, type, idx) end) + + # Assessment.assessment_types() + # |> Enum.map( + # &build(:assessment, %{ + # type: &1, + # is_published: true, + # open_at: time, + # close_at: Timex.shift(time, days: 10) + # }) + # ) + # |> Enum.shuffle() + # |> Enum.map(&insert(&1)) + # |> Enum.sort(&(type_order_map[&1.type] < type_order_map[&2.type])) + # end end - describe "PUT /user/game_states" do - @tag authenticate: :student - test "success, updating game state", %{conn: conn} do - user = conn.assigns.current_user + # describe "PUT /user/game_states" do + # @tag authenticate: :student + # test "success, updating game state", %{conn: conn} do + # user = conn.assigns.current_user - new_game_states = %{ - "gameSaveStates" => %{"1" => %{}, "2" => %{}}, - "userSaveState" => %{} - } + # new_game_states = %{ + # "gameSaveStates" => %{"1" => %{}, "2" => %{}}, + # "userSaveState" => %{} + # } - conn - |> put("/v2/user/game_states", %{"gameStates" => new_game_states}) - |> response(200) + # conn + # |> put("/v2/user/game_states", %{"gameStates" => new_game_states}) + # |> response(200) - updated_user = Repo.get(User, user.id) + # updated_user = Repo.get(User, user.id) - assert new_game_states == updated_user.game_states - end + # assert new_game_states == updated_user.game_states + # end - @tag authenticate: :student - test "success, retrieving student game state", %{conn: conn} do - resp = - conn - |> get("/v2/user") - |> json_response(200) + # @tag authenticate: :student + # test "success, retrieving student game state", %{conn: conn} do + # resp = + # conn + # |> get("/v2/user") + # |> json_response(200) - assert %{} == resp["gameStates"] - end - end + # assert %{} == resp["gameStates"] + # end + # end end diff --git a/test/factories/accounts/user_factory.ex b/test/factories/accounts/user_factory.ex index 165fec74b..3b868f739 100644 --- a/test/factories/accounts/user_factory.ex +++ b/test/factories/accounts/user_factory.ex @@ -15,7 +15,8 @@ defmodule Cadet.Accounts.UserFactory do sequence( :nusnet_id, &"E#{&1 |> Integer.to_string() |> String.pad_leading(7, "0")}" - ) + ), + latest_viewed: build(:course) } end @@ -26,7 +27,8 @@ defmodule Cadet.Accounts.UserFactory do sequence( :nusnet_id, &"E#{&1 |> Integer.to_string() |> String.pad_leading(7, "0")}" - ) + ), + latest_viewed: build(:course) } end end diff --git a/test/support/conn_case.ex b/test/support/conn_case.ex index a9a951d28..69b62571c 100644 --- a/test/support/conn_case.ex +++ b/test/support/conn_case.ex @@ -50,10 +50,12 @@ defmodule CadetWeb.ConnCase do conn = Phoenix.ConnTest.build_conn() if tags[:authenticate] do + course = Cadet.Factory.insert(:course) + user = Cadet.Factory.insert(:user, %{latest_viewed: course}) course_registration = cond do is_atom(tags[:authenticate]) -> - Cadet.Factory.insert(:course_registration, %{role: tags[:authenticate]}) + Cadet.Factory.insert(:course_registration, %{user: user, course: course, role: tags[:authenticate]}) # :TODO: This is_map case has not been handled. To recheck in the future. is_map(tags[:authenticate]) -> From 28e99728be75da1fd36227992da59f8dd9457c9d Mon Sep 17 00:00:00 2001 From: YaleChen299 Date: Sat, 12 Jun 2021 22:42:47 +0800 Subject: [PATCH 049/174] fix accounts test --- lib/cadet/accounts/query.ex | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/cadet/accounts/query.ex b/lib/cadet/accounts/query.ex index 1f5879067..62d7dc0be 100644 --- a/lib/cadet/accounts/query.ex +++ b/lib/cadet/accounts/query.ex @@ -19,6 +19,7 @@ defmodule Cadet.Accounts.Query do def username(username) do User |> of_username(username) + |> preload(:latest_viewed) end # :TODO test From ce185b33353f8912cc4e194f86f7204f13af26a1 Mon Sep 17 00:00:00 2001 From: YaleChen299 Date: Sun, 13 Jun 2021 22:54:37 +0800 Subject: [PATCH 050/174] added latest_viewed context functions with test --- lib/cadet/accounts/accounts.ex | 10 +- lib/cadet/accounts/course_registrations.ex | 11 +- lib/cadet/assessments/assessments.ex | 3 +- lib/cadet/courses/group.ex | 1 + .../admin_user_controller.ex | 9 +- lib/cadet_web/controllers/user_controller.ex | 188 +++++++++++--- lib/cadet_web/router.ex | 4 +- lib/cadet_web/views/user_view.ex | 109 ++++++--- .../20210608085548_update_assessments.exs | 1 - test/cadet/accounts/accounts_test.exs | 1 - .../admin_user_controller_test.exs | 7 +- .../controllers/user_controller_test.exs | 231 +++++++++++++++--- test/support/conn_case.ex | 17 +- 13 files changed, 460 insertions(+), 132 deletions(-) diff --git a/lib/cadet/accounts/accounts.ex b/lib/cadet/accounts/accounts.ex index eeeb41b72..fa3bce754 100644 --- a/lib/cadet/accounts/accounts.ex +++ b/lib/cadet/accounts/accounts.ex @@ -48,7 +48,8 @@ defmodule Cadet.Accounts do @doc """ Returns users matching a given set of criteria. """ - def get_users_by(filter \\ [], %CourseRegistration{course_id: course_id, role: role}) when role in @get_all_role do + def get_users_by(filter \\ [], %CourseRegistration{course_id: course_id, role: role}) + when role in @get_all_role do CourseRegistration |> where([cr], cr.course_id == ^course_id) |> join(:inner, [cr], u in assoc(cr, :user)) @@ -63,7 +64,8 @@ defmodule Cadet.Accounts do defp get_users_helper(query, [{:group, group} | filters]), do: query |> where([cr, u, g], g.name == ^group) |> get_users_helper(filters) - defp get_users_helper(query, [filter | filters]), do: query |> where(^[filter]) |> get_users_helper(filters) + defp get_users_helper(query, [filter | filters]), + do: query |> where(^[filter]) |> get_users_helper(filters) @spec sign_in(String.t(), Provider.token(), Provider.provider_instance()) :: {:error, :bad_request | :forbidden | :internal_server_error, String.t()} | {:ok, any} @@ -101,8 +103,8 @@ defmodule Cadet.Accounts do def update_latest_viewed(user = %User{}, latest_viewed_id) when is_ecto_id(latest_viewed_id) do case user - |> User.changeset(%{latest_viewed_id: latest_viewed_id}) - |> Repo.update() do + |> User.changeset(%{latest_viewed_id: latest_viewed_id}) + |> Repo.update() do result = {:ok, _} -> result {:error, changeset} -> {:error, {:internal_server_error, full_error_messages(changeset)}} end diff --git a/lib/cadet/accounts/course_registrations.ex b/lib/cadet/accounts/course_registrations.ex index 968e6391d..a98eec9bc 100644 --- a/lib/cadet/accounts/course_registrations.ex +++ b/lib/cadet/accounts/course_registrations.ex @@ -16,13 +16,6 @@ defmodule Cadet.Accounts.CourseRegistrations do # otherwise just use CourseRegistration def get_user_record(user_id, course_id) when is_ecto_id(user_id) and is_ecto_id(course_id) do - CourseRegistration - |> where([cr], cr.user_id == ^user_id) - |> where([cr], cr.course_id == ^course_id) - |> Repo.one() - end - - def get_user_course(user_id, course_id) when is_ecto_id(user_id) and is_ecto_id(course_id) do CourseRegistration |> where([cr], cr.user_id == ^user_id) |> where([cr], cr.course_id == ^course_id) @@ -96,8 +89,8 @@ defmodule Cadet.Accounts.CourseRegistrations do def update_game_states(cr = %CourseRegistration{}, new_game_state = %{}) do case cr - |> CourseRegistration.changeset(%{game_states: new_game_state}) - |> Repo.update() do + |> CourseRegistration.changeset(%{game_states: new_game_state}) + |> Repo.update() do result = {:ok, _} -> result {:error, changeset} -> {:error, {:internal_server_error, full_error_messages(changeset)}} end diff --git a/lib/cadet/assessments/assessments.ex b/lib/cadet/assessments/assessments.ex index f9c907756..a671d480a 100644 --- a/lib/cadet/assessments/assessments.ex +++ b/lib/cadet/assessments/assessments.ex @@ -131,7 +131,8 @@ defmodule Cadet.Assessments do story end - @spec get_user_story_by_type(%CourseRegistration{}, :unattempted | :attempted) :: String.t() | nil + @spec get_user_story_by_type(%CourseRegistration{}, :unattempted | :attempted) :: + String.t() | nil def get_user_story_by_type(%CourseRegistration{id: cr_id}, type) when is_atom(type) do filter_and_sort = fn query -> diff --git a/lib/cadet/courses/group.ex b/lib/cadet/courses/group.ex index a3138c866..bb94227ca 100644 --- a/lib/cadet/courses/group.ex +++ b/lib/cadet/courses/group.ex @@ -26,6 +26,7 @@ defmodule Cadet.Courses.Group do |> cast(attrs, @required_fields ++ @optional_fields) |> validate_required(@required_fields) |> add_belongs_to_id_from_model([:leader, :mentor, :course], attrs) + # |> validate_course end diff --git a/lib/cadet_web/admin_controllers/admin_user_controller.ex b/lib/cadet_web/admin_controllers/admin_user_controller.ex index 23a77116f..f1ef217fd 100644 --- a/lib/cadet_web/admin_controllers/admin_user_controller.ex +++ b/lib/cadet_web/admin_controllers/admin_user_controller.ex @@ -7,7 +7,8 @@ defmodule CadetWeb.AdminUserController do # This controller is used to find all users of a course def index(conn, filter) do - users = filter |> try_keywordise_string_keys() |> Accounts.get_users_by(conn.assigns.course_reg) + users = + filter |> try_keywordise_string_keys() |> Accounts.get_users_by(conn.assigns.course_reg) render(conn, "users.json", users: users) end @@ -33,7 +34,11 @@ defmodule CadetWeb.AdminUserController do properties do userId(:integer, "User's ID") name(:string, "Full name of the user") - role(:string, "Role of the user in this course. Can be 'student', 'staff', or 'admin'") + + role( + :string, + "Role of the user in this course. Can be 'student', 'staff', or 'admin'" + ) group( :string, diff --git a/lib/cadet_web/controllers/user_controller.ex b/lib/cadet_web/controllers/user_controller.ex index 659899571..d9f839358 100644 --- a/lib/cadet_web/controllers/user_controller.ex +++ b/lib/cadet_web/controllers/user_controller.ex @@ -10,25 +10,29 @@ defmodule CadetWeb.UserController do alias Cadet.Accounts.CourseRegistrations def index(conn, _) do - user = Accounts.get_user(conn.assigns.current_user.id) - user_courses = CourseRegistrations.get_courses(conn.assigns.current_user) - latest = CourseRegistrations.get_user_course(user.id, user.latest_viewed_id) + user = conn.assigns.current_user + courses = CourseRegistrations.get_courses(conn.assigns.current_user) + if user.latest_viewed_id do + latest = CourseRegistrations.get_user_record(user.id, user.latest_viewed_id) + %{total_grade: grade, total_xp: xp} = user_total_grade_xp(latest) + max_grade = user_max_grade(latest) + story = user_current_story(latest) - %{total_grade: grade, total_xp: xp} = user_total_grade_xp(latest) - max_grade = user_max_grade(latest) - story = user_current_story(latest) + render( + conn, + "index.json", + user: user, + courses: courses, + latest: latest, + grade: grade, + max_grade: max_grade, + story: story, + xp: xp + ) + else + render(conn, "index.json", user: user, courses: courses, latest: nil, grade: nil, max_grade: nil, story: nil, xp: nil) + end - render( - conn, - "index.json", - user: user, - courses: user_courses, - latest: latest, - grade: grade, - max_grade: max_grade, - story: story, - xp: xp - ) end # def index(conn, _) do # user = user_with_group(conn.assigns.current_user) @@ -47,10 +51,58 @@ defmodule CadetWeb.UserController do # ) # end + def get_latest_viewed(conn, _) do + user = conn.assigns.current_user + latest = + case user.latest_viewed_id do + nil -> nil + _ -> CourseRegistrations.get_user_record(user.id, user.latest_viewed_id) + end + + get_course_reg_config(conn, latest) + end + + def get_course_reg(conn, _) do + course_reg = conn.assigns.course_reg + get_course_reg_config(conn, course_reg) + end + + defp get_course_reg_config(conn, course_reg) when is_nil(course_reg) do + render(conn, "course.json", latest: nil, grade: nil, max_grade: nil, story: nil, xp: nil) + end + + defp get_course_reg_config(conn, course_reg) do + %{total_grade: grade, total_xp: xp} = user_total_grade_xp(course_reg) + max_grade = user_max_grade(course_reg) + story = user_current_story(course_reg) + + render( + conn, + "course.json", + latest: course_reg, + grade: grade, + max_grade: max_grade, + story: story, + xp: xp + ) + end + + def update_latest_viewed(conn, %{"course_id" => course_id}) do + case Accounts.update_latest_viewed(conn.assigns.current_user, course_id) do + {:ok, %{}} -> + text(conn, "OK") + + {:error, {status, message}} -> + conn + |> put_status(status) + |> text(message) + end + end + def update_game_states(conn, %{"gameStates" => new_game_states}) do - user = conn.assigns[:current_user] + cr = conn.assigns[:course_reg] - case CourseRegistrations.update_game_states(user, new_game_states) do + case CourseRegistrations.update_game_states(cr, new_game_states) do {:ok, %{}} -> text(conn, "OK") @@ -68,12 +120,36 @@ defmodule CadetWeb.UserController do security([%{JWT: []}]) produces("application/json") - response(200, "OK", Schema.ref(:UserInfo)) + response(200, "OK", Schema.ref(:IndexInfo)) + response(401, "Unauthorised") + end + + swagger_path :get_latest_viewed do + get("/v2/user/latest_viewed") + + summary("Get the latest_viewed_course of a user") + + security([%{JWT: []}]) + produces("application/json") + response(200, "OK", Schema.ref(:LatestViewedInfo)) response(401, "Unauthorised") end + swagger_path :update_latest_viewed do + put("/v2/user/latest_viewed/{course_id}") + summary("Update user's latest viewed course") + security([%{JWT: []}]) + consumes("application/json") + + parameters do + course_id(:path, :integer, "new latest viewed course", required: true) + end + + response(200, "OK") + end + swagger_path :update_game_states do - put("/user/game_states") + put("/v2/course/:course_id/user/game_states") summary("Update user's game states") security([%{JWT: []}]) consumes("application/json") @@ -87,6 +163,27 @@ defmodule CadetWeb.UserController do def swagger_definitions do %{ + IndexInfo: + swagger_schema do + title("User Index") + description("user, course_registration and course configuration of the latest course") + + properties do + user(Schema.ref(:UserInfo), "user info") + courseRegistration(Schema.ref(:CourseRegistration), "course registration of the latest viewed course") + courseConfiguration(Schema.ref(:CourseConfiguration), "course configuration of the latest viewed course") + end + end, + LatestViewedInfo: + swagger_schema do + title("Latest viewed course") + description("course_registration and course configuration of the latest course") + + properties do + courseRegistration(Schema.ref(:CourseRegistration), "course registration of the latest viewed course") + courseConfiguration(Schema.ref(:CourseConfiguration), "course configuration of the latest viewed course") + end + end, UserInfo: swagger_schema do title("User") @@ -94,46 +191,81 @@ defmodule CadetWeb.UserController do properties do userId(:integer, "User's ID", required: true) - name(:string, "Full name of the user", required: true) + end + end, + CourseRegistration: + swagger_schema do + title("CourseRegistration") + description("information about the CourseRegistration") + properties do role( :string, "Role of the user. Can be 'Student', 'Staff', or 'Admin'", required: true ) - group( :string, "Group the user belongs to. May be null if the user does not belong to any group.", required: true ) - story(Schema.ref(:UserStory), "Story to displayed to current user. ") - grade( :integer, "Amount of grade. Only provided for 'Student'. " <> "Value will be 0 for non-students." ) - maxGrade( :integer, "Total maximum grade achievable based on submitted assessments. " <> "Only provided for 'Student'" ) - xp( :integer, "Amount of xp. Only provided for 'Student'. " <> "Value will be 0 for non-students." ) - game_states( Schema.ref(:UserGameStates), "States for user's game, including users' game progress, settings and collectibles.\n" ) end end, + CourseConfiguration: + swagger_schema do + title("Course Configuration") + + properties do + name(:string, "Course name", required: true) + module_code(:string, "Course module code", required: true) + viewable(:boolean, "Course viewability", required: true) + enable_game(:boolean, "Enable game", required: true) + enable_achievements(:boolean, "Enable achievements", required: true) + enable_sourcecast(:boolean, "Enable sourcecast", required: true) + source_chapter(:integer, "Source Chapter number from 1 to 4", required: true) + source_variant(Schema.ref(:SourceVariant), "Source Variant name", required: true) + module_help_text(:string, "Module help text", required: true) + assessment_types(:list, "Assessment Types", required: true) + end + + example(%{ + name: "Programming Methodology", + module_code: "CS1101S", + viewable: true, + enable_game: true, + enable_achievements: true, + enable_sourcecast: true, + source_chapter: 1, + source_variant: "default", + module_help_text: "Help text", + assessment_types: ["Missions", "Quests", "Paths", "Contests", "Others"] + }) + end, + SourceVariant: + swagger_schema do + type(:string) + enum([:default, :concurrent, :gpu, :lazy, "non-det", :wasm]) + end, UserStory: swagger_schema do properties do diff --git a/lib/cadet_web/router.ex b/lib/cadet_web/router.ex index 8d3c1acb2..1fee647e3 100644 --- a/lib/cadet_web/router.ex +++ b/lib/cadet_web/router.ex @@ -55,7 +55,7 @@ defmodule CadetWeb.Router do get("/user", UserController, :index) get("/user/latest_viewed", UserController, :get_latest_viewed) - post("/user/latest_viewed", UserController, :update_latest_viewed) + put("/user/latest_viewed/:course_id", UserController, :update_latest_viewed) end # Authenticated Pages with course @@ -130,7 +130,7 @@ defmodule CadetWeb.Router do post("/users/:userid/goals/:uuid/progress", AdminGoalsController, :update_progress) put("/achievements", AdminAchievementsController, :bulk_update) - put("/achievements/:uuid", AdminAchievementsController, :update) + # put("/achievements/:uuid", AdminAchievementsController, :update) delete("/achievements/:uuid", AdminAchievementsController, :delete) get("/goals", AdminGoalsController, :index) diff --git a/lib/cadet_web/views/user_view.ex b/lib/cadet_web/views/user_view.ex index 382d7321f..6e048e916 100644 --- a/lib/cadet_web/views/user_view.ex +++ b/lib/cadet_web/views/user_view.ex @@ -45,13 +45,35 @@ defmodule CadetWeb.UserView do name: user.name, courses: render_many(courses, CadetWeb.UserView, "course.json", as: :cr) }, - latestViewedCourse: render_latest(%{ + courseRegistration: + render_latest(%{ + latest: latest, + grade: grade, + max_grade: max_grade, + xp: xp, + story: story + }), + courseConfiguration: render_config(latest) + } + end + + def render("course.json", %{ latest: latest, grade: grade, max_grade: max_grade, xp: xp, story: story - }) + }) do + %{ + courseRegistration: + render_latest(%{ + latest: latest, + grade: grade, + max_grade: max_grade, + xp: xp, + story: story + }), + courseConfiguration: render_config(latest) } end @@ -65,47 +87,56 @@ defmodule CadetWeb.UserView do end defp render_latest(%{ - latest: latest, - grade: grade, - max_grade: max_grade, - xp: xp, - story: story - }) do - + latest: latest, + grade: grade, + max_grade: max_grade, + xp: xp, + story: story + }) do case latest do - nil -> nil + nil -> + nil - _ ->%{ - course: transform_map_for_view(latest.course, [ - :name, - :module_code, - :viewable, - :enable_game, - :enable_achievements, - :enable_sourcecast, - :source_chapter, - :source_variant, - :module_help_text, - :assessment_types - ]), - role: latest.role, - group: - case latest.group do - nil -> nil - _ -> latest.group.name - end, - grade: grade, - xp: xp, - maxGrade: max_grade, - story: - transform_map_for_view(story, %{ - story: :story, - playStory: :play_story? - }), - gameStates: latest.game_states - } + _ -> + %{ + courseId: latest.course_id, + role: latest.role, + group: + case latest.group do + nil -> nil + _ -> latest.group.name + end, + grade: grade, + xp: xp, + maxGrade: max_grade, + story: + transform_map_for_view(story, %{ + story: :story, + playStory: :play_story? + }), + gameStates: latest.game_states + } end end + defp render_config(latest) do + case latest do + nil -> + nil + _ -> + transform_map_for_view(latest.course, %{ + moduleName: :name, + moduleCode: :module_code, + viewable: :viewable, + enableGame: :enable_game, + enableAchievements: :enable_achievements, + enableSourcecast: :enable_sourcecast, + sourceChapter: :source_chapter, + sourceVariant: :source_variant, + moduleHelpText: :module_help_text, + assessmentTypes: :assessment_types + }) + end + end end diff --git a/priv/repo/migrations/20210608085548_update_assessments.exs b/priv/repo/migrations/20210608085548_update_assessments.exs index e6d660c6f..73ead03e0 100644 --- a/priv/repo/migrations/20210608085548_update_assessments.exs +++ b/priv/repo/migrations/20210608085548_update_assessments.exs @@ -31,6 +31,5 @@ defmodule Cadet.Repo.Migrations.UpdateAssessments do alter table(:users) do add(:latest_viewed_id, references(:courses), null: true) end - end end diff --git a/test/cadet/accounts/accounts_test.exs b/test/cadet/accounts/accounts_test.exs index 1b421751e..2d79ce213 100644 --- a/test/cadet/accounts/accounts_test.exs +++ b/test/cadet/accounts/accounts_test.exs @@ -107,7 +107,6 @@ defmodule Cadet.AccountsTest do end end - describe "get_users_by" do setup do c1 = insert(:course, %{name: "c1"}) diff --git a/test/cadet_web/admin_controllers/admin_user_controller_test.exs b/test/cadet_web/admin_controllers/admin_user_controller_test.exs index fed1fde87..55558f556 100644 --- a/test/cadet_web/admin_controllers/admin_user_controller_test.exs +++ b/test/cadet_web/admin_controllers/admin_user_controller_test.exs @@ -38,9 +38,9 @@ defmodule CadetWeb.AdminUserControllerTest do insert(:course_registration, %{role: :staff, course: course, group: group}) resp = - conn - |> get(build_url(course_id) <> "?role=student") - |> json_response(200) + conn + |> get(build_url(course_id) <> "?role=student") + |> json_response(200) assert 1 == Enum.count(resp) assert "student" == List.first(resp)["role"] @@ -66,6 +66,7 @@ defmodule CadetWeb.AdminUserControllerTest do @tag authenticate: :student test "forbidden, when student retrieves users", %{conn: conn} do course_id = conn.assigns[:course_id] + assert conn |> get(build_url(course_id)) |> response(403) diff --git a/test/cadet_web/controllers/user_controller_test.exs b/test/cadet_web/controllers/user_controller_test.exs index afd70b13b..b9ffc4601 100644 --- a/test/cadet_web/controllers/user_controller_test.exs +++ b/test/cadet_web/controllers/user_controller_test.exs @@ -5,7 +5,7 @@ defmodule CadetWeb.UserControllerTest do alias Cadet.Repo alias CadetWeb.UserController - alias Cadet.Assessments.{Assessment, Submission} + # alias Cadet.Assessments.{Assessment, Submission} alias Cadet.Accounts.{User, CourseRegistration} test "swagger" do @@ -19,6 +19,7 @@ defmodule CadetWeb.UserControllerTest do user = conn.assigns.current_user course = user.latest_viewed cr = Repo.get_by(CourseRegistration, course_id: course.id, user_id: user.id) + another_cr = insert(:course_registration, %{user: user}) assessment = insert(:assessment, %{is_published: true, course: course}) question = insert(:question, %{assessment: assessment}) @@ -57,7 +58,7 @@ defmodule CadetWeb.UserControllerTest do conn |> get("/v2/user") |> json_response(200) - |> put_in(["latestViewedCourse", "story"], nil) + |> put_in(["courseRegistration", "story"], nil) |> IO.inspect() expected = %{ @@ -65,22 +66,22 @@ defmodule CadetWeb.UserControllerTest do "userId" => user.id, "name" => user.name, "courses" => [ - %{"course_id" => user.latest_viewed_id, "moduleCode" => "CS1101S", "name" => "Programming Methodology", "viewable" => true} + %{ + "course_id" => user.latest_viewed_id, + "moduleCode" => "CS1101S", + "name" => "Programming Methodology", + "viewable" => true + }, + %{ + "course_id" => another_cr.course_id, + "moduleCode" => "CS1101S", + "name" => "Programming Methodology", + "viewable" => true + } ] }, - "latestViewedCourse" => %{ - "course" => %{ - "assessment_types" => nil, - "enable_achievements" => true, - "enable_game" => true, - "enable_sourcecast" => true, - "module_code" => "CS1101S", - "module_help_text" => "Help Text", - "name" => "Programming Methodology", - "source_chapter" => 1, - "source_variant" => "default", - "viewable" => true - }, + "courseRegistration" => %{ + "courseId" => course.id, "role" => "#{cr.role}", "group" => nil, "xp" => 110, @@ -88,12 +89,46 @@ defmodule CadetWeb.UserControllerTest do "maxGrade" => question.max_grade, "gameStates" => %{}, "story" => nil + }, + "courseConfiguration" => %{ + "assessmentTypes" => nil, + "enableAchievements" => true, + "enableGame" => true, + "enableSourcecast" => true, + "moduleCode" => "CS1101S", + "moduleHelpText" => "Help Text", + "moduleName" => "Programming Methodology", + "sourceChapter" => 1, + "sourceVariant" => "default", + "viewable" => true } } assert expected == resp end + @tag sign_in: %{latest_viewed: nil} + test "success, no latest_viewed course", %{conn: conn} do + user = conn.assigns.current_user + + resp = + conn + |> get("/v2/user") + |> json_response(200) + + expected = %{ + "user" => %{ + "userId" => user.id, + "name" => user.name, + "courses" => [] + }, + "courseRegistration" => nil, + "courseConfiguration" => nil + } + + assert expected == resp + end + # # This also tests for the case where assessment has no submission # @tag authenticate: :student # test "success, student story ordering", %{conn: conn} do @@ -274,33 +309,151 @@ defmodule CadetWeb.UserControllerTest do # end end - # describe "PUT /user/game_states" do - # @tag authenticate: :student - # test "success, updating game state", %{conn: conn} do - # user = conn.assigns.current_user + describe "GET /v2/user/latest_viewed" do + @tag authenticate: :student + test "success, student non-story fields", %{conn: conn} do + user = conn.assigns.current_user + course = user.latest_viewed + cr = Repo.get_by(CourseRegistration, course_id: course.id, user_id: user.id) + _another_cr = insert(:course_registration, %{user: user}) + assessment = insert(:assessment, %{is_published: true, course: course}) + question = insert(:question, %{assessment: assessment}) + + submission = + insert(:submission, %{ + assessment: assessment, + student: cr, + status: :submitted, + xp_bonus: 100 + }) - # new_game_states = %{ - # "gameSaveStates" => %{"1" => %{}, "2" => %{}}, - # "userSaveState" => %{} - # } + insert(:answer, %{ + question: question, + submission: submission, + grade: 50, + adjustment: -10, + xp: 20, + xp_adjustment: -10 + }) - # conn - # |> put("/v2/user/game_states", %{"gameStates" => new_game_states}) - # |> response(200) + not_submitted_assessment = insert(:assessment, %{is_published: true, course: course}) + not_submitted_question = insert(:question, assessment: not_submitted_assessment) - # updated_user = Repo.get(User, user.id) + not_submitted_submission = + insert(:submission, %{assessment: not_submitted_assessment, student: cr}) - # assert new_game_states == updated_user.game_states - # end + insert( + :answer, + question: not_submitted_question, + submission: not_submitted_submission, + grade: 0, + adjustment: 0 + ) - # @tag authenticate: :student - # test "success, retrieving student game state", %{conn: conn} do - # resp = - # conn - # |> get("/v2/user") - # |> json_response(200) + resp = + conn + |> get("/v2/user/latest_viewed") + |> json_response(200) + |> put_in(["courseRegistration", "story"], nil) + |> IO.inspect() + + expected = %{ + "courseRegistration" => %{ + "courseId" => course.id, + "role" => "#{cr.role}", + "group" => nil, + "xp" => 110, + "grade" => 40, + "maxGrade" => question.max_grade, + "gameStates" => %{}, + "story" => nil + }, + "courseConfiguration" => %{ + "assessmentTypes" => nil, + "enableAchievements" => true, + "enableGame" => true, + "enableSourcecast" => true, + "moduleCode" => "CS1101S", + "moduleHelpText" => "Help Text", + "moduleName" => "Programming Methodology", + "sourceChapter" => 1, + "sourceVariant" => "default", + "viewable" => true + } + } + + assert expected == resp + end + + @tag sign_in: %{latest_viewed: nil} + test "success, no latest_viewed course", %{conn: conn} do + resp = + conn + |> get("/v2/user/latest_viewed") + |> json_response(200) + + expected = %{ + "courseRegistration" => nil, + "courseConfiguration" => nil + } + + assert expected == resp + end + + test "unauthorized", %{conn: conn} do + conn = get(conn, "/v2/user/latest_viewed", nil) + assert response(conn, 401) =~ "Unauthorised" + end + end + + describe "PUT /v2/user/latest_viewed/{course_id}" do + @tag authenticate: :student + test "success, updating game state", %{conn: conn} do + user = conn.assigns.current_user + new_course = insert(:course) + insert(:course_registration, %{user: user, course: new_course}) + + conn + |> put("/v2/user/latest_viewed/#{new_course.id}") + |> response(200) + + updated_user = Repo.get(User, user.id) + + assert new_course.id == updated_user.latest_viewed_id + end + end + + describe "PUT /v2/course/{course_id}/user/game_states" do + @tag authenticate: :student + test "success, updating game state", %{conn: conn} do + user = conn.assigns.current_user + course_id = conn.assigns.course_id + + new_game_states = %{ + "gameSaveStates" => %{"1" => %{}, "2" => %{}}, + "userSaveState" => %{} + } + + conn + |> put(build_url(course_id) <> "/game_states", %{"gameStates" => new_game_states}) + |> response(200) + + updated_cr = Repo.get_by(CourseRegistration, course_id: course_id, user_id: user.id) + + assert new_game_states == updated_cr.game_states + end + + @tag authenticate: :student + test "success, retrieving student game state", %{conn: conn} do + course_id = conn.assigns.course_id + resp = + conn + |> get(build_url(course_id)) + |> json_response(200) + + assert %{} == resp["courseRegistration"]["gameStates"] + end + end - # assert %{} == resp["gameStates"] - # end - # end + defp build_url(course_id), do: "/v2/course/#{course_id}/user" end diff --git a/test/support/conn_case.ex b/test/support/conn_case.ex index 69b62571c..3fb6d3a3f 100644 --- a/test/support/conn_case.ex +++ b/test/support/conn_case.ex @@ -52,14 +52,19 @@ defmodule CadetWeb.ConnCase do if tags[:authenticate] do course = Cadet.Factory.insert(:course) user = Cadet.Factory.insert(:user, %{latest_viewed: course}) + course_registration = cond do is_atom(tags[:authenticate]) -> - Cadet.Factory.insert(:course_registration, %{user: user, course: course, role: tags[:authenticate]}) + Cadet.Factory.insert(:course_registration, %{ + user: user, + course: course, + role: tags[:authenticate] + }) # :TODO: This is_map case has not been handled. To recheck in the future. is_map(tags[:authenticate]) -> - tags[:authenticate] + Cadet.Factory.insert(:course_registration, tags[:authenticate]) true -> nil @@ -75,7 +80,13 @@ defmodule CadetWeb.ConnCase do {:ok, conn: conn} else - {:ok, conn: conn} + if tags[:sign_in] do + user = Cadet.Factory.insert(:user, tags[:sign_in]) + conn = sign_in(conn, user) + {:ok, conn: conn} + else + {:ok, conn: conn} + end end end From 215cd390c3fded5a16d77590c79984763cbf3767 Mon Sep 17 00:00:00 2001 From: YaleChen299 Date: Sun, 13 Jun 2021 22:55:01 +0800 Subject: [PATCH 051/174] fix snake_case issue --- lib/cadet_web/views/courses_view.ex | 24 +++++++++---------- lib/cadet_web/views/stories_view.ex | 2 +- .../controllers/courses_controller_test.exs | 18 +++++++------- .../controllers/stories_controller_test.exs | 4 ++-- 4 files changed, 24 insertions(+), 24 deletions(-) diff --git a/lib/cadet_web/views/courses_view.ex b/lib/cadet_web/views/courses_view.ex index 09cfb9a17..ad32af398 100644 --- a/lib/cadet_web/views/courses_view.ex +++ b/lib/cadet_web/views/courses_view.ex @@ -4,18 +4,18 @@ defmodule CadetWeb.CoursesView do def render("config.json", %{config: config}) do %{ config: - transform_map_for_view(config, [ - :name, - :module_code, - :viewable, - :enable_game, - :enable_achievements, - :enable_sourcecast, - :source_chapter, - :source_variant, - :module_help_text, - :assessment_types - ]) + transform_map_for_view(config, %{ + moduleName: :name, + moduleCode: :module_code, + viewable: :viewable, + enableGame: :enable_game, + enableAchievements: :enable_achievements, + enableSourcecast: :enable_sourcecast, + sourceChapter: :source_chapter, + sourceVariant: :source_variant, + moduleHelpText: :module_help_text, + assessmentTypes: :assessment_types + }) } end end diff --git a/lib/cadet_web/views/stories_view.ex b/lib/cadet_web/views/stories_view.ex index 18a9de43a..00508e6c7 100644 --- a/lib/cadet_web/views/stories_view.ex +++ b/lib/cadet_web/views/stories_view.ex @@ -14,7 +14,7 @@ defmodule CadetWeb.StoriesView do isPublished: :is_published, openAt: &format_datetime(&1.open_at), closeAt: &format_datetime(&1.close_at), - course_id: :course_id + courseId: :course_id }) end end diff --git a/test/cadet_web/controllers/courses_controller_test.exs b/test/cadet_web/controllers/courses_controller_test.exs index 6f15bd7a0..f883f8414 100644 --- a/test/cadet_web/controllers/courses_controller_test.exs +++ b/test/cadet_web/controllers/courses_controller_test.exs @@ -29,16 +29,16 @@ defmodule CadetWeb.CoursesControllerTest do assert %{ "config" => %{ - "name" => "Programming Methodology", - "module_code" => "CS1101S", + "moduleName" => "Programming Methodology", + "moduleCode" => "CS1101S", "viewable" => true, - "enable_game" => true, - "enable_achievements" => true, - "enable_sourcecast" => true, - "source_chapter" => 1, - "source_variant" => "default", - "module_help_text" => "Help Text", - "assessment_types" => ["Missions", "Quests", "Paths"] + "enableGame" => true, + "enableAchievements" => true, + "enableSourcecast" => true, + "sourceChapter" => 1, + "sourceVariant" => "default", + "moduleHelpText" => "Help Text", + "assessmentTypes" => ["Missions", "Quests", "Paths"] } } = resp end diff --git a/test/cadet_web/controllers/stories_controller_test.exs b/test/cadet_web/controllers/stories_controller_test.exs index 8c9809936..064cc31f7 100644 --- a/test/cadet_web/controllers/stories_controller_test.exs +++ b/test/cadet_web/controllers/stories_controller_test.exs @@ -136,7 +136,7 @@ defmodule CadetWeb.StoriesControllerTest do |> response(200) |> Jason.decode() - required_fields = ~w(openAt closeAt isPublished id title filenames imageUrl course_id) + required_fields = ~w(openAt closeAt isPublished id title filenames imageUrl courseId) Enum.each(required_fields, fn required_field -> value = resp[required_field] @@ -146,7 +146,7 @@ defmodule CadetWeb.StoriesControllerTest do "id" -> assert is_integer(value) "filenames" -> assert is_list(value) "isPublished" -> assert is_boolean(value) - "course_id" -> assert is_integer(value) + "courseId" -> assert is_integer(value) _ -> assert is_binary(value) end end) From cef47b774a53aebb102ad017ab1a59b184175421 Mon Sep 17 00:00:00 2001 From: YaleChen299 Date: Mon, 14 Jun 2021 00:00:57 +0800 Subject: [PATCH 052/174] update seed.exs for initial test with frontend --- config/test.exs | 12 +- lib/cadet/accounts/notifications.ex | 18 +- lib/cadet/accounts/user.ex | 4 +- lib/cadet_web/controllers/user_controller.ex | 43 +- priv/repo/seeds.exs | 701 +++++++++--------- .../controllers/user_controller_test.exs | 1 + 6 files changed, 405 insertions(+), 374 deletions(-) diff --git a/config/test.exs b/config/test.exs index 9fbcf5f73..833df7d0c 100644 --- a/config/test.exs +++ b/config/test.exs @@ -52,22 +52,22 @@ config :cadet, token: "admin_token", code: "admin_code", name: "Test Admin", - username: "admin", - role: :admin + username: "admin" + # role: :admin }, %{ token: "staff_token", code: "staff_code", name: "Test Staff", - username: "staff", - role: :staff + username: "staff" + # role: :staff }, %{ token: "student_token", code: "student_code", name: "Test Student", - username: "student", - role: :student + username: "student" + # role: :student } ]} }, diff --git a/lib/cadet/accounts/notifications.ex b/lib/cadet/accounts/notifications.ex index 68832a899..8319f2801 100644 --- a/lib/cadet/accounts/notifications.ex +++ b/lib/cadet/accounts/notifications.ex @@ -8,7 +8,7 @@ defmodule Cadet.Accounts.Notifications do import Ecto.Query alias Cadet.Repo - alias Cadet.Accounts.{Notification, User} + alias Cadet.Accounts.{Notification, User, CourseRegistration} alias Cadet.Assessments.Submission alias Ecto.Multi @@ -121,9 +121,9 @@ defmodule Cadet.Accounts.Notifications do |> Repo.transaction() end - @spec acknowledge(:integer, %User{}) :: {:ok, Ecto.Schema.t()} | {:error, any()} - def acknowledge(notification_id, user = %User{}) do - notification = Repo.get_by(Notification, id: notification_id, user_id: user.id) + @spec acknowledge(:integer, %CourseRegistration{}) :: {:ok, Ecto.Schema.t()} | {:error, any()} + def acknowledge(notification_id, cr = %CourseRegistration{}) do + notification = Repo.get_by(Notification, id: notification_id, user_id: cr.user_id) case notification do nil -> @@ -131,7 +131,7 @@ defmodule Cadet.Accounts.Notifications do notification -> notification - |> Notification.changeset(%{role: user.role, read: true}) + |> Notification.changeset(%{role: cr.role, read: true}) |> Repo.update() end end @@ -139,15 +139,15 @@ defmodule Cadet.Accounts.Notifications do @doc """ Function that handles notifications when a submission is unsubmitted. """ - @spec handle_unsubmit_notifications(integer(), %User{}) :: + @spec handle_unsubmit_notifications(integer(), %CourseRegistration{}) :: {:ok, Ecto.Schema.t()} | {:error, Ecto.Changeset.t()} - def handle_unsubmit_notifications(assessment_id, student = %User{}) + def handle_unsubmit_notifications(assessment_id, student = %CourseRegistration{}) when is_ecto_id(assessment_id) do # Fetch and delete all notifications of :autograded and :graded # Add new notification :unsubmitted Notification - |> where(user_id: ^student.id) + |> where(user_id: ^student.user_id) |> where(assessment_id: ^assessment_id) |> where([n], n.type in ^[:autograded, :graded]) |> Repo.delete_all() @@ -155,7 +155,7 @@ defmodule Cadet.Accounts.Notifications do write(%{ type: :unsubmitted, role: student.role, - user_id: student.id, + user_id: student.user_id, assessment_id: assessment_id }) end diff --git a/lib/cadet/accounts/user.ex b/lib/cadet/accounts/user.ex index bd9ffce60..a4437839c 100644 --- a/lib/cadet/accounts/user.ex +++ b/lib/cadet/accounts/user.ex @@ -1,9 +1,7 @@ defmodule Cadet.Accounts.User do @moduledoc """ The User entity represents a user. - It stores basic information such as name and role - Each user is associated to one `role` which determines the access level - of the user. + It stores basic information such as name """ use Cadet, :model diff --git a/lib/cadet_web/controllers/user_controller.ex b/lib/cadet_web/controllers/user_controller.ex index d9f839358..dae5bb467 100644 --- a/lib/cadet_web/controllers/user_controller.ex +++ b/lib/cadet_web/controllers/user_controller.ex @@ -12,6 +12,7 @@ defmodule CadetWeb.UserController do def index(conn, _) do user = conn.assigns.current_user courses = CourseRegistrations.get_courses(conn.assigns.current_user) + if user.latest_viewed_id do latest = CourseRegistrations.get_user_record(user.id, user.latest_viewed_id) %{total_grade: grade, total_xp: xp} = user_total_grade_xp(latest) @@ -30,10 +31,18 @@ defmodule CadetWeb.UserController do xp: xp ) else - render(conn, "index.json", user: user, courses: courses, latest: nil, grade: nil, max_grade: nil, story: nil, xp: nil) + render(conn, "index.json", + user: user, + courses: courses, + latest: nil, + grade: nil, + max_grade: nil, + story: nil, + xp: nil + ) end - end + # def index(conn, _) do # user = user_with_group(conn.assigns.current_user) # %{total_grade: grade, total_xp: xp} = user_total_grade_xp(user) @@ -53,6 +62,7 @@ defmodule CadetWeb.UserController do def get_latest_viewed(conn, _) do user = conn.assigns.current_user + latest = case user.latest_viewed_id do nil -> nil @@ -170,8 +180,16 @@ defmodule CadetWeb.UserController do properties do user(Schema.ref(:UserInfo), "user info") - courseRegistration(Schema.ref(:CourseRegistration), "course registration of the latest viewed course") - courseConfiguration(Schema.ref(:CourseConfiguration), "course configuration of the latest viewed course") + + courseRegistration( + Schema.ref(:CourseRegistration), + "course registration of the latest viewed course" + ) + + courseConfiguration( + Schema.ref(:CourseConfiguration), + "course configuration of the latest viewed course" + ) end end, LatestViewedInfo: @@ -180,8 +198,15 @@ defmodule CadetWeb.UserController do description("course_registration and course configuration of the latest course") properties do - courseRegistration(Schema.ref(:CourseRegistration), "course registration of the latest viewed course") - courseConfiguration(Schema.ref(:CourseConfiguration), "course configuration of the latest viewed course") + courseRegistration( + Schema.ref(:CourseRegistration), + "course registration of the latest viewed course" + ) + + courseConfiguration( + Schema.ref(:CourseConfiguration), + "course configuration of the latest viewed course" + ) end end, UserInfo: @@ -205,26 +230,32 @@ defmodule CadetWeb.UserController do "Role of the user. Can be 'Student', 'Staff', or 'Admin'", required: true ) + group( :string, "Group the user belongs to. May be null if the user does not belong to any group.", required: true ) + story(Schema.ref(:UserStory), "Story to displayed to current user. ") + grade( :integer, "Amount of grade. Only provided for 'Student'. " <> "Value will be 0 for non-students." ) + maxGrade( :integer, "Total maximum grade achievable based on submitted assessments. " <> "Only provided for 'Student'" ) + xp( :integer, "Amount of xp. Only provided for 'Student'. " <> "Value will be 0 for non-students." ) + game_states( Schema.ref(:UserGameStates), "States for user's game, including users' game progress, settings and collectibles.\n" diff --git a/priv/repo/seeds.exs b/priv/repo/seeds.exs index b1e087e28..2a4b0c96c 100644 --- a/priv/repo/seeds.exs +++ b/priv/repo/seeds.exs @@ -19,36 +19,37 @@ alias Cadet.Assessments.SubmissionStatus if Cadet.Env.env() == :dev do # Course course1 = insert(:course) + course2 = insert(:course, %{name: "Algorithm", module_code: "CS2040S"}) # Users - avenger = insert(:user, %{name: "avenger", username: "E1234561"}) - mentor = insert(:user, %{name: "mentor", username: "E1234562"}) - admin = insert(:user, %{name: "admin", username: "E1234563"}) - studenta = insert(:user, %{username: "E1234564"}) - studentb = insert(:user, %{username: "E1234565"}) - studentc = insert(:user, %{username: "E1234566"}) + avenger1 = insert(:user, %{name: "avenger", username: "E1234561"}) + mentor1 = insert(:user, %{name: "mentor", username: "E1234562"}) + admin1 = insert(:user, %{name: "admin", username: "E1234563"}) + studenta1admin2 = insert(:user, %{username: "E1234564"}) + studentb1 = insert(:user, %{username: "E1234565"}) + studentc1 = insert(:user, %{username: "E1234566"}) # CourseRegistration and Group - avenger1 = insert(:course_registration, %{user: avenger, course: course1, role: :staff}) - mentor1 = insert(:course_registration, %{user: mentor, course: course1, role: :staff}) - admin1 = insert(:course_registration, %{user: admin, course: course1, role: :admin}) - group = insert(:group, %{leader: avenger1, mentor: mentor1}) - - studenta1 = - insert(:course_registration, %{user: studenta, course: course1, role: :student, group: group}) + avenger1_cr = insert(:course_registration, %{user: avenger1, course: course1, role: :staff}) + mentor1_cr = insert(:course_registration, %{user: mentor1, course: course1, role: :staff}) + admin1_cr = insert(:course_registration, %{user: admin1, course: course1, role: :admin}) + group = insert(:group, %{leader: avenger1_cr, mentor: mentor1_cr}) + + student1a_cr = + insert(:course_registration, %{ + user: studenta1admin2, + course: course1, + role: :student, + group: group + }) - studentb1 = - insert(:course_registration, %{user: studentb, course: course1, role: :student, group: group}) + student1b_cr = + insert(:course_registration, %{user: studentb1, course: course1, role: :student, group: group}) - studentc1 = - insert(:course_registration, %{user: studentc, course: course1, role: :student, group: group}) + student1c_cr = + insert(:course_registration, %{user: studentc1, course: course1, role: :student, group: group}) - students = [studenta1, studentb1, studentc1] - # :TODO fix assessment and notification then come back + students = [student1a_cr, student1b_cr, student1c_cr] - # avenger = insert(:user, %{name: "avenger", role: :staff}) - # mentor = insert(:user, %{name: "mentor", role: :staff}) - # group = insert(:group, %{leader: avenger, mentor: mentor}) - # students = insert_list(5, :student, %{group: group}) - # admin = insert(:user, %{name: "admin", role: :admin}) + admin2cr = insert(:course_registration, %{user: studenta1admin2, course: course2, role: :admin}) # Assessments for _ <- 1..5 do @@ -103,332 +104,332 @@ if Cadet.Env.env() == :dev do }) end - # Notifications - for submission <- submissions do - case submission.status do - :submitted -> - insert(:notification, %{ - type: :submitted, - read: false, - user_id: avenger.id, - submission_id: submission.id, - assessment_id: assessment.id - }) - - _ -> - nil - end - end - - for student <- students do - insert(:notification, %{ - type: :new, - user_id: student.id, - assessment_id: assessment.id - }) - end + # # Notifications + # for submission <- submissions do + # case submission.status do + # :submitted -> + # insert(:notification, %{ + # type: :submitted, + # read: false, + # user_id: avenger.id, + # submission_id: submission.id, + # assessment_id: assessment.id + # }) + + # _ -> + # nil + # end + # end + + # for student <- students do + # insert(:notification, %{ + # type: :new, + # user_id: student.id, + # assessment_id: assessment.id + # }) + # end end - goal_0 = - insert(:goal, %{ - text: "Complete Beyond the Second Dimension achievement", - max_xp: 250 - }) - - goal_1 = - insert(:goal, %{ - text: "Complete Colorful Carpet achievement", - max_xp: 250 - }) - - goal_2 = - insert(:goal, %{ - text: "Bonus for completing Rune Master achievement", - max_xp: 250 - }) - - goal_3 = - insert(:goal, %{ - text: "Complete Beyond the Second Dimension mission", - max_xp: 100 - }) - - goal_4 = - insert(:goal, %{ - text: "Score earned from Beyond the Second Dimension mission", - max_xp: 150 - }) - - goal_5 = - insert(:goal, %{ - text: "Complete Colorful Carpet mission", - max_xp: 100 - }) - - goal_6 = - insert(:goal, %{ - text: "Score earned from Colorful Carpet mission", - max_xp: 150 - }) - - goal_7 = - insert(:goal, %{ - text: "Complete Curve Introduction mission", - max_xp: 250 - }) - - goal_8 = - insert(:goal, %{ - text: "Complete Curve Manipulation mission", - max_xp: 250 - }) - - goal_9 = - insert(:goal, %{ - text: "Bonus for completing Curve Wizard achievement", - max_xp: 100 - }) - - goal_10 = - insert(:goal, %{ - text: "Complete Curve Introduction mission", - max_xp: 50 - }) - - goal_11 = - insert(:goal, %{ - text: "Score earned from Curve Introduction mission", - max_xp: 200 - }) - - goal_12 = - insert(:goal, %{ - text: "Complete Curve Manipulation mission", - max_xp: 50 - }) - - goal_13 = - insert(:goal, %{ - text: "Score earned from Curve Manipulation mission", - max_xp: 200 - }) - - goal_14 = - insert(:goal, %{ - text: "Complete Source 3 path", - max_xp: 100 - }) - - goal_15 = - insert(:goal, %{ - text: "Score earned from Source 3 path", - max_xp: 300 - }) - - goal_16 = - insert(:goal, %{ - text: "Complete Piazza Guru achievement", - max_xp: 100 - }) - - goal_17 = - insert(:goal, %{ - text: "Each Top Voted answer in Piazza gives 10 XP", - max_xp: 100 - }) - - goal_18 = - insert(:goal, %{ - text: "Submit 1 PR to Source Academy Github", - max_xp: 100 - }) - - # Achievements - achievement_0 = - insert(:achievement, %{ - title: "Rune Master", - ability: "Core", - is_task: true, - position: 1, - card_tile_url: - "https://source-academy-assets.s3-ap-southeast-1.amazonaws.com/achievement/card-tile/rune-master-tile.png", - goals: [ - %{goal_uuid: goal_0.uuid}, - %{goal_uuid: goal_1.uuid}, - %{goal_uuid: goal_2.uuid} - ] - }) - - achievement_1 = - insert(:achievement, %{ - title: "Beyond the Second Dimension", - ability: "Core", - is_task: false, - position: 0, - card_tile_url: - "https://source-academy-assets.s3-ap-southeast-1.amazonaws.com/achievement/card-tile/btsd-tile.png", - open_at: ~U[2020-07-16 16:00:00Z], - close_at: ~U[2020-07-20 16:00:00Z], - goals: [ - %{goal_uuid: goal_3.uuid}, - %{goal_uuid: goal_4.uuid} - ] - }) - - achievement_2 = - insert(:achievement, %{ - title: "Colorful Carpet", - ability: "Core", - is_task: false, - position: 0, - card_tile_url: - "https://source-academy-assets.s3-ap-southeast-1.amazonaws.com/achievement/card-tile/colorful-carpet-tile.png", - open_at: ~U[2020-07-11 16:00:00Z], - close_at: ~U[2020-07-15 16:00:00Z], - goals: [ - %{goal_uuid: goal_5.uuid}, - %{goal_uuid: goal_6.uuid} - ] - }) - - achievement_3 = - insert(:achievement, %{ - title: "Unpublished", - ability: "Core", - is_task: false, - position: 0, - card_tile_url: - "https://www.publicdomainpictures.net/pictures/30000/velka/plain-white-background.jpg" - }) - - achievement_4 = - insert(:achievement, %{ - title: "Curve Wizard", - ability: "Core", - is_task: true, - position: 4, - card_tile_url: - "https://source-academy-assets.s3-ap-southeast-1.amazonaws.com/achievement/card-tile/curve-wizard-tile.png", - open_at: ~U[2020-07-31 16:00:00Z], - close_at: ~U[2020-08-04 16:00:00Z], - goals: [ - %{goal_uuid: goal_7.uuid}, - %{goal_uuid: goal_8.uuid}, - %{goal_uuid: goal_9.uuid} - ] - }) - - achievement_5 = - insert(:achievement, %{ - title: "Curve Introduction", - ability: "Core", - is_task: false, - position: 0, - card_tile_url: - "https://source-academy-assets.s3-ap-southeast-1.amazonaws.com/achievement/card-tile/curve-introduction-tile.png", - open_at: ~U[2020-07-23 16:00:00Z], - close_at: ~U[2020-07-27 16:00:00Z], - goals: [ - %{goal_uuid: goal_10.uuid}, - %{goal_uuid: goal_11.uuid} - ] - }) - - achievement_6 = - insert(:achievement, %{ - title: "Curve Manipulation", - ability: "Core", - is_task: false, - position: 0, - card_tile_url: - "https://source-academy-assets.s3-ap-southeast-1.amazonaws.com/achievement/card-tile/curve-manipulation-tile.png", - open_at: ~U[2020-07-31 16:00:00Z], - close_at: ~U[2020-08-04 16:00:00Z], - goals: [ - %{goal_uuid: goal_12.uuid}, - %{goal_uuid: goal_13.uuid} - ] - }) - - achievement_7 = - insert(:achievement, %{ - title: "The Source-rer", - ability: "Effort", - is_task: true, - position: 3, - card_tile_url: - "https://source-academy-assets.s3-ap-southeast-1.amazonaws.com/achievement/card-tile/the-source-rer-tile.png", - open_at: ~U[2020-07-16 16:00:00Z], - close_at: ~U[2020-07-20 16:00:00Z], - goals: [ - %{goal_uuid: goal_14.uuid}, - %{goal_uuid: goal_15.uuid} - ] - }) - - achievement_8 = - insert(:achievement, %{ - title: "Power of Friendship", - ability: "Community", - is_task: true, - position: 2, - card_tile_url: - "https://source-academy-assets.s3-ap-southeast-1.amazonaws.com/achievement/card-tile/power-of-friendship-tile.png", - open_at: ~U[2020-07-16 16:00:00Z], - close_at: ~U[2020-07-20 16:00:00Z], - goals: [ - %{goal_uuid: goal_16.uuid} - ] - }) - - achievement_9 = - insert(:achievement, %{ - title: "Piazza Guru", - ability: "Community", - is_task: false, - position: 0, - card_tile_url: - "https://source-academy-assets.s3-ap-southeast-1.amazonaws.com/achievement/card-tile/piazza-guru-tile.png", - goals: [ - %{goal_uuid: goal_17.uuid} - ] - }) - - achievement_10 = - insert(:achievement, %{ - title: "Thats the Spirit", - ability: "Exploration", - is_task: true, - position: 5, - card_tile_url: - "https://source-academy-assets.s3-ap-southeast-1.amazonaws.com/achievement/card-tile/annotated-tile.png", - goals: [ - %{goal_uuid: goal_18.uuid} - ] - }) - - insert(:achievement_prerequisite, %{ - prerequisite_uuid: achievement_9.uuid, - achievement_uuid: achievement_8.uuid - }) - - insert(:achievement_prerequisite, %{ - prerequisite_uuid: achievement_5.uuid, - achievement_uuid: achievement_4.uuid - }) - - insert(:achievement_prerequisite, %{ - prerequisite_uuid: achievement_6.uuid, - achievement_uuid: achievement_4.uuid - }) - - insert(:achievement_prerequisite, %{ - prerequisite_uuid: achievement_1.uuid, - achievement_uuid: achievement_0.uuid - }) - - insert(:achievement_prerequisite, %{ - prerequisite_uuid: achievement_2.uuid, - achievement_uuid: achievement_0.uuid - }) + # goal_0 = + # insert(:goal, %{ + # text: "Complete Beyond the Second Dimension achievement", + # max_xp: 250 + # }) + + # goal_1 = + # insert(:goal, %{ + # text: "Complete Colorful Carpet achievement", + # max_xp: 250 + # }) + + # goal_2 = + # insert(:goal, %{ + # text: "Bonus for completing Rune Master achievement", + # max_xp: 250 + # }) + + # goal_3 = + # insert(:goal, %{ + # text: "Complete Beyond the Second Dimension mission", + # max_xp: 100 + # }) + + # goal_4 = + # insert(:goal, %{ + # text: "Score earned from Beyond the Second Dimension mission", + # max_xp: 150 + # }) + + # goal_5 = + # insert(:goal, %{ + # text: "Complete Colorful Carpet mission", + # max_xp: 100 + # }) + + # goal_6 = + # insert(:goal, %{ + # text: "Score earned from Colorful Carpet mission", + # max_xp: 150 + # }) + + # goal_7 = + # insert(:goal, %{ + # text: "Complete Curve Introduction mission", + # max_xp: 250 + # }) + + # goal_8 = + # insert(:goal, %{ + # text: "Complete Curve Manipulation mission", + # max_xp: 250 + # }) + + # goal_9 = + # insert(:goal, %{ + # text: "Bonus for completing Curve Wizard achievement", + # max_xp: 100 + # }) + + # goal_10 = + # insert(:goal, %{ + # text: "Complete Curve Introduction mission", + # max_xp: 50 + # }) + + # goal_11 = + # insert(:goal, %{ + # text: "Score earned from Curve Introduction mission", + # max_xp: 200 + # }) + + # goal_12 = + # insert(:goal, %{ + # text: "Complete Curve Manipulation mission", + # max_xp: 50 + # }) + + # goal_13 = + # insert(:goal, %{ + # text: "Score earned from Curve Manipulation mission", + # max_xp: 200 + # }) + + # goal_14 = + # insert(:goal, %{ + # text: "Complete Source 3 path", + # max_xp: 100 + # }) + + # goal_15 = + # insert(:goal, %{ + # text: "Score earned from Source 3 path", + # max_xp: 300 + # }) + + # goal_16 = + # insert(:goal, %{ + # text: "Complete Piazza Guru achievement", + # max_xp: 100 + # }) + + # goal_17 = + # insert(:goal, %{ + # text: "Each Top Voted answer in Piazza gives 10 XP", + # max_xp: 100 + # }) + + # goal_18 = + # insert(:goal, %{ + # text: "Submit 1 PR to Source Academy Github", + # max_xp: 100 + # }) + + # # Achievements + # achievement_0 = + # insert(:achievement, %{ + # title: "Rune Master", + # ability: "Core", + # is_task: true, + # position: 1, + # card_tile_url: + # "https://source-academy-assets.s3-ap-southeast-1.amazonaws.com/achievement/card-tile/rune-master-tile.png", + # goals: [ + # %{goal_uuid: goal_0.uuid}, + # %{goal_uuid: goal_1.uuid}, + # %{goal_uuid: goal_2.uuid} + # ] + # }) + + # achievement_1 = + # insert(:achievement, %{ + # title: "Beyond the Second Dimension", + # ability: "Core", + # is_task: false, + # position: 0, + # card_tile_url: + # "https://source-academy-assets.s3-ap-southeast-1.amazonaws.com/achievement/card-tile/btsd-tile.png", + # open_at: ~U[2020-07-16 16:00:00Z], + # close_at: ~U[2020-07-20 16:00:00Z], + # goals: [ + # %{goal_uuid: goal_3.uuid}, + # %{goal_uuid: goal_4.uuid} + # ] + # }) + + # achievement_2 = + # insert(:achievement, %{ + # title: "Colorful Carpet", + # ability: "Core", + # is_task: false, + # position: 0, + # card_tile_url: + # "https://source-academy-assets.s3-ap-southeast-1.amazonaws.com/achievement/card-tile/colorful-carpet-tile.png", + # open_at: ~U[2020-07-11 16:00:00Z], + # close_at: ~U[2020-07-15 16:00:00Z], + # goals: [ + # %{goal_uuid: goal_5.uuid}, + # %{goal_uuid: goal_6.uuid} + # ] + # }) + + # achievement_3 = + # insert(:achievement, %{ + # title: "Unpublished", + # ability: "Core", + # is_task: false, + # position: 0, + # card_tile_url: + # "https://www.publicdomainpictures.net/pictures/30000/velka/plain-white-background.jpg" + # }) + + # achievement_4 = + # insert(:achievement, %{ + # title: "Curve Wizard", + # ability: "Core", + # is_task: true, + # position: 4, + # card_tile_url: + # "https://source-academy-assets.s3-ap-southeast-1.amazonaws.com/achievement/card-tile/curve-wizard-tile.png", + # open_at: ~U[2020-07-31 16:00:00Z], + # close_at: ~U[2020-08-04 16:00:00Z], + # goals: [ + # %{goal_uuid: goal_7.uuid}, + # %{goal_uuid: goal_8.uuid}, + # %{goal_uuid: goal_9.uuid} + # ] + # }) + + # achievement_5 = + # insert(:achievement, %{ + # title: "Curve Introduction", + # ability: "Core", + # is_task: false, + # position: 0, + # card_tile_url: + # "https://source-academy-assets.s3-ap-southeast-1.amazonaws.com/achievement/card-tile/curve-introduction-tile.png", + # open_at: ~U[2020-07-23 16:00:00Z], + # close_at: ~U[2020-07-27 16:00:00Z], + # goals: [ + # %{goal_uuid: goal_10.uuid}, + # %{goal_uuid: goal_11.uuid} + # ] + # }) + + # achievement_6 = + # insert(:achievement, %{ + # title: "Curve Manipulation", + # ability: "Core", + # is_task: false, + # position: 0, + # card_tile_url: + # "https://source-academy-assets.s3-ap-southeast-1.amazonaws.com/achievement/card-tile/curve-manipulation-tile.png", + # open_at: ~U[2020-07-31 16:00:00Z], + # close_at: ~U[2020-08-04 16:00:00Z], + # goals: [ + # %{goal_uuid: goal_12.uuid}, + # %{goal_uuid: goal_13.uuid} + # ] + # }) + + # achievement_7 = + # insert(:achievement, %{ + # title: "The Source-rer", + # ability: "Effort", + # is_task: true, + # position: 3, + # card_tile_url: + # "https://source-academy-assets.s3-ap-southeast-1.amazonaws.com/achievement/card-tile/the-source-rer-tile.png", + # open_at: ~U[2020-07-16 16:00:00Z], + # close_at: ~U[2020-07-20 16:00:00Z], + # goals: [ + # %{goal_uuid: goal_14.uuid}, + # %{goal_uuid: goal_15.uuid} + # ] + # }) + + # achievement_8 = + # insert(:achievement, %{ + # title: "Power of Friendship", + # ability: "Community", + # is_task: true, + # position: 2, + # card_tile_url: + # "https://source-academy-assets.s3-ap-southeast-1.amazonaws.com/achievement/card-tile/power-of-friendship-tile.png", + # open_at: ~U[2020-07-16 16:00:00Z], + # close_at: ~U[2020-07-20 16:00:00Z], + # goals: [ + # %{goal_uuid: goal_16.uuid} + # ] + # }) + + # achievement_9 = + # insert(:achievement, %{ + # title: "Piazza Guru", + # ability: "Community", + # is_task: false, + # position: 0, + # card_tile_url: + # "https://source-academy-assets.s3-ap-southeast-1.amazonaws.com/achievement/card-tile/piazza-guru-tile.png", + # goals: [ + # %{goal_uuid: goal_17.uuid} + # ] + # }) + + # achievement_10 = + # insert(:achievement, %{ + # title: "Thats the Spirit", + # ability: "Exploration", + # is_task: true, + # position: 5, + # card_tile_url: + # "https://source-academy-assets.s3-ap-southeast-1.amazonaws.com/achievement/card-tile/annotated-tile.png", + # goals: [ + # %{goal_uuid: goal_18.uuid} + # ] + # }) + + # insert(:achievement_prerequisite, %{ + # prerequisite_uuid: achievement_9.uuid, + # achievement_uuid: achievement_8.uuid + # }) + + # insert(:achievement_prerequisite, %{ + # prerequisite_uuid: achievement_5.uuid, + # achievement_uuid: achievement_4.uuid + # }) + + # insert(:achievement_prerequisite, %{ + # prerequisite_uuid: achievement_6.uuid, + # achievement_uuid: achievement_4.uuid + # }) + + # insert(:achievement_prerequisite, %{ + # prerequisite_uuid: achievement_1.uuid, + # achievement_uuid: achievement_0.uuid + # }) + + # insert(:achievement_prerequisite, %{ + # prerequisite_uuid: achievement_2.uuid, + # achievement_uuid: achievement_0.uuid + # }) end diff --git a/test/cadet_web/controllers/user_controller_test.exs b/test/cadet_web/controllers/user_controller_test.exs index b9ffc4601..4f75546d8 100644 --- a/test/cadet_web/controllers/user_controller_test.exs +++ b/test/cadet_web/controllers/user_controller_test.exs @@ -446,6 +446,7 @@ defmodule CadetWeb.UserControllerTest do @tag authenticate: :student test "success, retrieving student game state", %{conn: conn} do course_id = conn.assigns.course_id + resp = conn |> get(build_url(course_id)) From 1b8760e53b5cf4290337d195d3ca52d52194ee75 Mon Sep 17 00:00:00 2001 From: YaleChen299 Date: Mon, 14 Jun 2021 00:05:07 +0800 Subject: [PATCH 053/174] updated seed with latest_viewed --- priv/repo/seeds.exs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/priv/repo/seeds.exs b/priv/repo/seeds.exs index 2a4b0c96c..c35959ff6 100644 --- a/priv/repo/seeds.exs +++ b/priv/repo/seeds.exs @@ -21,12 +21,12 @@ if Cadet.Env.env() == :dev do course1 = insert(:course) course2 = insert(:course, %{name: "Algorithm", module_code: "CS2040S"}) # Users - avenger1 = insert(:user, %{name: "avenger", username: "E1234561"}) - mentor1 = insert(:user, %{name: "mentor", username: "E1234562"}) - admin1 = insert(:user, %{name: "admin", username: "E1234563"}) - studenta1admin2 = insert(:user, %{username: "E1234564"}) - studentb1 = insert(:user, %{username: "E1234565"}) - studentc1 = insert(:user, %{username: "E1234566"}) + avenger1 = insert(:user, %{name: "avenger", username: "E1234561", latest_viewed: course1}) + mentor1 = insert(:user, %{name: "mentor", username: "E1234562", latest_viewed: course1}) + admin1 = insert(:user, %{name: "admin", username: "E1234563", latest_viewed: course1}) + studenta1admin2 = insert(:user, %{username: "E1234564", latest_viewed: course1}) + studentb1 = insert(:user, %{username: "E1234565", latest_viewed: course1}) + studentc1 = insert(:user, %{username: "E1234566", latest_viewed: course1}) # CourseRegistration and Group avenger1_cr = insert(:course_registration, %{user: avenger1, course: course1, role: :staff}) mentor1_cr = insert(:course_registration, %{user: mentor1, course: course1, role: :staff}) From aa02582ac40793f68bc23b34e03aacae82c095ee Mon Sep 17 00:00:00 2001 From: YaleChen299 Date: Mon, 14 Jun 2021 22:18:01 +0800 Subject: [PATCH 054/174] fix user view snake_case issue --- lib/cadet_web/views/user_view.ex | 2 +- test/cadet_web/controllers/user_controller_test.exs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/cadet_web/views/user_view.ex b/lib/cadet_web/views/user_view.ex index 6e048e916..b83976ebc 100644 --- a/lib/cadet_web/views/user_view.ex +++ b/lib/cadet_web/views/user_view.ex @@ -79,7 +79,7 @@ defmodule CadetWeb.UserView do def render("course.json", %{cr: cr}) do %{ - course_id: cr.course_id, + courseId: cr.course_id, name: cr.course.name, moduleCode: cr.course.module_code, viewable: cr.course.viewable diff --git a/test/cadet_web/controllers/user_controller_test.exs b/test/cadet_web/controllers/user_controller_test.exs index 4f75546d8..2e1139e81 100644 --- a/test/cadet_web/controllers/user_controller_test.exs +++ b/test/cadet_web/controllers/user_controller_test.exs @@ -73,7 +73,7 @@ defmodule CadetWeb.UserControllerTest do "viewable" => true }, %{ - "course_id" => another_cr.course_id, + "courseId" => another_cr.course_id, "moduleCode" => "CS1101S", "name" => "Programming Methodology", "viewable" => true From 3e06aebe43c0759e97f91a4224f21ca03900f2be Mon Sep 17 00:00:00 2001 From: YaleChen299 Date: Tue, 15 Jun 2021 13:07:24 +0800 Subject: [PATCH 055/174] refactor assessment_config to belong to assessment type with test and seeds --- config/test.exs | 4 +- lib/cadet/accounts/course_registrations.ex | 11 +++ lib/cadet/assessments/assessment.ex | 6 +- lib/cadet/assessments/assessments.ex | 82 ++++++++-------- lib/cadet/assessments/query.ex | 7 +- lib/cadet/courses/assessment_config.ex | 8 +- ...assessment_types.ex => assessment_type.ex} | 5 +- lib/cadet/courses/course.ex | 4 + lib/cadet/courses/courses.ex | 15 +-- .../admin_courses_controller.ex | 6 +- .../controllers/assessments_controller.ex | 10 +- .../controllers/courses_controller.ex | 4 + lib/cadet_web/controllers/user_controller.ex | 4 +- lib/cadet_web/router.ex | 2 +- lib/cadet_web/views/assessments_view.ex | 3 +- lib/cadet_web/views/user_view.ex | 31 +----- ...0210531155751_add_course_configuration.exs | 16 ++-- priv/repo/seeds.exs | 7 +- test/cadet/assessments/assessment_test.exs | 4 +- test/cadet/assessments/submission_test.exs | 2 +- ...ypes_test.exs => assessment_type_test.exs} | 6 +- test/cadet/courses/courses_test.exs | 96 ++++++++++--------- .../admin_courses_controller_test.exs | 36 ++++--- .../controllers/courses_controller_test.exs | 9 +- .../controllers/user_controller_test.exs | 4 +- .../accounts/course_registration_factory.ex | 2 +- .../assessments/assessment_factory.ex | 2 +- .../courses/assessment_config_factory.ex | 4 +- .../courses/assessment_type_factory.ex | 19 ++++ .../courses/assessment_types_factory.ex | 18 ---- test/factories/factory.ex | 2 +- 31 files changed, 225 insertions(+), 204 deletions(-) rename lib/cadet/courses/{assessment_types.ex => assessment_type.ex} (85%) rename test/cadet/courses/{assessment_types_test.exs => assessment_type_test.exs} (88%) create mode 100644 test/factories/courses/assessment_type_factory.ex delete mode 100644 test/factories/courses/assessment_types_factory.ex diff --git a/config/test.exs b/config/test.exs index 833df7d0c..802b32cd3 100644 --- a/config/test.exs +++ b/config/test.exs @@ -65,8 +65,8 @@ config :cadet, %{ token: "student_token", code: "student_code", - name: "Test Student", - username: "student" + name: "student 1", + username: "E1234564" # role: :student } ]} diff --git a/lib/cadet/accounts/course_registrations.ex b/lib/cadet/accounts/course_registrations.ex index a98eec9bc..17b6b84ff 100644 --- a/lib/cadet/accounts/course_registrations.ex +++ b/lib/cadet/accounts/course_registrations.ex @@ -24,6 +24,17 @@ defmodule Cadet.Accounts.CourseRegistrations do |> Repo.one() end + def get_user_course(user_id, course_id) when is_ecto_id(user_id) and is_ecto_id(course_id) do + CourseRegistration + |> where([cr], cr.user_id == ^user_id) + |> where([cr], cr.course_id == ^course_id) + |> join(:inner, [cr], c in assoc(cr, :course)) + |> join(:inner, [cr, c], t in assoc(c, :assessment_type)) + |> preload([cr, c, t],[course: {c, assessment_type: t}]) + |> preload(:group) + |> Repo.one() + end + def get_courses(%User{id: id}) do CourseRegistration |> where([cr], cr.user_id == ^id) diff --git a/lib/cadet/assessments/assessment.ex b/lib/cadet/assessments/assessment.ex index 81b0631c8..694aef839 100644 --- a/lib/cadet/assessments/assessment.ex +++ b/lib/cadet/assessments/assessment.ex @@ -8,7 +8,7 @@ defmodule Cadet.Assessments.Assessment do alias Cadet.Repo alias Cadet.Assessments.{AssessmentAccess, Question, SubmissionStatus, Upload} - alias Cadet.Courses.{Course, AssessmentTypes} + alias Cadet.Courses.{Course, AssessmentType} # @assessment_types ~w(contest mission path practical sidequest) # def assessment_types, do: @assessment_types @@ -36,7 +36,7 @@ defmodule Cadet.Assessments.Assessment do field(:reading, :string) field(:password, :string, default: nil) - belongs_to(:type, AssessmentTypes) + belongs_to(:type, AssessmentType) belongs_to(:course, Course) has_many(:questions, Question, on_delete: :delete_all) @@ -69,7 +69,7 @@ defmodule Cadet.Assessments.Assessment do type_id = get_field(changeset, :type_id) course_id = get_field(changeset, :course_id) - case Repo.get(AssessmentTypes, type_id) do + case Repo.get(AssessmentType, type_id) do nil -> add_error(changeset, :type, "does not exist") diff --git a/lib/cadet/assessments/assessments.ex b/lib/cadet/assessments/assessments.ex index a671d480a..5786414f2 100644 --- a/lib/cadet/assessments/assessments.ex +++ b/lib/cadet/assessments/assessments.ex @@ -24,44 +24,44 @@ defmodule Cadet.Assessments do # submitted answers @bypass_closed_roles ~w(staff admin)a - def delete_assessment(id) do - assessment = Repo.get(Assessment, id) + # def delete_assessment(id) do + # assessment = Repo.get(Assessment, id) - Submission - |> where(assessment_id: ^id) - |> delete_submission_assocation(id) + # Submission + # |> where(assessment_id: ^id) + # |> delete_submission_assocation(id) - Question - |> where(assessment_id: ^id) - |> Repo.all() - |> Enum.each(fn q -> - delete_submission_votes_association(q) - end) + # Question + # |> where(assessment_id: ^id) + # |> Repo.all() + # |> Enum.each(fn q -> + # delete_submission_votes_association(q) + # end) - Repo.delete(assessment) - end + # Repo.delete(assessment) + # end - defp delete_submission_votes_association(question) do - SubmissionVotes - |> where(question_id: ^question.id) - |> Repo.delete_all() - end + # defp delete_submission_votes_association(question) do + # SubmissionVotes + # |> where(question_id: ^question.id) + # |> Repo.delete_all() + # end - defp delete_submission_assocation(submissions, assessment_id) do - submissions - |> Repo.all() - |> Enum.each(fn submission -> - Answer - |> where(submission_id: ^submission.id) - |> Repo.delete_all() - end) + # defp delete_submission_assocation(submissions, assessment_id) do + # submissions + # |> Repo.all() + # |> Enum.each(fn submission -> + # Answer + # |> where(submission_id: ^submission.id) + # |> Repo.delete_all() + # end) - Notification - |> where(assessment_id: ^assessment_id) - |> Repo.delete_all() + # Notification + # |> where(assessment_id: ^assessment_id) + # |> Repo.delete_all() - Repo.delete_all(submissions) - end + # Repo.delete_all(submissions) + # end @spec user_max_grade(%CourseRegistration{}) :: integer() def user_max_grade(%CourseRegistration{id: cr_id}) do @@ -262,11 +262,11 @@ defmodule Cadet.Assessments do Returns a list of assessments with all fields and an indicator showing whether it has been attempted by the supplied user """ - def all_assessments(user = %User{}) do + def all_assessments(cr = %CourseRegistration{}) do submission_aggregates = Submission |> join(:left, [s], ans in Answer, on: ans.submission_id == s.id) - |> where([s], s.student_id == ^user.id) + |> where([s], s.student_id == ^cr.id) |> group_by([s], s.assessment_id) |> select([s, ans], %{ assessment_id: s.assessment_id, @@ -278,11 +278,11 @@ defmodule Cadet.Assessments do submission_status = Submission - |> where([s], s.student_id == ^user.id) + |> where([s], s.student_id == ^cr.id) |> select([s], [:assessment_id, :status]) assessments = - Query.all_assessments_with_aggregates() + Query.all_assessments_with_aggregates(cr.course_id) |> subquery() |> join( :left, @@ -298,15 +298,16 @@ defmodule Cadet.Assessments do graded_count: sa.graded_count, user_status: s.status }) - |> filter_published_assessments(user) + |> filter_published_assessments(cr) |> order_by(:open_at) + |> preload(:type) |> Repo.all() {:ok, assessments} end - def filter_published_assessments(assessments, user) do - role = user.role + def filter_published_assessments(assessments, cr) do + role = cr.role case role do :student -> where(assessments, is_published: true) @@ -674,11 +675,11 @@ defmodule Cadet.Assessments do end end - def get_submission(assessment_id, %User{id: user_id}) + def get_submission(assessment_id, %CourseRegistration{id: cr_id}) when is_ecto_id(assessment_id) do Submission |> where(assessment_id: ^assessment_id) - |> where(student_id: ^user_id) + |> where(student_id: ^cr_id) |> join(:inner, [s], a in assoc(s, :assessment)) |> preload([_, a], assessment: a) |> Repo.one() @@ -797,6 +798,7 @@ defmodule Cadet.Assessments do end end + # :TODO bonus logic with assessment config @spec update_submission_status_and_xp_bonus(%Submission{}) :: {:ok, %Submission{}} | {:error, Ecto.Changeset.t()} defp update_submission_status_and_xp_bonus(submission = %Submission{}) do diff --git a/lib/cadet/assessments/query.ex b/lib/cadet/assessments/query.ex index 34ced60e1..ea2e7f82f 100644 --- a/lib/cadet/assessments/query.ex +++ b/lib/cadet/assessments/query.ex @@ -2,6 +2,8 @@ defmodule Cadet.Assessments.Query do @moduledoc """ Generate queries related to the Assessments context """ + use Cadet, :context + import Ecto.Query alias Cadet.Assessments.{Assessment, Question} @@ -10,9 +12,10 @@ defmodule Cadet.Assessments.Query do Returns a query with the following bindings: [assessments_with_xp_and_grade, questions] """ - @spec all_assessments_with_aggregates :: Ecto.Query.t() - def all_assessments_with_aggregates do + @spec all_assessments_with_aggregates(integer()) :: Ecto.Query.t() + def all_assessments_with_aggregates(course_id) when is_ecto_id(course_id) do Assessment + |> where(course_id: ^course_id) |> join(:inner, [a], q in subquery(assessments_aggregates()), on: a.id == q.assessment_id) |> select([a, q], %Assessment{ a diff --git a/lib/cadet/courses/assessment_config.ex b/lib/cadet/courses/assessment_config.ex index 223d9fc80..97f5c94ea 100644 --- a/lib/cadet/courses/assessment_config.ex +++ b/lib/cadet/courses/assessment_config.ex @@ -5,27 +5,29 @@ defmodule Cadet.Courses.AssessmentConfig do """ use Cadet, :model - alias Cadet.Courses.Course + alias Cadet.Courses.AssessmentType schema "assessment_configs" do field(:early_submission_xp, :integer) field(:hours_before_early_xp_decay, :integer) field(:decay_rate_points_per_hour, :integer) - belongs_to(:course, Course) + belongs_to(:assessment_type, AssessmentType) timestamps() end @required_fields ~w(early_submission_xp hours_before_early_xp_decay decay_rate_points_per_hour)a + @optional_fields ~w(assessment_type_id)a def changeset(assessment_config, params) do assessment_config - |> cast(params, @required_fields) + |> cast(params, @required_fields ++ @optional_fields) |> validate_required(@required_fields) |> validate_number(:early_submission_xp, greater_than_or_equal_to: 0) |> validate_number(:hours_before_early_xp_decay, greater_than_or_equal_to: 0) |> validate_number(:decay_rate_points_per_hour, greater_than_or_equal_to: 0) + |> foreign_key_constraint(:assessment_type_id) |> validate_decay_rate() end diff --git a/lib/cadet/courses/assessment_types.ex b/lib/cadet/courses/assessment_type.ex similarity index 85% rename from lib/cadet/courses/assessment_types.ex rename to lib/cadet/courses/assessment_type.ex index 424923762..141f3ea88 100644 --- a/lib/cadet/courses/assessment_types.ex +++ b/lib/cadet/courses/assessment_type.ex @@ -1,16 +1,17 @@ -defmodule Cadet.Courses.AssessmentTypes do +defmodule Cadet.Courses.AssessmentType do @moduledoc """ The AssessmentType entity stores the assessment tyoes in a particular course. """ use Cadet, :model - alias Cadet.Courses.Course + alias Cadet.Courses.{Course, AssessmentConfig} schema "assessment_types" do field(:order, :integer) field(:type, :string) belongs_to(:course, Course) + has_one(:assessment_config, AssessmentConfig) timestamps() end diff --git a/lib/cadet/courses/course.ex b/lib/cadet/courses/course.ex index 57f4e0caa..67f71221d 100644 --- a/lib/cadet/courses/course.ex +++ b/lib/cadet/courses/course.ex @@ -4,6 +4,8 @@ defmodule Cadet.Courses.Course do """ use Cadet, :model + alias Cadet.Courses.AssessmentType + schema "courses" do field(:name, :string) field(:module_code, :string) @@ -15,6 +17,8 @@ defmodule Cadet.Courses.Course do field(:source_variant, :string) field(:module_help_text, :string) + has_many(:assessment_type, AssessmentType) + timestamps() end diff --git a/lib/cadet/courses/courses.ex b/lib/cadet/courses/courses.ex index 19d6caff3..69f33444f 100644 --- a/lib/cadet/courses/courses.ex +++ b/lib/cadet/courses/courses.ex @@ -11,7 +11,7 @@ defmodule Cadet.Courses do alias Cadet.Courses.{ AssessmentConfig, - AssessmentTypes, + AssessmentType, Course, Group, Sourcecast, @@ -30,7 +30,7 @@ defmodule Cadet.Courses do course -> assessment_types = - AssessmentTypes + AssessmentType |> where(course_id: ^course_id) |> Repo.all() |> Enum.sort(&(&1.order < &2.order)) @@ -66,11 +66,12 @@ defmodule Cadet.Courses do @doc """ Updates the assessment configuration for the specified course """ - @spec update_assessment_config(integer, integer, integer, integer) :: + @spec update_assessment_config(integer, integer, integer, integer, integer) :: {:ok, %AssessmentConfig{}} | {:error, Ecto.Changeset.t()} - def update_assessment_config(course_id, early_xp, hours_before_decay, decay_rate) do + def update_assessment_config(course_id, order, early_xp, hours_before_decay, decay_rate) do AssessmentConfig - |> where(course_id: ^course_id) + |> join(:inner, [ac], at in AssessmentType, on: at.order == ^order) + |> where([ac, at], at.course_id == ^ course_id) |> Repo.one() |> AssessmentConfig.changeset(%{ early_submission_xp: early_xp, @@ -101,13 +102,13 @@ defmodule Cadet.Courses do |> Enum.each(fn {elem, idx} -> case elem do nil -> - AssessmentTypes + AssessmentType |> where(course_id: ^course_id) |> where(order: ^idx) |> Repo.delete_all() _ -> - AssessmentTypes.changeset(%AssessmentTypes{}, %{ + AssessmentType.changeset(%AssessmentType{}, %{ course_id: course_id, order: idx, type: elem diff --git a/lib/cadet_web/admin_controllers/admin_courses_controller.ex b/lib/cadet_web/admin_controllers/admin_courses_controller.ex index 001beb2d9..a69444093 100644 --- a/lib/cadet_web/admin_controllers/admin_courses_controller.ex +++ b/lib/cadet_web/admin_controllers/admin_courses_controller.ex @@ -31,12 +31,13 @@ defmodule CadetWeb.AdminCoursesController do def update_assessment_config(conn, %{ "course_id" => course_id, + "order" => order, "early_submission_xp" => early_xp, "hours_before_early_xp_decay" => hours_before_decay, "decay_rate_points_per_hour" => decay_rate }) when is_ecto_id(course_id) do - case Courses.update_assessment_config(course_id, early_xp, hours_before_decay, decay_rate) do + case Courses.update_assessment_config(course_id, order, early_xp, hours_before_decay, decay_rate) do {:ok, _} -> text(conn, "OK") @@ -98,7 +99,7 @@ defmodule CadetWeb.AdminCoursesController do end swagger_path :update_assessment_config do - put("/v2/course/{course_id}/admin/assessment_config") + put("/v2/course/{course_id}/admin/assessment_config/{order}") summary("Updates the assessment configuration for the specified course") @@ -108,6 +109,7 @@ defmodule CadetWeb.AdminCoursesController do parameters do course_id(:path, :integer, "Course ID", required: true) + order(:path, :integer, "type order", required: true) early_submission_xp(:body, :integer, "Early submission xp") hours_before_early_xp_decay(:body, :integer, "Hours before early submission xp decay") decay_rate_points_per_hour(:body, :integer, "Decay rate in points per hour") diff --git a/lib/cadet_web/controllers/assessments_controller.ex b/lib/cadet_web/controllers/assessments_controller.ex index 5954bcdf9..161cfcfa0 100644 --- a/lib/cadet_web/controllers/assessments_controller.ex +++ b/lib/cadet_web/controllers/assessments_controller.ex @@ -10,13 +10,13 @@ defmodule CadetWeb.AssessmentsController do @bypass_closed_roles ~w(staff admin)a def submit(conn, %{"assessmentid" => assessment_id}) when is_ecto_id(assessment_id) do - user = conn.assigns[:current_user] + cr = conn.assigns.course_reg with {:submission, submission} when not is_nil(submission) <- - {:submission, Assessments.get_submission(assessment_id, user)}, + {:submission, Assessments.get_submission(assessment_id, cr)}, {:is_open?, true} <- {:is_open?, - user.role in @bypass_closed_roles or Assessments.is_open?(submission.assessment)}, + cr.role in @bypass_closed_roles or Assessments.is_open?(submission.assessment)}, {:ok, _nil} <- Assessments.finalise_submission(submission) do text(conn, "OK") else @@ -38,8 +38,8 @@ defmodule CadetWeb.AssessmentsController do end def index(conn, _) do - user = conn.assigns[:current_user] - {:ok, assessments} = Assessments.all_assessments(user) + cr = conn.assigns.course_reg + {:ok, assessments} = Assessments.all_assessments(cr) render(conn, "index.json", assessments: assessments) end diff --git a/lib/cadet_web/controllers/courses_controller.ex b/lib/cadet_web/controllers/courses_controller.ex index c33f04f38..2d7d2fdad 100644 --- a/lib/cadet_web/controllers/courses_controller.ex +++ b/lib/cadet_web/controllers/courses_controller.ex @@ -12,6 +12,10 @@ defmodule CadetWeb.CoursesController do end end + # def create_course(conn, _) do + + # end + swagger_path :get_course_config do get("/v2/course/{course_id}/config") diff --git a/lib/cadet_web/controllers/user_controller.ex b/lib/cadet_web/controllers/user_controller.ex index dae5bb467..94e127cce 100644 --- a/lib/cadet_web/controllers/user_controller.ex +++ b/lib/cadet_web/controllers/user_controller.ex @@ -14,7 +14,7 @@ defmodule CadetWeb.UserController do courses = CourseRegistrations.get_courses(conn.assigns.current_user) if user.latest_viewed_id do - latest = CourseRegistrations.get_user_record(user.id, user.latest_viewed_id) + latest = CourseRegistrations.get_user_course(user.id, user.latest_viewed_id) %{total_grade: grade, total_xp: xp} = user_total_grade_xp(latest) max_grade = user_max_grade(latest) story = user_current_story(latest) @@ -66,7 +66,7 @@ defmodule CadetWeb.UserController do latest = case user.latest_viewed_id do nil -> nil - _ -> CourseRegistrations.get_user_record(user.id, user.latest_viewed_id) + _ -> CourseRegistrations.get_user_course(user.id, user.latest_viewed_id) end get_course_reg_config(conn, latest) diff --git a/lib/cadet_web/router.ex b/lib/cadet_web/router.ex index 1fee647e3..b73882dea 100644 --- a/lib/cadet_web/router.ex +++ b/lib/cadet_web/router.ex @@ -139,7 +139,7 @@ defmodule CadetWeb.Router do delete("/goals/:uuid", AdminGoalsController, :delete) put("/course_config", AdminCoursesController, :update_course_config) - put("/assessment_config", AdminCoursesController, :update_assessment_config) + put("/assessment_config/:order", AdminCoursesController, :update_assessment_config) put("/assessment_types", AdminCoursesController, :update_assessment_types) end diff --git a/lib/cadet_web/views/assessments_view.ex b/lib/cadet_web/views/assessments_view.ex index 32f8fd897..ffd16be1f 100644 --- a/lib/cadet_web/views/assessments_view.ex +++ b/lib/cadet_web/views/assessments_view.ex @@ -11,11 +11,12 @@ defmodule CadetWeb.AssessmentsView do def render("overview.json", %{assessment: assessment}) do transform_map_for_view(assessment, %{ id: :id, + courseId: :course_id, title: :title, shortSummary: :summary_short, openAt: &format_datetime(&1.open_at), closeAt: &format_datetime(&1.close_at), - type: :type, + type: &(&1.type.type), story: :story, number: :number, reading: :reading, diff --git a/lib/cadet_web/views/user_view.ex b/lib/cadet_web/views/user_view.ex index b83976ebc..91ca226e9 100644 --- a/lib/cadet_web/views/user_view.ex +++ b/lib/cadet_web/views/user_view.ex @@ -1,35 +1,6 @@ defmodule CadetWeb.UserView do use CadetWeb, :view - # def render("index.json", %{ - # user: user, - # cr: cr, - # grade: grade, - # max_grade: max_grade, - # xp: xp, - # story: story - # }) do - # %{ - # userId: user.id, - # name: user.name, - # role: cr.role, - # group: - # case cr.group do - # nil -> nil - # _ -> cr.group.name - # end, - # grade: grade, - # xp: xp, - # maxGrade: max_grade, - # story: - # transform_map_for_view(story, %{ - # story: :story, - # playStory: :play_story? - # }), - # gameStates: cr.game_states - # } - # end - def render("index.json", %{ user: user, courses: courses, @@ -135,7 +106,7 @@ defmodule CadetWeb.UserView do sourceChapter: :source_chapter, sourceVariant: :source_variant, moduleHelpText: :module_help_text, - assessmentTypes: :assessment_types + assessmentTypes: &(Enum.map(&1.assessment_type, fn x -> x.type end)) }) end end diff --git a/priv/repo/migrations/20210531155751_add_course_configuration.exs b/priv/repo/migrations/20210531155751_add_course_configuration.exs index ec8fcac6f..616fc4d4b 100644 --- a/priv/repo/migrations/20210531155751_add_course_configuration.exs +++ b/priv/repo/migrations/20210531155751_add_course_configuration.exs @@ -23,14 +23,6 @@ defmodule Cadet.Repo.Migrations.AddCourseConfiguration do timestamps() end - create table(:assessment_configs) do - add(:early_submission_xp, :integer, null: false) - add(:hours_before_early_xp_decay, :integer, null: false) - add(:decay_rate_points_per_hour, :integer, null: false) - add(:course_id, references(:courses), null: false) - timestamps() - end - create table(:assessment_types) do add(:order, :integer, null: false) add(:type, :string, null: false) @@ -40,6 +32,14 @@ defmodule Cadet.Repo.Migrations.AddCourseConfiguration do create(unique_index(:assessment_types, [:course_id, :order])) + create table(:assessment_configs) do + add(:early_submission_xp, :integer, null: false) + add(:hours_before_early_xp_decay, :integer, null: false) + add(:decay_rate_points_per_hour, :integer, null: false) + add(:assessment_type_id, references(:assessment_types, on_delete: :delete_all), null: false) + timestamps() + end + create table(:course_registrations) do add(:role, :role, null: false) add(:game_states, :map, default: %{}) diff --git a/priv/repo/seeds.exs b/priv/repo/seeds.exs index c35959ff6..2bfa7d50c 100644 --- a/priv/repo/seeds.exs +++ b/priv/repo/seeds.exs @@ -24,7 +24,7 @@ if Cadet.Env.env() == :dev do avenger1 = insert(:user, %{name: "avenger", username: "E1234561", latest_viewed: course1}) mentor1 = insert(:user, %{name: "mentor", username: "E1234562", latest_viewed: course1}) admin1 = insert(:user, %{name: "admin", username: "E1234563", latest_viewed: course1}) - studenta1admin2 = insert(:user, %{username: "E1234564", latest_viewed: course1}) + studenta1admin2 = insert(:user, %{name: "student a", username: "E1234564", latest_viewed: course1}) studentb1 = insert(:user, %{username: "E1234565", latest_viewed: course1}) studentc1 = insert(:user, %{username: "E1234566", latest_viewed: course1}) # CourseRegistration and Group @@ -52,8 +52,9 @@ if Cadet.Env.env() == :dev do admin2cr = insert(:course_registration, %{user: studenta1admin2, course: course2, role: :admin}) # Assessments - for _ <- 1..5 do - assessment = insert(:assessment, %{is_published: true}) + for i <- 1..5 do + type = insert(:assessment_type, %{type: "mission#{i}", order: i, course: course1}) + assessment = insert(:assessment, %{is_published: true, type: type}) programming_questions = insert_list(3, :programming_question, %{ diff --git a/test/cadet/assessments/assessment_test.exs b/test/cadet/assessments/assessment_test.exs index 04ba58239..547df4df0 100644 --- a/test/cadet/assessments/assessment_test.exs +++ b/test/cadet/assessments/assessment_test.exs @@ -6,8 +6,8 @@ defmodule Cadet.Assessments.AssessmentTest do setup do course1 = insert(:course, %{module_code: "course 1"}) course2 = insert(:course, %{module_code: "course 2"}) - type1 = insert(:assessment_types, %{course: course1}) - type2 = insert(:assessment_types, %{course: course2}) + type1 = insert(:assessment_type, %{course: course1}) + type2 = insert(:assessment_type, %{course: course2}) {:ok, %{course1: course1, course2: course2, type1: type1, type2: type2}} end diff --git a/test/cadet/assessments/submission_test.exs b/test/cadet/assessments/submission_test.exs index bd1f579d9..9099c1eef 100644 --- a/test/cadet/assessments/submission_test.exs +++ b/test/cadet/assessments/submission_test.exs @@ -7,7 +7,7 @@ defmodule Cadet.Assessments.SubmissionTest do setup do course = insert(:course) - type = insert(:assessment_types, %{course: course}) + type = insert(:assessment_type, %{course: course}) assessment = insert(:assessment, %{type: type, course: course}) student = insert(:course_registration, %{course: course, role: :student}) diff --git a/test/cadet/courses/assessment_types_test.exs b/test/cadet/courses/assessment_type_test.exs similarity index 88% rename from test/cadet/courses/assessment_types_test.exs rename to test/cadet/courses/assessment_type_test.exs index e7ea7b4f4..714fa7f15 100644 --- a/test/cadet/courses/assessment_types_test.exs +++ b/test/cadet/courses/assessment_type_test.exs @@ -1,7 +1,7 @@ -defmodule Cadet.Courses.AssessmentTypesTest do - alias Cadet.Courses.AssessmentTypes +defmodule Cadet.Courses.AssessmentTypeTest do + alias Cadet.Courses.AssessmentType - use Cadet.ChangesetCase, entity: AssessmentTypes + use Cadet.ChangesetCase, entity: AssessmentType describe "Assessment Types Changesets" do test "valid changesets" do diff --git a/test/cadet/courses/courses_test.exs b/test/cadet/courses/courses_test.exs index 0788edb78..fe41305f3 100644 --- a/test/cadet/courses/courses_test.exs +++ b/test/cadet/courses/courses_test.exs @@ -7,8 +7,8 @@ defmodule Cadet.CoursesTest do describe "get course config" do test "succeeds" do course = insert(:course) - insert(:assessment_types, %{order: 1, type: "Missions", course_id: course.id}) - insert(:assessment_types, %{order: 2, type: "Quests", course_id: course.id}) + insert(:assessment_type, %{order: 1, type: "Missions", course: course}) + insert(:assessment_type, %{order: 2, type: "Quests", course: course}) {:ok, course} = Courses.get_course_config(course.id) assert course.name == "Programming Methodology" @@ -116,10 +116,12 @@ defmodule Cadet.CoursesTest do describe "update assessment config" do test "succeeds" do - assessment_config = insert(:assessment_config, %{course: insert(:course)}) + course = insert(:course) + type = insert(:assessment_type, %{course: course}) + _assessment_config = insert(:assessment_config, %{assessment_type: type}) {:ok, updated_config} = - Courses.update_assessment_config(assessment_config.course_id, 100, 24, 1) + Courses.update_assessment_config(course.id, type.order, 100, 24, 1) assert updated_config.early_submission_xp == 100 assert updated_config.hours_before_early_xp_decay == 24 @@ -127,27 +129,29 @@ defmodule Cadet.CoursesTest do end test "returns with error for failed updates" do - assessment_config = insert(:assessment_config, %{course: insert(:course)}) + course = insert(:course) + type = insert(:assessment_type, %{course: course}) + _assessment_config = insert(:assessment_config, %{assessment_type: type}) {:error, changeset} = - Courses.update_assessment_config(assessment_config.course_id, -1, 0, 0) + Courses.update_assessment_config(course.id, type.order, -1, 0, 0) assert %{early_submission_xp: ["must be greater than or equal to 0"]} = errors_on(changeset) {:error, changeset} = - Courses.update_assessment_config(assessment_config.course_id, 200, -1, 0) + Courses.update_assessment_config(course.id, type.order, 200, -1, 0) assert %{hours_before_early_xp_decay: ["must be greater than or equal to 0"]} = errors_on(changeset) {:error, changeset} = - Courses.update_assessment_config(assessment_config.course_id, 200, 48, -1) + Courses.update_assessment_config(course.id, type.order, 200, 48, -1) assert %{decay_rate_points_per_hour: ["must be greater than or equal to 0"]} = errors_on(changeset) {:error, changeset} = - Courses.update_assessment_config(assessment_config.course_id, 200, 48, 300) + Courses.update_assessment_config(course.id, type.order, 200, 48, 300) assert %{decay_rate_points_per_hour: ["must be less than or equal to 200"]} = errors_on(changeset) @@ -159,11 +163,11 @@ defmodule Cadet.CoursesTest do course = insert(:course) course_id = course.id - insert(:assessment_types, %{order: 1, type: "Missions", course: course}) - insert(:assessment_types, %{order: 2, type: "Quests", course: course}) - insert(:assessment_types, %{order: 3, type: "Paths", course: course}) - insert(:assessment_types, %{order: 4, type: "Contests", course: course}) - insert(:assessment_types, %{order: 5, type: "Others", course: course}) + insert(:assessment_type, %{order: 1, type: "Missions", course: course}) + insert(:assessment_type, %{order: 2, type: "Quests", course: course}) + insert(:assessment_type, %{order: 3, type: "Paths", course: course}) + insert(:assessment_type, %{order: 4, type: "Contests", course: course}) + insert(:assessment_type, %{order: 5, type: "Others", course: course}) :ok = Courses.update_assessment_types(course_id, [ @@ -189,11 +193,11 @@ defmodule Cadet.CoursesTest do course = insert(:course) course_id = course.id - insert(:assessment_types, %{order: 4, type: "Contests", course: course}) - insert(:assessment_types, %{order: 1, type: "Missions", course: course}) - insert(:assessment_types, %{order: 3, type: "Paths", course: course}) - insert(:assessment_types, %{order: 5, type: "Others", course: course}) - insert(:assessment_types, %{order: 2, type: "Quests", course: course}) + insert(:assessment_type, %{order: 4, type: "Contests", course: course}) + insert(:assessment_type, %{order: 1, type: "Missions", course: course}) + insert(:assessment_type, %{order: 3, type: "Paths", course: course}) + insert(:assessment_type, %{order: 5, type: "Others", course: course}) + insert(:assessment_type, %{order: 2, type: "Quests", course: course}) :ok = Courses.update_assessment_types(course_id, [ @@ -219,11 +223,11 @@ defmodule Cadet.CoursesTest do course = insert(:course) course_id = course.id - insert(:assessment_types, %{order: 1, type: "Missions", course: course}) - insert(:assessment_types, %{order: 2, type: "Quests", course: course}) - insert(:assessment_types, %{order: 3, type: "Paths", course: course}) - insert(:assessment_types, %{order: 4, type: "Contests", course: course}) - insert(:assessment_types, %{order: 5, type: "Others", course: course}) + insert(:assessment_type, %{order: 1, type: "Missions", course: course}) + insert(:assessment_type, %{order: 2, type: "Quests", course: course}) + insert(:assessment_type, %{order: 3, type: "Paths", course: course}) + insert(:assessment_type, %{order: 4, type: "Contests", course: course}) + insert(:assessment_type, %{order: 5, type: "Others", course: course}) :ok = Courses.update_assessment_types(course_id, [ @@ -249,9 +253,9 @@ defmodule Cadet.CoursesTest do course = insert(:course) course_id = course.id - insert(:assessment_types, %{order: 1, type: "Missions", course: course}) - insert(:assessment_types, %{order: 2, type: "Quests", course: course}) - insert(:assessment_types, %{order: 3, type: "Paths", course: course}) + insert(:assessment_type, %{order: 1, type: "Missions", course: course}) + insert(:assessment_type, %{order: 2, type: "Quests", course: course}) + insert(:assessment_type, %{order: 3, type: "Paths", course: course}) :ok = Courses.update_assessment_types(course_id, [ @@ -277,10 +281,10 @@ defmodule Cadet.CoursesTest do course = insert(:course) course_id = course.id - insert(:assessment_types, %{order: 1, type: "Missions", course: course}) - insert(:assessment_types, %{order: 2, type: "Quests", course: course}) - insert(:assessment_types, %{order: 3, type: "Paths", course: course}) - insert(:assessment_types, %{order: 4, type: "Contests", course: course}) + insert(:assessment_type, %{order: 1, type: "Missions", course: course}) + insert(:assessment_type, %{order: 2, type: "Quests", course: course}) + insert(:assessment_type, %{order: 3, type: "Paths", course: course}) + insert(:assessment_type, %{order: 4, type: "Contests", course: course}) :ok = Courses.update_assessment_types(course_id, ["Paths", "Quests", "Missions"]) {:ok, updated_course_config} = Courses.get_course_config(course_id) @@ -292,9 +296,9 @@ defmodule Cadet.CoursesTest do course = insert(:course) course_id = course.id - insert(:assessment_types, %{order: 1, type: "Missions", course: course}) - insert(:assessment_types, %{order: 2, type: "Quests", course: course}) - insert(:assessment_types, %{order: 3, type: "Paths", course: course}) + insert(:assessment_type, %{order: 1, type: "Missions", course: course}) + insert(:assessment_type, %{order: 2, type: "Quests", course: course}) + insert(:assessment_type, %{order: 3, type: "Paths", course: course}) assert {:error, {:bad_request, "Invalid parameter(s)"}} = Courses.update_assessment_types(course_id, [1, "Quests", "Missions"]) @@ -304,9 +308,9 @@ defmodule Cadet.CoursesTest do course = insert(:course) course_id = course.id - insert(:assessment_types, %{order: 1, type: "Missions", course: course}) - insert(:assessment_types, %{order: 2, type: "Quests", course: course}) - insert(:assessment_types, %{order: 3, type: "Paths", course: course}) + insert(:assessment_type, %{order: 1, type: "Missions", course: course}) + insert(:assessment_type, %{order: 2, type: "Quests", course: course}) + insert(:assessment_type, %{order: 3, type: "Paths", course: course}) assert {:error, {:bad_request, "Invalid parameter(s)"}} = Courses.update_assessment_types(course_id, ["Missions", "Quests", "Missions"]) @@ -316,9 +320,9 @@ defmodule Cadet.CoursesTest do course = insert(:course) course_id = course.id - insert(:assessment_types, %{order: 1, type: "Missions", course: course}) - insert(:assessment_types, %{order: 2, type: "Quests", course: course}) - insert(:assessment_types, %{order: 3, type: "Paths", course: course}) + insert(:assessment_type, %{order: 1, type: "Missions", course: course}) + insert(:assessment_type, %{order: 2, type: "Quests", course: course}) + insert(:assessment_type, %{order: 3, type: "Paths", course: course}) assert {:error, {:bad_request, "Invalid parameter(s)"}} = Courses.update_assessment_types(course_id, []) @@ -328,9 +332,9 @@ defmodule Cadet.CoursesTest do course = insert(:course) course_id = course.id - insert(:assessment_types, %{order: 1, type: "Missions", course: course}) - insert(:assessment_types, %{order: 2, type: "Quests", course: course}) - insert(:assessment_types, %{order: 3, type: "Paths", course: course}) + insert(:assessment_type, %{order: 1, type: "Missions", course: course}) + insert(:assessment_type, %{order: 2, type: "Quests", course: course}) + insert(:assessment_type, %{order: 3, type: "Paths", course: course}) assert {:error, {:bad_request, "Invalid parameter(s)"}} = Courses.update_assessment_types(course_id, [ @@ -347,9 +351,9 @@ defmodule Cadet.CoursesTest do course = insert(:course) course_id = course.id - insert(:assessment_types, %{order: 1, type: "Missions", course: course}) - insert(:assessment_types, %{order: 2, type: "Quests", course: course}) - insert(:assessment_types, %{order: 3, type: "Paths", course: course}) + insert(:assessment_type, %{order: 1, type: "Missions", course: course}) + insert(:assessment_type, %{order: 2, type: "Quests", course: course}) + insert(:assessment_type, %{order: 3, type: "Paths", course: course}) assert {:error, {:bad_request, "Invalid parameter(s)"}} = Courses.update_assessment_types(course_id, "Missions") diff --git a/test/cadet_web/admin_controllers/admin_courses_controller_test.exs b/test/cadet_web/admin_controllers/admin_courses_controller_test.exs index 00b1c3fd9..56753c235 100644 --- a/test/cadet_web/admin_controllers/admin_courses_controller_test.exs +++ b/test/cadet_web/admin_controllers/admin_courses_controller_test.exs @@ -1,6 +1,8 @@ defmodule CadetWeb.AdminCoursesControllerTest do use CadetWeb.ConnCase + alias Cadet.Repo + alias Cadet.Courses.Course alias CadetWeb.AdminCoursesController test "swagger" do @@ -122,10 +124,12 @@ defmodule CadetWeb.AdminCoursesControllerTest do @tag authenticate: :admin test "succeeds", %{conn: conn} do course_id = conn.assigns[:course_id] - insert(:assessment_config, %{course_id: course_id}) + course = Repo.get(Course, course_id) + type = insert(:assessment_type, %{course: course}) + insert(:assessment_config, %{assessment_type: type}) conn = - put(conn, build_url_assessment_config(course_id), %{ + put(conn, build_url_assessment_config(course_id, type.order), %{ "early_submission_xp" => 100, "hours_before_early_xp_decay" => 24, "decay_rate_points_per_hour" => 2 @@ -137,10 +141,12 @@ defmodule CadetWeb.AdminCoursesControllerTest do @tag authenticate: :student test "rejects forbidden request for non-staff users", %{conn: conn} do course_id = conn.assigns[:course_id] - insert(:assessment_config, %{course_id: course_id}) + course = Repo.get(Course, course_id) + type = insert(:assessment_type, %{course: course}) + insert(:assessment_config, %{assessment_type: type}) conn = - put(conn, build_url_assessment_config(course_id), %{ + put(conn, build_url_assessment_config(course_id, type.order), %{ "early_submission_xp" => 100, "hours_before_early_xp_decay" => 24, "decay_rate_points_per_hour" => 2 @@ -152,10 +158,12 @@ defmodule CadetWeb.AdminCoursesControllerTest do @tag authenticate: :staff test "rejects request if user does not belong to specified course", %{conn: conn} do course_id = conn.assigns[:course_id] - insert(:assessment_config, %{course_id: course_id}) + course = Repo.get(Course, course_id) + type = insert(:assessment_type, %{course: course}) + insert(:assessment_config, %{assessment_type: type}) conn = - put(conn, build_url_assessment_config(course_id + 1), %{ + put(conn, build_url_assessment_config(course_id + 1, type.order), %{ "early_submission_xp" => 100, "hours_before_early_xp_decay" => 24, "decay_rate_points_per_hour" => 2 @@ -167,10 +175,12 @@ defmodule CadetWeb.AdminCoursesControllerTest do @tag authenticate: :staff test "rejects requests with invalid params", %{conn: conn} do course_id = conn.assigns[:course_id] - insert(:assessment_config, %{course_id: course_id}) + course = Repo.get(Course, course_id) + type = insert(:assessment_type, %{course: course}) + insert(:assessment_config, %{assessment_type: type}) conn = - put(conn, build_url_assessment_config(course_id), %{ + put(conn, build_url_assessment_config(course_id, type.order), %{ "early_submission_xp" => 100, "hours_before_early_xp_decay" => -1, "decay_rate_points_per_hour" => 200 @@ -182,10 +192,12 @@ defmodule CadetWeb.AdminCoursesControllerTest do @tag authenticate: :staff test "rejects requests with missing params", %{conn: conn} do course_id = conn.assigns[:course_id] - insert(:assessment_config, %{course_id: course_id}) + course = Repo.get(Course, course_id) + type = insert(:assessment_type, %{course: course}) + insert(:assessment_config, %{assessment_type: type}) conn = - put(conn, build_url_assessment_config(course_id), %{ + put(conn, build_url_assessment_config(course_id, type.order), %{ "hours_before_early_xp_decay" => 24, "decay_rate_points_per_hour" => 2 }) @@ -267,8 +279,8 @@ defmodule CadetWeb.AdminCoursesControllerTest do defp build_url_course_config(course_id), do: "/v2/course/#{course_id}/admin/course_config" - defp build_url_assessment_config(course_id), - do: "/v2/course/#{course_id}/admin/assessment_config" + defp build_url_assessment_config(course_id, order), + do: "/v2/course/#{course_id}/admin/assessment_config/#{order}" defp build_url_assessment_types(course_id), do: "/v2/course/#{course_id}/admin/assessment_types" diff --git a/test/cadet_web/controllers/courses_controller_test.exs b/test/cadet_web/controllers/courses_controller_test.exs index f883f8414..80ed1f255 100644 --- a/test/cadet_web/controllers/courses_controller_test.exs +++ b/test/cadet_web/controllers/courses_controller_test.exs @@ -1,6 +1,8 @@ defmodule CadetWeb.CoursesControllerTest do use CadetWeb.ConnCase + alias Cadet.Repo + alias Cadet.Courses.Course alias CadetWeb.CoursesController test "swagger" do @@ -20,10 +22,11 @@ defmodule CadetWeb.CoursesControllerTest do @tag authenticate: :student test "succeeds", %{conn: conn} do course_id = conn.assigns[:course_id] + course = Repo.get(Course, course_id) - insert(:assessment_types, %{order: 1, type: "Missions", course_id: course_id}) - insert(:assessment_types, %{order: 2, type: "Quests", course_id: course_id}) - insert(:assessment_types, %{order: 3, type: "Paths", course_id: course_id}) + insert(:assessment_type, %{order: 1, type: "Missions", course: course}) + insert(:assessment_type, %{order: 2, type: "Quests", course: course}) + insert(:assessment_type, %{order: 3, type: "Paths", course: course}) resp = conn |> get(build_url_config(course_id)) |> json_response(200) diff --git a/test/cadet_web/controllers/user_controller_test.exs b/test/cadet_web/controllers/user_controller_test.exs index 2e1139e81..4424a9444 100644 --- a/test/cadet_web/controllers/user_controller_test.exs +++ b/test/cadet_web/controllers/user_controller_test.exs @@ -59,7 +59,6 @@ defmodule CadetWeb.UserControllerTest do |> get("/v2/user") |> json_response(200) |> put_in(["courseRegistration", "story"], nil) - |> IO.inspect() expected = %{ "user" => %{ @@ -67,7 +66,7 @@ defmodule CadetWeb.UserControllerTest do "name" => user.name, "courses" => [ %{ - "course_id" => user.latest_viewed_id, + "courseId" => user.latest_viewed_id, "moduleCode" => "CS1101S", "name" => "Programming Methodology", "viewable" => true @@ -355,7 +354,6 @@ defmodule CadetWeb.UserControllerTest do |> get("/v2/user/latest_viewed") |> json_response(200) |> put_in(["courseRegistration", "story"], nil) - |> IO.inspect() expected = %{ "courseRegistration" => %{ diff --git a/test/factories/accounts/course_registration_factory.ex b/test/factories/accounts/course_registration_factory.ex index 9e3a1a0f6..e1d75d126 100644 --- a/test/factories/accounts/course_registration_factory.ex +++ b/test/factories/accounts/course_registration_factory.ex @@ -5,7 +5,7 @@ defmodule Cadet.Accounts.CourseRegistrationFactory do defmacro __using__(_opts) do quote do - alias Cadet.Accounts.{Role, User, CourseRegistration} + alias Cadet.Accounts.{Role, CourseRegistration} # alias Cadet.Courses.{Course, Group} def course_registration_factory do diff --git a/test/factories/assessments/assessment_factory.ex b/test/factories/assessments/assessment_factory.ex index ca2b6f648..e615ccfe9 100644 --- a/test/factories/assessments/assessment_factory.ex +++ b/test/factories/assessments/assessment_factory.ex @@ -10,7 +10,7 @@ defmodule Cadet.Assessments.AssessmentFactory do def assessment_factory do # type = Enum.random(Assessment.assessment_types() -- ["practical"]) course = build(:course) - type = build(:assessment_types, %{course: course}) + type = build(:assessment_type, %{course: course}) type_title = type.type # These are actual story identifiers so front-end can use seeds to test more effectively diff --git a/test/factories/courses/assessment_config_factory.ex b/test/factories/courses/assessment_config_factory.ex index 15aac4b91..46832b3e1 100644 --- a/test/factories/courses/assessment_config_factory.ex +++ b/test/factories/courses/assessment_config_factory.ex @@ -11,8 +11,8 @@ defmodule Cadet.Courses.AssessmentConfigFactory do %AssessmentConfig{ early_submission_xp: 200, hours_before_early_xp_decay: 48, - decay_rate_points_per_hour: 1 - # course: build(:course) + decay_rate_points_per_hour: 1, + assessment_type: build(:assessment_type) } end end diff --git a/test/factories/courses/assessment_type_factory.ex b/test/factories/courses/assessment_type_factory.ex new file mode 100644 index 000000000..30530e276 --- /dev/null +++ b/test/factories/courses/assessment_type_factory.ex @@ -0,0 +1,19 @@ +defmodule Cadet.Courses.AssessmentTypeFactory do + @moduledoc """ + Factory for the AssessmentType entity + """ + + defmacro __using__(_opts) do + quote do + alias Cadet.Courses.AssessmentType + + def assessment_type_factory do + %AssessmentType{ + order: 1, + type: "Missions", + course: build(:course) + } + end + end + end +end diff --git a/test/factories/courses/assessment_types_factory.ex b/test/factories/courses/assessment_types_factory.ex deleted file mode 100644 index d11cc455e..000000000 --- a/test/factories/courses/assessment_types_factory.ex +++ /dev/null @@ -1,18 +0,0 @@ -defmodule Cadet.Courses.AssessmentTypesFactory do - @moduledoc """ - Factory for the AssessmentTypes entity - """ - - defmacro __using__(_opts) do - quote do - alias Cadet.Courses.AssessmentTypes - - def assessment_types_factory do - %AssessmentTypes{ - order: 1, - type: "Missions" - } - end - end - end -end diff --git a/test/factories/factory.ex b/test/factories/factory.ex index 5257cdd2c..63c9cfa82 100644 --- a/test/factories/factory.ex +++ b/test/factories/factory.ex @@ -24,7 +24,7 @@ defmodule Cadet.Factory do use Cadet.Courses.{ AssessmentConfigFactory, - AssessmentTypesFactory, + AssessmentTypeFactory, CourseFactory, GroupFactory, SourcecastFactory From 703223cdfa93d889cecb2451b936bad6f59d1a49 Mon Sep 17 00:00:00 2001 From: YaleChen299 Date: Wed, 16 Jun 2021 11:49:24 +0800 Subject: [PATCH 056/174] update_assessment_config: move order from url param to json boday --- .../admin_courses_controller.ex | 4 ++-- lib/cadet_web/router.ex | 2 +- .../admin_courses_controller_test.exs | 23 +++++++++++-------- 3 files changed, 17 insertions(+), 12 deletions(-) diff --git a/lib/cadet_web/admin_controllers/admin_courses_controller.ex b/lib/cadet_web/admin_controllers/admin_courses_controller.ex index a69444093..5ed70fe37 100644 --- a/lib/cadet_web/admin_controllers/admin_courses_controller.ex +++ b/lib/cadet_web/admin_controllers/admin_courses_controller.ex @@ -7,7 +7,7 @@ defmodule CadetWeb.AdminCoursesController do def update_course_config(conn, params = %{"course_id" => course_id}) when is_ecto_id(course_id) do - params = for {key, val} <- params, into: %{}, do: {String.to_atom(key), val} + params = params |> snake_casify_string_keys() |> (&for {key, val} <- &1, into: %{}, do: {String.to_atom(key), val}).() if (Map.has_key?(params, :source_chapter) and Map.has_key?(params, :source_variant)) or (not Map.has_key?(params, :source_chapter) and @@ -109,7 +109,7 @@ defmodule CadetWeb.AdminCoursesController do parameters do course_id(:path, :integer, "Course ID", required: true) - order(:path, :integer, "type order", required: true) + order(:body, :integer, "type order", required: true) early_submission_xp(:body, :integer, "Early submission xp") hours_before_early_xp_decay(:body, :integer, "Hours before early submission xp decay") decay_rate_points_per_hour(:body, :integer, "Decay rate in points per hour") diff --git a/lib/cadet_web/router.ex b/lib/cadet_web/router.ex index b73882dea..1fee647e3 100644 --- a/lib/cadet_web/router.ex +++ b/lib/cadet_web/router.ex @@ -139,7 +139,7 @@ defmodule CadetWeb.Router do delete("/goals/:uuid", AdminGoalsController, :delete) put("/course_config", AdminCoursesController, :update_course_config) - put("/assessment_config/:order", AdminCoursesController, :update_assessment_config) + put("/assessment_config", AdminCoursesController, :update_assessment_config) put("/assessment_types", AdminCoursesController, :update_assessment_types) end diff --git a/test/cadet_web/admin_controllers/admin_courses_controller_test.exs b/test/cadet_web/admin_controllers/admin_courses_controller_test.exs index 56753c235..bebd96d6d 100644 --- a/test/cadet_web/admin_controllers/admin_courses_controller_test.exs +++ b/test/cadet_web/admin_controllers/admin_courses_controller_test.exs @@ -19,8 +19,8 @@ defmodule CadetWeb.AdminCoursesControllerTest do conn = put(conn, build_url_course_config(course_id), %{ - "source_chapter" => Enum.random(1..4), - "source_variant" => "default" + "sourceChapter" => Enum.random(1..4), + "sourceVariant" => "default" }) assert response(conn, 200) == "OK" @@ -129,7 +129,8 @@ defmodule CadetWeb.AdminCoursesControllerTest do insert(:assessment_config, %{assessment_type: type}) conn = - put(conn, build_url_assessment_config(course_id, type.order), %{ + put(conn, build_url_assessment_config(course_id), %{ + "order" => type.order, "early_submission_xp" => 100, "hours_before_early_xp_decay" => 24, "decay_rate_points_per_hour" => 2 @@ -146,7 +147,8 @@ defmodule CadetWeb.AdminCoursesControllerTest do insert(:assessment_config, %{assessment_type: type}) conn = - put(conn, build_url_assessment_config(course_id, type.order), %{ + put(conn, build_url_assessment_config(course_id), %{ + "order" => type.order, "early_submission_xp" => 100, "hours_before_early_xp_decay" => 24, "decay_rate_points_per_hour" => 2 @@ -163,7 +165,8 @@ defmodule CadetWeb.AdminCoursesControllerTest do insert(:assessment_config, %{assessment_type: type}) conn = - put(conn, build_url_assessment_config(course_id + 1, type.order), %{ + put(conn, build_url_assessment_config(course_id + 1), %{ + "order" => type.order, "early_submission_xp" => 100, "hours_before_early_xp_decay" => 24, "decay_rate_points_per_hour" => 2 @@ -180,7 +183,8 @@ defmodule CadetWeb.AdminCoursesControllerTest do insert(:assessment_config, %{assessment_type: type}) conn = - put(conn, build_url_assessment_config(course_id, type.order), %{ + put(conn, build_url_assessment_config(course_id), %{ + "order" => type.order, "early_submission_xp" => 100, "hours_before_early_xp_decay" => -1, "decay_rate_points_per_hour" => 200 @@ -197,7 +201,8 @@ defmodule CadetWeb.AdminCoursesControllerTest do insert(:assessment_config, %{assessment_type: type}) conn = - put(conn, build_url_assessment_config(course_id, type.order), %{ + put(conn, build_url_assessment_config(course_id), %{ + "order" => type.order, "hours_before_early_xp_decay" => 24, "decay_rate_points_per_hour" => 2 }) @@ -279,8 +284,8 @@ defmodule CadetWeb.AdminCoursesControllerTest do defp build_url_course_config(course_id), do: "/v2/course/#{course_id}/admin/course_config" - defp build_url_assessment_config(course_id, order), - do: "/v2/course/#{course_id}/admin/assessment_config/#{order}" + defp build_url_assessment_config(course_id), + do: "/v2/course/#{course_id}/admin/assessment_config" defp build_url_assessment_types(course_id), do: "/v2/course/#{course_id}/admin/assessment_types" From 951e2e7de69f680eaa86a21878f67d7d1e065609 Mon Sep 17 00:00:00 2001 From: YaleChen299 Date: Wed, 16 Jun 2021 12:16:17 +0800 Subject: [PATCH 057/174] upadte courseName and courseShortName --- lib/cadet/accounts/course_registrations.ex | 2 +- lib/cadet_web/views/user_view.ex | 4 +-- .../controllers/user_controller_test.exs | 25 ++++++------------- 3 files changed, 10 insertions(+), 21 deletions(-) diff --git a/lib/cadet/accounts/course_registrations.ex b/lib/cadet/accounts/course_registrations.ex index 17b6b84ff..a21a1e9c8 100644 --- a/lib/cadet/accounts/course_registrations.ex +++ b/lib/cadet/accounts/course_registrations.ex @@ -29,7 +29,7 @@ defmodule Cadet.Accounts.CourseRegistrations do |> where([cr], cr.user_id == ^user_id) |> where([cr], cr.course_id == ^course_id) |> join(:inner, [cr], c in assoc(cr, :course)) - |> join(:inner, [cr, c], t in assoc(c, :assessment_type)) + |> join(:left, [cr, c], t in assoc(c, :assessment_type)) |> preload([cr, c, t],[course: {c, assessment_type: t}]) |> preload(:group) |> Repo.one() diff --git a/lib/cadet_web/views/user_view.ex b/lib/cadet_web/views/user_view.ex index 91ca226e9..00829ca29 100644 --- a/lib/cadet_web/views/user_view.ex +++ b/lib/cadet_web/views/user_view.ex @@ -97,8 +97,8 @@ defmodule CadetWeb.UserView do _ -> transform_map_for_view(latest.course, %{ - moduleName: :name, - moduleCode: :module_code, + courseName: :name, + courseShortName: :module_code, viewable: :viewable, enableGame: :enable_game, enableAchievements: :enable_achievements, diff --git a/test/cadet_web/controllers/user_controller_test.exs b/test/cadet_web/controllers/user_controller_test.exs index 4424a9444..23c97e2cd 100644 --- a/test/cadet_web/controllers/user_controller_test.exs +++ b/test/cadet_web/controllers/user_controller_test.exs @@ -18,6 +18,7 @@ defmodule CadetWeb.UserControllerTest do test "success, student non-story fields", %{conn: conn} do user = conn.assigns.current_user course = user.latest_viewed + insert(:assessment_type, %{type: "test type", course: course}) cr = Repo.get_by(CourseRegistration, course_id: course.id, user_id: user.id) another_cr = insert(:course_registration, %{user: user}) assessment = insert(:assessment, %{is_published: true, course: course}) @@ -90,13 +91,13 @@ defmodule CadetWeb.UserControllerTest do "story" => nil }, "courseConfiguration" => %{ - "assessmentTypes" => nil, + "assessmentTypes" => ["test type"], "enableAchievements" => true, "enableGame" => true, "enableSourcecast" => true, - "moduleCode" => "CS1101S", + "courseShortName" => "CS1101S", "moduleHelpText" => "Help Text", - "moduleName" => "Programming Methodology", + "courseName" => "Programming Methodology", "sourceChapter" => 1, "sourceVariant" => "default", "viewable" => true @@ -367,13 +368,13 @@ defmodule CadetWeb.UserControllerTest do "story" => nil }, "courseConfiguration" => %{ - "assessmentTypes" => nil, + "assessmentTypes" => [], "enableAchievements" => true, "enableGame" => true, "enableSourcecast" => true, - "moduleCode" => "CS1101S", + "courseShortName" => "CS1101S", "moduleHelpText" => "Help Text", - "moduleName" => "Programming Methodology", + "courseName" => "Programming Methodology", "sourceChapter" => 1, "sourceVariant" => "default", "viewable" => true @@ -440,18 +441,6 @@ defmodule CadetWeb.UserControllerTest do assert new_game_states == updated_cr.game_states end - - @tag authenticate: :student - test "success, retrieving student game state", %{conn: conn} do - course_id = conn.assigns.course_id - - resp = - conn - |> get(build_url(course_id)) - |> json_response(200) - - assert %{} == resp["courseRegistration"]["gameStates"] - end end defp build_url(course_id), do: "/v2/course/#{course_id}/user" From 840b20f6f343ee01fe4f39dede6d0f7e35f1423d Mon Sep 17 00:00:00 2001 From: YaleChen299 Date: Wed, 16 Jun 2021 12:42:14 +0800 Subject: [PATCH 058/174] update assessment_type test in user controller --- lib/cadet/accounts/course_registrations.ex | 5 ++++- lib/cadet/courses/courses.ex | 2 +- .../admin_controllers/admin_courses_controller.ex | 13 +++++++++++-- lib/cadet_web/views/assessments_view.ex | 2 +- lib/cadet_web/views/courses_view.ex | 4 ++-- lib/cadet_web/views/user_view.ex | 2 +- priv/repo/seeds.exs | 5 ++++- test/cadet/courses/courses_test.exs | 15 +++++---------- .../controllers/courses_controller_test.exs | 6 +++--- .../controllers/user_controller_test.exs | 6 ++++-- 10 files changed, 36 insertions(+), 24 deletions(-) diff --git a/lib/cadet/accounts/course_registrations.ex b/lib/cadet/accounts/course_registrations.ex index a21a1e9c8..b7b6b70c3 100644 --- a/lib/cadet/accounts/course_registrations.ex +++ b/lib/cadet/accounts/course_registrations.ex @@ -8,6 +8,7 @@ defmodule Cadet.Accounts.CourseRegistrations do alias Cadet.Repo alias Cadet.Accounts.{User, CourseRegistration} + alias Cadet.Courses.AssessmentType # guide # only join with User if need name or user name @@ -30,7 +31,9 @@ defmodule Cadet.Accounts.CourseRegistrations do |> where([cr], cr.course_id == ^course_id) |> join(:inner, [cr], c in assoc(cr, :course)) |> join(:left, [cr, c], t in assoc(c, :assessment_type)) - |> preload([cr, c, t],[course: {c, assessment_type: t}]) + |> preload([cr, c, t], + course: {c, assessment_type: ^from(t in AssessmentType, order_by: [asc: t.order])} + ) |> preload(:group) |> Repo.one() end diff --git a/lib/cadet/courses/courses.ex b/lib/cadet/courses/courses.ex index 69f33444f..c70018b58 100644 --- a/lib/cadet/courses/courses.ex +++ b/lib/cadet/courses/courses.ex @@ -71,7 +71,7 @@ defmodule Cadet.Courses do def update_assessment_config(course_id, order, early_xp, hours_before_decay, decay_rate) do AssessmentConfig |> join(:inner, [ac], at in AssessmentType, on: at.order == ^order) - |> where([ac, at], at.course_id == ^ course_id) + |> where([ac, at], at.course_id == ^course_id) |> Repo.one() |> AssessmentConfig.changeset(%{ early_submission_xp: early_xp, diff --git a/lib/cadet_web/admin_controllers/admin_courses_controller.ex b/lib/cadet_web/admin_controllers/admin_courses_controller.ex index 5ed70fe37..64a955029 100644 --- a/lib/cadet_web/admin_controllers/admin_courses_controller.ex +++ b/lib/cadet_web/admin_controllers/admin_courses_controller.ex @@ -7,7 +7,10 @@ defmodule CadetWeb.AdminCoursesController do def update_course_config(conn, params = %{"course_id" => course_id}) when is_ecto_id(course_id) do - params = params |> snake_casify_string_keys() |> (&for {key, val} <- &1, into: %{}, do: {String.to_atom(key), val}).() + params = + params + |> snake_casify_string_keys() + |> (&for({key, val} <- &1, into: %{}, do: {String.to_atom(key), val})).() if (Map.has_key?(params, :source_chapter) and Map.has_key?(params, :source_variant)) or (not Map.has_key?(params, :source_chapter) and @@ -37,7 +40,13 @@ defmodule CadetWeb.AdminCoursesController do "decay_rate_points_per_hour" => decay_rate }) when is_ecto_id(course_id) do - case Courses.update_assessment_config(course_id, order, early_xp, hours_before_decay, decay_rate) do + case Courses.update_assessment_config( + course_id, + order, + early_xp, + hours_before_decay, + decay_rate + ) do {:ok, _} -> text(conn, "OK") diff --git a/lib/cadet_web/views/assessments_view.ex b/lib/cadet_web/views/assessments_view.ex index ffd16be1f..437430607 100644 --- a/lib/cadet_web/views/assessments_view.ex +++ b/lib/cadet_web/views/assessments_view.ex @@ -16,7 +16,7 @@ defmodule CadetWeb.AssessmentsView do shortSummary: :summary_short, openAt: &format_datetime(&1.open_at), closeAt: &format_datetime(&1.close_at), - type: &(&1.type.type), + type: & &1.type.type, story: :story, number: :number, reading: :reading, diff --git a/lib/cadet_web/views/courses_view.ex b/lib/cadet_web/views/courses_view.ex index ad32af398..ae3809e71 100644 --- a/lib/cadet_web/views/courses_view.ex +++ b/lib/cadet_web/views/courses_view.ex @@ -5,8 +5,8 @@ defmodule CadetWeb.CoursesView do %{ config: transform_map_for_view(config, %{ - moduleName: :name, - moduleCode: :module_code, + courseName: :name, + courseShortName: :module_code, viewable: :viewable, enableGame: :enable_game, enableAchievements: :enable_achievements, diff --git a/lib/cadet_web/views/user_view.ex b/lib/cadet_web/views/user_view.ex index 00829ca29..91eae696f 100644 --- a/lib/cadet_web/views/user_view.ex +++ b/lib/cadet_web/views/user_view.ex @@ -106,7 +106,7 @@ defmodule CadetWeb.UserView do sourceChapter: :source_chapter, sourceVariant: :source_variant, moduleHelpText: :module_help_text, - assessmentTypes: &(Enum.map(&1.assessment_type, fn x -> x.type end)) + assessmentTypes: &Enum.map(&1.assessment_type, fn x -> x.type end) }) end end diff --git a/priv/repo/seeds.exs b/priv/repo/seeds.exs index 2bfa7d50c..82c19c22f 100644 --- a/priv/repo/seeds.exs +++ b/priv/repo/seeds.exs @@ -24,7 +24,10 @@ if Cadet.Env.env() == :dev do avenger1 = insert(:user, %{name: "avenger", username: "E1234561", latest_viewed: course1}) mentor1 = insert(:user, %{name: "mentor", username: "E1234562", latest_viewed: course1}) admin1 = insert(:user, %{name: "admin", username: "E1234563", latest_viewed: course1}) - studenta1admin2 = insert(:user, %{name: "student a", username: "E1234564", latest_viewed: course1}) + + studenta1admin2 = + insert(:user, %{name: "student a", username: "E1234564", latest_viewed: course1}) + studentb1 = insert(:user, %{username: "E1234565", latest_viewed: course1}) studentc1 = insert(:user, %{username: "E1234566", latest_viewed: course1}) # CourseRegistration and Group diff --git a/test/cadet/courses/courses_test.exs b/test/cadet/courses/courses_test.exs index fe41305f3..bf8dfa20f 100644 --- a/test/cadet/courses/courses_test.exs +++ b/test/cadet/courses/courses_test.exs @@ -120,8 +120,7 @@ defmodule Cadet.CoursesTest do type = insert(:assessment_type, %{course: course}) _assessment_config = insert(:assessment_config, %{assessment_type: type}) - {:ok, updated_config} = - Courses.update_assessment_config(course.id, type.order, 100, 24, 1) + {:ok, updated_config} = Courses.update_assessment_config(course.id, type.order, 100, 24, 1) assert updated_config.early_submission_xp == 100 assert updated_config.hours_before_early_xp_decay == 24 @@ -133,25 +132,21 @@ defmodule Cadet.CoursesTest do type = insert(:assessment_type, %{course: course}) _assessment_config = insert(:assessment_config, %{assessment_type: type}) - {:error, changeset} = - Courses.update_assessment_config(course.id, type.order, -1, 0, 0) + {:error, changeset} = Courses.update_assessment_config(course.id, type.order, -1, 0, 0) assert %{early_submission_xp: ["must be greater than or equal to 0"]} = errors_on(changeset) - {:error, changeset} = - Courses.update_assessment_config(course.id, type.order, 200, -1, 0) + {:error, changeset} = Courses.update_assessment_config(course.id, type.order, 200, -1, 0) assert %{hours_before_early_xp_decay: ["must be greater than or equal to 0"]} = errors_on(changeset) - {:error, changeset} = - Courses.update_assessment_config(course.id, type.order, 200, 48, -1) + {:error, changeset} = Courses.update_assessment_config(course.id, type.order, 200, 48, -1) assert %{decay_rate_points_per_hour: ["must be greater than or equal to 0"]} = errors_on(changeset) - {:error, changeset} = - Courses.update_assessment_config(course.id, type.order, 200, 48, 300) + {:error, changeset} = Courses.update_assessment_config(course.id, type.order, 200, 48, 300) assert %{decay_rate_points_per_hour: ["must be less than or equal to 200"]} = errors_on(changeset) diff --git a/test/cadet_web/controllers/courses_controller_test.exs b/test/cadet_web/controllers/courses_controller_test.exs index 80ed1f255..1b4587719 100644 --- a/test/cadet_web/controllers/courses_controller_test.exs +++ b/test/cadet_web/controllers/courses_controller_test.exs @@ -24,16 +24,16 @@ defmodule CadetWeb.CoursesControllerTest do course_id = conn.assigns[:course_id] course = Repo.get(Course, course_id) + insert(:assessment_type, %{order: 3, type: "Paths", course: course}) insert(:assessment_type, %{order: 1, type: "Missions", course: course}) insert(:assessment_type, %{order: 2, type: "Quests", course: course}) - insert(:assessment_type, %{order: 3, type: "Paths", course: course}) resp = conn |> get(build_url_config(course_id)) |> json_response(200) assert %{ "config" => %{ - "moduleName" => "Programming Methodology", - "moduleCode" => "CS1101S", + "courseName" => "Programming Methodology", + "courseShortName" => "CS1101S", "viewable" => true, "enableGame" => true, "enableAchievements" => true, diff --git a/test/cadet_web/controllers/user_controller_test.exs b/test/cadet_web/controllers/user_controller_test.exs index 23c97e2cd..525e3c3c5 100644 --- a/test/cadet_web/controllers/user_controller_test.exs +++ b/test/cadet_web/controllers/user_controller_test.exs @@ -18,7 +18,9 @@ defmodule CadetWeb.UserControllerTest do test "success, student non-story fields", %{conn: conn} do user = conn.assigns.current_user course = user.latest_viewed - insert(:assessment_type, %{type: "test type", course: course}) + insert(:assessment_type, %{order: 2, type: "test type 2", course: course}) + insert(:assessment_type, %{order: 3, type: "test type 3", course: course}) + insert(:assessment_type, %{order: 1, type: "test type 1", course: course}) cr = Repo.get_by(CourseRegistration, course_id: course.id, user_id: user.id) another_cr = insert(:course_registration, %{user: user}) assessment = insert(:assessment, %{is_published: true, course: course}) @@ -91,7 +93,7 @@ defmodule CadetWeb.UserControllerTest do "story" => nil }, "courseConfiguration" => %{ - "assessmentTypes" => ["test type"], + "assessmentTypes" => ["test type 1", "test type 2", "test type 3"], "enableAchievements" => true, "enableGame" => true, "enableSourcecast" => true, From 9f2e174382cf7f37e7dafb26f469243e94027d56 Mon Sep 17 00:00:00 2001 From: YaleChen299 Date: Wed, 16 Jun 2021 17:25:36 +0800 Subject: [PATCH 059/174] update seed --- priv/repo/seeds.exs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/priv/repo/seeds.exs b/priv/repo/seeds.exs index 82c19c22f..9e72ec0aa 100644 --- a/priv/repo/seeds.exs +++ b/priv/repo/seeds.exs @@ -57,7 +57,7 @@ if Cadet.Env.env() == :dev do # Assessments for i <- 1..5 do type = insert(:assessment_type, %{type: "mission#{i}", order: i, course: course1}) - assessment = insert(:assessment, %{is_published: true, type: type}) + assessment = insert(:assessment, %{is_published: true, type: type, course: course1}) programming_questions = insert_list(3, :programming_question, %{ From 3173dd159d4b93920450c0de34abe6be0f41880f Mon Sep 17 00:00:00 2001 From: YaleChen299 Date: Wed, 16 Jun 2021 19:29:59 +0800 Subject: [PATCH 060/174] update admin controller test to test updated result and fix course field names --- lib/cadet/courses/course.ex | 6 +- lib/cadet/helpers/shared_helper.ex | 6 + .../admin_courses_controller.ex | 15 +- .../controllers/courses_controller.ex | 8 +- lib/cadet_web/controllers/user_controller.ex | 8 +- lib/cadet_web/views/courses_view.ex | 4 +- lib/cadet_web/views/user_view.ex | 8 +- ...0210531155751_add_course_configuration.exs | 4 +- priv/repo/seeds.exs | 2 +- test/cadet/accounts/accounts_test.exs | 4 +- .../accounts/course_registration_test.exs | 4 +- test/cadet/assessments/assessment_test.exs | 4 +- test/cadet/courses/course_test.exs | 4 +- test/cadet/courses/courses_test.exs | 20 +- .../admin_courses_controller_test.exs | 193 ++++++++++++------ test/factories/courses/course_factory.ex | 4 +- 16 files changed, 178 insertions(+), 116 deletions(-) diff --git a/lib/cadet/courses/course.ex b/lib/cadet/courses/course.ex index 67f71221d..d0191f4ef 100644 --- a/lib/cadet/courses/course.ex +++ b/lib/cadet/courses/course.ex @@ -7,8 +7,8 @@ defmodule Cadet.Courses.Course do alias Cadet.Courses.AssessmentType schema "courses" do - field(:name, :string) - field(:module_code, :string) + field(:course_name, :string) + field(:course_short_name, :string) field(:viewable, :boolean, default: true) field(:enable_game, :boolean, default: true) field(:enable_achievements, :boolean, default: true) @@ -23,7 +23,7 @@ defmodule Cadet.Courses.Course do end @required_fields ~w(source_chapter source_variant)a - @optional_fields ~w(name module_code viewable enable_game enable_achievements enable_sourcecast module_help_text)a + @optional_fields ~w(course_name course_short_name viewable enable_game enable_achievements enable_sourcecast module_help_text)a def changeset(course, params) do if Map.has_key?(params, :source_chapter) or Map.has_key?(params, :source_variant) do diff --git a/lib/cadet/helpers/shared_helper.ex b/lib/cadet/helpers/shared_helper.ex index b1c7e622a..0c20d7fcf 100644 --- a/lib/cadet/helpers/shared_helper.ex +++ b/lib/cadet/helpers/shared_helper.ex @@ -32,6 +32,12 @@ defmodule Cadet.SharedHelper do do: {if(is_binary(key), do: Recase.to_snake(key), else: key), val} end + def to_snake_case_atom_keys(map = %{}) do + map + |> snake_casify_string_keys() + |> (&for({key, val} <- &1, into: %{}, do: {String.to_atom(key), val})).() + end + @doc """ Snake-casifies string keys, recursively. diff --git a/lib/cadet_web/admin_controllers/admin_courses_controller.ex b/lib/cadet_web/admin_controllers/admin_courses_controller.ex index 64a955029..aa897d1bd 100644 --- a/lib/cadet_web/admin_controllers/admin_courses_controller.ex +++ b/lib/cadet_web/admin_controllers/admin_courses_controller.ex @@ -7,10 +7,7 @@ defmodule CadetWeb.AdminCoursesController do def update_course_config(conn, params = %{"course_id" => course_id}) when is_ecto_id(course_id) do - params = - params - |> snake_casify_string_keys() - |> (&for({key, val} <- &1, into: %{}, do: {String.to_atom(key), val})).() + params = params |> to_snake_case_atom_keys() if (Map.has_key?(params, :source_chapter) and Map.has_key?(params, :source_variant)) or (not Map.has_key?(params, :source_chapter) and @@ -35,9 +32,9 @@ defmodule CadetWeb.AdminCoursesController do def update_assessment_config(conn, %{ "course_id" => course_id, "order" => order, - "early_submission_xp" => early_xp, - "hours_before_early_xp_decay" => hours_before_decay, - "decay_rate_points_per_hour" => decay_rate + "earlySubmissionXp" => early_xp, + "hoursBeforeEarlyXpDecay" => hours_before_decay, + "decayRatePointsPerHour" => decay_rate }) when is_ecto_id(course_id) do case Courses.update_assessment_config( @@ -92,8 +89,8 @@ defmodule CadetWeb.AdminCoursesController do parameters do course_id(:path, :integer, "Course ID", required: true) - name(:body, :string, "Course name") - module_code(:body, :string, "Course module code") + course_name(:body, :string, "Course name") + course_short_name(:body, :string, "Course module code") viewable(:body, :boolean, "Course viewability") enable_game(:body, :boolean, "Enable game") enable_achievements(:body, :boolean, "Enable achievements") diff --git a/lib/cadet_web/controllers/courses_controller.ex b/lib/cadet_web/controllers/courses_controller.ex index 2d7d2fdad..b20f9a415 100644 --- a/lib/cadet_web/controllers/courses_controller.ex +++ b/lib/cadet_web/controllers/courses_controller.ex @@ -40,8 +40,8 @@ defmodule CadetWeb.CoursesController do title("Course Configuration") properties do - name(:string, "Course name", required: true) - module_code(:string, "Course module code", required: true) + course_name(:string, "Course name", required: true) + course_short_name(:string, "Course module code", required: true) viewable(:boolean, "Course viewability", required: true) enable_game(:boolean, "Enable game", required: true) enable_achievements(:boolean, "Enable achievements", required: true) @@ -53,8 +53,8 @@ defmodule CadetWeb.CoursesController do end example(%{ - name: "Programming Methodology", - module_code: "CS1101S", + course_name: "Programming Methodology", + course_short_name: "CS1101S", viewable: true, enable_game: true, enable_achievements: true, diff --git a/lib/cadet_web/controllers/user_controller.ex b/lib/cadet_web/controllers/user_controller.ex index 94e127cce..aea44dbb3 100644 --- a/lib/cadet_web/controllers/user_controller.ex +++ b/lib/cadet_web/controllers/user_controller.ex @@ -267,8 +267,8 @@ defmodule CadetWeb.UserController do title("Course Configuration") properties do - name(:string, "Course name", required: true) - module_code(:string, "Course module code", required: true) + course_name(:string, "Course name", required: true) + course_short_name(:string, "Course module code", required: true) viewable(:boolean, "Course viewability", required: true) enable_game(:boolean, "Enable game", required: true) enable_achievements(:boolean, "Enable achievements", required: true) @@ -280,8 +280,8 @@ defmodule CadetWeb.UserController do end example(%{ - name: "Programming Methodology", - module_code: "CS1101S", + course_name: "Programming Methodology", + course_short_name: "CS1101S", viewable: true, enable_game: true, enable_achievements: true, diff --git a/lib/cadet_web/views/courses_view.ex b/lib/cadet_web/views/courses_view.ex index ae3809e71..aa1814141 100644 --- a/lib/cadet_web/views/courses_view.ex +++ b/lib/cadet_web/views/courses_view.ex @@ -5,8 +5,8 @@ defmodule CadetWeb.CoursesView do %{ config: transform_map_for_view(config, %{ - courseName: :name, - courseShortName: :module_code, + courseName: :course_name, + courseShortName: :course_short_name, viewable: :viewable, enableGame: :enable_game, enableAchievements: :enable_achievements, diff --git a/lib/cadet_web/views/user_view.ex b/lib/cadet_web/views/user_view.ex index 91eae696f..7812d1c0f 100644 --- a/lib/cadet_web/views/user_view.ex +++ b/lib/cadet_web/views/user_view.ex @@ -51,8 +51,8 @@ defmodule CadetWeb.UserView do def render("course.json", %{cr: cr}) do %{ courseId: cr.course_id, - name: cr.course.name, - moduleCode: cr.course.module_code, + name: cr.course.course_name, + moduleCode: cr.course.course_short_name, viewable: cr.course.viewable } end @@ -97,8 +97,8 @@ defmodule CadetWeb.UserView do _ -> transform_map_for_view(latest.course, %{ - courseName: :name, - courseShortName: :module_code, + courseName: :course_name, + courseShortName: :course_short_name, viewable: :viewable, enableGame: :enable_game, enableAchievements: :enable_achievements, diff --git a/priv/repo/migrations/20210531155751_add_course_configuration.exs b/priv/repo/migrations/20210531155751_add_course_configuration.exs index 616fc4d4b..ec4f28cea 100644 --- a/priv/repo/migrations/20210531155751_add_course_configuration.exs +++ b/priv/repo/migrations/20210531155751_add_course_configuration.exs @@ -11,8 +11,8 @@ defmodule Cadet.Repo.Migrations.AddCourseConfiguration do end create table(:courses) do - add(:name, :string, null: false) - add(:module_code, :string) + add(:course_name, :string, null: false) + add(:course_short_name, :string) add(:viewable, :boolean, null: false, default: true) add(:enable_game, :boolean, null: false, default: true) add(:enable_achievements, :boolean, null: false, default: true) diff --git a/priv/repo/seeds.exs b/priv/repo/seeds.exs index 9e72ec0aa..c4c76c9b0 100644 --- a/priv/repo/seeds.exs +++ b/priv/repo/seeds.exs @@ -19,7 +19,7 @@ alias Cadet.Assessments.SubmissionStatus if Cadet.Env.env() == :dev do # Course course1 = insert(:course) - course2 = insert(:course, %{name: "Algorithm", module_code: "CS2040S"}) + course2 = insert(:course, %{course_name: "Algorithm", course_short_name: "CS2040S"}) # Users avenger1 = insert(:user, %{name: "avenger", username: "E1234561", latest_viewed: course1}) mentor1 = insert(:user, %{name: "mentor", username: "E1234562", latest_viewed: course1}) diff --git a/test/cadet/accounts/accounts_test.exs b/test/cadet/accounts/accounts_test.exs index 2d79ce213..aa75438b5 100644 --- a/test/cadet/accounts/accounts_test.exs +++ b/test/cadet/accounts/accounts_test.exs @@ -109,8 +109,8 @@ defmodule Cadet.AccountsTest do describe "get_users_by" do setup do - c1 = insert(:course, %{name: "c1"}) - c2 = insert(:course, %{name: "c2"}) + c1 = insert(:course, %{course_name: "c1"}) + c2 = insert(:course, %{course_name: "c2"}) admin1 = insert(:course_registration, %{course: c1, role: :admin}) admin2 = insert(:course_registration, %{course: c2, role: :admin}) g1 = insert(:group, %{course: c1}) diff --git a/test/cadet/accounts/course_registration_test.exs b/test/cadet/accounts/course_registration_test.exs index 7d4845940..15ef20ac7 100644 --- a/test/cadet/accounts/course_registration_test.exs +++ b/test/cadet/accounts/course_registration_test.exs @@ -10,8 +10,8 @@ defmodule Cadet.Accounts.CourseRegistrationTest do user2 = insert(:user, %{name: "user 2"}) group1 = insert(:group, %{name: "group 1"}) group2 = insert(:group, %{name: "group 2"}) - course1 = insert(:course, %{module_code: "course 1"}) - course2 = insert(:course, %{module_code: "course 2"}) + course1 = insert(:course, %{course_short_name: "course 1"}) + course2 = insert(:course, %{course_short_name: "course 2"}) changeset = CourseRegistration.changeset(%CourseRegistration{}, %{ diff --git a/test/cadet/assessments/assessment_test.exs b/test/cadet/assessments/assessment_test.exs index 547df4df0..bb6c67083 100644 --- a/test/cadet/assessments/assessment_test.exs +++ b/test/cadet/assessments/assessment_test.exs @@ -4,8 +4,8 @@ defmodule Cadet.Assessments.AssessmentTest do use Cadet.ChangesetCase, entity: Assessment setup do - course1 = insert(:course, %{module_code: "course 1"}) - course2 = insert(:course, %{module_code: "course 2"}) + course1 = insert(:course, %{course_short_name: "course 1"}) + course2 = insert(:course, %{course_short_name: "course 2"}) type1 = insert(:assessment_type, %{course: course1}) type2 = insert(:assessment_type, %{course: course2}) diff --git a/test/cadet/courses/course_test.exs b/test/cadet/courses/course_test.exs index 2e47d8699..043e6e71e 100644 --- a/test/cadet/courses/course_test.exs +++ b/test/cadet/courses/course_test.exs @@ -5,8 +5,8 @@ defmodule Cadet.Courses.CourseTest do describe "Course Configuration Changesets" do test "valid changesets" do - assert_changeset(%{name: "Data Structures and Algorithms"}, :valid) - assert_changeset(%{module_code: "CS2040S"}, :valid) + assert_changeset(%{course_name: "Data Structures and Algorithms"}, :valid) + assert_changeset(%{course_short_name: "CS2040S"}, :valid) assert_changeset(%{viewable: false}, :valid) assert_changeset(%{enable_game: false}, :valid) assert_changeset(%{enable_achievements: false}, :valid) diff --git a/test/cadet/courses/courses_test.exs b/test/cadet/courses/courses_test.exs index bf8dfa20f..ed706c558 100644 --- a/test/cadet/courses/courses_test.exs +++ b/test/cadet/courses/courses_test.exs @@ -11,8 +11,8 @@ defmodule Cadet.CoursesTest do insert(:assessment_type, %{order: 2, type: "Quests", course: course}) {:ok, course} = Courses.get_course_config(course.id) - assert course.name == "Programming Methodology" - assert course.module_code == "CS1101S" + assert course.course_name == "Programming Methodology" + assert course.course_short_name == "CS1101S" assert course.viewable == true assert course.enable_game == true assert course.enable_achievements == true @@ -37,8 +37,8 @@ defmodule Cadet.CoursesTest do {:ok, updated_course} = Courses.update_course_config(course.id, %{ - name: "Data Structures and Algorithms", - module_code: "CS2040S", + course_name: "Data Structures and Algorithms", + course_short_name: "CS2040S", viewable: false, enable_game: false, enable_achievements: false, @@ -46,8 +46,8 @@ defmodule Cadet.CoursesTest do module_help_text: "" }) - assert updated_course.name == "Data Structures and Algorithms" - assert updated_course.module_code == "CS2040S" + assert updated_course.course_name == "Data Structures and Algorithms" + assert updated_course.course_short_name == "CS2040S" assert updated_course.viewable == false assert updated_course.enable_game == false assert updated_course.enable_achievements == false @@ -63,8 +63,8 @@ defmodule Cadet.CoursesTest do {:ok, updated_course} = Courses.update_course_config(course.id, %{ - name: "Data Structures and Algorithms", - module_code: "CS2040S", + course_name: "Data Structures and Algorithms", + course_short_name: "CS2040S", viewable: false, enable_game: false, enable_achievements: false, @@ -74,8 +74,8 @@ defmodule Cadet.CoursesTest do module_help_text: "help" }) - assert updated_course.name == "Data Structures and Algorithms" - assert updated_course.module_code == "CS2040S" + assert updated_course.course_name == "Data Structures and Algorithms" + assert updated_course.course_short_name == "CS2040S" assert updated_course.viewable == false assert updated_course.enable_game == false assert updated_course.enable_achievements == false diff --git a/test/cadet_web/admin_controllers/admin_courses_controller_test.exs b/test/cadet_web/admin_controllers/admin_courses_controller_test.exs index bebd96d6d..b16f5fd15 100644 --- a/test/cadet_web/admin_controllers/admin_courses_controller_test.exs +++ b/test/cadet_web/admin_controllers/admin_courses_controller_test.exs @@ -1,8 +1,10 @@ defmodule CadetWeb.AdminCoursesControllerTest do use CadetWeb.ConnCase + import Cadet.SharedHelper + import Ecto.Query alias Cadet.Repo - alias Cadet.Courses.Course + alias Cadet.Courses.{Course, AssessmentConfig, AssessmentType} alias CadetWeb.AdminCoursesController test "swagger" do @@ -16,63 +18,88 @@ defmodule CadetWeb.AdminCoursesControllerTest do @tag authenticate: :admin test "succeeds 1", %{conn: conn} do course_id = conn.assigns[:course_id] + old_course = Map.from_struct(Repo.get(Course, course_id)) - conn = - put(conn, build_url_course_config(course_id), %{ - "sourceChapter" => Enum.random(1..4), - "sourceVariant" => "default" - }) + params = %{ + "sourceChapter" => 2, + "sourceVariant" => "lazy" + } - assert response(conn, 200) == "OK" + resp = put(conn, build_url_course_config(course_id), params) + + assert response(resp, 200) == "OK" + updated_course = Map.from_struct(Repo.get(Course, course_id)) + refute old_course == updated_course + + assert Map.merge(old_course, to_snake_case_atom_keys(params), fn _k, _v1, v2 -> v2 end) == + updated_course end @tag authenticate: :admin test "succeeds 2", %{conn: conn} do course_id = conn.assigns[:course_id] - - conn = - put(conn, build_url_course_config(course_id), %{ - "name" => "Data Structures and Algorithms", - "module_code" => "CS2040S", - "enable_game" => false, - "enable_achievements" => false, - "enable_sourcecast" => true, - "source_chapter" => Enum.random(1..4), - "source_variant" => "default", - "module_help_text" => "help" - }) - - assert response(conn, 200) == "OK" + old_course = Map.from_struct(Repo.get(Course, course_id)) + + params = %{ + "courseName" => "Data Structures and Algorithms", + "courseShortName" => "CS2040S", + "enableGame" => false, + "enableAchievements" => false, + "enableSourcecast" => true, + "sourceChapter" => 1, + "sourceVariant" => "default", + "moduleHelpText" => "help" + } + + resp = put(conn, build_url_course_config(course_id), params) + + assert response(resp, 200) == "OK" + updated_course = Map.from_struct(Repo.get(Course, course_id)) + refute old_course == updated_course + + assert Map.merge(old_course, to_snake_case_atom_keys(params), fn _k, _v1, v2 -> v2 end) == + updated_course end @tag authenticate: :admin test "succeeds 3", %{conn: conn} do course_id = conn.assigns[:course_id] + old_course = Map.from_struct(Repo.get(Course, course_id)) - conn = - put(conn, build_url_course_config(course_id), %{ - "name" => "Data Structures and Algorithms", - "module_code" => "CS2040S", - "enable_game" => false, - "enable_achievements" => false, - "enable_sourcecast" => true, - "module_help_text" => "help" - }) + params = %{ + "courseName" => "Data Structures and Algorithms", + "courseShortName" => "CS2040S", + "enableGame" => false, + "enableAchievements" => false, + "enableSourcecast" => true, + "moduleHelpText" => "help" + } - assert response(conn, 200) == "OK" + resp = put(conn, build_url_course_config(course_id), params) + + assert response(resp, 200) == "OK" + updated_course = Map.from_struct(Repo.get(Course, course_id)) + refute old_course == updated_course + + assert Map.merge(old_course, to_snake_case_atom_keys(params), fn _k, _v1, v2 -> v2 end) == + updated_course end @tag authenticate: :student test "rejects forbidden request for non-staff users", %{conn: conn} do course_id = conn.assigns[:course_id] + old_course = Repo.get(Course, course_id) conn = put(conn, build_url_course_config(course_id), %{ - "source_chapter" => 3, - "source_variant" => "concurrent" + "sourceChapter" => 3, + "sourceVariant" => "concurrent" }) + same_course = Repo.get(Course, course_id) + assert response(conn, 403) == "Forbidden" + assert old_course == same_course end @tag authenticate: :staff @@ -81,8 +108,8 @@ defmodule CadetWeb.AdminCoursesControllerTest do conn = put(conn, build_url_course_config(course_id + 1), %{ - "source_chapter" => 3, - "source_variant" => "concurrent" + "sourceChapter" => 3, + "sourceVariant" => "concurrent" }) assert response(conn, 403) == "Forbidden" @@ -94,8 +121,8 @@ defmodule CadetWeb.AdminCoursesControllerTest do conn = put(conn, build_url_course_config(course_id), %{ - "source_chapter" => 4, - "source_variant" => "wasm" + "sourceChapter" => 4, + "sourceVariant" => "wasm" }) assert response(conn, 400) == "Invalid parameter(s)" @@ -107,13 +134,13 @@ defmodule CadetWeb.AdminCoursesControllerTest do conn = put(conn, build_url_course_config(course_id), %{ - "name" => "Data Structures and Algorithms", - "module_code" => "CS2040S", - "enable_game" => false, - "enable_achievements" => false, - "enable_sourcecast" => true, - "module_help_text" => "help", - "source_variant" => "default" + "courseName" => "Data Structures and Algorithms", + "courseShortName" => "CS2040S", + "enableGame" => false, + "enableAchievements" => false, + "enableSourcecast" => true, + "moduleHelpText" => "help", + "sourceVariant" => "default" }) assert response(conn, 400) == "Missing parameter(s)" @@ -125,18 +152,23 @@ defmodule CadetWeb.AdminCoursesControllerTest do test "succeeds", %{conn: conn} do course_id = conn.assigns[:course_id] course = Repo.get(Course, course_id) - type = insert(:assessment_type, %{course: course}) - insert(:assessment_config, %{assessment_type: type}) - - conn = - put(conn, build_url_assessment_config(course_id), %{ - "order" => type.order, - "early_submission_xp" => 100, - "hours_before_early_xp_decay" => 24, - "decay_rate_points_per_hour" => 2 - }) - - assert response(conn, 200) == "OK" + type = insert(:assessment_type, %{course: course, order: 2}) + old_config = insert(:assessment_config, %{assessment_type: type}) + + params = %{ + "order" => type.order, + "earlySubmissionXp" => 100, + "hoursBeforeEarlyXpDecay" => 24, + "decayRatePointsPerHour" => 2 + } + + resp = put(conn, build_url_assessment_config(course_id), params) + + assert response(resp, 200) == "OK" + updated_config = Repo.get(AssessmentConfig, old_config.id) + assert updated_config.decay_rate_points_per_hour == 2 + assert updated_config.early_submission_xp == 100 + assert updated_config.hours_before_early_xp_decay == 24 end @tag authenticate: :student @@ -149,9 +181,9 @@ defmodule CadetWeb.AdminCoursesControllerTest do conn = put(conn, build_url_assessment_config(course_id), %{ "order" => type.order, - "early_submission_xp" => 100, - "hours_before_early_xp_decay" => 24, - "decay_rate_points_per_hour" => 2 + "earlySubmissionXp" => 100, + "hoursBeforeEarlyXpDecay" => 24, + "decayRatePointsPerHour" => 2 }) assert response(conn, 403) == "Forbidden" @@ -167,9 +199,9 @@ defmodule CadetWeb.AdminCoursesControllerTest do conn = put(conn, build_url_assessment_config(course_id + 1), %{ "order" => type.order, - "early_submission_xp" => 100, - "hours_before_early_xp_decay" => 24, - "decay_rate_points_per_hour" => 2 + "earlySubmissionXp" => 100, + "hoursBeforeEarlyXpDecay" => 24, + "decayRatePointsPerHour" => 2 }) assert response(conn, 403) == "Forbidden" @@ -185,9 +217,9 @@ defmodule CadetWeb.AdminCoursesControllerTest do conn = put(conn, build_url_assessment_config(course_id), %{ "order" => type.order, - "early_submission_xp" => 100, - "hours_before_early_xp_decay" => -1, - "decay_rate_points_per_hour" => 200 + "earlySubmissionXp" => 100, + "hoursBeforeEarlyXpDecay" => -1, + "decayRatePointsPerHour" => 200 }) assert response(conn, 400) == "Invalid parameter(s)" @@ -203,8 +235,8 @@ defmodule CadetWeb.AdminCoursesControllerTest do conn = put(conn, build_url_assessment_config(course_id), %{ "order" => type.order, - "hours_before_early_xp_decay" => 24, - "decay_rate_points_per_hour" => 2 + "hoursBeforeEarlyXpDecay" => 24, + "decayRatePointsPerHour" => 2 }) assert response(conn, 400) == "Missing parameter(s)" @@ -215,13 +247,40 @@ defmodule CadetWeb.AdminCoursesControllerTest do @tag authenticate: :admin test "succeeds", %{conn: conn} do course_id = conn.assigns[:course_id] + insert(:assessment_type, %{course: Repo.get(Course, course_id)}) + + old_course = + Course + |> where(id: ^course_id) + |> join(:left, [c], at in assoc(c, :assessment_type)) + |> preload([c, at], + assessment_type: ^from(at in AssessmentType, order_by: [asc: at.order]) + ) + |> Repo.all() + |> hd() + + old_types = Enum.map(old_course.assessment_type, fn x -> x.type end) |> IO.inspect() conn = put(conn, build_url_assessment_types(course_id), %{ "assessment_types" => ["Missions", "Quests", "Contests"] }) + new_course = + Course + |> where(id: ^course_id) + |> join(:left, [c], at in assoc(c, :assessment_type)) + |> preload([c, at], + assessment_type: ^from(at in AssessmentType, order_by: [asc: at.order]) + ) + |> Repo.all() + |> hd() + + new_types = Enum.map(new_course.assessment_type, fn x -> x.type end) |> IO.inspect() + assert response(conn, 200) == "OK" + refute old_types == new_types + assert new_types == ["Missions", "Quests", "Contests"] end @tag authenticate: :student diff --git a/test/factories/courses/course_factory.ex b/test/factories/courses/course_factory.ex index c3711b586..db9ccb0b8 100644 --- a/test/factories/courses/course_factory.ex +++ b/test/factories/courses/course_factory.ex @@ -9,8 +9,8 @@ defmodule Cadet.Courses.CourseFactory do def course_factory do %Course{ - name: "Programming Methodology", - module_code: "CS1101S", + course_name: "Programming Methodology", + course_short_name: "CS1101S", viewable: true, enable_game: true, enable_achievements: true, From 8c9a8d080e49470724f0791c9a330f12db460ec0 Mon Sep 17 00:00:00 2001 From: YaleChen299 Date: Wed, 16 Jun 2021 19:58:45 +0800 Subject: [PATCH 061/174] refactor admin_course_controller test --- .../admin_courses_controller.ex | 2 +- .../admin_courses_controller_test.exs | 42 +++++++++---------- 2 files changed, 21 insertions(+), 23 deletions(-) diff --git a/lib/cadet_web/admin_controllers/admin_courses_controller.ex b/lib/cadet_web/admin_controllers/admin_courses_controller.ex index aa897d1bd..10b6e7a87 100644 --- a/lib/cadet_web/admin_controllers/admin_courses_controller.ex +++ b/lib/cadet_web/admin_controllers/admin_courses_controller.ex @@ -60,7 +60,7 @@ defmodule CadetWeb.AdminCoursesController do def update_assessment_types(conn, %{ "course_id" => course_id, - "assessment_types" => assessment_types + "assessmentTypes" => assessment_types }) when is_ecto_id(course_id) do case Courses.update_assessment_types(course_id, assessment_types) do diff --git a/test/cadet_web/admin_controllers/admin_courses_controller_test.exs b/test/cadet_web/admin_controllers/admin_courses_controller_test.exs index b16f5fd15..f6c82600a 100644 --- a/test/cadet_web/admin_controllers/admin_courses_controller_test.exs +++ b/test/cadet_web/admin_controllers/admin_courses_controller_test.exs @@ -18,7 +18,7 @@ defmodule CadetWeb.AdminCoursesControllerTest do @tag authenticate: :admin test "succeeds 1", %{conn: conn} do course_id = conn.assigns[:course_id] - old_course = Map.from_struct(Repo.get(Course, course_id)) + old_course = to_map(Repo.get(Course, course_id)) params = %{ "sourceChapter" => 2, @@ -28,17 +28,15 @@ defmodule CadetWeb.AdminCoursesControllerTest do resp = put(conn, build_url_course_config(course_id), params) assert response(resp, 200) == "OK" - updated_course = Map.from_struct(Repo.get(Course, course_id)) + updated_course = to_map(Repo.get(Course, course_id)) refute old_course == updated_course - - assert Map.merge(old_course, to_snake_case_atom_keys(params), fn _k, _v1, v2 -> v2 end) == - updated_course + assert update_map(old_course, params) == updated_course end @tag authenticate: :admin test "succeeds 2", %{conn: conn} do course_id = conn.assigns[:course_id] - old_course = Map.from_struct(Repo.get(Course, course_id)) + old_course = to_map(Repo.get(Course, course_id)) params = %{ "courseName" => "Data Structures and Algorithms", @@ -54,17 +52,15 @@ defmodule CadetWeb.AdminCoursesControllerTest do resp = put(conn, build_url_course_config(course_id), params) assert response(resp, 200) == "OK" - updated_course = Map.from_struct(Repo.get(Course, course_id)) + updated_course = to_map(Repo.get(Course, course_id)) refute old_course == updated_course - - assert Map.merge(old_course, to_snake_case_atom_keys(params), fn _k, _v1, v2 -> v2 end) == - updated_course + assert update_map(old_course, params) == updated_course end @tag authenticate: :admin test "succeeds 3", %{conn: conn} do course_id = conn.assigns[:course_id] - old_course = Map.from_struct(Repo.get(Course, course_id)) + old_course = to_map(Repo.get(Course, course_id)) params = %{ "courseName" => "Data Structures and Algorithms", @@ -78,11 +74,9 @@ defmodule CadetWeb.AdminCoursesControllerTest do resp = put(conn, build_url_course_config(course_id), params) assert response(resp, 200) == "OK" - updated_course = Map.from_struct(Repo.get(Course, course_id)) + updated_course = to_map(Repo.get(Course, course_id)) refute old_course == updated_course - - assert Map.merge(old_course, to_snake_case_atom_keys(params), fn _k, _v1, v2 -> v2 end) == - updated_course + assert update_map(old_course, params) == updated_course end @tag authenticate: :student @@ -259,11 +253,11 @@ defmodule CadetWeb.AdminCoursesControllerTest do |> Repo.all() |> hd() - old_types = Enum.map(old_course.assessment_type, fn x -> x.type end) |> IO.inspect() + old_types = Enum.map(old_course.assessment_type, fn x -> x.type end) conn = put(conn, build_url_assessment_types(course_id), %{ - "assessment_types" => ["Missions", "Quests", "Contests"] + "assessmentTypes" => ["Missions", "Quests", "Contests"] }) new_course = @@ -276,7 +270,7 @@ defmodule CadetWeb.AdminCoursesControllerTest do |> Repo.all() |> hd() - new_types = Enum.map(new_course.assessment_type, fn x -> x.type end) |> IO.inspect() + new_types = Enum.map(new_course.assessment_type, fn x -> x.type end) assert response(conn, 200) == "OK" refute old_types == new_types @@ -289,7 +283,7 @@ defmodule CadetWeb.AdminCoursesControllerTest do conn = put(conn, build_url_assessment_types(course_id), %{ - "assessment_types" => ["Missions", "Quests", "Contests"] + "assessmentTypes" => ["Missions", "Quests", "Contests"] }) assert response(conn, 403) == "Forbidden" @@ -301,7 +295,7 @@ defmodule CadetWeb.AdminCoursesControllerTest do conn = put(conn, build_url_assessment_types(course_id + 1), %{ - "assessment_types" => ["Missions", "Quests", "Contests"] + "assessmentTypes" => ["Missions", "Quests", "Contests"] }) assert response(conn, 403) == "Forbidden" @@ -313,7 +307,7 @@ defmodule CadetWeb.AdminCoursesControllerTest do conn = put(conn, build_url_assessment_types(course_id), %{ - "assessment_types" => "Missions" + "assessmentTypes" => "Missions" }) assert response(conn, 400) == "Invalid parameter(s)" @@ -325,7 +319,7 @@ defmodule CadetWeb.AdminCoursesControllerTest do conn = put(conn, build_url_assessment_types(course_id), %{ - "assessment_types" => [1, "Missions", "Quests"] + "assessmentTypes" => [1, "Missions", "Quests"] }) assert response(conn, 400) == "Invalid parameter(s)" @@ -348,4 +342,8 @@ defmodule CadetWeb.AdminCoursesControllerTest do defp build_url_assessment_types(course_id), do: "/v2/course/#{course_id}/admin/assessment_types" + + defp to_map(schema), do: Map.from_struct(schema) |> Map.drop([:updated_at]) + + defp update_map(map1, params), do: Map.merge(map1, to_snake_case_atom_keys(params), fn _k, _v1, v2 -> v2 end) end From fddac6c35807b1e67a7c5aae46cde5664789b1f5 Mon Sep 17 00:00:00 2001 From: YaleChen299 Date: Thu, 17 Jun 2021 00:03:44 +0800 Subject: [PATCH 062/174] fix change of field name in course in user view --- lib/cadet_web/views/user_view.ex | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/cadet_web/views/user_view.ex b/lib/cadet_web/views/user_view.ex index 7812d1c0f..b71f0f5fa 100644 --- a/lib/cadet_web/views/user_view.ex +++ b/lib/cadet_web/views/user_view.ex @@ -51,8 +51,8 @@ defmodule CadetWeb.UserView do def render("course.json", %{cr: cr}) do %{ courseId: cr.course_id, - name: cr.course.course_name, - moduleCode: cr.course.course_short_name, + courseName: cr.course.course_name, + courseShortName: cr.course.course_short_name, viewable: cr.course.viewable } end From 08c1906d61eec3e74c6bcfa6cfb6247e62dd29a2 Mon Sep 17 00:00:00 2001 From: YaleChen299 Date: Thu, 17 Jun 2021 16:49:26 +0800 Subject: [PATCH 063/174] remove grade/max_grade/adjustment in assessments context --- lib/cadet/assessments/answer.ex | 31 ++++--------- lib/cadet/assessments/assessment.ex | 4 +- lib/cadet/assessments/assessments.ex | 23 ++++------ lib/cadet/assessments/query.ex | 22 ++++----- lib/cadet/assessments/question.ex | 3 +- lib/cadet/assessments/submission.ex | 2 - .../controllers/assessments_controller.ex | 8 ---- lib/cadet_web/controllers/user_controller.ex | 46 ++++--------------- lib/cadet_web/views/assessments_helpers.ex | 1 - lib/cadet_web/views/assessments_view.ex | 1 - lib/cadet_web/views/user_view.ex | 22 ++++----- ...20210617084452_remove_assessment_grade.exs | 14 ++++++ priv/repo/seeds.exs | 4 -- .../controllers/user_controller_test.exs | 22 +++------ .../factories/assessments/question_factory.ex | 3 -- 15 files changed, 70 insertions(+), 136 deletions(-) create mode 100644 priv/repo/migrations/20210617084452_remove_assessment_grade.exs diff --git a/lib/cadet/assessments/answer.ex b/lib/cadet/assessments/answer.ex index 55277e881..1f8a04f5c 100644 --- a/lib/cadet/assessments/answer.ex +++ b/lib/cadet/assessments/answer.ex @@ -6,16 +6,14 @@ defmodule Cadet.Assessments.Answer do use Cadet, :model alias Cadet.Repo - alias Cadet.Accounts.User + alias Cadet.Accounts.CourseRegistration alias Cadet.Assessments.Answer.AutogradingStatus alias Cadet.Assessments.AnswerTypes.{MCQAnswer, ProgrammingAnswer, VotingAnswer} alias Cadet.Assessments.{Question, QuestionType, Submission} schema "answers" do - field(:grade, :integer, default: 0) # used to compare answers with others field(:relative_score, :float, default: 0.0) - field(:adjustment, :integer, default: 0) field(:xp, :integer, default: 0) field(:xp_adjustment, :integer, default: 0) field(:comments, :string) @@ -24,7 +22,7 @@ defmodule Cadet.Assessments.Answer do field(:answer, :map) field(:type, QuestionType, virtual: true) - belongs_to(:grader, User) + belongs_to(:grader, CourseRegistration) belongs_to(:submission, Submission) belongs_to(:question, Question) @@ -32,7 +30,7 @@ defmodule Cadet.Assessments.Answer do end @required_fields ~w(answer submission_id question_id type)a - @optional_fields ~w(xp xp_adjustment grade adjustment grader_id comments)a + @optional_fields ~w(xp xp_adjustment grader_id comments)a def changeset(answer, params) do answer @@ -43,7 +41,7 @@ defmodule Cadet.Assessments.Answer do |> foreign_key_constraint(:submission_id) |> foreign_key_constraint(:question_id) |> validate_answer_content() - |> validate_xp_grade_adjustment_total() + |> validate_xp_adjustment_total() end @spec grading_changeset(%__MODULE__{} | Ecto.Changeset.t(), map()) :: Ecto.Changeset.t() @@ -55,8 +53,6 @@ defmodule Cadet.Assessments.Answer do grader_id xp xp_adjustment - grade - adjustment autograding_results autograding_status comments @@ -64,29 +60,26 @@ defmodule Cadet.Assessments.Answer do ) |> add_belongs_to_id_from_model(:grader, params) |> foreign_key_constraint(:grader_id) - |> validate_xp_grade_adjustment_total() + |> validate_xp_adjustment_total() end @spec autograding_changeset(%__MODULE__{} | Ecto.Changeset.t(), map()) :: Ecto.Changeset.t() def autograding_changeset(answer, params) do answer - |> cast(params, ~w(grade adjustment xp xp_adjustment autograding_status autograding_results)a) - |> validate_xp_grade_adjustment_total() + |> cast(params, ~w(xp xp_adjustment autograding_status autograding_results)a) + |> validate_xp_adjustment_total() end - @spec validate_xp_grade_adjustment_total(Ecto.Changeset.t()) :: Ecto.Changeset.t() - defp validate_xp_grade_adjustment_total(changeset) do + @spec validate_xp_adjustment_total(Ecto.Changeset.t()) :: Ecto.Changeset.t() + defp validate_xp_adjustment_total(changeset) do answer = apply_changes(changeset) - total_grade = answer.grade + answer.adjustment - total_xp = answer.xp + answer.xp_adjustment with {:question_id, question_id} when is_ecto_id(question_id) <- {:question_id, answer.question_id}, - {:question, %{max_grade: max_grade, max_xp: max_xp}} <- + {:question, %{max_xp: max_xp}} <- {:question, Repo.get(Question, question_id)}, - {:total_grade, true} <- {:total_grade, total_grade >= 0 and total_grade <= max_grade}, {:total_xp, true} <- {:total_xp, total_xp >= 0 and total_xp <= max_xp} do changeset else @@ -96,13 +89,9 @@ defmodule Cadet.Assessments.Answer do {:question, _} -> add_error(changeset, :question_id, "refers to non-existent question") - {:total_grade, false} -> - add_error(changeset, :adjustment, "must make total be between 0 and question.max_grade") - {:total_xp, false} -> add_error(changeset, :xp_adjustment, "must make total be between 0 and question.max_xp") end - |> validate_number(:grade, greater_than_or_equal_to: 0) |> validate_number(:xp, greater_than_or_equal_to: 0) end diff --git a/lib/cadet/assessments/assessment.ex b/lib/cadet/assessments/assessment.ex index 694aef839..bc3266fc1 100644 --- a/lib/cadet/assessments/assessment.ex +++ b/lib/cadet/assessments/assessment.ex @@ -15,8 +15,8 @@ defmodule Cadet.Assessments.Assessment do schema "assessments" do field(:access, AssessmentAccess, virtual: true, default: :public) - field(:max_grade, :integer, virtual: true) - field(:grade, :integer, virtual: true, default: 0) + # field(:max_grade, :integer, virtual: true) + # field(:grade, :integer, virtual: true, default: 0) field(:max_xp, :integer, virtual: true) field(:xp, :integer, virtual: true, default: 0) field(:user_status, SubmissionStatus, virtual: true) diff --git a/lib/cadet/assessments/assessments.ex b/lib/cadet/assessments/assessments.ex index 5786414f2..ea1d89ad9 100644 --- a/lib/cadet/assessments/assessments.ex +++ b/lib/cadet/assessments/assessments.ex @@ -63,45 +63,44 @@ defmodule Cadet.Assessments do # Repo.delete_all(submissions) # end - @spec user_max_grade(%CourseRegistration{}) :: integer() - def user_max_grade(%CourseRegistration{id: cr_id}) do + @spec user_max_xp(%CourseRegistration{}) :: integer() + def user_max_xp(%CourseRegistration{id: cr_id}) do Submission |> where(status: ^:submitted) |> where(student_id: ^cr_id) |> join( :inner, [s], - a in subquery(Query.all_assessments_with_max_grade()), + a in subquery(Query.all_assessments_with_max_xp()), on: s.assessment_id == a.id ) - |> select([_, a], sum(a.max_grade)) + |> select([_, a], sum(a.max_xp)) |> Repo.one() |> decimal_to_integer() end - def user_total_grade_xp(%CourseRegistration{id: cr_id}) do - submission_grade_xp = + def user_total_xp(%CourseRegistration{id: cr_id}) do + submission_xp = Submission |> where(student_id: ^cr_id) |> join(:inner, [s], a in Answer, on: s.id == a.submission_id) |> group_by([s], s.id) |> select([s, a], %{ - total_grade: sum(a.grade) + sum(a.adjustment), # grouping by submission, so s.xp_bonus will be the same, but we need an # aggregate function total_xp: sum(a.xp) + sum(a.xp_adjustment) + max(s.xp_bonus) }) total = - submission_grade_xp + submission_xp |> subquery |> select([s], %{ - total_grade: sum(s.total_grade), total_xp: sum(s.total_xp) }) |> Repo.one() - for {key, val} <- total, into: %{}, do: {key, decimal_to_integer(val)} + # for {key, val} <- total, into: %{}, do: {key, decimal_to_integer(val)} + decimal_to_integer(total.total_xp) end defp decimal_to_integer(decimal) do @@ -270,7 +269,6 @@ defmodule Cadet.Assessments do |> group_by([s], s.assessment_id) |> select([s, ans], %{ assessment_id: s.assessment_id, - grade: fragment("? + ?", sum(ans.grade), sum(ans.adjustment)), # s.xp_bonus should be the same across the group, but we need an aggregate function here xp: fragment("? + ? + ?", sum(ans.xp), sum(ans.xp_adjustment), max(s.xp_bonus)), graded_count: ans.id |> count() |> filter(not is_nil(ans.grader_id)) @@ -294,7 +292,6 @@ defmodule Cadet.Assessments do |> select([a, sa, s], %{ a | xp: sa.xp, - grade: sa.grade, graded_count: sa.graded_count, user_status: s.status }) @@ -758,8 +755,6 @@ defmodule Cadet.Assessments do {:cont, answer |> Answer.grading_changeset(%{ - grade: 0, - adjustment: 0, xp: 0, xp_adjustment: 0, autograding_status: :none, diff --git a/lib/cadet/assessments/query.ex b/lib/cadet/assessments/query.ex index ea2e7f82f..85cca7468 100644 --- a/lib/cadet/assessments/query.ex +++ b/lib/cadet/assessments/query.ex @@ -10,7 +10,7 @@ defmodule Cadet.Assessments.Query do @doc """ Returns a query with the following bindings: - [assessments_with_xp_and_grade, questions] + [assessments_with_xp, questions] """ @spec all_assessments_with_aggregates(integer()) :: Ecto.Query.t() def all_assessments_with_aggregates(course_id) when is_ecto_id(course_id) do @@ -19,28 +19,27 @@ defmodule Cadet.Assessments.Query do |> join(:inner, [a], q in subquery(assessments_aggregates()), on: a.id == q.assessment_id) |> select([a, q], %Assessment{ a - | max_grade: q.max_grade, - max_xp: q.max_xp, + | max_xp: q.max_xp, question_count: q.question_count }) end @doc """ Returns a query with the following bindings: - [assessments_with_grade, questions] + [assessments_with_xp, questions] """ - @spec all_assessments_with_max_grade :: Ecto.Query.t() - def all_assessments_with_max_grade do + @spec all_assessments_with_max_xp :: Ecto.Query.t() + def all_assessments_with_max_xp do Assessment - |> join(:inner, [a], q in subquery(assessments_max_grade()), on: a.id == q.assessment_id) - |> select([a, q], %Assessment{a | max_grade: q.max_grade}) + |> join(:inner, [a], q in subquery(assessments_max_xp()), on: a.id == q.assessment_id) + |> select([a, q], %Assessment{a | max_xp: q.max_xp}) end - @spec assessments_max_grade :: Ecto.Query.t() - def assessments_max_grade do + @spec assessments_max_xp :: Ecto.Query.t() + def assessments_max_xp do Question |> group_by(:assessment_id) - |> select([q], %{assessment_id: q.assessment_id, max_grade: sum(q.max_grade)}) + |> select([q], %{assessment_id: q.assessment_id, max_xp: sum(q.max_xp)}) end @spec assessments_aggregates :: Ecto.Query.t() @@ -49,7 +48,6 @@ defmodule Cadet.Assessments.Query do |> group_by(:assessment_id) |> select([q], %{ assessment_id: q.assessment_id, - max_grade: sum(q.max_grade), max_xp: sum(q.max_xp), question_count: count(q.id) }) diff --git a/lib/cadet/assessments/question.ex b/lib/cadet/assessments/question.ex index e1aadf3e5..3420b9473 100644 --- a/lib/cadet/assessments/question.ex +++ b/lib/cadet/assessments/question.ex @@ -12,7 +12,6 @@ defmodule Cadet.Assessments.Question do field(:display_order, :integer) field(:question, :map) field(:type, QuestionType) - field(:max_grade, :integer) field(:max_xp, :integer) field(:answer, :map, virtual: true) embeds_one(:library, Library, on_replace: :update) @@ -22,7 +21,7 @@ defmodule Cadet.Assessments.Question do end @required_fields ~w(question type assessment_id)a - @optional_fields ~w(display_order max_grade max_xp)a + @optional_fields ~w(display_order max_xp)a @required_embeds ~w(library)a def changeset(question, params) do diff --git a/lib/cadet/assessments/submission.ex b/lib/cadet/assessments/submission.ex index 31e5be223..e00414858 100644 --- a/lib/cadet/assessments/submission.ex +++ b/lib/cadet/assessments/submission.ex @@ -6,8 +6,6 @@ defmodule Cadet.Assessments.Submission do alias Cadet.Assessments.{Answer, Assessment, SubmissionStatus} schema "submissions" do - field(:grade, :integer, virtual: true) - field(:adjustment, :integer, virtual: true) field(:xp, :integer, virtual: true) field(:xp_adjustment, :integer, virtual: true) field(:xp_bonus, :integer, default: 0) diff --git a/lib/cadet_web/controllers/assessments_controller.ex b/lib/cadet_web/controllers/assessments_controller.ex index 161cfcfa0..f23969f43 100644 --- a/lib/cadet_web/controllers/assessments_controller.ex +++ b/lib/cadet_web/controllers/assessments_controller.ex @@ -174,12 +174,6 @@ defmodule CadetWeb.AssessmentsController do required: true ) - maxGrade( - :integer, - "The maximum grade for this assessment", - required: true - ) - maxXp( :integer, "The maximum XP for this assessment", @@ -188,8 +182,6 @@ defmodule CadetWeb.AssessmentsController do xp(:integer, "The XP earned for this assessment", required: true) - grade(:integer, "The grade earned for this assessment", required: true) - coverImage(:string, "The URL to the cover picture", required: true) private(:boolean, "Is this an private assessment?", required: true) diff --git a/lib/cadet_web/controllers/user_controller.ex b/lib/cadet_web/controllers/user_controller.ex index aea44dbb3..3c861fe86 100644 --- a/lib/cadet_web/controllers/user_controller.ex +++ b/lib/cadet_web/controllers/user_controller.ex @@ -15,8 +15,8 @@ defmodule CadetWeb.UserController do if user.latest_viewed_id do latest = CourseRegistrations.get_user_course(user.id, user.latest_viewed_id) - %{total_grade: grade, total_xp: xp} = user_total_grade_xp(latest) - max_grade = user_max_grade(latest) + xp = user_total_xp(latest) + max_xp = user_max_xp(latest) story = user_current_story(latest) render( @@ -25,8 +25,7 @@ defmodule CadetWeb.UserController do user: user, courses: courses, latest: latest, - grade: grade, - max_grade: max_grade, + max_xp: max_xp, story: story, xp: xp ) @@ -35,31 +34,13 @@ defmodule CadetWeb.UserController do user: user, courses: courses, latest: nil, - grade: nil, - max_grade: nil, + max_xp: nil, story: nil, xp: nil ) end end - # def index(conn, _) do - # user = user_with_group(conn.assigns.current_user) - # %{total_grade: grade, total_xp: xp} = user_total_grade_xp(user) - # max_grade = user_max_grade(user) - # story = user_current_story(user) - - # render( - # conn, - # "index.json", - # user: user, - # grade: grade, - # max_grade: max_grade, - # story: story, - # xp: xp - # ) - # end - def get_latest_viewed(conn, _) do user = conn.assigns.current_user @@ -78,20 +59,19 @@ defmodule CadetWeb.UserController do end defp get_course_reg_config(conn, course_reg) when is_nil(course_reg) do - render(conn, "course.json", latest: nil, grade: nil, max_grade: nil, story: nil, xp: nil) + render(conn, "course.json", latest: nil, story: nil, xp: nil, max_xp: nil) end defp get_course_reg_config(conn, course_reg) do - %{total_grade: grade, total_xp: xp} = user_total_grade_xp(course_reg) - max_grade = user_max_grade(course_reg) + xp = user_total_xp(course_reg) + max_xp = user_max_xp(course_reg) story = user_current_story(course_reg) render( conn, "course.json", latest: course_reg, - grade: grade, - max_grade: max_grade, + max_xp: max_xp, story: story, xp: xp ) @@ -239,15 +219,9 @@ defmodule CadetWeb.UserController do story(Schema.ref(:UserStory), "Story to displayed to current user. ") - grade( - :integer, - "Amount of grade. Only provided for 'Student'. " <> - "Value will be 0 for non-students." - ) - - maxGrade( + maxXp( :integer, - "Total maximum grade achievable based on submitted assessments. " <> + "Total maximum xp achievable based on submitted assessments. " <> "Only provided for 'Student'" ) diff --git a/lib/cadet_web/views/assessments_helpers.ex b/lib/cadet_web/views/assessments_helpers.ex index 5fdb23282..fdffd1e47 100644 --- a/lib/cadet_web/views/assessments_helpers.ex +++ b/lib/cadet_web/views/assessments_helpers.ex @@ -98,7 +98,6 @@ defmodule CadetWeb.AssessmentsHelpers do grader: grader_builder(grader), gradedAt: graded_at_builder(grader), xp: &((&1.xp || 0) + (&1.xp_adjustment || 0)), - grade: &((&1.grade || 0) + (&1.adjustment || 0)), autogradingStatus: :autograding_status, autogradingResults: build_results(%{results: answer.autograding_results}), comments: :comments diff --git a/lib/cadet_web/views/assessments_view.ex b/lib/cadet_web/views/assessments_view.ex index 437430607..e05372049 100644 --- a/lib/cadet_web/views/assessments_view.ex +++ b/lib/cadet_web/views/assessments_view.ex @@ -24,7 +24,6 @@ defmodule CadetWeb.AssessmentsView do maxGrade: :max_grade, maxXp: :max_xp, xp: &(&1.xp || 0), - grade: &(&1.grade || 0), coverImage: :cover_picture, private: &password_protected?(&1.password), isPublished: :is_published, diff --git a/lib/cadet_web/views/user_view.ex b/lib/cadet_web/views/user_view.ex index b71f0f5fa..0260711e1 100644 --- a/lib/cadet_web/views/user_view.ex +++ b/lib/cadet_web/views/user_view.ex @@ -5,8 +5,7 @@ defmodule CadetWeb.UserView do user: user, courses: courses, latest: latest, - grade: grade, - max_grade: max_grade, + max_xp: max_xp, xp: xp, story: story }) do @@ -14,13 +13,12 @@ defmodule CadetWeb.UserView do user: %{ userId: user.id, name: user.name, - courses: render_many(courses, CadetWeb.UserView, "course.json", as: :cr) + courses: render_many(courses, CadetWeb.UserView, "courses.json", as: :cr) }, courseRegistration: render_latest(%{ latest: latest, - grade: grade, - max_grade: max_grade, + max_xp: max_xp, xp: xp, story: story }), @@ -30,8 +28,7 @@ defmodule CadetWeb.UserView do def render("course.json", %{ latest: latest, - grade: grade, - max_grade: max_grade, + max_xp: max_xp, xp: xp, story: story }) do @@ -39,8 +36,7 @@ defmodule CadetWeb.UserView do courseRegistration: render_latest(%{ latest: latest, - grade: grade, - max_grade: max_grade, + max_xp: max_xp, xp: xp, story: story }), @@ -48,7 +44,7 @@ defmodule CadetWeb.UserView do } end - def render("course.json", %{cr: cr}) do + def render("courses.json", %{cr: cr}) do %{ courseId: cr.course_id, courseName: cr.course.course_name, @@ -59,8 +55,7 @@ defmodule CadetWeb.UserView do defp render_latest(%{ latest: latest, - grade: grade, - max_grade: max_grade, + max_xp: max_xp, xp: xp, story: story }) do @@ -77,9 +72,8 @@ defmodule CadetWeb.UserView do nil -> nil _ -> latest.group.name end, - grade: grade, xp: xp, - maxGrade: max_grade, + maxXp: max_xp, story: transform_map_for_view(story, %{ story: :story, diff --git a/priv/repo/migrations/20210617084452_remove_assessment_grade.exs b/priv/repo/migrations/20210617084452_remove_assessment_grade.exs new file mode 100644 index 000000000..eef3ec8f8 --- /dev/null +++ b/priv/repo/migrations/20210617084452_remove_assessment_grade.exs @@ -0,0 +1,14 @@ +defmodule Cadet.Repo.Migrations.RemoveAssessmentGrade do + use Ecto.Migration + + def change do + alter table(:answers) do + remove(:grade) + remove(:adjustment) + end + + alter table(:questions) do + remove(:max_grade) + end + end +end diff --git a/priv/repo/seeds.exs b/priv/repo/seeds.exs index c4c76c9b0..6b78f2d2a 100644 --- a/priv/repo/seeds.exs +++ b/priv/repo/seeds.exs @@ -62,14 +62,12 @@ if Cadet.Env.env() == :dev do programming_questions = insert_list(3, :programming_question, %{ assessment: assessment, - max_grade: 200, max_xp: 1_000 }) mcq_questions = insert_list(3, :mcq_question, %{ assessment: assessment, - max_grade: 40, max_xp: 500 }) @@ -88,7 +86,6 @@ if Cadet.Env.env() == :dev do for submission <- submissions, question <- programming_questions do insert(:answer, %{ - grade: Enum.random(0..200), xp: Enum.random(0..1_000), question: question, submission: submission, @@ -100,7 +97,6 @@ if Cadet.Env.env() == :dev do for submission <- submissions, question <- mcq_questions do insert(:answer, %{ - grade: Enum.random(0..40), xp: Enum.random(0..500), question: question, submission: submission, diff --git a/test/cadet_web/controllers/user_controller_test.exs b/test/cadet_web/controllers/user_controller_test.exs index 525e3c3c5..e8d32c14f 100644 --- a/test/cadet_web/controllers/user_controller_test.exs +++ b/test/cadet_web/controllers/user_controller_test.exs @@ -37,8 +37,6 @@ defmodule CadetWeb.UserControllerTest do insert(:answer, %{ question: question, submission: submission, - grade: 50, - adjustment: -10, xp: 20, xp_adjustment: -10 }) @@ -53,8 +51,6 @@ defmodule CadetWeb.UserControllerTest do :answer, question: not_submitted_question, submission: not_submitted_submission, - grade: 0, - adjustment: 0 ) resp = @@ -70,14 +66,14 @@ defmodule CadetWeb.UserControllerTest do "courses" => [ %{ "courseId" => user.latest_viewed_id, - "moduleCode" => "CS1101S", - "name" => "Programming Methodology", + "courseShortName" => "CS1101S", + "courseName" => "Programming Methodology", "viewable" => true }, %{ "courseId" => another_cr.course_id, - "moduleCode" => "CS1101S", - "name" => "Programming Methodology", + "courseShortName" => "CS1101S", + "courseName" => "Programming Methodology", "viewable" => true } ] @@ -87,8 +83,7 @@ defmodule CadetWeb.UserControllerTest do "role" => "#{cr.role}", "group" => nil, "xp" => 110, - "grade" => 40, - "maxGrade" => question.max_grade, + "maxXp" => question.max_xp, "gameStates" => %{}, "story" => nil }, @@ -332,8 +327,6 @@ defmodule CadetWeb.UserControllerTest do insert(:answer, %{ question: question, submission: submission, - grade: 50, - adjustment: -10, xp: 20, xp_adjustment: -10 }) @@ -348,8 +341,6 @@ defmodule CadetWeb.UserControllerTest do :answer, question: not_submitted_question, submission: not_submitted_submission, - grade: 0, - adjustment: 0 ) resp = @@ -364,8 +355,7 @@ defmodule CadetWeb.UserControllerTest do "role" => "#{cr.role}", "group" => nil, "xp" => 110, - "grade" => 40, - "maxGrade" => question.max_grade, + "maxXp" => question.max_xp, "gameStates" => %{}, "story" => nil }, diff --git a/test/factories/assessments/question_factory.ex b/test/factories/assessments/question_factory.ex index 4685406f5..945508581 100644 --- a/test/factories/assessments/question_factory.ex +++ b/test/factories/assessments/question_factory.ex @@ -16,7 +16,6 @@ defmodule Cadet.Assessments.QuestionFactory do %Question{ type: :programming, - max_grade: 10, max_xp: 100, assessment: build(:assessment, %{is_published: true}), library: library, @@ -54,7 +53,6 @@ defmodule Cadet.Assessments.QuestionFactory do %Question{ type: :mcq, - max_grade: 10, max_xp: 100, assessment: build(:assessment, %{is_published: true}), library: build(:library), @@ -79,7 +77,6 @@ defmodule Cadet.Assessments.QuestionFactory do %Question{ type: :voting, - max_grade: 10, max_xp: 100, assessment: build(:assessment, %{is_published: true}), library: build(:library), From fb5ee7d5f4267f6a303014983d4bb364c08f3c51 Mon Sep 17 00:00:00 2001 From: YaleChen299 Date: Fri, 18 Jun 2021 11:56:15 +0800 Subject: [PATCH 064/174] fix assessments show & update submission_votes voter --- lib/cadet/assessments/assessments.ex | 107 +++++++++--------- lib/cadet/assessments/submission_votes.ex | 10 +- .../controllers/assessments_controller.ex | 4 +- lib/cadet_web/views/assessments_helpers.ex | 2 +- lib/cadet_web/views/assessments_view.ex | 2 +- .../20210608085548_update_assessments.exs | 5 + .../controllers/user_controller_test.exs | 4 +- .../assessments/submission_vote_factory.ex | 2 +- 8 files changed, 70 insertions(+), 66 deletions(-) diff --git a/lib/cadet/assessments/assessments.ex b/lib/cadet/assessments/assessments.ex index ea1d89ad9..6ed576aa6 100644 --- a/lib/cadet/assessments/assessments.ex +++ b/lib/cadet/assessments/assessments.ex @@ -160,45 +160,44 @@ defmodule Cadet.Assessments do def assessment_with_questions_and_answers( assessment = %Assessment{password: nil}, - user = %User{}, + cr = %CourseRegistration{}, nil ) do - assessment_with_questions_and_answers(assessment, user) + assessment_with_questions_and_answers(assessment, cr) end def assessment_with_questions_and_answers( assessment = %Assessment{password: nil}, - user = %User{}, + cr = %CourseRegistration{}, _ ) do - assessment_with_questions_and_answers(assessment, user) + assessment_with_questions_and_answers(assessment, cr) end def assessment_with_questions_and_answers( assessment = %Assessment{password: password}, - user = %User{}, + cr = %CourseRegistration{}, given_password ) do cond do Timex.compare(Timex.now(), assessment.close_at) >= 0 -> - assessment_with_questions_and_answers(assessment, user) + assessment_with_questions_and_answers(assessment, cr) - match?({:ok, _}, find_submission(user, assessment)) -> - assessment_with_questions_and_answers(assessment, user) + match?({:ok, _}, find_submission(cr, assessment)) -> + assessment_with_questions_and_answers(assessment, cr) given_password == nil -> {:error, {:forbidden, "Missing Password."}} password == given_password -> - find_or_create_submission(user, assessment) - assessment_with_questions_and_answers(assessment, user) + find_or_create_submission(cr, assessment) + assessment_with_questions_and_answers(assessment, cr) true -> {:error, {:forbidden, "Invalid Password."}} end end - # def assessment_with_questions_and_answers(id, user = %User{}, password) def assessment_with_questions_and_answers(id, cr = %CourseRegistration{}, password) when is_ecto_id(id) do role = cr.role @@ -222,39 +221,39 @@ defmodule Cadet.Assessments do end end - # def assessment_with_questions_and_answers( - # assessment = %Assessment{id: id}, - # user = %User{role: role} - # ) do - # if Timex.compare(Timex.now(), assessment.open_at) >= 0 or role in @open_all_assessment_roles 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), on: q.id == a.question_id) - # |> join(:left, [_, a], g in assoc(a, :grader)) - # |> select([q, a, g], {q, a, g}) - # |> order_by(:display_order) - # |> Repo.all() - # |> Enum.map(fn - # {q, nil, _} -> %{q | answer: %Answer{grader: nil}} - # {q, a, g} -> %{q | answer: %Answer{a | grader: g}} - # end) - # |> load_contest_voting_entries(user.id) - - # assessment = Map.put(assessment, :questions, questions) - # {:ok, assessment} - # else - # {:error, {:unauthorized, "Assessment not open"}} - # end - # end + def assessment_with_questions_and_answers( + assessment = %Assessment{id: id}, + cr = %CourseRegistration{role: role} + ) do + if Timex.compare(Timex.now(), assessment.open_at) >= 0 or role in @open_all_assessment_roles do + answer_query = + Answer + |> join(:inner, [a], s in assoc(a, :submission)) + |> where([_, s], s.student_id == ^cr.id) + + questions = + Question + |> where(assessment_id: ^id) + |> join(:left, [q], a in subquery(answer_query), on: q.id == a.question_id) + |> join(:left, [_, a], g in assoc(a, :grader)) + |> select([q, a, g], {q, a, g}) + |> order_by(:display_order) + |> Repo.all() + |> Enum.map(fn + {q, nil, _} -> %{q | answer: %Answer{grader: nil}} + {q, a, g} -> %{q | answer: %Answer{a | grader: g}} + end) + # |> load_contest_voting_entries(cr.id) + + assessment = Map.put(assessment, :questions, questions) + {:ok, assessment} + else + {:error, {:unauthorized, "Assessment not open"}} + end + end - def assessment_with_questions_and_answers(id, user = %User{}) do - assessment_with_questions_and_answers(id, user, nil) + def assessment_with_questions_and_answers(id, cr = %CourseRegistration{}) do + assessment_with_questions_and_answers(id, cr, nil) end @doc """ @@ -570,7 +569,7 @@ defmodule Cadet.Assessments do new_submission_votes = votes |> Enum.map(fn s_id -> - %SubmissionVotes{user_id: user_id, submission_id: s_id, question_id: question_id} + %SubmissionVotes{voter_id: user_id, submission_id: s_id, question_id: question_id} end) |> Enum.concat(submission_votes) @@ -650,10 +649,10 @@ defmodule Cadet.Assessments do `{:bad_request, "Missing or invalid parameter(s)"}` """ - def answer_question(question = %Question{}, user = %User{id: user_id}, raw_answer, force_submit) do - with {:ok, submission} <- find_or_create_submission(user, question.assessment), + def answer_question(question = %Question{}, cr = %CourseRegistration{id: cr_id}, raw_answer, force_submit) do + with {:ok, submission} <- find_or_create_submission(cr, question.assessment), {:status, true} <- {:status, force_submit or submission.status != :submitted}, - {:ok, _answer} <- insert_or_update_answer(submission, question, raw_answer, user_id) do + {:ok, _answer} <- insert_or_update_answer(submission, question, raw_answer, cr_id) do update_submission_status_router(submission, question) {:ok, nil} @@ -1323,10 +1322,10 @@ defmodule Cadet.Assessments do {:error, {:forbidden, "User is not permitted to grade."}} end - defp find_submission(user = %User{}, assessment = %Assessment{}) do + defp find_submission(cr = %CourseRegistration{}, assessment = %Assessment{}) do submission = Submission - |> where(student_id: ^user.id) + |> where(student_id: ^cr.id) |> where(assessment_id: ^assessment.id) |> Repo.one() @@ -1392,9 +1391,9 @@ defmodule Cadet.Assessments do {:ok, rows} end - defp create_empty_submission(user = %User{}, assessment = %Assessment{}) do + defp create_empty_submission(cr = %CourseRegistration{}, assessment = %Assessment{}) do %Submission{} - |> Submission.changeset(%{student: user, assessment: assessment}) + |> Submission.changeset(%{student: cr, assessment: assessment}) |> Repo.insert() |> case do {:ok, submission} -> {:ok, submission} @@ -1402,10 +1401,10 @@ defmodule Cadet.Assessments do end end - defp find_or_create_submission(user = %User{}, assessment = %Assessment{}) do - case find_submission(user, assessment) do + defp find_or_create_submission(cr = %CourseRegistration{}, assessment = %Assessment{}) do + case find_submission(cr, assessment) do {:ok, submission} -> {:ok, submission} - {:error, _} -> create_empty_submission(user, assessment) + {:error, _} -> create_empty_submission(cr, assessment) end end diff --git a/lib/cadet/assessments/submission_votes.ex b/lib/cadet/assessments/submission_votes.ex index e476e9dc9..1a010264c 100644 --- a/lib/cadet/assessments/submission_votes.ex +++ b/lib/cadet/assessments/submission_votes.ex @@ -2,27 +2,27 @@ defmodule Cadet.Assessments.SubmissionVotes do @moduledoc false use Cadet, :model - alias Cadet.Accounts.User + alias Cadet.Accounts.CourseRegistration alias Cadet.Assessments.{Question, Submission} schema "submission_votes" do field(:rank, :integer) - belongs_to(:user, User) + belongs_to(:voter, CourseRegistration) belongs_to(:submission, Submission) belongs_to(:question, Question) timestamps() end - @required_fields ~w(user_id submission_id question_id)a + @required_fields ~w(voter_id submission_id question_id)a @optional_fields ~w(rank)a def changeset(submission_vote, params) do submission_vote |> cast(params, @required_fields ++ @optional_fields) - |> add_belongs_to_id_from_model([:user, :submission, :question], params) + |> add_belongs_to_id_from_model([:voter, :submission, :question], params) |> validate_required(@required_fields) - |> foreign_key_constraint(:user_id) + |> foreign_key_constraint(:voter_id) |> foreign_key_constraint(:submission_id) |> foreign_key_constraint(:question_id) |> unique_constraint(:vote_not_unique, name: :unique_score) diff --git a/lib/cadet_web/controllers/assessments_controller.ex b/lib/cadet_web/controllers/assessments_controller.ex index f23969f43..ff765f4e7 100644 --- a/lib/cadet_web/controllers/assessments_controller.ex +++ b/lib/cadet_web/controllers/assessments_controller.ex @@ -45,9 +45,9 @@ defmodule CadetWeb.AssessmentsController do end def show(conn, %{"assessmentid" => assessment_id}) when is_ecto_id(assessment_id) do - user = conn.assigns[:current_user] + cr = conn.assigns.course_reg - case Assessments.assessment_with_questions_and_answers(assessment_id, user) do + case Assessments.assessment_with_questions_and_answers(assessment_id, cr) do {:ok, assessment} -> render(conn, "show.json", assessment: assessment) {:error, {status, message}} -> send_resp(conn, status, message) end diff --git a/lib/cadet_web/views/assessments_helpers.ex b/lib/cadet_web/views/assessments_helpers.ex index fdffd1e47..6146418ee 100644 --- a/lib/cadet_web/views/assessments_helpers.ex +++ b/lib/cadet_web/views/assessments_helpers.ex @@ -30,7 +30,7 @@ defmodule CadetWeb.AssessmentsHelpers do build_question_content_by_type( %{ question: question, - assessment_type: assessment_type + assessment_type: assessment_type.type }, all_testcases? ) diff --git a/lib/cadet_web/views/assessments_view.ex b/lib/cadet_web/views/assessments_view.ex index e05372049..3cd26afc0 100644 --- a/lib/cadet_web/views/assessments_view.ex +++ b/lib/cadet_web/views/assessments_view.ex @@ -38,7 +38,7 @@ defmodule CadetWeb.AssessmentsView do %{ id: :id, title: :title, - type: :type, + type: & &1.type.type, story: :story, number: :number, reading: :reading, diff --git a/priv/repo/migrations/20210608085548_update_assessments.exs b/priv/repo/migrations/20210608085548_update_assessments.exs index 73ead03e0..88f81db0e 100644 --- a/priv/repo/migrations/20210608085548_update_assessments.exs +++ b/priv/repo/migrations/20210608085548_update_assessments.exs @@ -31,5 +31,10 @@ defmodule Cadet.Repo.Migrations.UpdateAssessments do alter table(:users) do add(:latest_viewed_id, references(:courses), null: true) end + + alter table(:submission_votes) do + remove(:user_id) + add(:voter_id, references(:course_registrations), null: false) + end end end diff --git a/test/cadet_web/controllers/user_controller_test.exs b/test/cadet_web/controllers/user_controller_test.exs index e8d32c14f..5669310cc 100644 --- a/test/cadet_web/controllers/user_controller_test.exs +++ b/test/cadet_web/controllers/user_controller_test.exs @@ -50,7 +50,7 @@ defmodule CadetWeb.UserControllerTest do insert( :answer, question: not_submitted_question, - submission: not_submitted_submission, + submission: not_submitted_submission ) resp = @@ -340,7 +340,7 @@ defmodule CadetWeb.UserControllerTest do insert( :answer, question: not_submitted_question, - submission: not_submitted_submission, + submission: not_submitted_submission ) resp = diff --git a/test/factories/assessments/submission_vote_factory.ex b/test/factories/assessments/submission_vote_factory.ex index e65ac9192..ec1e1b927 100644 --- a/test/factories/assessments/submission_vote_factory.ex +++ b/test/factories/assessments/submission_vote_factory.ex @@ -9,7 +9,7 @@ defmodule Cadet.Assessments.SubmissionVotesFactory do def submission_vote_factory do %SubmissionVotes{ - user: build(:user, %{role: :student}), + voter: build(:course_registration, %{role: :student}), question: build(:voting_question), submission: build(:submission) } From ea7fb6e476fa5998626518d9729ef9258672cb48 Mon Sep 17 00:00:00 2001 From: YaleChen299 Date: Fri, 18 Jun 2021 17:33:42 +0800 Subject: [PATCH 065/174] fix assessment submit and query avenger_of? with test --- lib/cadet/accounts/query.ex | 27 +++++-------- lib/cadet/assessments/assessments.ex | 4 +- lib/cadet/courses/assessment_type.ex | 4 +- lib/cadet/courses/group.ex | 13 +++++- .../controllers/assessments_controller.ex | 4 +- lib/cadet_web/views/assessments_helpers.ex | 11 ++--- ...0210531155751_add_course_configuration.exs | 1 + test/cadet/accounts/query_test.exs | 40 +++++++++++++++++-- 8 files changed, 70 insertions(+), 34 deletions(-) diff --git a/lib/cadet/accounts/query.ex b/lib/cadet/accounts/query.ex index 62d7dc0be..15d5d094a 100644 --- a/lib/cadet/accounts/query.ex +++ b/lib/cadet/accounts/query.ex @@ -5,10 +5,8 @@ defmodule Cadet.Accounts.Query do import Ecto.Query alias Cadet.Accounts.{User, CourseRegistration} - alias Cadet.Course.Group alias Cadet.Repo - # :TODO test def all_students(course_id) do User |> in_course(course_id) @@ -22,27 +20,21 @@ defmodule Cadet.Accounts.Query do |> preload(:latest_viewed) end - # :TODO test @spec students_of(%CourseRegistration{}) :: Ecto.Query.t() - def students_of(%CourseRegistration{user_id: id, role: :staff, course_id: course_id}) do + def students_of(course_reg = %CourseRegistration{course_id: course_id}) do + # Note that staff role is not check here as we assume that + # group leader is assign to a staff validated by group changeset CourseRegistration |> where([cr], cr.course_id == ^course_id) - |> join(:inner, [cr], g in Group, on: cr.group_id == g.id) - |> where([cr, g], g.leader_id == ^id) + |> join(:inner, [cr], g in assoc(cr, :group)) + |> where([cr, g], g.leader_id == ^course_reg.id) end - # :TODO test - def avenger_of?(avenger_id, course_id, student_id) do - avengerInCourse = - CourseRegistration - |> where([cr], cr.course_id == ^course_id) - |> where([cr], cr.user_id == ^avenger_id) - |> Repo.one() - - students = students_of(avengerInCourse) + def avenger_of?(avenger, student_id) do + students = students_of(avenger) students - |> Repo.get_by(user_id: student_id) + |> Repo.get_by(id: student_id) |> case do nil -> false _ -> true @@ -53,10 +45,9 @@ defmodule Cadet.Accounts.Query do query |> where([a], a.username == ^username) end - # :TODO test defp in_course(user, course_id) do user |> join(:inner, [u], cr in CourseRegistration, on: u.id == cr.user_id) - |> where([_, cr], cr.id == ^course_id) + |> where([_, cr], cr.course_id == ^course_id) end end diff --git a/lib/cadet/assessments/assessments.ex b/lib/cadet/assessments/assessments.ex index 6ed576aa6..742ee9e10 100644 --- a/lib/cadet/assessments/assessments.ex +++ b/lib/cadet/assessments/assessments.ex @@ -706,7 +706,7 @@ defmodule Cadet.Assessments do # def unsubmit_submission(submission_id, user = %User{id: user_id, role: role}) def unsubmit_submission( submission_id, - userCourse = %CourseRegistration{user_id: user_id, role: role, course_id: course_id} + cr = %CourseRegistration{user_id: user_id, role: role, course_id: course_id} ) when is_ecto_id(submission_id) do submission = @@ -723,7 +723,7 @@ defmodule Cadet.Assessments do {:allowed_to_unsubmit?, true} <- {:allowed_to_unsubmit?, role == :admin or bypass or - Cadet.Accounts.Query.avenger_of?(userCourse, submission.student_id)} do + Cadet.Accounts.Query.avenger_of?(cr, submission.student_id)} do Multi.new() |> Multi.run( :rollback_submission, diff --git a/lib/cadet/courses/assessment_type.ex b/lib/cadet/courses/assessment_type.ex index 141f3ea88..2039c87a3 100644 --- a/lib/cadet/courses/assessment_type.ex +++ b/lib/cadet/courses/assessment_type.ex @@ -10,13 +10,15 @@ defmodule Cadet.Courses.AssessmentType do schema "assessment_types" do field(:order, :integer) field(:type, :string) + field(:is_graded, :boolean, default: true) + belongs_to(:course, Course) has_one(:assessment_config, AssessmentConfig) timestamps() end - @required_fields ~w(order type course_id)a + @required_fields ~w(order type course_id is_graded)a def changeset(assessment_type, params) do params = capitalize(params, :type) diff --git a/lib/cadet/courses/group.ex b/lib/cadet/courses/group.ex index bb94227ca..c082c1484 100644 --- a/lib/cadet/courses/group.ex +++ b/lib/cadet/courses/group.ex @@ -5,7 +5,7 @@ defmodule Cadet.Courses.Group do """ use Cadet, :model - # alias Cadet.Repo + alias Cadet.Repo alias Cadet.Accounts.CourseRegistration alias Cadet.Courses.Course @@ -26,10 +26,21 @@ defmodule Cadet.Courses.Group do |> cast(attrs, @required_fields ++ @optional_fields) |> validate_required(@required_fields) |> add_belongs_to_id_from_model([:leader, :mentor, :course], attrs) + |> validate_role # |> validate_course end + defp validate_role(changeset) do + leader_id = get_field(changeset, :leader_id) + + if leader_id != nil && Repo.get(CourseRegistration, leader_id).role != :staff do + add_error(changeset, :leader, "is not a staff") + else + changeset + end + end + # defp validate_course(changeset) do # course_id = get_field(changeset, :course_id) # leader_id = get_field(changeset, :leader_id) diff --git a/lib/cadet_web/controllers/assessments_controller.ex b/lib/cadet_web/controllers/assessments_controller.ex index ff765f4e7..0448c014c 100644 --- a/lib/cadet_web/controllers/assessments_controller.ex +++ b/lib/cadet_web/controllers/assessments_controller.ex @@ -55,9 +55,9 @@ defmodule CadetWeb.AssessmentsController do def unlock(conn, %{"assessmentid" => assessment_id, "password" => password}) when is_ecto_id(assessment_id) do - user = conn.assigns[:current_user] + cr = conn.assigns.course_reg - case Assessments.assessment_with_questions_and_answers(assessment_id, user, password) do + case Assessments.assessment_with_questions_and_answers(assessment_id, cr, password) do {:ok, assessment} -> render(conn, "show.json", assessment: assessment) {:error, {status, message}} -> send_resp(conn, status, message) end diff --git a/lib/cadet_web/views/assessments_helpers.ex b/lib/cadet_web/views/assessments_helpers.ex index 6146418ee..6b267044c 100644 --- a/lib/cadet_web/views/assessments_helpers.ex +++ b/lib/cadet_web/views/assessments_helpers.ex @@ -4,8 +4,6 @@ defmodule CadetWeb.AssessmentsHelpers do """ import CadetWeb.ViewHelper - @graded_assessment_types ~w(mission sidequest contest) - defp build_library(%{library: library}) do transform_map_for_view(library, %{ chapter: :chapter, @@ -30,7 +28,7 @@ defmodule CadetWeb.AssessmentsHelpers do build_question_content_by_type( %{ question: question, - assessment_type: assessment_type.type + assessment_type: assessment_type }, all_testcases? ) @@ -44,7 +42,7 @@ defmodule CadetWeb.AssessmentsHelpers do components = [ build_question_by_assessment_type(%{ question: question, - assessment_type: assessment.type + assessment_type: assessment.type.type }), build_answer_fields_by_question_type(%{question: question}), build_solution_if_ungraded_by_type(%{question: question, assessment: assessment}) @@ -60,8 +58,7 @@ defmodule CadetWeb.AssessmentsHelpers do id: :id, type: :type, library: &build_library(%{library: &1.library}), - maxXp: :max_xp, - maxGrade: :max_grade + maxXp: :max_xp }) end @@ -69,7 +66,7 @@ defmodule CadetWeb.AssessmentsHelpers do question: %{question: question, type: question_type}, assessment: %{type: assessment_type} }) do - if assessment_type not in @graded_assessment_types do + if !assessment_type.is_graded do solution_getter = case question_type do :programming -> &Map.get(&1, "solution") diff --git a/priv/repo/migrations/20210531155751_add_course_configuration.exs b/priv/repo/migrations/20210531155751_add_course_configuration.exs index ec4f28cea..7265dbe8c 100644 --- a/priv/repo/migrations/20210531155751_add_course_configuration.exs +++ b/priv/repo/migrations/20210531155751_add_course_configuration.exs @@ -27,6 +27,7 @@ defmodule Cadet.Repo.Migrations.AddCourseConfiguration do add(:order, :integer, null: false) add(:type, :string, null: false) add(:course_id, references(:courses), null: false) + add(:is_graded, :boolean, null: false) timestamps() end diff --git a/test/cadet/accounts/query_test.exs b/test/cadet/accounts/query_test.exs index 405348cf4..aa0e7e4a0 100644 --- a/test/cadet/accounts/query_test.exs +++ b/test/cadet/accounts/query_test.exs @@ -4,10 +4,44 @@ defmodule Cadet.Accounts.QueryTest do alias Cadet.Accounts.Query test "all_students" do - insert(:student) + course = insert(:course) + insert(:course_registration, %{course: course, role: :student}) - result = Query.all_students() + result = Query.all_students(course.id) - assert 1 = Enum.count(result) + assert 1 == Enum.count(result) + end + + describe "avenger of function:" do + setup do + user_a = insert(:user) + user_b = insert(:user) + user_c = insert(:user) + course1 = insert(:course, course_name: "course 1") + course2 = insert(:course, course_name: "course 2") + staff_a1 = insert(:course_registration, %{user: user_a, course: course1, role: :staff}) + group1 = insert(:group, %{leader: staff_a1, course: course1}) + student_b1 = insert(:course_registration, %{user: user_b, course: course1, role: :student, group: group1}) + student_c1 = insert(:course_registration, %{user: user_c, course: course1, role: :student}) + staff_a2 = insert(:course_registration, %{user: user_a, course: course2, role: :staff}) + + {:ok, %{c1: course1, c2: course2, sta_a1: staff_a1, stu_b1: student_b1, stu_c1: student_c1, sta_a2: staff_a2}} + end + + test "true, when in same course same group", %{sta_a1: sta_a1, stu_b1: stu_b1} do + assert Query.avenger_of?(sta_a1, stu_b1.id) + end + + test "false, when in same course different group", %{sta_a1: sta_a1, stu_c1: stu_c1} do + refute Query.avenger_of?(sta_a1, stu_c1.id) + end + + test "false, when asked by a student", %{sta_a1: sta_a1, stu_b1: stu_b1} do + refute Query.avenger_of?(stu_b1, sta_a1.id) + end + + test "false, when asked in different course", %{sta_a2: sta_a2, stu_b1: stu_b1} do + refute Query.avenger_of?(sta_a2, stu_b1.id) + end end end From aa6105b87d26d1216c274d15a1d1119b6885956c Mon Sep 17 00:00:00 2001 From: YaleChen299 Date: Fri, 18 Jun 2021 18:36:39 +0800 Subject: [PATCH 066/174] updated bonus_xp logit with assessment config --- lib/cadet/assessments/assessments.ex | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/lib/cadet/assessments/assessments.ex b/lib/cadet/assessments/assessments.ex index 742ee9e10..01389557b 100644 --- a/lib/cadet/assessments/assessments.ex +++ b/lib/cadet/assessments/assessments.ex @@ -9,15 +9,13 @@ defmodule Cadet.Assessments do alias Cadet.Accounts.{Notification, Notifications, User, CourseRegistration} alias Cadet.Assessments.{Answer, Assessment, Query, Question, Submission, SubmissionVotes} alias Cadet.Autograder.GradingJob - alias Cadet.Courses.Group + alias Cadet.Courses.{Group, AssessmentConfig} alias Cadet.Jobs.Log alias Cadet.ProgramAnalysis.Lexer alias Ecto.Multi require Decimal - @xp_early_submission_max_bonus 100 - @xp_bonus_assessment_type ~w(mission sidequest) @open_all_assessment_roles ~w(staff admin)a # These roles can save and finalise answers for closed assessments and @@ -706,7 +704,7 @@ defmodule Cadet.Assessments do # def unsubmit_submission(submission_id, user = %User{id: user_id, role: role}) def unsubmit_submission( submission_id, - cr = %CourseRegistration{user_id: user_id, role: role, course_id: course_id} + cr = %CourseRegistration{user_id: user_id, role: role} ) when is_ecto_id(submission_id) do submission = @@ -792,23 +790,25 @@ defmodule Cadet.Assessments do end end - # :TODO bonus logic with assessment config + # :TODO test bonus logic @spec update_submission_status_and_xp_bonus(%Submission{}) :: {:ok, %Submission{}} | {:error, Ecto.Changeset.t()} defp update_submission_status_and_xp_bonus(submission = %Submission{}) do assessment = submission.assessment + assessment_conifg = Repo.get_by(AssessmentConfig, assessment_type_id: assessment.type_id) + + max_bonus_xp = assessment_conifg.early_submission_xp + early_hours = assessment_conifg.hours_before_early_xp_decay + rate = assessment_conifg.decay_rate_points_per_hour xp_bonus = cond do - assessment.type not in @xp_bonus_assessment_type -> - 0 - - Timex.before?(Timex.now(), Timex.shift(assessment.open_at, hours: 48)) -> - @xp_early_submission_max_bonus + Timex.before?(Timex.now(), Timex.shift(assessment.open_at, hours: early_hours)) -> + max_bonus_xp true -> - deduction = Timex.diff(Timex.now(), assessment.open_at, :hours) - 48 - Enum.max([0, @xp_early_submission_max_bonus - deduction]) + exceed_hours = Timex.diff(Timex.now(), assessment.open_at, :hours) - early_hours + Enum.max([0, max_bonus_xp - exceed_hours * rate]) end submission From b2e89ca177a1ce2a79c8fdd74731c13541aa894a Mon Sep 17 00:00:00 2001 From: YaleChen299 Date: Sat, 19 Jun 2021 20:44:02 +0800 Subject: [PATCH 067/174] update formatting and seed --- lib/cadet/assessments/assessments.ex | 10 ++++++++-- priv/repo/seeds.exs | 7 ++++++- test/cadet/accounts/query_test.exs | 20 +++++++++++++++++-- .../admin_courses_controller_test.exs | 3 ++- 4 files changed, 34 insertions(+), 6 deletions(-) diff --git a/lib/cadet/assessments/assessments.ex b/lib/cadet/assessments/assessments.ex index 01389557b..f47c66d59 100644 --- a/lib/cadet/assessments/assessments.ex +++ b/lib/cadet/assessments/assessments.ex @@ -241,7 +241,8 @@ defmodule Cadet.Assessments do {q, nil, _} -> %{q | answer: %Answer{grader: nil}} {q, a, g} -> %{q | answer: %Answer{a | grader: g}} end) - # |> load_contest_voting_entries(cr.id) + + # |> load_contest_voting_entries(cr.id) assessment = Map.put(assessment, :questions, questions) {:ok, assessment} @@ -647,7 +648,12 @@ defmodule Cadet.Assessments do `{:bad_request, "Missing or invalid parameter(s)"}` """ - def answer_question(question = %Question{}, cr = %CourseRegistration{id: cr_id}, raw_answer, force_submit) do + def answer_question( + question = %Question{}, + cr = %CourseRegistration{id: cr_id}, + raw_answer, + force_submit + ) do with {:ok, submission} <- find_or_create_submission(cr, question.assessment), {:status, true} <- {:status, force_submit or submission.status != :submitted}, {:ok, _answer} <- insert_or_update_answer(submission, question, raw_answer, cr_id) do diff --git a/priv/repo/seeds.exs b/priv/repo/seeds.exs index 6b78f2d2a..b391a9f4d 100644 --- a/priv/repo/seeds.exs +++ b/priv/repo/seeds.exs @@ -56,9 +56,14 @@ if Cadet.Env.env() == :dev do # Assessments for i <- 1..5 do - type = insert(:assessment_type, %{type: "mission#{i}", order: i, course: course1}) + type = insert(:assessment_type, %{type: "Mission#{i}", order: i, course: course1}) + config = insert(:assessment_config, %{assessment_type: type}) assessment = insert(:assessment, %{is_published: true, type: type, course: course1}) + type2 = insert(:assessment_type, %{type: "Homework#{i}", order: i, course: course2}) + config2 = insert(:assessment_config, %{assessment_type: type2}) + assessment2 = insert(:assessment, %{is_published: true, type: type2, course: course2}) + programming_questions = insert_list(3, :programming_question, %{ assessment: assessment, diff --git a/test/cadet/accounts/query_test.exs b/test/cadet/accounts/query_test.exs index aa0e7e4a0..57c826809 100644 --- a/test/cadet/accounts/query_test.exs +++ b/test/cadet/accounts/query_test.exs @@ -21,11 +21,27 @@ defmodule Cadet.Accounts.QueryTest do course2 = insert(:course, course_name: "course 2") staff_a1 = insert(:course_registration, %{user: user_a, course: course1, role: :staff}) group1 = insert(:group, %{leader: staff_a1, course: course1}) - student_b1 = insert(:course_registration, %{user: user_b, course: course1, role: :student, group: group1}) + + student_b1 = + insert(:course_registration, %{ + user: user_b, + course: course1, + role: :student, + group: group1 + }) + student_c1 = insert(:course_registration, %{user: user_c, course: course1, role: :student}) staff_a2 = insert(:course_registration, %{user: user_a, course: course2, role: :staff}) - {:ok, %{c1: course1, c2: course2, sta_a1: staff_a1, stu_b1: student_b1, stu_c1: student_c1, sta_a2: staff_a2}} + {:ok, + %{ + c1: course1, + c2: course2, + sta_a1: staff_a1, + stu_b1: student_b1, + stu_c1: student_c1, + sta_a2: staff_a2 + }} end test "true, when in same course same group", %{sta_a1: sta_a1, stu_b1: stu_b1} do diff --git a/test/cadet_web/admin_controllers/admin_courses_controller_test.exs b/test/cadet_web/admin_controllers/admin_courses_controller_test.exs index f6c82600a..5dbc140e1 100644 --- a/test/cadet_web/admin_controllers/admin_courses_controller_test.exs +++ b/test/cadet_web/admin_controllers/admin_courses_controller_test.exs @@ -345,5 +345,6 @@ defmodule CadetWeb.AdminCoursesControllerTest do defp to_map(schema), do: Map.from_struct(schema) |> Map.drop([:updated_at]) - defp update_map(map1, params), do: Map.merge(map1, to_snake_case_atom_keys(params), fn _k, _v1, v2 -> v2 end) + defp update_map(map1, params), + do: Map.merge(map1, to_snake_case_atom_keys(params), fn _k, _v1, v2 -> v2 end) end From 897504b89c9bac0b335ce78c33935164f16419cb Mon Sep 17 00:00:00 2001 From: Chow En Rong <53928333+chownces@users.noreply.github.com> Date: Sat, 19 Jun 2021 22:18:06 +0800 Subject: [PATCH 068/174] Updated google claim extractor --- lib/cadet/auth/providers/google_claim_extractor.ex | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/lib/cadet/auth/providers/google_claim_extractor.ex b/lib/cadet/auth/providers/google_claim_extractor.ex index 25358b4e6..ab0d41a7a 100644 --- a/lib/cadet/auth/providers/google_claim_extractor.ex +++ b/lib/cadet/auth/providers/google_claim_extractor.ex @@ -13,7 +13,9 @@ defmodule Cadet.Auth.Providers.GoogleClaimExtractor do end end - def get_name(_claims), do: nil + def get_name(claims) do + claims["name"] + end def get_role(_claims), do: nil From 1caa711ec3dd9e9287500ee6b7fe62eb6f3479ca Mon Sep 17 00:00:00 2001 From: Chow En Rong <53928333+chownces@users.noreply.github.com> Date: Sat, 19 Jun 2021 22:18:19 +0800 Subject: [PATCH 069/174] Added github auth provider --- lib/cadet/auth/providers/github.ex | 76 ++++++++++++++++++++++++++++++ 1 file changed, 76 insertions(+) create mode 100644 lib/cadet/auth/providers/github.ex diff --git a/lib/cadet/auth/providers/github.ex b/lib/cadet/auth/providers/github.ex new file mode 100644 index 000000000..5b9f80e31 --- /dev/null +++ b/lib/cadet/auth/providers/github.ex @@ -0,0 +1,76 @@ +defmodule Cadet.Auth.Providers.GitHub do + @moduledoc """ + Provides identity using GitHub OAuth. + """ + alias Cadet.Auth.Provider + + @behaviour Provider + + @type config :: %{client_secret: String.t()} + + @token_url "https://github.com/login/oauth/access_token" + @user_api "https://api.github.com/user" + + @spec authorise(config(), Provider.code(), Provider.client_id(), Provider.redirect_uri()) :: + {:ok, %{token: Provider.token(), username: String.t()}} + | {:error, Provider.error(), String.t()} + def authorise(config, code, client_id, redirect_uri) do + token_query = + URI.encode_query(%{ + client_id: client_id, + client_secret: config.client_secret, + code: code, + redirect_uri: redirect_uri + }) + + token_headers = [ + {"Content-Type", "application/x-www-form-urlencoded"}, + {"Accept", "application/json"} + ] + + with {:token, {:ok, %{body: body, status_code: 200}}} <- + {:token, HTTPoison.post(@token_url, token_query, token_headers)}, + {:token_response, %{"access_token" => token}} <- {:token_response, Jason.decode!(body)}, + {:user, {:ok, %{"login" => username}}} <- {:user, api_call(@user_api, token)} do + {:ok, %{token: token, username: username}} + else + {:token, {:ok, %{status_code: status}}} -> + {:error, :upstream, "Status code #{status} from GitHub"} + + {:token_response, %{"error" => error}} -> + {:error, :invalid_credentials, "Error from GitHub: #{error}"} + + {:user, {:error, _, _} = error} -> + error + end + end + + @spec get_name(config(), Provider.token()) :: + {:ok, String.t()} | {:error, Provider.error(), String.t()} + def get_name(_, token) do + case api_call(@user_api, token) do + {:ok, %{"name" => name}} -> + {:ok, name} + + {:error, _, _} = error -> + error + end + end + + def get_role(_config, _claims) do + # There is no role specified for the GitHub provider + {:error, :invalid_credentials, "No role specified in token"} + end + + defp api_call(url, token) do + headers = [{"Authorization", "token " <> token}] + + case HTTPoison.get(url, headers) do + {:ok, %{body: body, status_code: 200}} -> + {:ok, Jason.decode!(body)} + + {:ok, %{status_code: status}} -> + {:error, :upstream, "Status code #{status} from GitHub"} + end + end +end From 44978d962abe9640e25344c76f429d98efe3300a Mon Sep 17 00:00:00 2001 From: Chow En Rong <53928333+chownces@users.noreply.github.com> Date: Sat, 19 Jun 2021 22:18:42 +0800 Subject: [PATCH 070/174] Namespacing for different auth providers --- lib/cadet/auth/provider.ex | 7 +++++++ lib/cadet/auth/providers/config.ex | 7 +++++-- lib/cadet/auth/providers/github.ex | 2 +- lib/cadet/auth/providers/luminus.ex | 2 +- lib/cadet/auth/providers/openid.ex | 11 +++++++++-- 5 files changed, 23 insertions(+), 6 deletions(-) diff --git a/lib/cadet/auth/provider.ex b/lib/cadet/auth/provider.ex index 3901c3c4c..3f916e5b5 100644 --- a/lib/cadet/auth/provider.ex +++ b/lib/cadet/auth/provider.ex @@ -13,6 +13,8 @@ defmodule Cadet.Auth.Provider do @type redirect_uri :: String.t() @type error :: :upstream | :invalid_credentials | :other @type provider_instance :: String.t() + @type username :: String.t() + @type prefix :: String.t() @doc "Exchanges the OAuth2 authorisation code for a token and the user ID." @callback authorise(any(), code, client_id, redirect_uri) :: @@ -53,4 +55,9 @@ defmodule Cadet.Auth.Provider do _ -> {:error, :other, "Invalid or nonexistent provider config"} end end + + @spec namespace(username, prefix) :: String.t() + def namespace(username, prefix) do + prefix <> "/" <> username + end end diff --git a/lib/cadet/auth/providers/config.ex b/lib/cadet/auth/providers/config.ex index 1d64d1140..d322f8dc0 100644 --- a/lib/cadet/auth/providers/config.ex +++ b/lib/cadet/auth/providers/config.ex @@ -20,8 +20,11 @@ defmodule Cadet.Auth.Providers.Config do | {:error, Provider.error(), String.t()} def authorise(config, code, _client_id, _redirect_uri) do case Enum.find(config, nil, fn %{code: this_code} -> code == this_code end) do - %{token: token, username: username} -> {:ok, %{token: token, username: username}} - _ -> {:error, :invalid_credentials, "Invalid code"} + %{token: token, username: username} -> + {:ok, %{token: token, username: Provider.namespace(username, "test")}} + + _ -> + {:error, :invalid_credentials, "Invalid code"} end end diff --git a/lib/cadet/auth/providers/github.ex b/lib/cadet/auth/providers/github.ex index 5b9f80e31..9dca7e515 100644 --- a/lib/cadet/auth/providers/github.ex +++ b/lib/cadet/auth/providers/github.ex @@ -32,7 +32,7 @@ defmodule Cadet.Auth.Providers.GitHub do {:token, HTTPoison.post(@token_url, token_query, token_headers)}, {:token_response, %{"access_token" => token}} <- {:token_response, Jason.decode!(body)}, {:user, {:ok, %{"login" => username}}} <- {:user, api_call(@user_api, token)} do - {:ok, %{token: token, username: username}} + {:ok, %{token: token, username: Provider.namespace(username, "github")}} else {:token, {:ok, %{status_code: status}}} -> {:error, :upstream, "Status code #{status} from GitHub"} diff --git a/lib/cadet/auth/providers/luminus.ex b/lib/cadet/auth/providers/luminus.ex index 627f4a1a4..7caa73d89 100644 --- a/lib/cadet/auth/providers/luminus.ex +++ b/lib/cadet/auth/providers/luminus.ex @@ -36,7 +36,7 @@ defmodule Cadet.Auth.Providers.LumiNUS do {:verify_jwt, {:ok, _}} <- {:verify_jwt, Guardian.Token.Jwt.Verify.verify_claims(Cadet.Auth.EmptyGuardian, claims, nil)} do - {:ok, %{token: token, username: username}} + {:ok, %{token: token, username: Provider.namespace(username, "luminus")}} else {:token, {:ok, %{body: body, status_code: status}}} -> {:error, :upstream, "Status code #{status} from LumiNUS: #{body}"} diff --git a/lib/cadet/auth/providers/openid.ex b/lib/cadet/auth/providers/openid.ex index 77a4f9410..1d16c6088 100644 --- a/lib/cadet/auth/providers/openid.ex +++ b/lib/cadet/auth/providers/openid.ex @@ -30,8 +30,15 @@ defmodule Cadet.Auth.Providers.OpenID do nil )} do case claim_extractor.get_username(claims) do - nil -> {:error, :invalid_credentials, "No username specified in token"} - username -> {:ok, %{token: token, username: username}} + nil -> + {:error, :invalid_credentials, "No username specified in token"} + + username -> + {:ok, + %{ + token: token, + username: Provider.namespace(username, Atom.to_string(openid_provider)) + }} end else {:token, {:error, _, _}} -> From dcc506f17d158f62ea5e2d7856259ef8cace66c8 Mon Sep 17 00:00:00 2001 From: Chow En Rong <53928333+chownces@users.noreply.github.com> Date: Sat, 19 Jun 2021 22:27:19 +0800 Subject: [PATCH 071/174] Updated dev.secrets.exs.example --- config/dev.secrets.exs.example | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/config/dev.secrets.exs.example b/config/dev.secrets.exs.example index 94801fc3c..310bd8081 100644 --- a/config/dev.secrets.exs.example +++ b/config/dev.secrets.exs.example @@ -26,6 +26,12 @@ config :cadet, # # You may need to write your own claim extractor for other providers # claim_extractor: Cadet.Auth.Providers.CognitoClaimExtractor # }}, + # # To use authentication with GitHub + # "github" => + # {Cadet.Auth.Providers.GitHub, + # %{ + # client_secret: "" + # }}, "test" => {Cadet.Auth.Providers.Config, [ From 0554b047fe0a12afb7763bca073528d7d5287228 Mon Sep 17 00:00:00 2001 From: Chow En Rong <53928333+chownces@users.noreply.github.com> Date: Sun, 20 Jun 2021 12:29:44 +0800 Subject: [PATCH 072/174] Updated seed --- test/factories/accounts/user_factory.ex | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/factories/accounts/user_factory.ex b/test/factories/accounts/user_factory.ex index ebc2950ff..d51abc482 100644 --- a/test/factories/accounts/user_factory.ex +++ b/test/factories/accounts/user_factory.ex @@ -14,7 +14,7 @@ defmodule Cadet.Accounts.UserFactory do username: sequence( :nusnet_id, - &"E#{&1 |> Integer.to_string() |> String.pad_leading(7, "0")}" + &"test/E#{&1 |> Integer.to_string() |> String.pad_leading(7, "0")}" ), game_states: %{} } @@ -27,7 +27,7 @@ defmodule Cadet.Accounts.UserFactory do username: sequence( :nusnet_id, - &"E#{&1 |> Integer.to_string() |> String.pad_leading(7, "0")}" + &"test/E#{&1 |> Integer.to_string() |> String.pad_leading(7, "0")}" ), game_states: %{} } From c170b6576a6a888330ec096162a44bb1928aad64 Mon Sep 17 00:00:00 2001 From: Chow En Rong <53928333+chownces@users.noreply.github.com> Date: Sun, 20 Jun 2021 13:23:43 +0800 Subject: [PATCH 073/174] Update github auth config --- config/dev.secrets.exs.example | 3 ++- lib/cadet/auth/providers/github.ex | 23 ++++++++++++-------- lib/cadet_web/controllers/auth_controller.ex | 2 +- 3 files changed, 17 insertions(+), 11 deletions(-) diff --git a/config/dev.secrets.exs.example b/config/dev.secrets.exs.example index 310bd8081..f27245f54 100644 --- a/config/dev.secrets.exs.example +++ b/config/dev.secrets.exs.example @@ -29,8 +29,9 @@ config :cadet, # # To use authentication with GitHub # "github" => # {Cadet.Auth.Providers.GitHub, + # # A map of GitHub client_id => client_secret # %{ - # client_secret: "" + # "client_id": "client_secret" # }}, "test" => {Cadet.Auth.Providers.Config, diff --git a/lib/cadet/auth/providers/github.ex b/lib/cadet/auth/providers/github.ex index 9dca7e515..a15353f10 100644 --- a/lib/cadet/auth/providers/github.ex +++ b/lib/cadet/auth/providers/github.ex @@ -15,25 +15,30 @@ defmodule Cadet.Auth.Providers.GitHub do {:ok, %{token: Provider.token(), username: String.t()}} | {:error, Provider.error(), String.t()} def authorise(config, code, client_id, redirect_uri) do - token_query = - URI.encode_query(%{ - client_id: client_id, - client_secret: config.client_secret, - code: code, - redirect_uri: redirect_uri - }) - token_headers = [ {"Content-Type", "application/x-www-form-urlencoded"}, {"Accept", "application/json"} ] - with {:token, {:ok, %{body: body, status_code: 200}}} <- + with {:validate_client, {:ok, client_secret}} <- + {:validate_client, Map.fetch(config, client_id)}, + {:token_query, token_query} <- + {:token_query, + URI.encode_query(%{ + client_id: client_id, + client_secret: client_secret, + code: code, + redirect_uri: redirect_uri + })}, + {:token, {:ok, %{body: body, status_code: 200}}} <- {:token, HTTPoison.post(@token_url, token_query, token_headers)}, {:token_response, %{"access_token" => token}} <- {:token_response, Jason.decode!(body)}, {:user, {:ok, %{"login" => username}}} <- {:user, api_call(@user_api, token)} do {:ok, %{token: token, username: Provider.namespace(username, "github")}} else + {:validate_client, :error} -> + {:error, :invalid_credentials, "Invalid client id"} + {:token, {:ok, %{status_code: status}}} -> {:error, :upstream, "Status code #{status} from GitHub"} diff --git a/lib/cadet_web/controllers/auth_controller.ex b/lib/cadet_web/controllers/auth_controller.ex index 8571db6cf..3b589274c 100644 --- a/lib/cadet_web/controllers/auth_controller.ex +++ b/lib/cadet_web/controllers/auth_controller.ex @@ -34,7 +34,7 @@ defmodule CadetWeb.AuthController do {:authorise, {:error, :upstream, reason}} -> conn |> put_status(:bad_request) - |> text("Unable to retrieve token from ADFS: #{reason}") + |> text("Unable to retrieve token from authentication provider: #{reason}") {:authorise, {:error, :invalid_credentials, reason}} -> conn From 8886589a1b8a54b7738940b0176fead0ce9633cd Mon Sep 17 00:00:00 2001 From: Chow En Rong <53928333+chownces@users.noreply.github.com> Date: Sun, 20 Jun 2021 13:36:23 +0800 Subject: [PATCH 074/174] Update user seeds --- priv/repo/seeds.exs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/priv/repo/seeds.exs b/priv/repo/seeds.exs index b391a9f4d..c6c756456 100644 --- a/priv/repo/seeds.exs +++ b/priv/repo/seeds.exs @@ -21,15 +21,15 @@ if Cadet.Env.env() == :dev do course1 = insert(:course) course2 = insert(:course, %{course_name: "Algorithm", course_short_name: "CS2040S"}) # Users - avenger1 = insert(:user, %{name: "avenger", username: "E1234561", latest_viewed: course1}) - mentor1 = insert(:user, %{name: "mentor", username: "E1234562", latest_viewed: course1}) - admin1 = insert(:user, %{name: "admin", username: "E1234563", latest_viewed: course1}) + avenger1 = insert(:user, %{name: "avenger", latest_viewed: course1}) + mentor1 = insert(:user, %{name: "mentor", latest_viewed: course1}) + admin1 = insert(:user, %{name: "admin", latest_viewed: course1}) studenta1admin2 = - insert(:user, %{name: "student a", username: "E1234564", latest_viewed: course1}) + insert(:user, %{name: "student a", latest_viewed: course1}) - studentb1 = insert(:user, %{username: "E1234565", latest_viewed: course1}) - studentc1 = insert(:user, %{username: "E1234566", latest_viewed: course1}) + studentb1 = insert(:user, %{latest_viewed: course1}) + studentc1 = insert(:user, %{latest_viewed: course1}) # CourseRegistration and Group avenger1_cr = insert(:course_registration, %{user: avenger1, course: course1, role: :staff}) mentor1_cr = insert(:course_registration, %{user: mentor1, course: course1, role: :staff}) From ded4536040499affb94c9bcf2cd25c0128b647d9 Mon Sep 17 00:00:00 2001 From: YaleChen299 Date: Sun, 20 Jun 2021 14:20:58 +0800 Subject: [PATCH 075/174] added get assessment config --- lib/cadet/courses/courses.ex | 9 +++ .../admin_courses_controller.ex | 5 ++ .../admin_views/admin_courses_view.ex | 18 ++++++ lib/cadet_web/router.ex | 1 + test/cadet/courses/courses_test.exs | 17 ++++++ .../admin_courses_controller_test.exs | 60 +++++++++++++++++++ 6 files changed, 110 insertions(+) create mode 100644 lib/cadet_web/admin_views/admin_courses_view.ex diff --git a/lib/cadet/courses/courses.ex b/lib/cadet/courses/courses.ex index c70018b58..98595c4c7 100644 --- a/lib/cadet/courses/courses.ex +++ b/lib/cadet/courses/courses.ex @@ -63,6 +63,15 @@ defmodule Cadet.Courses do |> Repo.one() end + def get_assessment_configs(course_id) when is_ecto_id(course_id) do + AssessmentConfig + |> join(:inner, [ac], at in assoc(ac, :assessment_type)) + |> where([ac, at], at.course_id == ^course_id) + |> preload(:assessment_type) + |> order_by([ac, at], at.order) + |> Repo.all() + end + @doc """ Updates the assessment configuration for the specified course """ diff --git a/lib/cadet_web/admin_controllers/admin_courses_controller.ex b/lib/cadet_web/admin_controllers/admin_courses_controller.ex index 10b6e7a87..17b890503 100644 --- a/lib/cadet_web/admin_controllers/admin_courses_controller.ex +++ b/lib/cadet_web/admin_controllers/admin_courses_controller.ex @@ -29,6 +29,11 @@ defmodule CadetWeb.AdminCoursesController do end end + def get_assessment_configs(conn, %{"course_id" => course_id}) when is_ecto_id(course_id) do + assessment_configs = Courses.get_assessment_configs(course_id) + render(conn, "assessment_configs.json", %{configs: assessment_configs}) + end + def update_assessment_config(conn, %{ "course_id" => course_id, "order" => order, diff --git a/lib/cadet_web/admin_views/admin_courses_view.ex b/lib/cadet_web/admin_views/admin_courses_view.ex new file mode 100644 index 000000000..6686ccac3 --- /dev/null +++ b/lib/cadet_web/admin_views/admin_courses_view.ex @@ -0,0 +1,18 @@ +defmodule CadetWeb.AdminCoursesView do + use CadetWeb, :view + + def render("assessment_configs.json", %{configs: configs}) do + render_many(configs, CadetWeb.AdminCoursesView, "config.json", as: :config) + end + + def render("config.json", %{config: config}) do + transform_map_for_view(config, %{ + order: &(&1.assessment_type.order), + type: &(&1.assessment_type.type), + isGraded: &(&1.assessment_type.is_graded), + decayRatePointsPerHour: :decay_rate_points_per_hour, + earlySubmissionXp: :early_submission_xp, + hoursBeforeEarlyXpDecay: :hours_before_early_xp_decay + }) + end +end diff --git a/lib/cadet_web/router.ex b/lib/cadet_web/router.ex index 1fee647e3..70bdfa5fd 100644 --- a/lib/cadet_web/router.ex +++ b/lib/cadet_web/router.ex @@ -139,6 +139,7 @@ defmodule CadetWeb.Router do delete("/goals/:uuid", AdminGoalsController, :delete) put("/course_config", AdminCoursesController, :update_course_config) + get("/assessment_configs", AdminCoursesController, :get_assessment_configs) put("/assessment_config", AdminCoursesController, :update_assessment_config) put("/assessment_types", AdminCoursesController, :update_assessment_types) end diff --git a/test/cadet/courses/courses_test.exs b/test/cadet/courses/courses_test.exs index ed706c558..ec7e96e4c 100644 --- a/test/cadet/courses/courses_test.exs +++ b/test/cadet/courses/courses_test.exs @@ -114,6 +114,23 @@ defmodule Cadet.CoursesTest do end end + describe "get assessment configs" do + test "succeeds" do + course = insert(:course) + for i <- 1..5 do + type = insert(:assessment_type, %{order: 6-i, type: "Mission#{i}", course: course}) + insert(:assessment_config, %{decay_rate_points_per_hour: i , assessment_type: type}) + end + + assessment_configs = Courses.get_assessment_configs(course.id) + + # This test that the assessment_type is preloaded and is ordered by order + for i <- 1..5 do + assert Enum.at(assessment_configs, i-1).assessment_type.order == i + end + end + end + describe "update assessment config" do test "succeeds" do course = insert(:course) diff --git a/test/cadet_web/admin_controllers/admin_courses_controller_test.exs b/test/cadet_web/admin_controllers/admin_courses_controller_test.exs index 5dbc140e1..2a58e243c 100644 --- a/test/cadet_web/admin_controllers/admin_courses_controller_test.exs +++ b/test/cadet_web/admin_controllers/admin_courses_controller_test.exs @@ -141,6 +141,66 @@ defmodule CadetWeb.AdminCoursesControllerTest do end end + describe "GET /v2/course/{course_id}/admin/assessment_configs" do + @tag authenticate: :admin + test "succeeds", %{conn: conn} do + course_id = conn.assigns[:course_id] + course = Repo.get(Course, course_id) + + type1 = insert(:assessment_type, %{order: 1, type: "Mission1", course: course}) + insert(:assessment_config, %{assessment_type: type1}) + + type3 = insert(:assessment_type, %{order: 3, type: "Mission3", course: course}) + insert(:assessment_config, %{assessment_type: type3}) + + type2 = insert(:assessment_type, %{is_graded: false, order: 2, type: "Mission2", course: course}) + insert(:assessment_config, %{assessment_type: type2}) + + resp = + conn + |> get(build_url_assessment_config(course_id) <> "s") + |> json_response(200) + + expected = [ + %{ + "decayRatePointsPerHour" => 1, + "earlySubmissionXp" => 200, + "hoursBeforeEarlyXpDecay" => 48, + "isGraded" => true, + "order" => 1, + "type" => "Mission1" + }, + %{ + "decayRatePointsPerHour" => 1, + "earlySubmissionXp" => 200, + "hoursBeforeEarlyXpDecay" => 48, + "isGraded" => false, + "order" => 2, + "type" => "Mission2" + }, + %{ + "decayRatePointsPerHour" => 1, + "earlySubmissionXp" => 200, + "hoursBeforeEarlyXpDecay" => 48, + "isGraded" => true, + "order" => 3, + "type" => "Mission3" + } + ] + + assert expected == resp + end + + @tag authenticate: :student + test "rejects forbidden request for non-staff users", %{conn: conn} do + course_id = conn.assigns[:course_id] + + resp = get(conn, build_url_assessment_config(course_id) <> "s") + + assert response(resp, 403) == "Forbidden" + end + end + describe "PUT /v2/course/{course_id}/admin/assessment_config" do @tag authenticate: :admin test "succeeds", %{conn: conn} do From f8f496b8266d3b2a269436a26882227fc7c19cc6 Mon Sep 17 00:00:00 2001 From: YaleChen299 Date: Sun, 20 Jun 2021 23:07:26 +0800 Subject: [PATCH 076/174] updated config route, combine assessment config table --- lib/cadet/accounts/course_registrations.ex | 16 +- lib/cadet/assessments/assessment.ex | 31 +- lib/cadet/assessments/assessments.ex | 4 +- lib/cadet/courses/assessment_config.ex | 35 +- lib/cadet/courses/assessment_type.ex | 38 -- lib/cadet/courses/course.ex | 4 +- lib/cadet/courses/courses.ex | 99 +++-- .../admin_courses_controller.ex | 87 ++--- .../admin_grading_controller.ex | 2 +- .../admin_user_controller.ex | 2 +- .../admin_views/admin_courses_view.ex | 36 +- .../admin_views/admin_grading_view.ex | 4 +- .../controllers/assessments_controller.ex | 8 +- .../controllers/courses_controller.ex | 2 +- .../controllers/stories_controller.ex | 8 +- lib/cadet_web/controllers/user_controller.ex | 2 +- lib/cadet_web/router.ex | 13 +- lib/cadet_web/views/assessments_helpers.ex | 28 +- lib/cadet_web/views/courses_view.ex | 2 +- lib/cadet_web/views/user_view.ex | 2 +- ...0210531155751_add_course_configuration.exs | 11 +- .../20210608085548_update_assessments.exs | 2 +- priv/repo/seeds.exs | 13 +- .../accounts/course_registration_test.exs | 29 +- test/cadet/assessments/assessment_test.exs | 51 +-- test/cadet/assessments/submission_test.exs | 4 +- test/cadet/courses/assessment_config_test.exs | 68 ++-- test/cadet/courses/assessment_type_test.exs | 28 -- test/cadet/courses/courses_test.exs | 337 ++++++++---------- .../admin_courses_controller_test.exs | 216 +++-------- .../admin_user_controller_test.exs | 4 +- .../controllers/courses_controller_test.exs | 14 +- .../sourcecast_controller_test.exs | 20 +- .../controllers/stories_controller_test.exs | 18 +- .../controllers/user_controller_test.exs | 14 +- .../assessments/assessment_factory.ex | 7 +- .../courses/assessment_config_factory.ex | 4 +- .../courses/assessment_type_factory.ex | 19 - test/factories/factory.ex | 1 - 39 files changed, 501 insertions(+), 782 deletions(-) delete mode 100644 lib/cadet/courses/assessment_type.ex delete mode 100644 test/cadet/courses/assessment_type_test.exs delete mode 100644 test/factories/courses/assessment_type_factory.ex diff --git a/lib/cadet/accounts/course_registrations.ex b/lib/cadet/accounts/course_registrations.ex index b7b6b70c3..6b66a6620 100644 --- a/lib/cadet/accounts/course_registrations.ex +++ b/lib/cadet/accounts/course_registrations.ex @@ -8,7 +8,7 @@ defmodule Cadet.Accounts.CourseRegistrations do alias Cadet.Repo alias Cadet.Accounts.{User, CourseRegistration} - alias Cadet.Courses.AssessmentType + alias Cadet.Courses.AssessmentConfig # guide # only join with User if need name or user name @@ -30,9 +30,9 @@ defmodule Cadet.Accounts.CourseRegistrations do |> where([cr], cr.user_id == ^user_id) |> where([cr], cr.course_id == ^course_id) |> join(:inner, [cr], c in assoc(cr, :course)) - |> join(:left, [cr, c], t in assoc(c, :assessment_type)) - |> preload([cr, c, t], - course: {c, assessment_type: ^from(t in AssessmentType, order_by: [asc: t.order])} + |> join(:left, [cr, c], ac in assoc(c, :assessment_config)) + |> preload([cr, c, ac], + course: {c, assessment_config: ^from(ac in AssessmentConfig, order_by: [asc: ac.order])} ) |> preload(:group) |> Repo.one() @@ -87,7 +87,8 @@ defmodule Cadet.Accounts.CourseRegistrations do |> Repo.insert_or_update() end - @spec delete_record(map()) :: {:ok, Ecto.Schema.t()} | {:error, Ecto.Changeset.t()} + @spec delete_record(map()) :: + {:ok, Ecto.Schema.t()} | {:error, Ecto.Changeset.t()} | {:error, :no_such_enrty} def delete_record(params = %{user_id: user_id, course_id: course_id}) when is_ecto_id(user_id) and is_ecto_id(course_id) do CourseRegistration @@ -95,10 +96,9 @@ defmodule Cadet.Accounts.CourseRegistrations do |> where(course_id: ^course_id) |> Repo.one() |> case do - nil -> CourseRegistration.changeset(%CourseRegistration{}, params) - cr -> CourseRegistration.changeset(cr, params) + nil -> {:error, :no_such_enrty} + cr -> CourseRegistration.changeset(cr, params) |> Repo.delete() end - |> Repo.delete() end def update_game_states(cr = %CourseRegistration{}, new_game_state = %{}) do diff --git a/lib/cadet/assessments/assessment.ex b/lib/cadet/assessments/assessment.ex index bc3266fc1..da1b2e5c0 100644 --- a/lib/cadet/assessments/assessment.ex +++ b/lib/cadet/assessments/assessment.ex @@ -8,15 +8,10 @@ defmodule Cadet.Assessments.Assessment do alias Cadet.Repo alias Cadet.Assessments.{AssessmentAccess, Question, SubmissionStatus, Upload} - alias Cadet.Courses.{Course, AssessmentType} - - # @assessment_types ~w(contest mission path practical sidequest) - # def assessment_types, do: @assessment_types + alias Cadet.Courses.{Course, AssessmentConfig} schema "assessments" do field(:access, AssessmentAccess, virtual: true, default: :public) - # field(:max_grade, :integer, virtual: true) - # field(:grade, :integer, virtual: true, default: 0) field(:max_xp, :integer, virtual: true) field(:xp, :integer, virtual: true, default: 0) field(:user_status, SubmissionStatus, virtual: true) @@ -36,14 +31,14 @@ defmodule Cadet.Assessments.Assessment do field(:reading, :string) field(:password, :string, default: nil) - belongs_to(:type, AssessmentType) + belongs_to(:config, AssessmentConfig) belongs_to(:course, Course) has_many(:questions, Question, on_delete: :delete_all) timestamps() end - @required_fields ~w(title open_at close_at number course_id type_id)a + @required_fields ~w(title open_at close_at number course_id config_id)a @optional_fields ~w(reading summary_short summary_long is_published story cover_picture access password)a @optional_file_fields ~w(mission_pdf)a @@ -58,26 +53,26 @@ defmodule Cadet.Assessments.Assessment do |> cast_attachments(params, @optional_file_fields) |> cast(params, @required_fields ++ @optional_fields) |> validate_required(@required_fields) - |> add_belongs_to_id_from_model([:type, :course], params) - |> foreign_key_constraint(:type_id) + |> add_belongs_to_id_from_model([:config, :course], params) + |> foreign_key_constraint(:config_id) |> foreign_key_constraint(:course_id) - |> validate_type_course + |> validate_config_course |> validate_open_close_date end - defp validate_type_course(changeset) do - type_id = get_field(changeset, :type_id) + defp validate_config_course(changeset) do + config_id = get_field(changeset, :config_id) course_id = get_field(changeset, :course_id) - case Repo.get(AssessmentType, type_id) do + case Repo.get(AssessmentConfig, config_id) do nil -> - add_error(changeset, :type, "does not exist") + add_error(changeset, :config, "does not exist") - type -> - if type.course_id == course_id do + config -> + if config.course_id == course_id do changeset else - add_error(changeset, :type, "does not belong to the same course as this assessment") + add_error(changeset, :config, "does not belong to the same course as this assessment") end end end diff --git a/lib/cadet/assessments/assessments.ex b/lib/cadet/assessments/assessments.ex index f47c66d59..19a4b07ab 100644 --- a/lib/cadet/assessments/assessments.ex +++ b/lib/cadet/assessments/assessments.ex @@ -150,7 +150,7 @@ defmodule Cadet.Assessments do |> where([a], a.open_at <= from_now(0, "second") and a.close_at >= from_now(0, "second")) |> join(:left, [a], s in Submission, on: s.assessment_id == a.id and s.student_id == ^cr_id) |> filter_and_sort.() - |> order_by([a], a.type_id) + |> order_by([a], a.config_id) |> select([a], a.story) |> first() |> Repo.one() @@ -295,7 +295,7 @@ defmodule Cadet.Assessments do }) |> filter_published_assessments(cr) |> order_by(:open_at) - |> preload(:type) + |> preload(:config) |> Repo.all() {:ok, assessments} diff --git a/lib/cadet/courses/assessment_config.ex b/lib/cadet/courses/assessment_config.ex index 97f5c94ea..edfc9a1fb 100644 --- a/lib/cadet/courses/assessment_config.ex +++ b/lib/cadet/courses/assessment_config.ex @@ -1,34 +1,47 @@ defmodule Cadet.Courses.AssessmentConfig do @moduledoc """ - The AssessmentConfig entity stores the assessment configuration - of a particular course. + The AssessmentConfig entity stores the assessment tyoes in a + particular course. """ use Cadet, :model - alias Cadet.Courses.AssessmentType + alias Cadet.Courses.Course schema "assessment_configs" do - field(:early_submission_xp, :integer) - field(:hours_before_early_xp_decay, :integer) - field(:decay_rate_points_per_hour, :integer) - belongs_to(:assessment_type, AssessmentType) + field(:order, :integer) + field(:type, :string) + field(:is_graded, :boolean, default: true) + field(:early_submission_xp, :integer, default: 0) + field(:hours_before_early_xp_decay, :integer, default: 0) + field(:decay_rate_points_per_hour, :integer, default: 0) + + belongs_to(:course, Course) timestamps() end - @required_fields ~w(early_submission_xp hours_before_early_xp_decay - decay_rate_points_per_hour)a - @optional_fields ~w(assessment_type_id)a + @required_fields ~w(order course_id)a + @optional_fields ~w(type early_submission_xp hours_before_early_xp_decay + decay_rate_points_per_hour is_graded)a def changeset(assessment_config, params) do + params = capitalize(params, :type) + assessment_config |> cast(params, @required_fields ++ @optional_fields) |> validate_required(@required_fields) + |> validate_number(:order, greater_than: 0) + |> validate_number(:order, less_than_or_equal_to: 5) |> validate_number(:early_submission_xp, greater_than_or_equal_to: 0) |> validate_number(:hours_before_early_xp_decay, greater_than_or_equal_to: 0) |> validate_number(:decay_rate_points_per_hour, greater_than_or_equal_to: 0) - |> foreign_key_constraint(:assessment_type_id) |> validate_decay_rate() + |> unique_constraint([:type, :course_id]) + |> unique_constraint([:order, :course_id]) + end + + defp capitalize(params, field) do + Map.update(params, field, nil, &String.capitalize/1) end defp validate_decay_rate(changeset) do diff --git a/lib/cadet/courses/assessment_type.ex b/lib/cadet/courses/assessment_type.ex deleted file mode 100644 index 2039c87a3..000000000 --- a/lib/cadet/courses/assessment_type.ex +++ /dev/null @@ -1,38 +0,0 @@ -defmodule Cadet.Courses.AssessmentType do - @moduledoc """ - The AssessmentType entity stores the assessment tyoes in a - particular course. - """ - use Cadet, :model - - alias Cadet.Courses.{Course, AssessmentConfig} - - schema "assessment_types" do - field(:order, :integer) - field(:type, :string) - field(:is_graded, :boolean, default: true) - - belongs_to(:course, Course) - has_one(:assessment_config, AssessmentConfig) - - timestamps() - end - - @required_fields ~w(order type course_id is_graded)a - - def changeset(assessment_type, params) do - params = capitalize(params, :type) - - assessment_type - |> cast(params, @required_fields) - |> validate_required(@required_fields) - |> validate_number(:order, greater_than: 0) - |> validate_number(:order, less_than_or_equal_to: 5) - |> unique_constraint([:type, :course_id]) - |> unique_constraint([:order, :course_id]) - end - - defp capitalize(params, field) do - Map.update(params, field, nil, &String.capitalize/1) - end -end diff --git a/lib/cadet/courses/course.ex b/lib/cadet/courses/course.ex index d0191f4ef..88290f71f 100644 --- a/lib/cadet/courses/course.ex +++ b/lib/cadet/courses/course.ex @@ -4,7 +4,7 @@ defmodule Cadet.Courses.Course do """ use Cadet, :model - alias Cadet.Courses.AssessmentType + alias Cadet.Courses.AssessmentConfig schema "courses" do field(:course_name, :string) @@ -17,7 +17,7 @@ defmodule Cadet.Courses.Course do field(:source_variant, :string) field(:module_help_text, :string) - has_many(:assessment_type, AssessmentType) + has_many(:assessment_config, AssessmentConfig) timestamps() end diff --git a/lib/cadet/courses/courses.ex b/lib/cadet/courses/courses.ex index 98595c4c7..c8d29aac3 100644 --- a/lib/cadet/courses/courses.ex +++ b/lib/cadet/courses/courses.ex @@ -11,7 +11,6 @@ defmodule Cadet.Courses do alias Cadet.Courses.{ AssessmentConfig, - AssessmentType, Course, Group, Sourcecast, @@ -29,14 +28,14 @@ defmodule Cadet.Courses do {:error, {:bad_request, "Invalid course id"}} course -> - assessment_types = - AssessmentType + assessment_configs = + AssessmentConfig |> where(course_id: ^course_id) |> Repo.all() |> Enum.sort(&(&1.order < &2.order)) |> Enum.map(& &1.type) - {:ok, Map.put_new(course, :assessment_types, assessment_types)} + {:ok, Map.put_new(course, :assessment_configs, assessment_configs)} end end @@ -65,76 +64,62 @@ defmodule Cadet.Courses do def get_assessment_configs(course_id) when is_ecto_id(course_id) do AssessmentConfig - |> join(:inner, [ac], at in assoc(ac, :assessment_type)) - |> where([ac, at], at.course_id == ^course_id) - |> preload(:assessment_type) - |> order_by([ac, at], at.order) + |> where([at], at.course_id == ^course_id) + |> order_by(:order) |> Repo.all() end - @doc """ - Updates the assessment configuration for the specified course - """ - @spec update_assessment_config(integer, integer, integer, integer, integer) :: - {:ok, %AssessmentConfig{}} | {:error, Ecto.Changeset.t()} - def update_assessment_config(course_id, order, early_xp, hours_before_decay, decay_rate) do - AssessmentConfig - |> join(:inner, [ac], at in AssessmentType, on: at.order == ^order) - |> where([ac, at], at.course_id == ^course_id) - |> Repo.one() - |> AssessmentConfig.changeset(%{ - early_submission_xp: early_xp, - hours_before_early_xp_decay: hours_before_decay, - decay_rate_points_per_hour: decay_rate - }) - |> Repo.update() - end - - @doc """ - Updates the Assessment Types for the specified course - """ - @spec update_assessment_types(integer, list()) :: :ok | {:error, {:bad_request, String.t()}} - def update_assessment_types(course_id, params) do - if not is_list(params) do + def mass_upsert_or_delete_assessment_configs(course_id, configs) do + if not is_list(configs) do {:error, {:bad_request, "Invalid parameter(s)"}} else - params_length = params |> length() + configs_length = configs |> length() - with true <- params_length <= 5, - true <- params_length >= 1, - true <- params |> Enum.reduce(true, fn elem, acc -> acc and is_binary(elem) end), + with true <- configs_length <= 5, + true <- configs_length >= 1, true <- - params |> Enum.map(fn elem -> String.capitalize(elem) end) |> Enum.uniq() |> length() === - params_length do - (params ++ List.duplicate(nil, 5 - params_length)) + configs + |> Enum.with_index(1) + |> Enum.all?(fn {elem, i} -> Map.has_key?(elem, :order) && elem.order == i end) do + (configs ++ List.duplicate(nil, 5 - configs_length)) |> Enum.with_index(1) |> Enum.each(fn {elem, idx} -> case elem do - nil -> - AssessmentType - |> where(course_id: ^course_id) - |> where(order: ^idx) - |> Repo.delete_all() - - _ -> - AssessmentType.changeset(%AssessmentType{}, %{ - course_id: course_id, - order: idx, - type: elem - }) - |> Repo.insert( - on_conflict: {:replace, [:type]}, - conflict_target: [:course_id, :order] - ) + nil -> delete_assessment_config(%{course_id: course_id, order: idx}) + elem -> insert_or_update_assessment_config(elem) end end) else - false -> - {:error, {:bad_request, "Invalid parameter(s)"}} + false -> {:error, {:bad_request, "Invalid parameter(s)"}} end end end + def insert_or_update_assessment_config(params = %{course_id: course_id, order: order}) do + AssessmentConfig + |> where(course_id: ^course_id) + |> where(order: ^order) + |> Repo.one() + |> case do + nil -> AssessmentConfig.changeset(%AssessmentConfig{}, params) + at -> AssessmentConfig.changeset(at, params) + end + |> Repo.insert_or_update() + end + + @spec delete_assessment_config(map()) :: + {:ok, Ecto.Schema.t()} | {:error, Ecto.Changeset.t()} | {:error, :no_such_enrty} + def delete_assessment_config(params = %{course_id: course_id, order: order}) do + AssessmentConfig + |> where(course_id: ^course_id) + |> where(order: ^order) + |> Repo.one() + |> case do + nil -> {:error, :no_such_enrty} + at -> AssessmentConfig.changeset(at, params) |> Repo.delete() + end + end + @doc """ Get a group based on the group name or create one if it doesn't exist """ diff --git a/lib/cadet_web/admin_controllers/admin_courses_controller.ex b/lib/cadet_web/admin_controllers/admin_courses_controller.ex index 17b890503..1dfab462e 100644 --- a/lib/cadet_web/admin_controllers/admin_courses_controller.ex +++ b/lib/cadet_web/admin_controllers/admin_courses_controller.ex @@ -34,57 +34,34 @@ defmodule CadetWeb.AdminCoursesController do render(conn, "assessment_configs.json", %{configs: assessment_configs}) end - def update_assessment_config(conn, %{ + def update_assessment_configs(conn, %{ "course_id" => course_id, - "order" => order, - "earlySubmissionXp" => early_xp, - "hoursBeforeEarlyXpDecay" => hours_before_decay, - "decayRatePointsPerHour" => decay_rate + "assessmentConfigs" => assessment_configs }) - when is_ecto_id(course_id) do - case Courses.update_assessment_config( - course_id, - order, - early_xp, - hours_before_decay, - decay_rate - ) do - {:ok, _} -> - text(conn, "OK") - - {:error, _} -> - conn - |> put_status(:bad_request) - |> text("Invalid parameter(s)") - end - end + when is_ecto_id(course_id) and is_list(assessment_configs) do + if Enum.all?(assessment_configs, &is_map/1) do + configs = assessment_configs |> Enum.map(&to_snake_case_atom_keys/1) - def update_assessment_config(conn, _) do - send_resp(conn, :bad_request, "Missing parameter(s)") - end + case Courses.mass_upsert_or_delete_assessment_configs(course_id, configs) do + :ok -> + text(conn, "OK") - def update_assessment_types(conn, %{ - "course_id" => course_id, - "assessmentTypes" => assessment_types - }) - when is_ecto_id(course_id) do - case Courses.update_assessment_types(course_id, assessment_types) do - :ok -> - text(conn, "OK") - - {:error, {status, message}} -> - conn - |> put_status(status) - |> text(message) + {:error, {status, message}} -> + conn + |> put_status(status) + |> text(message) + end + else + send_resp(conn, :bad_request, "List parameter does not contain all maps") end end - def update_assessment_types(conn, _) do - send_resp(conn, :bad_request, "Missing parameter(s)") + def update_assessment_configs(conn, _) do + send_resp(conn, :bad_request, "Missing List parameter(s)") end swagger_path :update_course_config do - put("/v2/course/{course_id}/admin/course_config") + put("/v2/courses/{course_id}/admin/onfig") summary("Updates the course configuration for the specified course") @@ -109,8 +86,8 @@ defmodule CadetWeb.AdminCoursesController do response(403, "Forbidden") end - swagger_path :update_assessment_config do - put("/v2/course/{course_id}/admin/assessment_config/{order}") + swagger_path :update_assessment_configs do + put("/v2/courses/{course_id}/admin/config/assessment_configs") summary("Updates the assessment configuration for the specified course") @@ -120,29 +97,7 @@ defmodule CadetWeb.AdminCoursesController do parameters do course_id(:path, :integer, "Course ID", required: true) - order(:body, :integer, "type order", required: true) - early_submission_xp(:body, :integer, "Early submission xp") - hours_before_early_xp_decay(:body, :integer, "Hours before early submission xp decay") - decay_rate_points_per_hour(:body, :integer, "Decay rate in points per hour") - end - - response(200, "OK") - response(400, "Missing or invalid parameter(s)") - response(403, "Forbidden") - end - - swagger_path :update_assessment_types do - put("/admin/courses/{course_id}/assessment_types") - - summary("Updates the assessment types for the specified course") - - security([%{JWT: []}]) - - consumes("application/json") - - parameters do - course_id(:path, :integer, "Course ID", required: true) - assessment_types(:body, :list, "Assessment Types") + assessment_configs(:body, :list, "Assessment Configs") end response(200, "OK") diff --git a/lib/cadet_web/admin_controllers/admin_grading_controller.ex b/lib/cadet_web/admin_controllers/admin_grading_controller.ex index 2074e47b5..53330148a 100644 --- a/lib/cadet_web/admin_controllers/admin_grading_controller.ex +++ b/lib/cadet_web/admin_controllers/admin_grading_controller.ex @@ -295,7 +295,7 @@ defmodule CadetWeb.AdminGradingController do properties do id(:integer, "assessment id", required: true) - type(Schema.ref(:AssessmentType), "Either mission/sidequest/path/contest", + config(Schema.ref(:AssessmentConfig), "Either mission/sidequest/path/contest", required: true ) diff --git a/lib/cadet_web/admin_controllers/admin_user_controller.ex b/lib/cadet_web/admin_controllers/admin_user_controller.ex index f1ef217fd..0ed85caf9 100644 --- a/lib/cadet_web/admin_controllers/admin_user_controller.ex +++ b/lib/cadet_web/admin_controllers/admin_user_controller.ex @@ -14,7 +14,7 @@ defmodule CadetWeb.AdminUserController do end swagger_path :index do - get("/v2/course/{course_id}/admin/users") + get("/v2/courses/{course_id}/admin/users") summary("Returns a list of users in the course owned by the admin") diff --git a/lib/cadet_web/admin_views/admin_courses_view.ex b/lib/cadet_web/admin_views/admin_courses_view.ex index 6686ccac3..cc5d11506 100644 --- a/lib/cadet_web/admin_views/admin_courses_view.ex +++ b/lib/cadet_web/admin_views/admin_courses_view.ex @@ -1,18 +1,18 @@ -defmodule CadetWeb.AdminCoursesView do - use CadetWeb, :view - - def render("assessment_configs.json", %{configs: configs}) do - render_many(configs, CadetWeb.AdminCoursesView, "config.json", as: :config) - end - - def render("config.json", %{config: config}) do - transform_map_for_view(config, %{ - order: &(&1.assessment_type.order), - type: &(&1.assessment_type.type), - isGraded: &(&1.assessment_type.is_graded), - decayRatePointsPerHour: :decay_rate_points_per_hour, - earlySubmissionXp: :early_submission_xp, - hoursBeforeEarlyXpDecay: :hours_before_early_xp_decay - }) - end -end +defmodule CadetWeb.AdminCoursesView do + use CadetWeb, :view + + def render("assessment_configs.json", %{configs: configs}) do + render_many(configs, CadetWeb.AdminCoursesView, "config.json", as: :config) + end + + def render("config.json", %{config: config}) do + transform_map_for_view(config, %{ + order: :order, + type: :type, + isGraded: :is_graded, + decayRatePointsPerHour: :decay_rate_points_per_hour, + earlySubmissionXp: :early_submission_xp, + hoursBeforeEarlyXpDecay: :hours_before_early_xp_decay + }) + end +end diff --git a/lib/cadet_web/admin_views/admin_grading_view.ex b/lib/cadet_web/admin_views/admin_grading_view.ex index 12d3ab74a..934a50010 100644 --- a/lib/cadet_web/admin_views/admin_grading_view.ex +++ b/lib/cadet_web/admin_views/admin_grading_view.ex @@ -34,8 +34,8 @@ defmodule CadetWeb.AdminGradingView do defp build_grading_question(answer) do results = build_autograding_results(answer.autograding_results) - %{question: answer.question, assessment_type: answer.question.assessment.type} - |> build_question_by_assessment_type(true) + %{question: answer.question, assessment_config: answer.question.assessment.config} + |> build_question_by_assessment_config(true) |> Map.put(:answer, answer.answer["code"] || answer.answer["choice_id"]) |> Map.put(:autogradingStatus, answer.autograding_status) |> Map.put(:autogradingResults, results) diff --git a/lib/cadet_web/controllers/assessments_controller.ex b/lib/cadet_web/controllers/assessments_controller.ex index 0448c014c..4b8b6053d 100644 --- a/lib/cadet_web/controllers/assessments_controller.ex +++ b/lib/cadet_web/controllers/assessments_controller.ex @@ -153,7 +153,7 @@ defmodule CadetWeb.AssessmentsController do id(:integer, "The assessment ID", required: true) title(:string, "The title of the assessment", required: true) - type(Schema.ref(:AssessmentType), "The assessment type", required: true) + config(Schema.ref(:AssessmentConfig), "The assessment config", required: true) shortSummary(:string, "Short summary", required: true) @@ -203,7 +203,7 @@ defmodule CadetWeb.AssessmentsController do id(:integer, "The assessment ID", required: true) title(:string, "The title of the assessment", required: true) - type(Schema.ref(:AssessmentType), "The assessment type", required: true) + config(Schema.ref(:AssessmentConfig), "The assessment config", required: true) number( :string, @@ -219,9 +219,9 @@ defmodule CadetWeb.AssessmentsController do questions(Schema.ref(:Questions), "The list of questions for this assessment") end end, - AssessmentType: + AssessmentConfig: swagger_schema do - description("Assessment type") + description("Assessment config") type(:string) enum([:mission, :sidequest, :path, :contest, :practical]) end, diff --git a/lib/cadet_web/controllers/courses_controller.ex b/lib/cadet_web/controllers/courses_controller.ex index b20f9a415..1ff82efae 100644 --- a/lib/cadet_web/controllers/courses_controller.ex +++ b/lib/cadet_web/controllers/courses_controller.ex @@ -17,7 +17,7 @@ defmodule CadetWeb.CoursesController do # end swagger_path :get_course_config do - get("/v2/course/{course_id}/config") + get("/v2/courses/{course_id}/config") summary("Retrieves the course configuration of the specified course") diff --git a/lib/cadet_web/controllers/stories_controller.ex b/lib/cadet_web/controllers/stories_controller.ex index 1a69eb639..214cf462f 100644 --- a/lib/cadet_web/controllers/stories_controller.ex +++ b/lib/cadet_web/controllers/stories_controller.ex @@ -63,7 +63,7 @@ defmodule CadetWeb.StoriesController do end swagger_path :index do - get("/v2/course/{course_id}/stories") + get("/v2/courses/{course_id}/stories") summary("Get a list of all stories") @@ -73,7 +73,7 @@ defmodule CadetWeb.StoriesController do end swagger_path :create do - post("/v2/course/{course_id}/stories") + post("/v2s{course_id}/stories") summary("Creates a new story") @@ -85,7 +85,7 @@ defmodule CadetWeb.StoriesController do end swagger_path :delete do - PhoenixSwagger.Path.delete("/v2/course/{course_id}/stories/{storyId}") + PhoenixSwagger.Path.delete("/v2/courses/{course_id}/stories/{storyId}") summary("Delete a story from database by id") @@ -101,7 +101,7 @@ defmodule CadetWeb.StoriesController do end swagger_path :update do - post("/v2/course/{course_id}/stories/{storyId}") + post("/v2/courses/{course_id}/stories/{storyId}") summary("Update details regarding a story") diff --git a/lib/cadet_web/controllers/user_controller.ex b/lib/cadet_web/controllers/user_controller.ex index 3c861fe86..2c2936030 100644 --- a/lib/cadet_web/controllers/user_controller.ex +++ b/lib/cadet_web/controllers/user_controller.ex @@ -139,7 +139,7 @@ defmodule CadetWeb.UserController do end swagger_path :update_game_states do - put("/v2/course/:course_id/user/game_states") + put("/v2/courses/:course_id/user/game_states") summary("Update user's game states") security([%{JWT: []}]) consumes("application/json") diff --git a/lib/cadet_web/router.ex b/lib/cadet_web/router.ex index 70bdfa5fd..6513b10d7 100644 --- a/lib/cadet_web/router.ex +++ b/lib/cadet_web/router.ex @@ -59,7 +59,7 @@ defmodule CadetWeb.Router do end # Authenticated Pages with course - scope "/v2/course/:course_id", CadetWeb do + scope "/v2/courses/:course_id", CadetWeb do pipe_through([:api, :auth, :ensure_auth, :course]) get("/sourcecast", SourcecastController, :index) @@ -94,7 +94,7 @@ defmodule CadetWeb.Router do end # Authenticated Pages - scope "/v2/course/:course_id/self", CadetWeb do + scope "/v2/courses/:course_id/self", CadetWeb do pipe_through([:api, :auth, :ensure_auth, :course]) get("/goals", IncentivesController, :index_goals) @@ -102,7 +102,7 @@ defmodule CadetWeb.Router do end # Admin pages - scope "/v2/course/:course_id/admin", CadetWeb do + scope "/v2/courses/:course_id/admin", CadetWeb do pipe_through([:api, :auth, :ensure_auth, :course, :ensure_staff]) get("/assets/:foldername", AdminAssetsController, :index) @@ -138,10 +138,9 @@ defmodule CadetWeb.Router do put("/goals/:uuid", AdminGoalsController, :update) delete("/goals/:uuid", AdminGoalsController, :delete) - put("/course_config", AdminCoursesController, :update_course_config) - get("/assessment_configs", AdminCoursesController, :get_assessment_configs) - put("/assessment_config", AdminCoursesController, :update_assessment_config) - put("/assessment_types", AdminCoursesController, :update_assessment_types) + put("/config", AdminCoursesController, :update_course_config) + get("/config/assessment_configs", AdminCoursesController, :get_assessment_configs) + put("/config/assessment_configs", AdminCoursesController, :update_assessment_configs) end # Other scopes may use custom stacks. diff --git a/lib/cadet_web/views/assessments_helpers.ex b/lib/cadet_web/views/assessments_helpers.ex index 6b267044c..2f48ec64b 100644 --- a/lib/cadet_web/views/assessments_helpers.ex +++ b/lib/cadet_web/views/assessments_helpers.ex @@ -16,19 +16,19 @@ defmodule CadetWeb.AssessmentsHelpers do transform_map_for_view(external_library, [:name, :symbols]) end - def build_question_by_assessment_type( + def build_question_by_assessment_config( %{ question: question, - assessment_type: assessment_type + assessment_config: assessment_config }, all_testcases? \\ false ) do Map.merge( build_generic_question_fields(%{question: question}), - build_question_content_by_type( + build_question_content_by_config( %{ question: question, - assessment_type: assessment_type + assessment_config: assessment_config }, all_testcases? ) @@ -40,12 +40,12 @@ defmodule CadetWeb.AssessmentsHelpers do assessment: assessment }) do components = [ - build_question_by_assessment_type(%{ + build_question_by_assessment_config(%{ question: question, - assessment_type: assessment.type.type + assessment_config: assessment.config.type }), build_answer_fields_by_question_type(%{question: question}), - build_solution_if_ungraded_by_type(%{question: question, assessment: assessment}) + build_solution_if_ungraded_by_config(%{question: question, assessment: assessment}) ] components @@ -62,11 +62,11 @@ defmodule CadetWeb.AssessmentsHelpers do }) end - defp build_solution_if_ungraded_by_type(%{ + defp build_solution_if_ungraded_by_config(%{ question: %{question: question, type: question_type}, - assessment: %{type: assessment_type} + assessment: %{config: assessment_config} }) do - if !assessment_type.is_graded do + if !assessment_config.is_graded do solution_getter = case question_type do :programming -> &Map.get(&1, "solution") @@ -201,10 +201,10 @@ defmodule CadetWeb.AssessmentsHelpers do end end - defp build_question_content_by_type( + defp build_question_content_by_config( %{ question: %{question: question, type: question_type}, - assessment_type: assessment_type + assessment_config: assessment_config }, all_testcases? ) do @@ -214,8 +214,8 @@ defmodule CadetWeb.AssessmentsHelpers do content: "content", prepend: "prepend", solutionTemplate: "template", - postpend: build_postpend(%{assessment_type: assessment_type}, all_testcases?), - testcases: build_testcases(%{assessment_type: assessment_type}, all_testcases?) + postpend: build_postpend(%{assessment_config: assessment_config}, all_testcases?), + testcases: build_testcases(%{assessment_config: assessment_config}, all_testcases?) }) :mcq -> diff --git a/lib/cadet_web/views/courses_view.ex b/lib/cadet_web/views/courses_view.ex index aa1814141..f6c1fd6a6 100644 --- a/lib/cadet_web/views/courses_view.ex +++ b/lib/cadet_web/views/courses_view.ex @@ -14,7 +14,7 @@ defmodule CadetWeb.CoursesView do sourceChapter: :source_chapter, sourceVariant: :source_variant, moduleHelpText: :module_help_text, - assessmentTypes: :assessment_types + assessmentTypeNames: :assessment_configs }) } end diff --git a/lib/cadet_web/views/user_view.ex b/lib/cadet_web/views/user_view.ex index 0260711e1..83ea368cb 100644 --- a/lib/cadet_web/views/user_view.ex +++ b/lib/cadet_web/views/user_view.ex @@ -100,7 +100,7 @@ defmodule CadetWeb.UserView do sourceChapter: :source_chapter, sourceVariant: :source_variant, moduleHelpText: :module_help_text, - assessmentTypes: &Enum.map(&1.assessment_type, fn x -> x.type end) + assessmentTypeNames: &Enum.map(&1.assessment_config, fn x -> x.type end) }) end end diff --git a/priv/repo/migrations/20210531155751_add_course_configuration.exs b/priv/repo/migrations/20210531155751_add_course_configuration.exs index 7265dbe8c..3937c3edf 100644 --- a/priv/repo/migrations/20210531155751_add_course_configuration.exs +++ b/priv/repo/migrations/20210531155751_add_course_configuration.exs @@ -23,24 +23,19 @@ defmodule Cadet.Repo.Migrations.AddCourseConfiguration do timestamps() end - create table(:assessment_types) do + create table(:assessment_configs) do add(:order, :integer, null: false) add(:type, :string, null: false) add(:course_id, references(:courses), null: false) add(:is_graded, :boolean, null: false) - timestamps() - end - - create(unique_index(:assessment_types, [:course_id, :order])) - - create table(:assessment_configs) do add(:early_submission_xp, :integer, null: false) add(:hours_before_early_xp_decay, :integer, null: false) add(:decay_rate_points_per_hour, :integer, null: false) - add(:assessment_type_id, references(:assessment_types, on_delete: :delete_all), null: false) timestamps() end + create(unique_index(:assessment_configs, [:course_id, :order])) + create table(:course_registrations) do add(:role, :role, null: false) add(:game_states, :map, default: %{}) diff --git a/priv/repo/migrations/20210608085548_update_assessments.exs b/priv/repo/migrations/20210608085548_update_assessments.exs index 88f81db0e..848198cab 100644 --- a/priv/repo/migrations/20210608085548_update_assessments.exs +++ b/priv/repo/migrations/20210608085548_update_assessments.exs @@ -4,7 +4,7 @@ defmodule Cadet.Repo.Migrations.UpdateAssessments do def change do alter table(:assessments) do remove(:type) - add(:type_id, references(:assessment_types), null: false) + add(:config_id, references(:assessment_configs), null: false) add(:course_id, references(:courses), null: false) end diff --git a/priv/repo/seeds.exs b/priv/repo/seeds.exs index c6c756456..d0f7249e0 100644 --- a/priv/repo/seeds.exs +++ b/priv/repo/seeds.exs @@ -25,8 +25,7 @@ if Cadet.Env.env() == :dev do mentor1 = insert(:user, %{name: "mentor", latest_viewed: course1}) admin1 = insert(:user, %{name: "admin", latest_viewed: course1}) - studenta1admin2 = - insert(:user, %{name: "student a", latest_viewed: course1}) + studenta1admin2 = insert(:user, %{name: "student a", latest_viewed: course1}) studentb1 = insert(:user, %{latest_viewed: course1}) studentc1 = insert(:user, %{latest_viewed: course1}) @@ -56,13 +55,11 @@ if Cadet.Env.env() == :dev do # Assessments for i <- 1..5 do - type = insert(:assessment_type, %{type: "Mission#{i}", order: i, course: course1}) - config = insert(:assessment_config, %{assessment_type: type}) - assessment = insert(:assessment, %{is_published: true, type: type, course: course1}) + config = insert(:assessment_config, %{type: "Mission#{i}", order: i, course: course1}) + assessment = insert(:assessment, %{is_published: true, config: config, course: course1}) - type2 = insert(:assessment_type, %{type: "Homework#{i}", order: i, course: course2}) - config2 = insert(:assessment_config, %{assessment_type: type2}) - assessment2 = insert(:assessment, %{is_published: true, type: type2, course: course2}) + config2 = insert(:assessment_config, %{type: "Homework#{i}", order: i, course: course2}) + assessment2 = insert(:assessment, %{is_published: true, config: config2, course: course2}) programming_questions = insert_list(3, :programming_question, %{ diff --git a/test/cadet/accounts/course_registration_test.exs b/test/cadet/accounts/course_registration_test.exs index 15ef20ac7..3bca7fa0d 100644 --- a/test/cadet/accounts/course_registration_test.exs +++ b/test/cadet/accounts/course_registration_test.exs @@ -260,35 +260,32 @@ defmodule Cadet.Accounts.CourseRegistrationTest do assert CourseRegistrations.get_users(course1.id) == [] - assert_raise Ecto.NoPrimaryKeyValueError, fn -> - CourseRegistrations.delete_record(%{ - user_id: user1.id, - course_id: course1.id, - role: :student - }) - end + assert {:error, :no_such_enrty} == + CourseRegistrations.delete_record(%{ + user_id: user1.id, + course_id: course1.id, + role: :student + }) end test "failed due to non existing entry", %{course1: course1, user2: user2} do assert length(CourseRegistrations.get_users(course1.id)) == 1 - assert_raise Ecto.NoPrimaryKeyValueError, fn -> - CourseRegistrations.delete_record(%{ - user_id: user2.id, - course_id: course1.id, - role: :student - }) - end + assert {:error, :no_such_enrty} == + CourseRegistrations.delete_record(%{ + user_id: user2.id, + course_id: course1.id, + role: :student + }) end test "failed due to invalid changeset", %{course1: course1, user2: user2} do assert length(CourseRegistrations.get_users(course1.id)) == 1 - {:error, changeset} = + {:error, :no_such_enrty} = CourseRegistrations.delete_record(%{user_id: user2.id, course_id: course1.id}) assert length(CourseRegistrations.get_users(course1.id)) == 1 - refute changeset.valid? end end end diff --git a/test/cadet/assessments/assessment_test.exs b/test/cadet/assessments/assessment_test.exs index bb6c67083..5c52e2ed7 100644 --- a/test/cadet/assessments/assessment_test.exs +++ b/test/cadet/assessments/assessment_test.exs @@ -6,17 +6,22 @@ defmodule Cadet.Assessments.AssessmentTest do setup do course1 = insert(:course, %{course_short_name: "course 1"}) course2 = insert(:course, %{course_short_name: "course 2"}) - type1 = insert(:assessment_type, %{course: course1}) - type2 = insert(:assessment_type, %{course: course2}) + config1 = insert(:assessment_config, %{course: course1}) + config2 = insert(:assessment_config, %{course: course2}) - {:ok, %{course1: course1, course2: course2, type1: type1, type2: type2}} + {:ok, %{course1: course1, course2: course2, config1: config1, config2: config2}} end describe "Changesets" do - test "valid changesets", %{course1: course1, course2: course2, type1: type1, type2: type2} do + test "valid changesets", %{ + course1: course1, + course2: course2, + config1: config1, + config2: config2 + } do assert_changeset( %{ - type_id: type1.id, + config_id: config1.id, course_id: course1.id, title: "mission", number: "M#{Enum.random(0..10)}", @@ -28,7 +33,7 @@ defmodule Cadet.Assessments.AssessmentTest do assert_changeset( %{ - type_id: type2.id, + config_id: config2.id, course_id: course2.id, title: "mission", number: "M#{Enum.random(0..10)}", @@ -41,10 +46,10 @@ defmodule Cadet.Assessments.AssessmentTest do ) end - test "invalid changesets missing required params", %{course1: course1, type1: type1} do + test "invalid changesets missing required params", %{course1: course1, config1: config1} do assert_changeset( %{ - type_id: type1.id, + config_id: config1.id, course_id: course1.id, title: "mission", number: "M#{Enum.random(0..10)}" @@ -54,7 +59,7 @@ defmodule Cadet.Assessments.AssessmentTest do assert_changeset( %{ - type_id: type1.id, + config_id: config1.id, course_id: course1.id, title: "mission", open_at: Timex.now() |> Timex.to_unix() |> Integer.to_string(), @@ -64,10 +69,14 @@ defmodule Cadet.Assessments.AssessmentTest do ) end - test "invalid changesets due to type_course", %{course1: course1, type1: type1, type2: type2} do - type_not_in_course = + test "invalid changesets due to config_course", %{ + course1: course1, + config1: config1, + config2: config2 + } do + config_not_in_course = Assessment.changeset(%Assessment{}, %{ - type_id: type2.id, + config_id: config2.id, course_id: course1.id, title: "mission", number: "4", @@ -75,19 +84,19 @@ defmodule Cadet.Assessments.AssessmentTest do close_at: Timex.shift(Timex.now(), days: 7) }) - {:error, changeset} = Repo.insert(type_not_in_course) + {:error, changeset} = Repo.insert(config_not_in_course) assert changeset.errors == [ - {:type, {"does not belong to the same course as this assessment", []}} + {:config, {"does not belong to the same course as this assessment", []}} ] refute changeset.valid? - type_not_exist = + config_not_exist = Assessment.changeset(%Assessment{}, %{ - type_id: type1.id + type2.id, + config_id: config1.id + config2.id, course_id: course1.id, - title: "invalid type", + title: "invalid config", number: "M#{Enum.random(1..10)}", open_at: Timex.now() |> Timex.to_unix() |> Integer.to_string(), close_at: @@ -97,15 +106,15 @@ defmodule Cadet.Assessments.AssessmentTest do |> Integer.to_string() }) - {:error, changeset2} = Repo.insert(type_not_exist) - assert changeset2.errors == [{:type, {"does not exist", []}}] + {:error, changeset2} = Repo.insert(config_not_exist) + assert changeset2.errors == [{:config, {"does not exist", []}}] refute changeset2.valid? end - test "invalid changesets due to invalid dates", %{course1: course1, type1: type1} do + test "invalid changesets due to invalid dates", %{course1: course1, config1: config1} do invalid_date = Assessment.changeset(%Assessment{}, %{ - type_id: type1.id, + config_id: config1.id, course_id: course1.id, title: "mission", number: "4", diff --git a/test/cadet/assessments/submission_test.exs b/test/cadet/assessments/submission_test.exs index 9099c1eef..4d7438d8d 100644 --- a/test/cadet/assessments/submission_test.exs +++ b/test/cadet/assessments/submission_test.exs @@ -7,8 +7,8 @@ defmodule Cadet.Assessments.SubmissionTest do setup do course = insert(:course) - type = insert(:assessment_type, %{course: course}) - assessment = insert(:assessment, %{type: type, course: course}) + config = insert(:assessment_config, %{course: course}) + assessment = insert(:assessment, %{config: config, course: course}) student = insert(:course_registration, %{course: course, role: :student}) valid_params = %{student_id: student.id, assessment_id: assessment.id} diff --git a/test/cadet/courses/assessment_config_test.exs b/test/cadet/courses/assessment_config_test.exs index 6f2a698d5..3fa7f5323 100644 --- a/test/cadet/courses/assessment_config_test.exs +++ b/test/cadet/courses/assessment_config_test.exs @@ -3,10 +3,34 @@ defmodule Cadet.Courses.AssessmentConfigTest do use Cadet.ChangesetCase, entity: AssessmentConfig - describe "Assessment Configuration Changesets" do + describe "Assessment Configs Changesets" do + test "valid changesets" do + assert_changeset(%{order: 1, type: "Missions", course_id: 1}, :valid) + assert_changeset(%{order: 2, type: "quests", course_id: 1}, :valid) + assert_changeset(%{order: 3, type: "Paths", course_id: 1}, :valid) + assert_changeset(%{order: 4, type: "contests", course_id: 1}, :valid) + assert_changeset(%{order: 5, type: "Others", course_id: 1}, :valid) + end + + test "invalid changeset missing required params" do + assert_changeset(%{order: 1}, :invalid) + assert_changeset(%{course_id: 1}, :invalid) + assert_changeset(%{order: 1, type: "Missions"}, :invalid) + end + + test "invalid changeset with invalid order" do + assert_changeset(%{order: 0, type: "Missions", course_id: 1}, :invalid) + assert_changeset(%{order: 6, type: "Missions", course_id: 1}, :invalid) + end + end + + describe "Configuration-related Changesets" do test "valid changesets" do assert_changeset( %{ + order: 1, + type: "Missions", + course_id: 1, early_submission_xp: 200, hours_before_early_xp_decay: 48, decay_rate_points_per_hour: 1 @@ -16,6 +40,9 @@ defmodule Cadet.Courses.AssessmentConfigTest do assert_changeset( %{ + order: 1, + type: "Missions", + course_id: 1, early_submission_xp: 0, hours_before_early_xp_decay: 0, decay_rate_points_per_hour: 0 @@ -25,6 +52,9 @@ defmodule Cadet.Courses.AssessmentConfigTest do assert_changeset( %{ + order: 1, + type: "Missions", + course_id: 1, early_submission_xp: 200, hours_before_early_xp_decay: 0, decay_rate_points_per_hour: 10 @@ -33,33 +63,12 @@ defmodule Cadet.Courses.AssessmentConfigTest do ) end - test "invalid changeset missing required params" do - assert_changeset( - %{ - early_submission_xp: 0, - hours_before_early_xp_decay: 0 - }, - :invalid - ) - - assert_changeset( - %{ - early_submission_xp: 0 - }, - :invalid - ) - - assert_changeset( - %{ - decay_rate_points_per_hour: 1 - }, - :invalid - ) - end - test "invalid changeset with invalid early xp" do assert_changeset( %{ + order: 1, + type: "Missions", + course_id: 1, early_submission_xp: -1, hours_before_early_xp_decay: 0, decay_rate_points_per_hour: 10 @@ -71,6 +80,9 @@ defmodule Cadet.Courses.AssessmentConfigTest do test "invalid changeset with invalid hours before decay" do assert_changeset( %{ + order: 1, + type: "Missions", + course_id: 1, early_submission_xp: 200, hours_before_early_xp_decay: -1, decay_rate_points_per_hour: 10 @@ -82,6 +94,9 @@ defmodule Cadet.Courses.AssessmentConfigTest do test "invalid changeset with invalid decay rate" do assert_changeset( %{ + order: 1, + type: "Missions", + course_id: 1, early_submission_xp: 200, hours_before_early_xp_decay: 0, decay_rate_points_per_hour: -1 @@ -93,6 +108,9 @@ defmodule Cadet.Courses.AssessmentConfigTest do test "invalid changeset with decay rate greater than early submission xp" do assert_changeset( %{ + order: 1, + type: "Missions", + course_id: 1, early_submission_xp: 200, hours_before_early_xp_decay: 48, decay_rate_points_per_hour: 300 diff --git a/test/cadet/courses/assessment_type_test.exs b/test/cadet/courses/assessment_type_test.exs deleted file mode 100644 index 714fa7f15..000000000 --- a/test/cadet/courses/assessment_type_test.exs +++ /dev/null @@ -1,28 +0,0 @@ -defmodule Cadet.Courses.AssessmentTypeTest do - alias Cadet.Courses.AssessmentType - - use Cadet.ChangesetCase, entity: AssessmentType - - describe "Assessment Types Changesets" do - test "valid changesets" do - assert_changeset(%{order: 1, type: "Missions", course_id: 1}, :valid) - assert_changeset(%{order: 2, type: "quests", course_id: 1}, :valid) - assert_changeset(%{order: 3, type: "Paths", course_id: 1}, :valid) - assert_changeset(%{order: 4, type: "contests", course_id: 1}, :valid) - assert_changeset(%{order: 5, type: "Others", course_id: 1}, :valid) - end - - test "invalid changeset missing required params" do - assert_changeset(%{order: 1}, :invalid) - assert_changeset(%{type: "Missions"}, :invalid) - assert_changeset(%{course_id: 1}, :invalid) - assert_changeset(%{order: 1, type: "Missions"}, :invalid) - assert_changeset(%{order: 1, course_id: 1}, :invalid) - end - - test "invalid changeset with invalid order" do - assert_changeset(%{order: 0, type: "Missions", course_id: 1}, :invalid) - assert_changeset(%{order: 6, type: "Missions", course_id: 1}, :invalid) - end - end -end diff --git a/test/cadet/courses/courses_test.exs b/test/cadet/courses/courses_test.exs index ec7e96e4c..5fc1f8953 100644 --- a/test/cadet/courses/courses_test.exs +++ b/test/cadet/courses/courses_test.exs @@ -7,8 +7,8 @@ defmodule Cadet.CoursesTest do describe "get course config" do test "succeeds" do course = insert(:course) - insert(:assessment_type, %{order: 1, type: "Missions", course: course}) - insert(:assessment_type, %{order: 2, type: "Quests", course: course}) + insert(:assessment_config, %{order: 1, type: "Missions", course: course}) + insert(:assessment_config, %{order: 2, type: "Quests", course: course}) {:ok, course} = Courses.get_course_config(course.id) assert course.course_name == "Programming Methodology" @@ -20,7 +20,7 @@ defmodule Cadet.CoursesTest do assert course.source_chapter == 1 assert course.source_variant == "default" assert course.module_help_text == "Help Text" - assert course.assessment_types == ["Missions", "Quests"] + assert course.assessment_configs == ["Missions", "Quests"] end test "returns with error for invalid course id" do @@ -117,258 +117,203 @@ defmodule Cadet.CoursesTest do describe "get assessment configs" do test "succeeds" do course = insert(:course) - for i <- 1..5 do - type = insert(:assessment_type, %{order: 6-i, type: "Mission#{i}", course: course}) - insert(:assessment_config, %{decay_rate_points_per_hour: i , assessment_type: type}) - end - assessment_configs = Courses.get_assessment_configs(course.id) - - # This test that the assessment_type is preloaded and is ordered by order for i <- 1..5 do - assert Enum.at(assessment_configs, i-1).assessment_type.order == i + insert(:assessment_config, %{order: 6 - i, type: "Mission#{i}", course: course}) end - end - end - - describe "update assessment config" do - test "succeeds" do - course = insert(:course) - type = insert(:assessment_type, %{course: course}) - _assessment_config = insert(:assessment_config, %{assessment_type: type}) - - {:ok, updated_config} = Courses.update_assessment_config(course.id, type.order, 100, 24, 1) - - assert updated_config.early_submission_xp == 100 - assert updated_config.hours_before_early_xp_decay == 24 - assert updated_config.decay_rate_points_per_hour == 1 - end - - test "returns with error for failed updates" do - course = insert(:course) - type = insert(:assessment_type, %{course: course}) - _assessment_config = insert(:assessment_config, %{assessment_type: type}) - {:error, changeset} = Courses.update_assessment_config(course.id, type.order, -1, 0, 0) - - assert %{early_submission_xp: ["must be greater than or equal to 0"]} = errors_on(changeset) - - {:error, changeset} = Courses.update_assessment_config(course.id, type.order, 200, -1, 0) - - assert %{hours_before_early_xp_decay: ["must be greater than or equal to 0"]} = - errors_on(changeset) - - {:error, changeset} = Courses.update_assessment_config(course.id, type.order, 200, 48, -1) - - assert %{decay_rate_points_per_hour: ["must be greater than or equal to 0"]} = - errors_on(changeset) + assessment_configs = Courses.get_assessment_configs(course.id) - {:error, changeset} = Courses.update_assessment_config(course.id, type.order, 200, 48, 300) + assert length(assessment_configs) <= 5 - assert %{decay_rate_points_per_hour: ["must be less than or equal to 200"]} = - errors_on(changeset) + assessment_configs + |> Enum.with_index(1) + |> Enum.each(fn {at, idx} -> + assert at.order == idx + assert at.type == "Mission#{6 - idx}" + end) end end - describe "update assessment types" do - test "succeeds" do + describe "mass_upsert_or_delete_assessment_configs" do + setup do course = insert(:course) - course_id = course.id - - insert(:assessment_type, %{order: 1, type: "Missions", course: course}) - insert(:assessment_type, %{order: 2, type: "Quests", course: course}) - insert(:assessment_type, %{order: 3, type: "Paths", course: course}) - insert(:assessment_type, %{order: 4, type: "Contests", course: course}) - insert(:assessment_type, %{order: 5, type: "Others", course: course}) + insert(:assessment_config, %{order: 1, type: "Missions", course: course}) + insert(:assessment_config, %{order: 2, type: "Quests", course: course}) + insert(:assessment_config, %{order: 3, type: "Paths", course: course}) + insert(:assessment_config, %{order: 4, type: "Contests", course: course}) + expected = ["Paths", "Quests", "Missions", "Others", "Contests"] + {:ok, %{course: course, expected: expected}} + end + test "succeeds", %{course: course, expected: expected} do :ok = - Courses.update_assessment_types(course_id, [ - "Paths", - "Quests", - "Missions", - "Others", - "Contests" + Courses.mass_upsert_or_delete_assessment_configs(course.id, [ + %{course_id: course.id, order: 1, type: "Paths"}, + %{course_id: course.id, order: 2, type: "Quests"}, + %{course_id: course.id, order: 3, type: "Missions"}, + %{course_id: course.id, order: 4, type: "Others"}, + %{course_id: course.id, order: 5, type: "Contests"} ]) - {:ok, updated_course_config} = Courses.get_course_config(course_id) + assessment_configs = Courses.get_assessment_configs(course.id) - assert updated_course_config.assessment_types == [ - "Paths", - "Quests", - "Missions", - "Others", - "Contests" - ] + assert Enum.map(assessment_configs, & &1.type) == expected end - test "succeeds when database entries are not in order" do - course = insert(:course) - course_id = course.id - - insert(:assessment_type, %{order: 4, type: "Contests", course: course}) - insert(:assessment_type, %{order: 1, type: "Missions", course: course}) - insert(:assessment_type, %{order: 3, type: "Paths", course: course}) - insert(:assessment_type, %{order: 5, type: "Others", course: course}) - insert(:assessment_type, %{order: 2, type: "Quests", course: course}) - + test "succeeds to capitalise", %{course: course, expected: expected} do :ok = - Courses.update_assessment_types(course_id, [ - "Paths", - "Quests", - "Missions", - "Others", - "Contests" + Courses.mass_upsert_or_delete_assessment_configs(course.id, [ + %{course_id: course.id, order: 1, type: "Paths"}, + %{course_id: course.id, order: 2, type: "quests"}, + %{course_id: course.id, order: 3, type: "missions"}, + %{course_id: course.id, order: 4, type: "Others"}, + %{course_id: course.id, order: 5, type: "contests"} ]) - {:ok, updated_course_config} = Courses.get_course_config(course_id) + assessment_configs = Courses.get_assessment_configs(course.id) - assert updated_course_config.assessment_types == [ - "Paths", - "Quests", - "Missions", - "Others", - "Contests" - ] + assert Enum.map(assessment_configs, & &1.type) == expected end - test "succeeds and capitalizes the types during database insertion" do - course = insert(:course) - course_id = course.id - - insert(:assessment_type, %{order: 1, type: "Missions", course: course}) - insert(:assessment_type, %{order: 2, type: "Quests", course: course}) - insert(:assessment_type, %{order: 3, type: "Paths", course: course}) - insert(:assessment_type, %{order: 4, type: "Contests", course: course}) - insert(:assessment_type, %{order: 5, type: "Others", course: course}) - + test "succeed to delete", %{course: course} do :ok = - Courses.update_assessment_types(course_id, [ - "Paths", - "quests", - "Missions", - "Others", - "contests" + Courses.mass_upsert_or_delete_assessment_configs(course.id, [ + %{course_id: course.id, order: 1, type: "Paths"}, + %{course_id: course.id, order: 2, type: "quests"}, + %{course_id: course.id, order: 3, type: "missions"} ]) - {:ok, updated_course_config} = Courses.get_course_config(course_id) + assessment_configs = Courses.get_assessment_configs(course.id) - assert updated_course_config.assessment_types == [ - "Paths", - "Quests", - "Missions", - "Others", - "Contests" - ] + assert Enum.map(assessment_configs, & &1.type) == ["Paths", "Quests", "Missions"] end - test "succeeds when inserting more types than existing database entries" do - course = insert(:course) - course_id = course.id - - insert(:assessment_type, %{order: 1, type: "Missions", course: course}) - insert(:assessment_type, %{order: 2, type: "Quests", course: course}) - insert(:assessment_type, %{order: 3, type: "Paths", course: course}) - - :ok = - Courses.update_assessment_types(course_id, [ - "Paths", - "Quests", - "Missions", - "Others", - "Contests" - ]) - - {:ok, updated_course_config} = Courses.get_course_config(course_id) - - assert updated_course_config.assessment_types == [ - "Paths", - "Quests", - "Missions", - "Others", - "Contests" - ] + test "returns with error for empty list parameter", %{course: course} do + assert {:error, {:bad_request, "Invalid parameter(s)"}} = + Courses.mass_upsert_or_delete_assessment_configs(course.id, []) end - test "succeeds when inserting less types than existing database entries" do - course = insert(:course) - course_id = course.id + test "returns with error for list parameter of greater than length 5", %{course: course} do + params = [ + %{course_id: course.id, order: 1, type: "Paths"}, + %{course_id: course.id, order: 2, type: "Quests"}, + %{course_id: course.id, order: 3, type: "Missions"}, + %{course_id: course.id, order: 4, type: "Others"}, + %{course_id: course.id, order: 5, type: "Contests"}, + %{course_id: course.id, order: 6, type: "Homework"} + ] - insert(:assessment_type, %{order: 1, type: "Missions", course: course}) - insert(:assessment_type, %{order: 2, type: "Quests", course: course}) - insert(:assessment_type, %{order: 3, type: "Paths", course: course}) - insert(:assessment_type, %{order: 4, type: "Contests", course: course}) + assert {:error, {:bad_request, "Invalid parameter(s)"}} = + Courses.mass_upsert_or_delete_assessment_configs(course.id, params) + end - :ok = Courses.update_assessment_types(course_id, ["Paths", "Quests", "Missions"]) - {:ok, updated_course_config} = Courses.get_course_config(course_id) + test "returns with error for non-list parameter", %{course: course} do + params = %{course_id: course.id, order: 1, type: "Paths"} - assert updated_course_config.assessment_types == ["Paths", "Quests", "Missions"] + assert {:error, {:bad_request, "Invalid parameter(s)"}} = + Courses.mass_upsert_or_delete_assessment_configs(course.id, params) end + end - test "returns with error for invalid parameters" do + describe "insert_or_update_assessment_config" do + test "succeeds with insert to empty configs" do course = insert(:course) - course_id = course.id + old_configs = Courses.get_assessment_configs(course.id) + + params = %{ + course_id: course.id, + order: 1, + type: "Mission", + early_submission_xp: 100, + hours_before_early_xp_decay: 24, + decay_rate_points_per_hour: 1 + } - insert(:assessment_type, %{order: 1, type: "Missions", course: course}) - insert(:assessment_type, %{order: 2, type: "Quests", course: course}) - insert(:assessment_type, %{order: 3, type: "Paths", course: course}) + {:ok, updated_config} = Courses.insert_or_update_assessment_config(params) - assert {:error, {:bad_request, "Invalid parameter(s)"}} = - Courses.update_assessment_types(course_id, [1, "Quests", "Missions"]) + new_configs = Courses.get_assessment_configs(course.id) + assert length(old_configs) == 0 + assert length(new_configs) == 1 + assert updated_config.early_submission_xp == 100 + assert updated_config.hours_before_early_xp_decay == 24 + assert updated_config.decay_rate_points_per_hour == 1 end - test "returns with error for duplicate parameters" do + test "succeeds with insert to existing configs" do course = insert(:course) - course_id = course.id + insert(:assessment_config, %{order: 1, course: course}) + old_configs = Courses.get_assessment_configs(course.id) + + params = %{ + course_id: course.id, + order: 2, + type: "Mission", + early_submission_xp: 100, + hours_before_early_xp_decay: 24, + decay_rate_points_per_hour: 1 + } - insert(:assessment_type, %{order: 1, type: "Missions", course: course}) - insert(:assessment_type, %{order: 2, type: "Quests", course: course}) - insert(:assessment_type, %{order: 3, type: "Paths", course: course}) + {:ok, updated_config} = Courses.insert_or_update_assessment_config(params) - assert {:error, {:bad_request, "Invalid parameter(s)"}} = - Courses.update_assessment_types(course_id, ["Missions", "Quests", "Missions"]) + new_configs = Courses.get_assessment_configs(course.id) + assert length(old_configs) == 1 + assert length(new_configs) == 2 + assert updated_config.early_submission_xp == 100 + assert updated_config.hours_before_early_xp_decay == 24 + assert updated_config.decay_rate_points_per_hour == 1 end - test "returns with error for empty list parameter" do + test "succeeds with update" do course = insert(:course) - course_id = course.id + config = insert(:assessment_config, %{course: course}) + + params = %{ + course_id: course.id, + order: config.order, + type: "Mission", + early_submission_xp: 100, + hours_before_early_xp_decay: 24, + decay_rate_points_per_hour: 1 + } - insert(:assessment_type, %{order: 1, type: "Missions", course: course}) - insert(:assessment_type, %{order: 2, type: "Quests", course: course}) - insert(:assessment_type, %{order: 3, type: "Paths", course: course}) + {:ok, updated_config} = Courses.insert_or_update_assessment_config(params) - assert {:error, {:bad_request, "Invalid parameter(s)"}} = - Courses.update_assessment_types(course_id, []) + assert updated_config.type == "Mission" + assert updated_config.early_submission_xp == 100 + assert updated_config.hours_before_early_xp_decay == 24 + assert updated_config.decay_rate_points_per_hour == 1 end + end - test "returns with error for list parameter of greater than length 5" do + describe "delete_assessment_config" do + test "succeeds" do course = insert(:course) - course_id = course.id + config = insert(:assessment_config, %{order: 1, course: course}) + old_configs = Courses.get_assessment_configs(course.id) - insert(:assessment_type, %{order: 1, type: "Missions", course: course}) - insert(:assessment_type, %{order: 2, type: "Quests", course: course}) - insert(:assessment_type, %{order: 3, type: "Paths", course: course}) + params = %{ + course_id: course.id, + order: config.order + } - assert {:error, {:bad_request, "Invalid parameter(s)"}} = - Courses.update_assessment_types(course_id, [ - "Missions", - "Quests", - "Paths", - "Contests", - "Others", - "Assessments" - ]) + {:ok, _} = Courses.delete_assessment_config(params) + + new_configs = Courses.get_assessment_configs(course.id) + assert length(old_configs) == 1 + assert length(new_configs) == 0 end - test "returns with error for non-list parameter" do + test "error" do course = insert(:course) - course_id = course.id + insert(:assessment_config, %{order: 1, course: course}) - insert(:assessment_type, %{order: 1, type: "Missions", course: course}) - insert(:assessment_type, %{order: 2, type: "Quests", course: course}) - insert(:assessment_type, %{order: 3, type: "Paths", course: course}) + params = %{ + course_id: course.id, + order: 2 + } - assert {:error, {:bad_request, "Invalid parameter(s)"}} = - Courses.update_assessment_types(course_id, "Missions") + assert {:error, :no_such_enrty} == Courses.delete_assessment_config(params) end end diff --git a/test/cadet_web/admin_controllers/admin_courses_controller_test.exs b/test/cadet_web/admin_controllers/admin_courses_controller_test.exs index 2a58e243c..a343e5f3a 100644 --- a/test/cadet_web/admin_controllers/admin_courses_controller_test.exs +++ b/test/cadet_web/admin_controllers/admin_courses_controller_test.exs @@ -2,19 +2,19 @@ defmodule CadetWeb.AdminCoursesControllerTest do use CadetWeb.ConnCase import Cadet.SharedHelper - import Ecto.Query + alias Cadet.Repo - alias Cadet.Courses.{Course, AssessmentConfig, AssessmentType} + alias Cadet.Courses + alias Cadet.Courses.Course alias CadetWeb.AdminCoursesController test "swagger" do AdminCoursesController.swagger_definitions() AdminCoursesController.swagger_path_update_course_config(nil) - AdminCoursesController.swagger_path_update_assessment_config(nil) - AdminCoursesController.swagger_path_update_assessment_types(nil) + AdminCoursesController.swagger_path_update_assessment_configs(nil) end - describe "PUT /v2/course/{course_id}/admin/course_config" do + describe "PUT /v2/courses/{course_id}/admin/config" do @tag authenticate: :admin test "succeeds 1", %{conn: conn} do course_id = conn.assigns[:course_id] @@ -141,24 +141,18 @@ defmodule CadetWeb.AdminCoursesControllerTest do end end - describe "GET /v2/course/{course_id}/admin/assessment_configs" do + describe "GET /v2/courses/{course_id}/admin/configs/assessment_configs" do @tag authenticate: :admin test "succeeds", %{conn: conn} do course_id = conn.assigns[:course_id] course = Repo.get(Course, course_id) - - type1 = insert(:assessment_type, %{order: 1, type: "Mission1", course: course}) - insert(:assessment_config, %{assessment_type: type1}) - - type3 = insert(:assessment_type, %{order: 3, type: "Mission3", course: course}) - insert(:assessment_config, %{assessment_type: type3}) - - type2 = insert(:assessment_type, %{is_graded: false, order: 2, type: "Mission2", course: course}) - insert(:assessment_config, %{assessment_type: type2}) + insert(:assessment_config, %{order: 1, type: "Mission1", course: course}) + insert(:assessment_config, %{order: 3, type: "Mission3", course: course}) + insert(:assessment_config, %{is_graded: false, order: 2, type: "Mission2", course: course}) resp = conn - |> get(build_url_assessment_config(course_id) <> "s") + |> get(build_url_assessment_configs(course_id)) |> json_response(200) expected = [ @@ -195,146 +189,51 @@ defmodule CadetWeb.AdminCoursesControllerTest do test "rejects forbidden request for non-staff users", %{conn: conn} do course_id = conn.assigns[:course_id] - resp = get(conn, build_url_assessment_config(course_id) <> "s") + resp = get(conn, build_url_assessment_configs(course_id)) assert response(resp, 403) == "Forbidden" end end - describe "PUT /v2/course/{course_id}/admin/assessment_config" do + describe "PUT /v2/courses/{course_id}/admin/config/assessment_configs" do @tag authenticate: :admin test "succeeds", %{conn: conn} do course_id = conn.assigns[:course_id] - course = Repo.get(Course, course_id) - type = insert(:assessment_type, %{course: course, order: 2}) - old_config = insert(:assessment_config, %{assessment_type: type}) + insert(:assessment_config, %{course: Repo.get(Course, course_id)}) + + old_configs = Courses.get_assessment_configs(course_id) |> Enum.map(& &1.type) params = %{ - "order" => type.order, - "earlySubmissionXp" => 100, - "hoursBeforeEarlyXpDecay" => 24, - "decayRatePointsPerHour" => 2 + "assessmentConfigs" => [ + %{ + "courseId" => course_id, + "order" => 1, + "type" => "Missions", + "earlySubmissionXp" => 100, + "hoursBeforeEarlyXpDecay" => 24, + "decayRatePointsPerHour" => 1 + }, + %{ + "courseId" => course_id, + "order" => 2, + "type" => "Paths", + "earlySubmissionXp" => 100, + "hoursBeforeEarlyXpDecay" => 24, + "decayRatePointsPerHour" => 1 + } + ] } - resp = put(conn, build_url_assessment_config(course_id), params) - - assert response(resp, 200) == "OK" - updated_config = Repo.get(AssessmentConfig, old_config.id) - assert updated_config.decay_rate_points_per_hour == 2 - assert updated_config.early_submission_xp == 100 - assert updated_config.hours_before_early_xp_decay == 24 - end - - @tag authenticate: :student - test "rejects forbidden request for non-staff users", %{conn: conn} do - course_id = conn.assigns[:course_id] - course = Repo.get(Course, course_id) - type = insert(:assessment_type, %{course: course}) - insert(:assessment_config, %{assessment_type: type}) - - conn = - put(conn, build_url_assessment_config(course_id), %{ - "order" => type.order, - "earlySubmissionXp" => 100, - "hoursBeforeEarlyXpDecay" => 24, - "decayRatePointsPerHour" => 2 - }) - - assert response(conn, 403) == "Forbidden" - end - - @tag authenticate: :staff - test "rejects request if user does not belong to specified course", %{conn: conn} do - course_id = conn.assigns[:course_id] - course = Repo.get(Course, course_id) - type = insert(:assessment_type, %{course: course}) - insert(:assessment_config, %{assessment_type: type}) - - conn = - put(conn, build_url_assessment_config(course_id + 1), %{ - "order" => type.order, - "earlySubmissionXp" => 100, - "hoursBeforeEarlyXpDecay" => 24, - "decayRatePointsPerHour" => 2 - }) - - assert response(conn, 403) == "Forbidden" - end - - @tag authenticate: :staff - test "rejects requests with invalid params", %{conn: conn} do - course_id = conn.assigns[:course_id] - course = Repo.get(Course, course_id) - type = insert(:assessment_type, %{course: course}) - insert(:assessment_config, %{assessment_type: type}) - - conn = - put(conn, build_url_assessment_config(course_id), %{ - "order" => type.order, - "earlySubmissionXp" => 100, - "hoursBeforeEarlyXpDecay" => -1, - "decayRatePointsPerHour" => 200 - }) - - assert response(conn, 400) == "Invalid parameter(s)" - end - - @tag authenticate: :staff - test "rejects requests with missing params", %{conn: conn} do - course_id = conn.assigns[:course_id] - course = Repo.get(Course, course_id) - type = insert(:assessment_type, %{course: course}) - insert(:assessment_config, %{assessment_type: type}) - - conn = - put(conn, build_url_assessment_config(course_id), %{ - "order" => type.order, - "hoursBeforeEarlyXpDecay" => 24, - "decayRatePointsPerHour" => 2 - }) - - assert response(conn, 400) == "Missing parameter(s)" - end - end - - describe "PUT /v2/course/{course_id}/admin/assessment_types" do - @tag authenticate: :admin - test "succeeds", %{conn: conn} do - course_id = conn.assigns[:course_id] - insert(:assessment_type, %{course: Repo.get(Course, course_id)}) - - old_course = - Course - |> where(id: ^course_id) - |> join(:left, [c], at in assoc(c, :assessment_type)) - |> preload([c, at], - assessment_type: ^from(at in AssessmentType, order_by: [asc: at.order]) - ) - |> Repo.all() - |> hd() - - old_types = Enum.map(old_course.assessment_type, fn x -> x.type end) + resp = + conn + |> put(build_url_assessment_configs(course_id), params) + |> response(200) - conn = - put(conn, build_url_assessment_types(course_id), %{ - "assessmentTypes" => ["Missions", "Quests", "Contests"] - }) + assert resp == "OK" - new_course = - Course - |> where(id: ^course_id) - |> join(:left, [c], at in assoc(c, :assessment_type)) - |> preload([c, at], - assessment_type: ^from(at in AssessmentType, order_by: [asc: at.order]) - ) - |> Repo.all() - |> hd() - - new_types = Enum.map(new_course.assessment_type, fn x -> x.type end) - - assert response(conn, 200) == "OK" - refute old_types == new_types - assert new_types == ["Missions", "Quests", "Contests"] + new_configs = Courses.get_assessment_configs(course_id) |> Enum.map(& &1.type) + refute old_configs == new_configs + assert new_configs == ["Missions", "Paths"] end @tag authenticate: :student @@ -342,8 +241,8 @@ defmodule CadetWeb.AdminCoursesControllerTest do course_id = conn.assigns[:course_id] conn = - put(conn, build_url_assessment_types(course_id), %{ - "assessmentTypes" => ["Missions", "Quests", "Contests"] + put(conn, build_url_assessment_configs(course_id), %{ + "assessmentConfigs" => [] }) assert response(conn, 403) == "Forbidden" @@ -354,8 +253,8 @@ defmodule CadetWeb.AdminCoursesControllerTest do course_id = conn.assigns[:course_id] conn = - put(conn, build_url_assessment_types(course_id + 1), %{ - "assessmentTypes" => ["Missions", "Quests", "Contests"] + put(conn, build_url_assessment_configs(course_id + 1), %{ + "assessmentConfigs" => [] }) assert response(conn, 403) == "Forbidden" @@ -366,11 +265,11 @@ defmodule CadetWeb.AdminCoursesControllerTest do course_id = conn.assigns[:course_id] conn = - put(conn, build_url_assessment_types(course_id), %{ - "assessmentTypes" => "Missions" + put(conn, build_url_assessment_configs(course_id), %{ + "assessmentConfigs" => "Missions" }) - assert response(conn, 400) == "Invalid parameter(s)" + assert response(conn, 400) == "Missing List parameter(s)" end @tag authenticate: :staff @@ -378,30 +277,27 @@ defmodule CadetWeb.AdminCoursesControllerTest do course_id = conn.assigns[:course_id] conn = - put(conn, build_url_assessment_types(course_id), %{ - "assessmentTypes" => [1, "Missions", "Quests"] + put(conn, build_url_assessment_configs(course_id), %{ + "assessmentConfigs" => [1, "Missions", "Quests"] }) - assert response(conn, 400) == "Invalid parameter(s)" + assert response(conn, 400) == "List parameter does not contain all maps" end @tag authenticate: :staff test "rejects requests with missing params", %{conn: conn} do course_id = conn.assigns[:course_id] - conn = put(conn, build_url_assessment_types(course_id), %{}) + conn = put(conn, build_url_assessment_configs(course_id), %{}) - assert response(conn, 400) == "Missing parameter(s)" + assert response(conn, 400) == "Missing List parameter(s)" end end - defp build_url_course_config(course_id), do: "/v2/course/#{course_id}/admin/course_config" - - defp build_url_assessment_config(course_id), - do: "/v2/course/#{course_id}/admin/assessment_config" + defp build_url_course_config(course_id), do: "/v2/courses/#{course_id}/admin/config" - defp build_url_assessment_types(course_id), - do: "/v2/course/#{course_id}/admin/assessment_types" + defp build_url_assessment_configs(course_id), + do: "/v2/courses/#{course_id}/admin/config/assessment_configs" defp to_map(schema), do: Map.from_struct(schema) |> Map.drop([:updated_at]) diff --git a/test/cadet_web/admin_controllers/admin_user_controller_test.exs b/test/cadet_web/admin_controllers/admin_user_controller_test.exs index 55558f556..74d56d6f8 100644 --- a/test/cadet_web/admin_controllers/admin_user_controller_test.exs +++ b/test/cadet_web/admin_controllers/admin_user_controller_test.exs @@ -12,7 +12,7 @@ defmodule CadetWeb.AdminUserControllerTest do assert is_map(AdminUserController.swagger_path_index(nil)) end - describe "GET /v2/course/{course_id}/admin/users" do + describe "GET /v2/courses/{course_id}/admin/users" do @tag authenticate: :staff test "success, when staff retrieves all users", %{conn: conn} do course_id = conn.assigns[:course_id] @@ -80,5 +80,5 @@ defmodule CadetWeb.AdminUserControllerTest do # end end - defp build_url(course_id), do: "/v2/course/#{course_id}/admin/users" + defp build_url(course_id), do: "/v2/courses/#{course_id}/admin/users" end diff --git a/test/cadet_web/controllers/courses_controller_test.exs b/test/cadet_web/controllers/courses_controller_test.exs index 1b4587719..4c54cc40f 100644 --- a/test/cadet_web/controllers/courses_controller_test.exs +++ b/test/cadet_web/controllers/courses_controller_test.exs @@ -10,7 +10,7 @@ defmodule CadetWeb.CoursesControllerTest do CoursesController.swagger_path_get_course_config(nil) end - describe "GET /v2/course/course_id/config, unauthenticated" do + describe "GET /v2/courses/course_id/config, unauthenticated" do test "unauthorized", %{conn: conn} do course = insert(:course) conn = get(conn, build_url_config(course.id)) @@ -18,15 +18,15 @@ defmodule CadetWeb.CoursesControllerTest do end end - describe "GET /v2/course/course_id/config" do + describe "GET /v2/courses/course_id/config" do @tag authenticate: :student test "succeeds", %{conn: conn} do course_id = conn.assigns[:course_id] course = Repo.get(Course, course_id) - insert(:assessment_type, %{order: 3, type: "Paths", course: course}) - insert(:assessment_type, %{order: 1, type: "Missions", course: course}) - insert(:assessment_type, %{order: 2, type: "Quests", course: course}) + insert(:assessment_config, %{order: 3, type: "Paths", course: course}) + insert(:assessment_config, %{order: 1, type: "Missions", course: course}) + insert(:assessment_config, %{order: 2, type: "Quests", course: course}) resp = conn |> get(build_url_config(course_id)) |> json_response(200) @@ -41,7 +41,7 @@ defmodule CadetWeb.CoursesControllerTest do "sourceChapter" => 1, "sourceVariant" => "default", "moduleHelpText" => "Help Text", - "assessmentTypes" => ["Missions", "Quests", "Paths"] + "assessmentTypeNames" => ["Missions", "Quests", "Paths"] } } = resp end @@ -58,5 +58,5 @@ defmodule CadetWeb.CoursesControllerTest do end end - defp build_url_config(course_id), do: "/v2/course/#{course_id}/config" + defp build_url_config(course_id), do: "/v2/courses/#{course_id}/config" end diff --git a/test/cadet_web/controllers/sourcecast_controller_test.exs b/test/cadet_web/controllers/sourcecast_controller_test.exs index 40a26aa5b..c8e03aa11 100644 --- a/test/cadet_web/controllers/sourcecast_controller_test.exs +++ b/test/cadet_web/controllers/sourcecast_controller_test.exs @@ -52,7 +52,7 @@ defmodule CadetWeb.SourcecastControllerTest do end end - describe "POST /v2/course/{course_id}/sourcecast, unauthenticated" do + describe "POST /v2/courses/{course_id}/sourcecast, unauthenticated" do test "unauthorized", %{conn: conn} do course = insert(:course) conn = post(conn, build_url(course.id), %{}) @@ -60,7 +60,7 @@ defmodule CadetWeb.SourcecastControllerTest do end end - describe "DELETE /v2/course/{course_id}/sourcecast, unauthenticated" do + describe "DELETE /v2/courses/{course_id}/sourcecast, unauthenticated" do test "unauthorized", %{conn: conn} do course = insert(:course) seed_db(course.id) @@ -69,7 +69,7 @@ defmodule CadetWeb.SourcecastControllerTest do end end - describe "GET /v2/course/{course_id}/sourcecast, returns course sourcecasts" do + describe "GET /v2/courses/{course_id}/sourcecast, returns course sourcecasts" do @tag authenticate: :student test "renders a list of all course sourcecast entries", %{ conn: conn @@ -108,7 +108,7 @@ defmodule CadetWeb.SourcecastControllerTest do end end - describe "POST /v2/course/{course_id}/sourcecast, student" do + describe "POST /v2/courses/{course_id}/sourcecast, student" do @tag authenticate: :student test "prohibited", %{conn: conn} do course_id = conn.assigns[:course_id] @@ -132,7 +132,7 @@ defmodule CadetWeb.SourcecastControllerTest do end end - describe "DELETE /v2/course/{course_id}/sourcecast, student" do + describe "DELETE /v2/courses/{course_id}/sourcecast, student" do @tag authenticate: :student test "prohibited", %{conn: conn} do course_id = conn.assigns[:course_id] @@ -143,7 +143,7 @@ defmodule CadetWeb.SourcecastControllerTest do end end - describe "POST /v2/course/{course_id}/sourcecast, staff" do + describe "POST /v2/courses/{course_id}/sourcecast, staff" do @tag authenticate: :staff test "successful for public sourcecast", %{conn: conn} do course_id = conn.assigns[:course_id] @@ -252,7 +252,7 @@ defmodule CadetWeb.SourcecastControllerTest do end end - describe "DELETE /v2/course/{course_id}/sourcecast, staff" do + describe "DELETE /v2/courses/{course_id}/sourcecast, staff" do @tag authenticate: :staff test "successful for public sourcecast", %{conn: conn} do course_id = conn.assigns[:course_id] @@ -278,7 +278,7 @@ defmodule CadetWeb.SourcecastControllerTest do end end - describe "POST /v2/course/{course_id}/sourcecast, admin" do + describe "POST /v2/courses/{course_id}/sourcecast, admin" do @tag authenticate: :admin test "successful for public sourcecast", %{conn: conn} do course_id = conn.assigns[:course_id] @@ -333,7 +333,7 @@ defmodule CadetWeb.SourcecastControllerTest do end end - describe "DELETE /v2/course/{course_id}/sourcecast, admin" do + describe "DELETE /v2/courses/{course_id}/sourcecast, admin" do @tag authenticate: :admin test "successful for public sourcecast", %{conn: conn} do course_id = conn.assigns[:course_id] @@ -360,7 +360,7 @@ defmodule CadetWeb.SourcecastControllerTest do end defp build_url(), do: "/v2/sourcecast/" - defp build_url(course_id), do: "/v2/course/#{course_id}/sourcecast/" + defp build_url(course_id), do: "/v2/courses/#{course_id}/sourcecast/" defp build_url(course_id, sourcecast_id), do: "#{build_url(course_id)}#{sourcecast_id}/" defp seed_db(course_id) do diff --git a/test/cadet_web/controllers/stories_controller_test.exs b/test/cadet_web/controllers/stories_controller_test.exs index 064cc31f7..9019f0f55 100644 --- a/test/cadet_web/controllers/stories_controller_test.exs +++ b/test/cadet_web/controllers/stories_controller_test.exs @@ -36,32 +36,32 @@ defmodule CadetWeb.StoriesControllerTest do end describe "unauthenticated" do - test "GET /v2/course/{course_id}/stories/", %{conn: conn} do + test "GET /v2/courses/{course_id}/stories/", %{conn: conn} do course = insert(:course) conn = get(conn, build_url(course.id), %{}) assert response(conn, 401) =~ "Unauthorised" end - test "POST /v2/course/{course_id}/stories/", %{conn: conn} do + test "POST /v2/courses/{course_id}/stories/", %{conn: conn} do course = insert(:course) conn = post(conn, build_url(course.id), %{}) assert response(conn, 401) =~ "Unauthorised" end - test "DELETE /v2/course/{course_id}/stories/:storyid", %{conn: conn} do + test "DELETE /v2/courses/{course_id}/stories/:storyid", %{conn: conn} do course = insert(:course) conn = delete(conn, build_url(course.id, "storyid"), %{}) assert response(conn, 401) =~ "Unauthorised" end - test "POST /v2/course/{course_id}/stories/:storyid", %{conn: conn} do + test "POST /v2/courses/{course_id}/stories/:storyid", %{conn: conn} do course = insert(:course) conn = post(conn, build_url(course.id, "storyid"), %{}) assert response(conn, 401) =~ "Unauthorised" end end - describe "GET /v2/course/{course_id}/stories" do + describe "GET /v2/courses/{course_id}/stories" do @tag authenticate: :student test "student permission, only obtain published open stories from own course", %{ conn: conn, @@ -153,7 +153,7 @@ defmodule CadetWeb.StoriesControllerTest do end end - describe "DELETE /v2/course/{course_id}/stories/:storyid" do + describe "DELETE /v2/courses/{course_id}/stories/:storyid" do @tag authenticate: :student test "student permission, forbidden", %{conn: conn} do course_id = conn.assigns[:course_id] @@ -190,7 +190,7 @@ defmodule CadetWeb.StoriesControllerTest do end end - describe "POST /v2/course/{course_id}/stories/" do + describe "POST /v2/courses/{course_id}/stories/" do @tag authenticate: :student test "student permission, forbidden", %{conn: conn, valid_params: params} do course_id = conn.assigns[:course_id] @@ -216,7 +216,7 @@ defmodule CadetWeb.StoriesControllerTest do end end - describe "POST /v2/course/{course_id}/stories/:storyid" do + describe "POST /v2/courses/{course_id}/stories/:storyid" do @tag authenticate: :student test "student permission, forbidden", %{conn: conn, valid_params: params} do course_id = conn.assigns[:course_id] @@ -264,7 +264,7 @@ defmodule CadetWeb.StoriesControllerTest do end end - defp build_url(course_id), do: "/v2/course/#{course_id}/stories" + defp build_url(course_id), do: "/v2/courses/#{course_id}/stories" defp build_url(course_id, story_id), do: "#{build_url(course_id)}/#{story_id}" defp stringify_camelise_keys(map) do diff --git a/test/cadet_web/controllers/user_controller_test.exs b/test/cadet_web/controllers/user_controller_test.exs index 5669310cc..e6bf8886a 100644 --- a/test/cadet_web/controllers/user_controller_test.exs +++ b/test/cadet_web/controllers/user_controller_test.exs @@ -18,9 +18,9 @@ defmodule CadetWeb.UserControllerTest do test "success, student non-story fields", %{conn: conn} do user = conn.assigns.current_user course = user.latest_viewed - insert(:assessment_type, %{order: 2, type: "test type 2", course: course}) - insert(:assessment_type, %{order: 3, type: "test type 3", course: course}) - insert(:assessment_type, %{order: 1, type: "test type 1", course: course}) + insert(:assessment_config, %{order: 2, type: "test type 2", course: course}) + insert(:assessment_config, %{order: 3, type: "test type 3", course: course}) + insert(:assessment_config, %{order: 1, type: "test type 1", course: course}) cr = Repo.get_by(CourseRegistration, course_id: course.id, user_id: user.id) another_cr = insert(:course_registration, %{user: user}) assessment = insert(:assessment, %{is_published: true, course: course}) @@ -88,7 +88,7 @@ defmodule CadetWeb.UserControllerTest do "story" => nil }, "courseConfiguration" => %{ - "assessmentTypes" => ["test type 1", "test type 2", "test type 3"], + "assessmentTypeNames" => ["test type 1", "test type 2", "test type 3"], "enableAchievements" => true, "enableGame" => true, "enableSourcecast" => true, @@ -360,7 +360,7 @@ defmodule CadetWeb.UserControllerTest do "story" => nil }, "courseConfiguration" => %{ - "assessmentTypes" => [], + "assessmentTypeNames" => [], "enableAchievements" => true, "enableGame" => true, "enableSourcecast" => true, @@ -414,7 +414,7 @@ defmodule CadetWeb.UserControllerTest do end end - describe "PUT /v2/course/{course_id}/user/game_states" do + describe "PUT /v2/courses/{course_id}/user/game_states" do @tag authenticate: :student test "success, updating game state", %{conn: conn} do user = conn.assigns.current_user @@ -435,5 +435,5 @@ defmodule CadetWeb.UserControllerTest do end end - defp build_url(course_id), do: "/v2/course/#{course_id}/user" + defp build_url(course_id), do: "/v2/courses/#{course_id}/user" end diff --git a/test/factories/assessments/assessment_factory.ex b/test/factories/assessments/assessment_factory.ex index e615ccfe9..71682e52e 100644 --- a/test/factories/assessments/assessment_factory.ex +++ b/test/factories/assessments/assessment_factory.ex @@ -8,10 +8,9 @@ defmodule Cadet.Assessments.AssessmentFactory do alias Cadet.Assessments.Assessment def assessment_factory do - # type = Enum.random(Assessment.assessment_types() -- ["practical"]) course = build(:course) - type = build(:assessment_type, %{course: course}) - type_title = type.type + config = build(:assessment_config, %{course: course}) + type_title = config.type # These are actual story identifiers so front-end can use seeds to test more effectively valid_stories = [ @@ -35,7 +34,7 @@ defmodule Cadet.Assessments.AssessmentFactory do ), story: Enum.random(valid_stories), reading: Faker.Lorem.sentence(), - type: type, + config: config, course: course, open_at: Timex.now(), close_at: Timex.shift(Timex.now(), days: Enum.random(1..30)), diff --git a/test/factories/courses/assessment_config_factory.ex b/test/factories/courses/assessment_config_factory.ex index 46832b3e1..4005a8ca8 100644 --- a/test/factories/courses/assessment_config_factory.ex +++ b/test/factories/courses/assessment_config_factory.ex @@ -9,10 +9,12 @@ defmodule Cadet.Courses.AssessmentConfigFactory do def assessment_config_factory do %AssessmentConfig{ + order: 1, + type: "Missions", early_submission_xp: 200, hours_before_early_xp_decay: 48, decay_rate_points_per_hour: 1, - assessment_type: build(:assessment_type) + course: build(:course) } end end diff --git a/test/factories/courses/assessment_type_factory.ex b/test/factories/courses/assessment_type_factory.ex deleted file mode 100644 index 30530e276..000000000 --- a/test/factories/courses/assessment_type_factory.ex +++ /dev/null @@ -1,19 +0,0 @@ -defmodule Cadet.Courses.AssessmentTypeFactory do - @moduledoc """ - Factory for the AssessmentType entity - """ - - defmacro __using__(_opts) do - quote do - alias Cadet.Courses.AssessmentType - - def assessment_type_factory do - %AssessmentType{ - order: 1, - type: "Missions", - course: build(:course) - } - end - end - end -end diff --git a/test/factories/factory.ex b/test/factories/factory.ex index 63c9cfa82..6d8fe077c 100644 --- a/test/factories/factory.ex +++ b/test/factories/factory.ex @@ -24,7 +24,6 @@ defmodule Cadet.Factory do use Cadet.Courses.{ AssessmentConfigFactory, - AssessmentTypeFactory, CourseFactory, GroupFactory, SourcecastFactory From 3fed686674d56437ddd8b8cc02776e4fc444020a Mon Sep 17 00:00:00 2001 From: YaleChen299 Date: Mon, 21 Jun 2021 13:27:04 +0800 Subject: [PATCH 077/174] debuging for working with frontend --- lib/cadet/courses/courses.ex | 12 ++--- lib/cadet_web/controllers/user_controller.ex | 6 +-- lib/cadet_web/router.ex | 2 +- lib/cadet_web/views/assessments_view.ex | 5 +- lib/cadet_web/views/courses_view.ex | 2 +- lib/cadet_web/views/user_view.ex | 2 +- test/cadet/courses/courses_test.exs | 53 +++++++++---------- .../controllers/courses_controller_test.exs | 2 +- .../controllers/user_controller_test.exs | 6 +-- 9 files changed, 42 insertions(+), 48 deletions(-) diff --git a/lib/cadet/courses/courses.ex b/lib/cadet/courses/courses.ex index c8d29aac3..4fa089ac5 100644 --- a/lib/cadet/courses/courses.ex +++ b/lib/cadet/courses/courses.ex @@ -85,8 +85,8 @@ defmodule Cadet.Courses do |> Enum.with_index(1) |> Enum.each(fn {elem, idx} -> case elem do - nil -> delete_assessment_config(%{course_id: course_id, order: idx}) - elem -> insert_or_update_assessment_config(elem) + nil -> delete_assessment_config(course_id, %{order: idx}) + elem -> insert_or_update_assessment_config(course_id, elem) end end) else @@ -95,21 +95,21 @@ defmodule Cadet.Courses do end end - def insert_or_update_assessment_config(params = %{course_id: course_id, order: order}) do + def insert_or_update_assessment_config(course_id, params = %{order: order}) do AssessmentConfig |> where(course_id: ^course_id) |> where(order: ^order) |> Repo.one() |> case do - nil -> AssessmentConfig.changeset(%AssessmentConfig{}, params) + nil -> AssessmentConfig.changeset(%AssessmentConfig{course_id: course_id}, params) at -> AssessmentConfig.changeset(at, params) end |> Repo.insert_or_update() end - @spec delete_assessment_config(map()) :: + @spec delete_assessment_config(integer(), map()) :: {:ok, Ecto.Schema.t()} | {:error, Ecto.Changeset.t()} | {:error, :no_such_enrty} - def delete_assessment_config(params = %{course_id: course_id, order: order}) do + def delete_assessment_config(course_id, params = %{order: order}) do AssessmentConfig |> where(course_id: ^course_id) |> where(order: ^order) diff --git a/lib/cadet_web/controllers/user_controller.ex b/lib/cadet_web/controllers/user_controller.ex index 2c2936030..2575fee78 100644 --- a/lib/cadet_web/controllers/user_controller.ex +++ b/lib/cadet_web/controllers/user_controller.ex @@ -77,7 +77,7 @@ defmodule CadetWeb.UserController do ) end - def update_latest_viewed(conn, %{"course_id" => course_id}) do + def update_latest_viewed(conn, %{"courseId" => course_id}) do case Accounts.update_latest_viewed(conn.assigns.current_user, course_id) do {:ok, %{}} -> text(conn, "OK") @@ -126,13 +126,13 @@ defmodule CadetWeb.UserController do end swagger_path :update_latest_viewed do - put("/v2/user/latest_viewed/{course_id}") + put("/v2/user/latest_viewed") summary("Update user's latest viewed course") security([%{JWT: []}]) consumes("application/json") parameters do - course_id(:path, :integer, "new latest viewed course", required: true) + course_id(:body, :integer, "new latest viewed course", required: true) end response(200, "OK") diff --git a/lib/cadet_web/router.ex b/lib/cadet_web/router.ex index 6513b10d7..ac6aaabf9 100644 --- a/lib/cadet_web/router.ex +++ b/lib/cadet_web/router.ex @@ -55,7 +55,7 @@ defmodule CadetWeb.Router do get("/user", UserController, :index) get("/user/latest_viewed", UserController, :get_latest_viewed) - put("/user/latest_viewed/:course_id", UserController, :update_latest_viewed) + put("/user/latest_viewed", UserController, :update_latest_viewed) end # Authenticated Pages with course diff --git a/lib/cadet_web/views/assessments_view.ex b/lib/cadet_web/views/assessments_view.ex index 3cd26afc0..6518e5026 100644 --- a/lib/cadet_web/views/assessments_view.ex +++ b/lib/cadet_web/views/assessments_view.ex @@ -16,12 +16,11 @@ defmodule CadetWeb.AssessmentsView do shortSummary: :summary_short, openAt: &format_datetime(&1.open_at), closeAt: &format_datetime(&1.close_at), - type: & &1.type.type, + type: & &1.config.type, story: :story, number: :number, reading: :reading, status: &(&1.user_status || "not_attempted"), - maxGrade: :max_grade, maxXp: :max_xp, xp: &(&1.xp || 0), coverImage: :cover_picture, @@ -38,7 +37,7 @@ defmodule CadetWeb.AssessmentsView do %{ id: :id, title: :title, - type: & &1.type.type, + config: & &1.config.type, story: :story, number: :number, reading: :reading, diff --git a/lib/cadet_web/views/courses_view.ex b/lib/cadet_web/views/courses_view.ex index f6c1fd6a6..285e78584 100644 --- a/lib/cadet_web/views/courses_view.ex +++ b/lib/cadet_web/views/courses_view.ex @@ -14,7 +14,7 @@ defmodule CadetWeb.CoursesView do sourceChapter: :source_chapter, sourceVariant: :source_variant, moduleHelpText: :module_help_text, - assessmentTypeNames: :assessment_configs + assessmentTypes: :assessment_configs }) } end diff --git a/lib/cadet_web/views/user_view.ex b/lib/cadet_web/views/user_view.ex index 83ea368cb..bd3d3dff3 100644 --- a/lib/cadet_web/views/user_view.ex +++ b/lib/cadet_web/views/user_view.ex @@ -100,7 +100,7 @@ defmodule CadetWeb.UserView do sourceChapter: :source_chapter, sourceVariant: :source_variant, moduleHelpText: :module_help_text, - assessmentTypeNames: &Enum.map(&1.assessment_config, fn x -> x.type end) + assessmentTypes: &Enum.map(&1.assessment_config, fn x -> x.type end) }) end end diff --git a/test/cadet/courses/courses_test.exs b/test/cadet/courses/courses_test.exs index 5fc1f8953..8a584ae76 100644 --- a/test/cadet/courses/courses_test.exs +++ b/test/cadet/courses/courses_test.exs @@ -149,11 +149,11 @@ defmodule Cadet.CoursesTest do test "succeeds", %{course: course, expected: expected} do :ok = Courses.mass_upsert_or_delete_assessment_configs(course.id, [ - %{course_id: course.id, order: 1, type: "Paths"}, - %{course_id: course.id, order: 2, type: "Quests"}, - %{course_id: course.id, order: 3, type: "Missions"}, - %{course_id: course.id, order: 4, type: "Others"}, - %{course_id: course.id, order: 5, type: "Contests"} + %{order: 1, type: "Paths"}, + %{order: 2, type: "Quests"}, + %{order: 3, type: "Missions"}, + %{order: 4, type: "Others"}, + %{order: 5, type: "Contests"} ]) assessment_configs = Courses.get_assessment_configs(course.id) @@ -164,11 +164,11 @@ defmodule Cadet.CoursesTest do test "succeeds to capitalise", %{course: course, expected: expected} do :ok = Courses.mass_upsert_or_delete_assessment_configs(course.id, [ - %{course_id: course.id, order: 1, type: "Paths"}, - %{course_id: course.id, order: 2, type: "quests"}, - %{course_id: course.id, order: 3, type: "missions"}, - %{course_id: course.id, order: 4, type: "Others"}, - %{course_id: course.id, order: 5, type: "contests"} + %{order: 1, type: "Paths"}, + %{order: 2, type: "quests"}, + %{order: 3, type: "missions"}, + %{order: 4, type: "Others"}, + %{order: 5, type: "contests"} ]) assessment_configs = Courses.get_assessment_configs(course.id) @@ -179,9 +179,9 @@ defmodule Cadet.CoursesTest do test "succeed to delete", %{course: course} do :ok = Courses.mass_upsert_or_delete_assessment_configs(course.id, [ - %{course_id: course.id, order: 1, type: "Paths"}, - %{course_id: course.id, order: 2, type: "quests"}, - %{course_id: course.id, order: 3, type: "missions"} + %{order: 1, type: "Paths"}, + %{order: 2, type: "quests"}, + %{order: 3, type: "missions"} ]) assessment_configs = Courses.get_assessment_configs(course.id) @@ -196,12 +196,12 @@ defmodule Cadet.CoursesTest do test "returns with error for list parameter of greater than length 5", %{course: course} do params = [ - %{course_id: course.id, order: 1, type: "Paths"}, - %{course_id: course.id, order: 2, type: "Quests"}, - %{course_id: course.id, order: 3, type: "Missions"}, - %{course_id: course.id, order: 4, type: "Others"}, - %{course_id: course.id, order: 5, type: "Contests"}, - %{course_id: course.id, order: 6, type: "Homework"} + %{order: 1, type: "Paths"}, + %{order: 2, type: "Quests"}, + %{order: 3, type: "Missions"}, + %{order: 4, type: "Others"}, + %{order: 5, type: "Contests"}, + %{order: 6, type: "Homework"} ] assert {:error, {:bad_request, "Invalid parameter(s)"}} = @@ -222,7 +222,6 @@ defmodule Cadet.CoursesTest do old_configs = Courses.get_assessment_configs(course.id) params = %{ - course_id: course.id, order: 1, type: "Mission", early_submission_xp: 100, @@ -230,7 +229,7 @@ defmodule Cadet.CoursesTest do decay_rate_points_per_hour: 1 } - {:ok, updated_config} = Courses.insert_or_update_assessment_config(params) + {:ok, updated_config} = Courses.insert_or_update_assessment_config(course.id, params) new_configs = Courses.get_assessment_configs(course.id) assert length(old_configs) == 0 @@ -246,7 +245,6 @@ defmodule Cadet.CoursesTest do old_configs = Courses.get_assessment_configs(course.id) params = %{ - course_id: course.id, order: 2, type: "Mission", early_submission_xp: 100, @@ -254,7 +252,7 @@ defmodule Cadet.CoursesTest do decay_rate_points_per_hour: 1 } - {:ok, updated_config} = Courses.insert_or_update_assessment_config(params) + {:ok, updated_config} = Courses.insert_or_update_assessment_config(course.id, params) new_configs = Courses.get_assessment_configs(course.id) assert length(old_configs) == 1 @@ -269,7 +267,6 @@ defmodule Cadet.CoursesTest do config = insert(:assessment_config, %{course: course}) params = %{ - course_id: course.id, order: config.order, type: "Mission", early_submission_xp: 100, @@ -277,7 +274,7 @@ defmodule Cadet.CoursesTest do decay_rate_points_per_hour: 1 } - {:ok, updated_config} = Courses.insert_or_update_assessment_config(params) + {:ok, updated_config} = Courses.insert_or_update_assessment_config(course.id, params) assert updated_config.type == "Mission" assert updated_config.early_submission_xp == 100 @@ -293,11 +290,10 @@ defmodule Cadet.CoursesTest do old_configs = Courses.get_assessment_configs(course.id) params = %{ - course_id: course.id, order: config.order } - {:ok, _} = Courses.delete_assessment_config(params) + {:ok, _} = Courses.delete_assessment_config(course.id, params) new_configs = Courses.get_assessment_configs(course.id) assert length(old_configs) == 1 @@ -309,11 +305,10 @@ defmodule Cadet.CoursesTest do insert(:assessment_config, %{order: 1, course: course}) params = %{ - course_id: course.id, order: 2 } - assert {:error, :no_such_enrty} == Courses.delete_assessment_config(params) + assert {:error, :no_such_enrty} == Courses.delete_assessment_config(course.id, params) end end diff --git a/test/cadet_web/controllers/courses_controller_test.exs b/test/cadet_web/controllers/courses_controller_test.exs index 4c54cc40f..cb6daf1e7 100644 --- a/test/cadet_web/controllers/courses_controller_test.exs +++ b/test/cadet_web/controllers/courses_controller_test.exs @@ -41,7 +41,7 @@ defmodule CadetWeb.CoursesControllerTest do "sourceChapter" => 1, "sourceVariant" => "default", "moduleHelpText" => "Help Text", - "assessmentTypeNames" => ["Missions", "Quests", "Paths"] + "assessmentTypes" => ["Missions", "Quests", "Paths"] } } = resp end diff --git a/test/cadet_web/controllers/user_controller_test.exs b/test/cadet_web/controllers/user_controller_test.exs index e6bf8886a..7ed280665 100644 --- a/test/cadet_web/controllers/user_controller_test.exs +++ b/test/cadet_web/controllers/user_controller_test.exs @@ -88,7 +88,7 @@ defmodule CadetWeb.UserControllerTest do "story" => nil }, "courseConfiguration" => %{ - "assessmentTypeNames" => ["test type 1", "test type 2", "test type 3"], + "assessmentTypes" => ["test type 1", "test type 2", "test type 3"], "enableAchievements" => true, "enableGame" => true, "enableSourcecast" => true, @@ -360,7 +360,7 @@ defmodule CadetWeb.UserControllerTest do "story" => nil }, "courseConfiguration" => %{ - "assessmentTypeNames" => [], + "assessmentTypes" => [], "enableAchievements" => true, "enableGame" => true, "enableSourcecast" => true, @@ -405,7 +405,7 @@ defmodule CadetWeb.UserControllerTest do insert(:course_registration, %{user: user, course: new_course}) conn - |> put("/v2/user/latest_viewed/#{new_course.id}") + |> put("/v2/user/latest_viewed", %{"courseId" => new_course.id}) |> response(200) updated_user = Repo.get(User, user.id) From 980042e01bde3107bab94721155c75411658551d Mon Sep 17 00:00:00 2001 From: YaleChen299 Date: Sat, 26 Jun 2021 01:37:23 +0800 Subject: [PATCH 078/174] remove decay rate and update assessment config reorder logit with test --- lib/cadet/assessments/assessments.ex | 2 +- lib/cadet/courses/assessment_config.ex | 16 +- lib/cadet/courses/courses.ex | 35 ++-- .../admin_courses_controller.ex | 2 +- .../admin_views/admin_courses_view.ex | 3 +- ...0210531155751_add_course_configuration.exs | 3 +- test/cadet/courses/assessment_config_test.exs | 45 +----- test/cadet/courses/courses_test.exs | 152 +++++++++--------- .../admin_courses_controller_test.exs | 29 ++-- .../courses/assessment_config_factory.ex | 1 - 10 files changed, 124 insertions(+), 164 deletions(-) diff --git a/lib/cadet/assessments/assessments.ex b/lib/cadet/assessments/assessments.ex index 19a4b07ab..4b8946137 100644 --- a/lib/cadet/assessments/assessments.ex +++ b/lib/cadet/assessments/assessments.ex @@ -796,7 +796,7 @@ defmodule Cadet.Assessments do end end - # :TODO test bonus logic + # :TODO update bonus logic @spec update_submission_status_and_xp_bonus(%Submission{}) :: {:ok, %Submission{}} | {:error, Ecto.Changeset.t()} defp update_submission_status_and_xp_bonus(submission = %Submission{}) do diff --git a/lib/cadet/courses/assessment_config.ex b/lib/cadet/courses/assessment_config.ex index edfc9a1fb..7cec9791f 100644 --- a/lib/cadet/courses/assessment_config.ex +++ b/lib/cadet/courses/assessment_config.ex @@ -13,7 +13,6 @@ defmodule Cadet.Courses.AssessmentConfig do field(:is_graded, :boolean, default: true) field(:early_submission_xp, :integer, default: 0) field(:hours_before_early_xp_decay, :integer, default: 0) - field(:decay_rate_points_per_hour, :integer, default: 0) belongs_to(:course, Course) @@ -21,8 +20,7 @@ defmodule Cadet.Courses.AssessmentConfig do end @required_fields ~w(order course_id)a - @optional_fields ~w(type early_submission_xp hours_before_early_xp_decay - decay_rate_points_per_hour is_graded)a + @optional_fields ~w(type early_submission_xp hours_before_early_xp_decay is_graded)a def changeset(assessment_config, params) do params = capitalize(params, :type) @@ -31,23 +29,13 @@ defmodule Cadet.Courses.AssessmentConfig do |> cast(params, @required_fields ++ @optional_fields) |> validate_required(@required_fields) |> validate_number(:order, greater_than: 0) - |> validate_number(:order, less_than_or_equal_to: 5) + |> validate_number(:order, less_than_or_equal_to: 8) |> validate_number(:early_submission_xp, greater_than_or_equal_to: 0) |> validate_number(:hours_before_early_xp_decay, greater_than_or_equal_to: 0) - |> validate_number(:decay_rate_points_per_hour, greater_than_or_equal_to: 0) - |> validate_decay_rate() |> unique_constraint([:type, :course_id]) - |> unique_constraint([:order, :course_id]) end defp capitalize(params, field) do Map.update(params, field, nil, &String.capitalize/1) end - - defp validate_decay_rate(changeset) do - changeset - |> validate_number(:decay_rate_points_per_hour, - less_than_or_equal_to: get_field(changeset, :early_submission_xp) - ) - end end diff --git a/lib/cadet/courses/courses.ex b/lib/cadet/courses/courses.ex index 4fa089ac5..80e88edd8 100644 --- a/lib/cadet/courses/courses.ex +++ b/lib/cadet/courses/courses.ex @@ -69,25 +69,25 @@ defmodule Cadet.Courses do |> Repo.all() end - def mass_upsert_or_delete_assessment_configs(course_id, configs) do + def mass_upsert_assessment_configs(course_id, configs) do if not is_list(configs) do {:error, {:bad_request, "Invalid parameter(s)"}} else configs_length = configs |> length() - with true <- configs_length <= 5, - true <- configs_length >= 1, - true <- - configs - |> Enum.with_index(1) - |> Enum.all?(fn {elem, i} -> Map.has_key?(elem, :order) && elem.order == i end) do - (configs ++ List.duplicate(nil, 5 - configs_length)) + with true <- configs_length <= 8, + true <- configs_length >= 1 do + configs + |> tl() |> Enum.with_index(1) |> Enum.each(fn {elem, idx} -> - case elem do - nil -> delete_assessment_config(course_id, %{order: idx}) - elem -> insert_or_update_assessment_config(course_id, elem) - end + insert_or_update_assessment_config(course_id, Map.put(elem, :type, <>)) + end) + + configs + |> Enum.with_index(1) + |> Enum.each(fn {elem, idx} -> + insert_or_update_assessment_config(course_id, Map.put(elem, :order, idx)) end) else false -> {:error, {:bad_request, "Invalid parameter(s)"}} @@ -95,10 +95,13 @@ defmodule Cadet.Courses do end end - def insert_or_update_assessment_config(course_id, params = %{order: order}) do + def insert_or_update_assessment_config( + course_id, + params = %{assessment_config_id: assessment_config_id} + ) do AssessmentConfig |> where(course_id: ^course_id) - |> where(order: ^order) + |> where(id: ^assessment_config_id) |> Repo.one() |> case do nil -> AssessmentConfig.changeset(%AssessmentConfig{course_id: course_id}, params) @@ -109,10 +112,10 @@ defmodule Cadet.Courses do @spec delete_assessment_config(integer(), map()) :: {:ok, Ecto.Schema.t()} | {:error, Ecto.Changeset.t()} | {:error, :no_such_enrty} - def delete_assessment_config(course_id, params = %{order: order}) do + def delete_assessment_config(course_id, params = %{assessment_config_id: assessment_config_id}) do AssessmentConfig |> where(course_id: ^course_id) - |> where(order: ^order) + |> where(id: ^assessment_config_id) |> Repo.one() |> case do nil -> {:error, :no_such_enrty} diff --git a/lib/cadet_web/admin_controllers/admin_courses_controller.ex b/lib/cadet_web/admin_controllers/admin_courses_controller.ex index 1dfab462e..92269d30f 100644 --- a/lib/cadet_web/admin_controllers/admin_courses_controller.ex +++ b/lib/cadet_web/admin_controllers/admin_courses_controller.ex @@ -42,7 +42,7 @@ defmodule CadetWeb.AdminCoursesController do if Enum.all?(assessment_configs, &is_map/1) do configs = assessment_configs |> Enum.map(&to_snake_case_atom_keys/1) - case Courses.mass_upsert_or_delete_assessment_configs(course_id, configs) do + case Courses.mass_upsert_assessment_configs(course_id, configs) do :ok -> text(conn, "OK") diff --git a/lib/cadet_web/admin_views/admin_courses_view.ex b/lib/cadet_web/admin_views/admin_courses_view.ex index cc5d11506..fb7bb72f4 100644 --- a/lib/cadet_web/admin_views/admin_courses_view.ex +++ b/lib/cadet_web/admin_views/admin_courses_view.ex @@ -7,10 +7,9 @@ defmodule CadetWeb.AdminCoursesView do def render("config.json", %{config: config}) do transform_map_for_view(config, %{ - order: :order, + AssessmentConfigId: :id, type: :type, isGraded: :is_graded, - decayRatePointsPerHour: :decay_rate_points_per_hour, earlySubmissionXp: :early_submission_xp, hoursBeforeEarlyXpDecay: :hours_before_early_xp_decay }) diff --git a/priv/repo/migrations/20210531155751_add_course_configuration.exs b/priv/repo/migrations/20210531155751_add_course_configuration.exs index 3937c3edf..757ae4694 100644 --- a/priv/repo/migrations/20210531155751_add_course_configuration.exs +++ b/priv/repo/migrations/20210531155751_add_course_configuration.exs @@ -30,11 +30,10 @@ defmodule Cadet.Repo.Migrations.AddCourseConfiguration do add(:is_graded, :boolean, null: false) add(:early_submission_xp, :integer, null: false) add(:hours_before_early_xp_decay, :integer, null: false) - add(:decay_rate_points_per_hour, :integer, null: false) timestamps() end - create(unique_index(:assessment_configs, [:course_id, :order])) + create(unique_index(:assessment_configs, [:course_id, :type])) create table(:course_registrations) do add(:role, :role, null: false) diff --git a/test/cadet/courses/assessment_config_test.exs b/test/cadet/courses/assessment_config_test.exs index 3fa7f5323..a5fc3db99 100644 --- a/test/cadet/courses/assessment_config_test.exs +++ b/test/cadet/courses/assessment_config_test.exs @@ -20,7 +20,7 @@ defmodule Cadet.Courses.AssessmentConfigTest do test "invalid changeset with invalid order" do assert_changeset(%{order: 0, type: "Missions", course_id: 1}, :invalid) - assert_changeset(%{order: 6, type: "Missions", course_id: 1}, :invalid) + assert_changeset(%{order: 9, type: "Missions", course_id: 1}, :invalid) end end @@ -32,8 +32,7 @@ defmodule Cadet.Courses.AssessmentConfigTest do type: "Missions", course_id: 1, early_submission_xp: 200, - hours_before_early_xp_decay: 48, - decay_rate_points_per_hour: 1 + hours_before_early_xp_decay: 48 }, :valid ) @@ -44,8 +43,7 @@ defmodule Cadet.Courses.AssessmentConfigTest do type: "Missions", course_id: 1, early_submission_xp: 0, - hours_before_early_xp_decay: 0, - decay_rate_points_per_hour: 0 + hours_before_early_xp_decay: 0 }, :valid ) @@ -56,8 +54,7 @@ defmodule Cadet.Courses.AssessmentConfigTest do type: "Missions", course_id: 1, early_submission_xp: 200, - hours_before_early_xp_decay: 0, - decay_rate_points_per_hour: 10 + hours_before_early_xp_decay: 0 }, :valid ) @@ -70,8 +67,7 @@ defmodule Cadet.Courses.AssessmentConfigTest do type: "Missions", course_id: 1, early_submission_xp: -1, - hours_before_early_xp_decay: 0, - decay_rate_points_per_hour: 10 + hours_before_early_xp_decay: 0 }, :invalid ) @@ -84,36 +80,7 @@ defmodule Cadet.Courses.AssessmentConfigTest do type: "Missions", course_id: 1, early_submission_xp: 200, - hours_before_early_xp_decay: -1, - decay_rate_points_per_hour: 10 - }, - :invalid - ) - end - - test "invalid changeset with invalid decay rate" do - assert_changeset( - %{ - order: 1, - type: "Missions", - course_id: 1, - early_submission_xp: 200, - hours_before_early_xp_decay: 0, - decay_rate_points_per_hour: -1 - }, - :invalid - ) - end - - test "invalid changeset with decay rate greater than early submission xp" do - assert_changeset( - %{ - order: 1, - type: "Missions", - course_id: 1, - early_submission_xp: 200, - hours_before_early_xp_decay: 48, - decay_rate_points_per_hour: 300 + hours_before_early_xp_decay: -1 }, :invalid ) diff --git a/test/cadet/courses/courses_test.exs b/test/cadet/courses/courses_test.exs index 8a584ae76..c88e84aed 100644 --- a/test/cadet/courses/courses_test.exs +++ b/test/cadet/courses/courses_test.exs @@ -135,25 +135,41 @@ defmodule Cadet.CoursesTest do end end - describe "mass_upsert_or_delete_assessment_configs" do + describe "mass_upsert_assessment_configs" do setup do course = insert(:course) - insert(:assessment_config, %{order: 1, type: "Missions", course: course}) - insert(:assessment_config, %{order: 2, type: "Quests", course: course}) - insert(:assessment_config, %{order: 3, type: "Paths", course: course}) - insert(:assessment_config, %{order: 4, type: "Contests", course: course}) + config1 = insert(:assessment_config, %{order: 1, type: "Missions", course: course}) + config2 = insert(:assessment_config, %{order: 2, type: "Quests", course: course}) + config3 = insert(:assessment_config, %{order: 3, type: "Paths", course: course}) + config4 = insert(:assessment_config, %{order: 4, type: "Contests", course: course}) expected = ["Paths", "Quests", "Missions", "Others", "Contests"] - {:ok, %{course: course, expected: expected}} + + {:ok, + %{ + course: course, + expected: expected, + config1: config1, + config2: config2, + config3: config3, + config4: config4 + }} end - test "succeeds", %{course: course, expected: expected} do + test "succeeds", %{ + course: course, + expected: expected, + config1: config1, + config2: config2, + config3: config3, + config4: config4 + } do :ok = - Courses.mass_upsert_or_delete_assessment_configs(course.id, [ - %{order: 1, type: "Paths"}, - %{order: 2, type: "Quests"}, - %{order: 3, type: "Missions"}, - %{order: 4, type: "Others"}, - %{order: 5, type: "Contests"} + Courses.mass_upsert_assessment_configs(course.id, [ + %{assessment_config_id: config1.id, type: "Paths"}, + %{assessment_config_id: config2.id, type: "Quests"}, + %{assessment_config_id: config3.id, type: "Missions"}, + %{assessment_config_id: config4.id, type: "Others"}, + %{assessment_config_id: -1, type: "Contests"} ]) assessment_configs = Courses.get_assessment_configs(course.id) @@ -161,14 +177,21 @@ defmodule Cadet.CoursesTest do assert Enum.map(assessment_configs, & &1.type) == expected end - test "succeeds to capitalise", %{course: course, expected: expected} do + test "succeeds to capitalise", %{ + course: course, + expected: expected, + config1: config1, + config2: config2, + config3: config3, + config4: config4 + } do :ok = - Courses.mass_upsert_or_delete_assessment_configs(course.id, [ - %{order: 1, type: "Paths"}, - %{order: 2, type: "quests"}, - %{order: 3, type: "missions"}, - %{order: 4, type: "Others"}, - %{order: 5, type: "contests"} + Courses.mass_upsert_assessment_configs(course.id, [ + %{assessment_config_id: config1.id, type: "Paths"}, + %{assessment_config_id: config2.id, type: "Quests"}, + %{assessment_config_id: config3.id, type: "Missions"}, + %{assessment_config_id: config4.id, type: "Others"}, + %{assessment_config_id: -1, type: "Contests"} ]) assessment_configs = Courses.get_assessment_configs(course.id) @@ -176,57 +199,66 @@ defmodule Cadet.CoursesTest do assert Enum.map(assessment_configs, & &1.type) == expected end - test "succeed to delete", %{course: course} do - :ok = - Courses.mass_upsert_or_delete_assessment_configs(course.id, [ - %{order: 1, type: "Paths"}, - %{order: 2, type: "quests"}, - %{order: 3, type: "missions"} - ]) + # test "succeed to delete", %{course: course} do + # :ok = + # Courses.mass_upsert_assessment_configs(course.id, [ + # %{order: 1, type: "Paths"}, + # %{order: 2, type: "quests"}, + # %{order: 3, type: "missions"} + # ]) - assessment_configs = Courses.get_assessment_configs(course.id) + # assessment_configs = Courses.get_assessment_configs(course.id) - assert Enum.map(assessment_configs, & &1.type) == ["Paths", "Quests", "Missions"] - end + # assert Enum.map(assessment_configs, & &1.type) == ["Paths", "Quests", "Missions"] + # end test "returns with error for empty list parameter", %{course: course} do assert {:error, {:bad_request, "Invalid parameter(s)"}} = - Courses.mass_upsert_or_delete_assessment_configs(course.id, []) + Courses.mass_upsert_assessment_configs(course.id, []) end - test "returns with error for list parameter of greater than length 5", %{course: course} do + test "returns with error for list parameter of greater than length 8", %{ + course: course, + config1: config1, + config2: config2, + config3: config3, + config4: config4 + } do params = [ - %{order: 1, type: "Paths"}, - %{order: 2, type: "Quests"}, - %{order: 3, type: "Missions"}, - %{order: 4, type: "Others"}, - %{order: 5, type: "Contests"}, - %{order: 6, type: "Homework"} + %{assessment_config_id: config1.id, type: "Paths"}, + %{assessment_config_id: config2.id, type: "Quests"}, + %{assessment_config_id: config3.id, type: "Missions"}, + %{assessment_config_id: config4.id, type: "Others"}, + %{assessment_config_id: -1, type: "Contests"}, + %{assessment_config_id: -1, type: "Contests"}, + %{assessment_config_id: -1, type: "Contests"}, + %{assessment_config_id: -1, type: "Contests"}, + %{assessment_config_id: -1, type: "Contests"} ] assert {:error, {:bad_request, "Invalid parameter(s)"}} = - Courses.mass_upsert_or_delete_assessment_configs(course.id, params) + Courses.mass_upsert_assessment_configs(course.id, params) end test "returns with error for non-list parameter", %{course: course} do params = %{course_id: course.id, order: 1, type: "Paths"} assert {:error, {:bad_request, "Invalid parameter(s)"}} = - Courses.mass_upsert_or_delete_assessment_configs(course.id, params) + Courses.mass_upsert_assessment_configs(course.id, params) end end describe "insert_or_update_assessment_config" do - test "succeeds with insert to empty configs" do + test "succeeds with insert configs" do course = insert(:course) old_configs = Courses.get_assessment_configs(course.id) params = %{ + assessment_config_id: -1, order: 1, type: "Mission", early_submission_xp: 100, - hours_before_early_xp_decay: 24, - decay_rate_points_per_hour: 1 + hours_before_early_xp_decay: 24 } {:ok, updated_config} = Courses.insert_or_update_assessment_config(course.id, params) @@ -236,30 +268,6 @@ defmodule Cadet.CoursesTest do assert length(new_configs) == 1 assert updated_config.early_submission_xp == 100 assert updated_config.hours_before_early_xp_decay == 24 - assert updated_config.decay_rate_points_per_hour == 1 - end - - test "succeeds with insert to existing configs" do - course = insert(:course) - insert(:assessment_config, %{order: 1, course: course}) - old_configs = Courses.get_assessment_configs(course.id) - - params = %{ - order: 2, - type: "Mission", - early_submission_xp: 100, - hours_before_early_xp_decay: 24, - decay_rate_points_per_hour: 1 - } - - {:ok, updated_config} = Courses.insert_or_update_assessment_config(course.id, params) - - new_configs = Courses.get_assessment_configs(course.id) - assert length(old_configs) == 1 - assert length(new_configs) == 2 - assert updated_config.early_submission_xp == 100 - assert updated_config.hours_before_early_xp_decay == 24 - assert updated_config.decay_rate_points_per_hour == 1 end test "succeeds with update" do @@ -267,11 +275,10 @@ defmodule Cadet.CoursesTest do config = insert(:assessment_config, %{course: course}) params = %{ - order: config.order, + assessment_config_id: config.id, type: "Mission", early_submission_xp: 100, - hours_before_early_xp_decay: 24, - decay_rate_points_per_hour: 1 + hours_before_early_xp_decay: 24 } {:ok, updated_config} = Courses.insert_or_update_assessment_config(course.id, params) @@ -279,7 +286,6 @@ defmodule Cadet.CoursesTest do assert updated_config.type == "Mission" assert updated_config.early_submission_xp == 100 assert updated_config.hours_before_early_xp_decay == 24 - assert updated_config.decay_rate_points_per_hour == 1 end end @@ -290,7 +296,7 @@ defmodule Cadet.CoursesTest do old_configs = Courses.get_assessment_configs(course.id) params = %{ - order: config.order + assessment_config_id: config.id } {:ok, _} = Courses.delete_assessment_config(course.id, params) @@ -305,7 +311,7 @@ defmodule Cadet.CoursesTest do insert(:assessment_config, %{order: 1, course: course}) params = %{ - order: 2 + assessment_config_id: -1 } assert {:error, :no_such_enrty} == Courses.delete_assessment_config(course.id, params) diff --git a/test/cadet_web/admin_controllers/admin_courses_controller_test.exs b/test/cadet_web/admin_controllers/admin_courses_controller_test.exs index a343e5f3a..a12d1b8cc 100644 --- a/test/cadet_web/admin_controllers/admin_courses_controller_test.exs +++ b/test/cadet_web/admin_controllers/admin_courses_controller_test.exs @@ -146,9 +146,11 @@ defmodule CadetWeb.AdminCoursesControllerTest do test "succeeds", %{conn: conn} do course_id = conn.assigns[:course_id] course = Repo.get(Course, course_id) - insert(:assessment_config, %{order: 1, type: "Mission1", course: course}) - insert(:assessment_config, %{order: 3, type: "Mission3", course: course}) - insert(:assessment_config, %{is_graded: false, order: 2, type: "Mission2", course: course}) + config1 = insert(:assessment_config, %{order: 1, type: "Mission1", course: course}) + config3 = insert(:assessment_config, %{order: 3, type: "Mission3", course: course}) + + config2 = + insert(:assessment_config, %{is_graded: false, order: 2, type: "Mission2", course: course}) resp = conn @@ -157,28 +159,25 @@ defmodule CadetWeb.AdminCoursesControllerTest do expected = [ %{ - "decayRatePointsPerHour" => 1, "earlySubmissionXp" => 200, "hoursBeforeEarlyXpDecay" => 48, "isGraded" => true, - "order" => 1, - "type" => "Mission1" + "type" => "Mission1", + "AssessmentConfigId" => config1.id }, %{ - "decayRatePointsPerHour" => 1, "earlySubmissionXp" => 200, "hoursBeforeEarlyXpDecay" => 48, "isGraded" => false, - "order" => 2, - "type" => "Mission2" + "type" => "Mission2", + "AssessmentConfigId" => config2.id }, %{ - "decayRatePointsPerHour" => 1, "earlySubmissionXp" => 200, "hoursBeforeEarlyXpDecay" => 48, "isGraded" => true, - "order" => 3, - "type" => "Mission3" + "type" => "Mission3", + "AssessmentConfigId" => config3.id } ] @@ -199,23 +198,23 @@ defmodule CadetWeb.AdminCoursesControllerTest do @tag authenticate: :admin test "succeeds", %{conn: conn} do course_id = conn.assigns[:course_id] - insert(:assessment_config, %{course: Repo.get(Course, course_id)}) + config = insert(:assessment_config, %{course: Repo.get(Course, course_id)}) old_configs = Courses.get_assessment_configs(course_id) |> Enum.map(& &1.type) params = %{ "assessmentConfigs" => [ %{ + "AssessmentConfigId" => config.id, "courseId" => course_id, - "order" => 1, "type" => "Missions", "earlySubmissionXp" => 100, "hoursBeforeEarlyXpDecay" => 24, "decayRatePointsPerHour" => 1 }, %{ + "AssessmentConfigId" => -1, "courseId" => course_id, - "order" => 2, "type" => "Paths", "earlySubmissionXp" => 100, "hoursBeforeEarlyXpDecay" => 24, diff --git a/test/factories/courses/assessment_config_factory.ex b/test/factories/courses/assessment_config_factory.ex index 4005a8ca8..80fab667a 100644 --- a/test/factories/courses/assessment_config_factory.ex +++ b/test/factories/courses/assessment_config_factory.ex @@ -13,7 +13,6 @@ defmodule Cadet.Courses.AssessmentConfigFactory do type: "Missions", early_submission_xp: 200, hours_before_early_xp_decay: 48, - decay_rate_points_per_hour: 1, course: build(:course) } end From d74234888e08b3e2050e23dc8e7d82893c966302 Mon Sep 17 00:00:00 2001 From: chownces <53928333+chownces@users.noreply.github.com> Date: Sat, 26 Jun 2021 14:18:31 +0800 Subject: [PATCH 079/174] Updated sign_in flow and user table to allow for NULL name --- lib/cadet/accounts/accounts.ex | 46 +++++++++---------- lib/cadet/accounts/user.ex | 4 +- ...055445_alter_user_table_for_onboarding.exs | 17 +++++++ test/cadet/accounts/accounts_test.exs | 16 ++++++- test/cadet/accounts/user_test.exs | 4 +- 5 files changed, 57 insertions(+), 30 deletions(-) create mode 100644 priv/repo/migrations/20210626055445_alter_user_table_for_onboarding.exs diff --git a/lib/cadet/accounts/accounts.ex b/lib/cadet/accounts/accounts.ex index fa3bce754..8284b7ecd 100644 --- a/lib/cadet/accounts/accounts.ex +++ b/lib/cadet/accounts/accounts.ex @@ -73,31 +73,27 @@ defmodule Cadet.Accounts do Sign in using given user ID """ def sign_in(username, token, provider) do - case Repo.one(Query.username(username)) do - nil -> - # user is not registered in our database - # :TODO recheck when designing onboarding process (assign role to module) - # :TODO get_role process to be put in course creation? - # with {:ok, role} <- Provider.get_role(provider, token), - # {:ok, name} <- Provider.get_name(provider, token), - # {:ok, _} <- register(%{name: name, username: username}, role) do - # sign_in(username, name, token) - with {:ok, name} <- Provider.get_name(provider, token), - {:ok, _} <- register(%{name: name, username: username}) do - sign_in(username, name, token) - else - {:error, :invalid_credentials, err} -> - {:error, :forbidden, err} - - {:error, :upstream, err} -> - {:error, :bad_request, err} - - {:error, _err} -> - {:error, :internal_server_error} - end - - user -> - {:ok, user} + user = Repo.one(Query.username(username)) + + if is_nil(user) or is_nil(user.name) do + # user is not registered in our database or does not have a name + # (accounts pre-created by instructors do not have a name, and has to be fetched + # from the auth provider during sign_in) + with {:ok, name} <- Provider.get_name(provider, token), + {:ok, _} <- register(%{name: name, username: username}) do + sign_in(username, name, token) + else + {:error, :invalid_credentials, err} -> + {:error, :forbidden, err} + + {:error, :upstream, err} -> + {:error, :bad_request, err} + + {:error, _err} -> + {:error, :internal_server_error} + end + else + {:ok, user} end end diff --git a/lib/cadet/accounts/user.ex b/lib/cadet/accounts/user.ex index a4437839c..ac06d4382 100644 --- a/lib/cadet/accounts/user.ex +++ b/lib/cadet/accounts/user.ex @@ -18,8 +18,8 @@ defmodule Cadet.Accounts.User do timestamps() end - @required_fields ~w(name)a - @optional_fields ~w(username latest_viewed_id)a + @required_fields ~w(username)a + @optional_fields ~w(name latest_viewed_id)a def changeset(user, params \\ %{}) do user diff --git a/priv/repo/migrations/20210626055445_alter_user_table_for_onboarding.exs b/priv/repo/migrations/20210626055445_alter_user_table_for_onboarding.exs new file mode 100644 index 000000000..6c4056d37 --- /dev/null +++ b/priv/repo/migrations/20210626055445_alter_user_table_for_onboarding.exs @@ -0,0 +1,17 @@ +defmodule Cadet.Repo.Migrations.AlterUserTableForOnboarding do + use Ecto.Migration + + def up do + alter table(:users) do + modify(:name, :string, null: true) + modify(:username, :string, null: false) + end + end + + def down do + alter table(:users) do + modify(:name, :string, null: false) + modify(:username, :string, null: true) + end + end +end diff --git a/test/cadet/accounts/accounts_test.exs b/test/cadet/accounts/accounts_test.exs index aa75438b5..4b2920723 100644 --- a/test/cadet/accounts/accounts_test.exs +++ b/test/cadet/accounts/accounts_test.exs @@ -38,7 +38,21 @@ defmodule Cadet.AccountsTest do describe "sign in using auth provider" do test "unregistered user" do {:ok, _user} = Accounts.sign_in("student", "student_token", "test") - assert Repo.one(Query.username("student")).username == "student" + user = Repo.one(Query.username("student")) + assert user.username == "student" + + # as set in config/test.exs + assert user.name == "student 1" + end + + test "pre-created user during first login" do + user = insert(:user, %{username: "student", name: nil}) + {:ok, _user} = Accounts.sign_in("student", "student_token", "test") + user = Repo.one(Query.username("student")) + assert user.username == "student" + + # as set in config/test.exs + assert user.name == "student 1" end test "registered user" do diff --git a/test/cadet/accounts/user_test.exs b/test/cadet/accounts/user_test.exs index 932db6588..e6a05cc5c 100644 --- a/test/cadet/accounts/user_test.exs +++ b/test/cadet/accounts/user_test.exs @@ -5,8 +5,8 @@ defmodule Cadet.Accounts.UserTest do describe "Changesets" do test "valid changeset" do - assert_changeset(%{name: "happy people", role: :admin}, :valid) - assert_changeset(%{name: "happy", role: :student}, :valid) + assert_changeset(%{username: "luminus/E0000000"}, :valid) + assert_changeset(%{username: "luminus/E0000001", name: "Avenger"}, :valid) end test "invalid changeset" do From 80aa57184b73e4b99b72791d494df3a89864bda7 Mon Sep 17 00:00:00 2001 From: YaleChen299 Date: Sat, 26 Jun 2021 16:27:35 +0800 Subject: [PATCH 080/174] refactor reorder logit with test --- lib/cadet/courses/assessment_config.ex | 2 +- lib/cadet/courses/courses.ex | 30 ++++++++++++------- .../admin_courses_controller.ex | 4 +-- ...0210531155751_add_course_configuration.exs | 4 +-- test/cadet/courses/courses_test.exs | 18 +++++------ 5 files changed, 34 insertions(+), 24 deletions(-) diff --git a/lib/cadet/courses/assessment_config.ex b/lib/cadet/courses/assessment_config.ex index 7cec9791f..4c425b730 100644 --- a/lib/cadet/courses/assessment_config.ex +++ b/lib/cadet/courses/assessment_config.ex @@ -32,7 +32,7 @@ defmodule Cadet.Courses.AssessmentConfig do |> validate_number(:order, less_than_or_equal_to: 8) |> validate_number(:early_submission_xp, greater_than_or_equal_to: 0) |> validate_number(:hours_before_early_xp_decay, greater_than_or_equal_to: 0) - |> unique_constraint([:type, :course_id]) + |> unique_constraint([:order, :course_id]) end defp capitalize(params, field) do diff --git a/lib/cadet/courses/courses.ex b/lib/cadet/courses/courses.ex index 80e88edd8..4c51bae69 100644 --- a/lib/cadet/courses/courses.ex +++ b/lib/cadet/courses/courses.ex @@ -69,7 +69,7 @@ defmodule Cadet.Courses do |> Repo.all() end - def mass_upsert_assessment_configs(course_id, configs) do + def mass_upsert_and_reorder_assessment_configs(course_id, configs) do if not is_list(configs) do {:error, {:bad_request, "Invalid parameter(s)"}} else @@ -78,17 +78,11 @@ defmodule Cadet.Courses do with true <- configs_length <= 8, true <- configs_length >= 1 do configs - |> tl() - |> Enum.with_index(1) - |> Enum.each(fn {elem, idx} -> - insert_or_update_assessment_config(course_id, Map.put(elem, :type, <>)) + |> Enum.each(fn elem -> + insert_or_update_assessment_config(course_id, elem) end) - configs - |> Enum.with_index(1) - |> Enum.each(fn {elem, idx} -> - insert_or_update_assessment_config(course_id, Map.put(elem, :order, idx)) - end) + reorder_assessment_configs(course_id, configs) else false -> {:error, {:bad_request, "Invalid parameter(s)"}} end @@ -110,6 +104,22 @@ defmodule Cadet.Courses do |> Repo.insert_or_update() end + def reorder_assessment_configs(course_id, configs) do + Repo.transaction(fn -> + configs + |> Enum.each(fn elem -> + insert_or_update_assessment_config(course_id, Map.put(elem, :order, nil)) + end) + + configs + |> Enum.with_index(1) + |> Enum.each(fn {elem, idx} -> + insert_or_update_assessment_config(course_id, Map.put(elem, :order, idx)) + end) + end + ) + end + @spec delete_assessment_config(integer(), map()) :: {:ok, Ecto.Schema.t()} | {:error, Ecto.Changeset.t()} | {:error, :no_such_enrty} def delete_assessment_config(course_id, params = %{assessment_config_id: assessment_config_id}) do diff --git a/lib/cadet_web/admin_controllers/admin_courses_controller.ex b/lib/cadet_web/admin_controllers/admin_courses_controller.ex index 92269d30f..0bc810668 100644 --- a/lib/cadet_web/admin_controllers/admin_courses_controller.ex +++ b/lib/cadet_web/admin_controllers/admin_courses_controller.ex @@ -42,8 +42,8 @@ defmodule CadetWeb.AdminCoursesController do if Enum.all?(assessment_configs, &is_map/1) do configs = assessment_configs |> Enum.map(&to_snake_case_atom_keys/1) - case Courses.mass_upsert_assessment_configs(course_id, configs) do - :ok -> + case Courses.mass_upsert_and_reorder_assessment_configs(course_id, configs) do + {:ok, _} -> text(conn, "OK") {:error, {status, message}} -> diff --git a/priv/repo/migrations/20210531155751_add_course_configuration.exs b/priv/repo/migrations/20210531155751_add_course_configuration.exs index 757ae4694..b361514de 100644 --- a/priv/repo/migrations/20210531155751_add_course_configuration.exs +++ b/priv/repo/migrations/20210531155751_add_course_configuration.exs @@ -24,7 +24,7 @@ defmodule Cadet.Repo.Migrations.AddCourseConfiguration do end create table(:assessment_configs) do - add(:order, :integer, null: false) + add(:order, :integer, null: true) add(:type, :string, null: false) add(:course_id, references(:courses), null: false) add(:is_graded, :boolean, null: false) @@ -33,7 +33,7 @@ defmodule Cadet.Repo.Migrations.AddCourseConfiguration do timestamps() end - create(unique_index(:assessment_configs, [:course_id, :type])) + create(unique_index(:assessment_configs, [:course_id, :order])) create table(:course_registrations) do add(:role, :role, null: false) diff --git a/test/cadet/courses/courses_test.exs b/test/cadet/courses/courses_test.exs index c88e84aed..5879d4c4d 100644 --- a/test/cadet/courses/courses_test.exs +++ b/test/cadet/courses/courses_test.exs @@ -135,7 +135,7 @@ defmodule Cadet.CoursesTest do end end - describe "mass_upsert_assessment_configs" do + describe "mass_upsert_and_reorder_assessment_configs" do setup do course = insert(:course) config1 = insert(:assessment_config, %{order: 1, type: "Missions", course: course}) @@ -163,8 +163,8 @@ defmodule Cadet.CoursesTest do config3: config3, config4: config4 } do - :ok = - Courses.mass_upsert_assessment_configs(course.id, [ + {:ok, _} = + Courses.mass_upsert_and_reorder_assessment_configs(course.id, [ %{assessment_config_id: config1.id, type: "Paths"}, %{assessment_config_id: config2.id, type: "Quests"}, %{assessment_config_id: config3.id, type: "Missions"}, @@ -185,8 +185,8 @@ defmodule Cadet.CoursesTest do config3: config3, config4: config4 } do - :ok = - Courses.mass_upsert_assessment_configs(course.id, [ + {:ok, _} = + Courses.mass_upsert_and_reorder_assessment_configs(course.id, [ %{assessment_config_id: config1.id, type: "Paths"}, %{assessment_config_id: config2.id, type: "Quests"}, %{assessment_config_id: config3.id, type: "Missions"}, @@ -201,7 +201,7 @@ defmodule Cadet.CoursesTest do # test "succeed to delete", %{course: course} do # :ok = - # Courses.mass_upsert_assessment_configs(course.id, [ + # Courses.mass_upsert_and_reorder_assessment_configs(course.id, [ # %{order: 1, type: "Paths"}, # %{order: 2, type: "quests"}, # %{order: 3, type: "missions"} @@ -214,7 +214,7 @@ defmodule Cadet.CoursesTest do test "returns with error for empty list parameter", %{course: course} do assert {:error, {:bad_request, "Invalid parameter(s)"}} = - Courses.mass_upsert_assessment_configs(course.id, []) + Courses.mass_upsert_and_reorder_assessment_configs(course.id, []) end test "returns with error for list parameter of greater than length 8", %{ @@ -237,14 +237,14 @@ defmodule Cadet.CoursesTest do ] assert {:error, {:bad_request, "Invalid parameter(s)"}} = - Courses.mass_upsert_assessment_configs(course.id, params) + Courses.mass_upsert_and_reorder_assessment_configs(course.id, params) end test "returns with error for non-list parameter", %{course: course} do params = %{course_id: course.id, order: 1, type: "Paths"} assert {:error, {:bad_request, "Invalid parameter(s)"}} = - Courses.mass_upsert_assessment_configs(course.id, params) + Courses.mass_upsert_and_reorder_assessment_configs(course.id, params) end end From 62da75d91d5dba0aa5f333646315a04ee19cd24f Mon Sep 17 00:00:00 2001 From: En Rong <53928333+chownces@users.noreply.github.com> Date: Sat, 26 Jun 2021 16:41:16 +0800 Subject: [PATCH 081/174] Added update_role and delete_user endpoints (#781) * Update views to match frontend requirements * Added update user role route * Added delete user from course --- lib/cadet/accounts/accounts.ex | 88 ++++++ .../admin_user_controller.ex | 74 +++++ lib/cadet_web/admin_views/admin_user_view.ex | 1 + lib/cadet_web/router.ex | 2 + lib/cadet_web/views/user_view.ex | 1 + test/cadet/accounts/accounts_test.exs | 123 ++++++++ .../admin_user_controller_test.exs | 269 +++++++++++++++++- 7 files changed, 553 insertions(+), 5 deletions(-) diff --git a/lib/cadet/accounts/accounts.ex b/lib/cadet/accounts/accounts.ex index fa3bce754..6a6acbd46 100644 --- a/lib/cadet/accounts/accounts.ex +++ b/lib/cadet/accounts/accounts.ex @@ -8,6 +8,7 @@ defmodule Cadet.Accounts do alias Cadet.Accounts.{Query, User, CourseRegistration} alias Cadet.Auth.Provider + alias Cadet.Assessments.{Answer, Submission} @doc """ Register new User entity using Cadet.Accounts.Form.Registration @@ -109,4 +110,91 @@ defmodule Cadet.Accounts do {:error, changeset} -> {:error, {:internal_server_error, full_error_messages(changeset)}} end end + + @update_role_roles ~w(admin)a + def update_role( + _admin_course_reg = %CourseRegistration{ + id: admin_course_reg_id, + course_id: admin_course_id, + role: admin_role + }, + role, + coursereg_id + ) do + with {:validate_role, true} <- {:validate_role, admin_role in @update_role_roles}, + {:validate_not_self, true} <- {:validate_not_self, admin_course_reg_id != coursereg_id}, + {:get_cr, user_course_reg} when not is_nil(user_course_reg) <- + {:get_cr, CourseRegistration |> where(id: ^coursereg_id) |> Repo.one()}, + {:validate_same_course, true} <- + {:validate_same_course, user_course_reg.course_id == admin_course_id}, + {:update_db, {:ok, _} = result} <- + {:update_db, + user_course_reg |> CourseRegistration.changeset(%{role: role}) |> Repo.update()} do + result + else + {:validate_role, false} -> + {:error, {:forbidden, "User is not permitted to change others' roles"}} + + {:validate_not_self, false} -> + {:error, {:bad_request, "Admin not allowed to downgrade own role"}} + + {:get_cr, _} -> + {:error, {:bad_request, "User course registration does not exist"}} + + {:validate_same_course, false} -> + {:error, {:forbidden, "Wrong course"}} + + {:update_db, {:error, changeset}} -> + {:error, {:bad_request, full_error_messages(changeset)}} + end + end + + @delete_user_roles ~w(admin)a + def delete_user( + _admin_course_reg = %CourseRegistration{ + id: admin_course_reg_id, + course_id: admin_course_id, + role: admin_role + }, + coursereg_id + ) do + with {:validate_role, true} <- {:validate_role, admin_role in @delete_user_roles}, + {:validate_not_self, true} <- {:validate_not_self, admin_course_reg_id != coursereg_id}, + {:get_cr, user_course_reg} when not is_nil(user_course_reg) <- + {:get_cr, CourseRegistration |> where(id: ^coursereg_id) |> Repo.one()}, + {:prevent_delete_admin, true} <- {:prevent_delete_admin, user_course_reg.role != :admin}, + {:validate_same_course, true} <- + {:validate_same_course, user_course_reg.course_id == admin_course_id} do + # TODO: Handle deletions of achievement entries, etc. too + + # Delete submissions and answers before deleting user + Submission + |> where(student_id: ^user_course_reg.id) + |> Repo.all() + |> Enum.each(fn x -> + Answer + |> where(submission_id: ^x.id) + |> Repo.delete_all() + + Repo.delete(x) + end) + + Repo.delete(user_course_reg) + else + {:validate_role, false} -> + {:error, {:forbidden, "User is not permitted to delete other users"}} + + {:validate_not_self, false} -> + {:error, {:bad_request, "Admin not allowed to delete ownself from course"}} + + {:get_cr, _} -> + {:error, {:bad_request, "User course registration does not exist"}} + + {:prevent_delete_admin, false} -> + {:error, {:bad_request, "Admins cannot be deleted"}} + + {:validate_same_course, false} -> + {:error, {:forbidden, "Wrong course"}} + end + end end diff --git a/lib/cadet_web/admin_controllers/admin_user_controller.ex b/lib/cadet_web/admin_controllers/admin_user_controller.ex index 0ed85caf9..fb80a779c 100644 --- a/lib/cadet_web/admin_controllers/admin_user_controller.ex +++ b/lib/cadet_web/admin_controllers/admin_user_controller.ex @@ -13,6 +13,30 @@ defmodule CadetWeb.AdminUserController do render(conn, "users.json", users: users) end + def update_role(conn, %{"role" => role, "crId" => coursereg_id}) do + case Accounts.update_role(conn.assigns.course_reg, role, coursereg_id) do + {:ok, %{}} -> + text(conn, "OK") + + {:error, {status, message}} -> + conn + |> put_status(status) + |> text(message) + end + end + + def delete_user(conn, %{"crId" => coursereg_id}) do + case Accounts.delete_user(conn.assigns.course_reg, coursereg_id) do + {:ok, %{}} -> + text(conn, "OK") + + {:error, {status, message}} -> + conn + |> put_status(status) + |> text(message) + end + end + swagger_path :index do get("/v2/courses/{course_id}/admin/users") @@ -24,6 +48,56 @@ defmodule CadetWeb.AdminUserController do response(401, "Unauthorised") end + swagger_path :update_role do + put("/v2/courses/{course_id}/admin/users/role") + + summary("Updates the role of the given user in the the course") + security([%{JWT: []}]) + consumes("application/json") + + parameters do + course_id(:path, :integer, "Course ID", required: true) + role(:body, :role, "The new role", required: true) + + crId(:body, :integer, "The course registration of the user whose role is to be updated", + required: true + ) + end + + response(200, "OK") + + response( + 400, + "Bad Request. User course registration does not exist or admin not allowed to downgrade own role" + ) + + response(403, "Forbidden. User is in different course, or you are not an admin") + end + + swagger_path :delete_user do + delete("/v2/courses/{course_id}/admin/users") + + summary("Deletes a user from a course") + consumes("application/json") + + parameters do + course_id(:path, :integer, "Course ID", required: true) + + crId(:body, :integer, "The course registration of the user whose role is to be updated", + required: true + ) + end + + response(200, "OK") + + response( + 400, + "Bad Request. User course registration does not exist or admin not allowed to delete ownself from course or admins cannot be deleted" + ) + + response(403, "Forbidden. User is in different course, or you are not an admin") + end + def swagger_definitions do %{ AdminUserInfo: diff --git a/lib/cadet_web/admin_views/admin_user_view.ex b/lib/cadet_web/admin_views/admin_user_view.ex index 4b48af13d..70b9d8a51 100644 --- a/lib/cadet_web/admin_views/admin_user_view.ex +++ b/lib/cadet_web/admin_views/admin_user_view.ex @@ -10,6 +10,7 @@ defmodule CadetWeb.AdminUserView do crId: cr.id, course_id: cr.course_id, name: cr.user.name, + username: cr.user.username, role: cr.role, group: case cr.group do diff --git a/lib/cadet_web/router.ex b/lib/cadet_web/router.ex index ac6aaabf9..83f63901f 100644 --- a/lib/cadet_web/router.ex +++ b/lib/cadet_web/router.ex @@ -127,6 +127,8 @@ defmodule CadetWeb.Router do ) get("/users", AdminUserController, :index) + put("/users/role", AdminUserController, :update_role) + delete("/users", AdminUserController, :delete_user) post("/users/:userid/goals/:uuid/progress", AdminGoalsController, :update_progress) put("/achievements", AdminAchievementsController, :bulk_update) diff --git a/lib/cadet_web/views/user_view.ex b/lib/cadet_web/views/user_view.ex index bd3d3dff3..15c5c24b9 100644 --- a/lib/cadet_web/views/user_view.ex +++ b/lib/cadet_web/views/user_view.ex @@ -65,6 +65,7 @@ defmodule CadetWeb.UserView do _ -> %{ + crId: latest.id, courseId: latest.course_id, role: latest.role, group: diff --git a/test/cadet/accounts/accounts_test.exs b/test/cadet/accounts/accounts_test.exs index aa75438b5..83dd511fe 100644 --- a/test/cadet/accounts/accounts_test.exs +++ b/test/cadet/accounts/accounts_test.exs @@ -155,4 +155,127 @@ defmodule Cadet.AccountsTest do assert length(all_stu_in_c1g2) == 0 end end + + describe "update_role" do + setup do + c1 = insert(:course, %{course_name: "c1"}) + c2 = insert(:course, %{course_name: "c2"}) + admin1 = insert(:course_registration, %{course: c1, role: :admin}) + staff1 = insert(:course_registration, %{course: c1, role: :staff}) + student1 = insert(:course_registration, %{course: c1, role: :student}) + student2 = insert(:course_registration, %{course: c2, role: :student}) + + {:ok, %{a1: admin1, s1: student1, s2: student2, st1: staff1}} + end + + test "successful when admin is admin of the course the user is in (student)", %{ + a1: admin1, + s1: %{id: coursereg_id} + } do + {:ok, updated_coursereg} = Accounts.update_role(admin1, "student", coursereg_id) + assert updated_coursereg.role == :student + end + + test "successful when admin is admin of the course the user is in (staff)", %{ + a1: admin1, + s1: %{id: coursereg_id} + } do + {:ok, updated_coursereg} = Accounts.update_role(admin1, "staff", coursereg_id) + assert updated_coursereg.role == :staff + end + + test "successful when admin is admin of the course the user is in (admin)", %{ + a1: admin1, + s1: %{id: coursereg_id} + } do + {:ok, updated_coursereg} = Accounts.update_role(admin1, "admin", coursereg_id) + assert updated_coursereg.role == :admin + end + + test "fails when admin tries to downgrade own role", %{a1: %{id: coursereg_id} = admin1} do + assert {:error, {:bad_request, "Admin not allowed to downgrade own role"}} == + Accounts.update_role(admin1, "staff", coursereg_id) + end + + test "fails when user course registration does not exist", %{ + a1: admin1, + s2: %{id: coursereg_id} + } do + assert {:error, {:bad_request, "User course registration does not exist"}} == + Accounts.update_role(admin1, "staff", coursereg_id + 1) + end + + test "admin is not admin of the course the user is in", %{a1: admin1, s2: %{id: coursereg_id}} do + assert {:error, {:forbidden, "Wrong course"}} == + Accounts.update_role(admin1, "staff", coursereg_id) + end + + test "invalid role provided", %{a1: admin1, s1: %{id: coursereg_id}} do + assert {:error, {:bad_request, "role is invalid"}} == + Accounts.update_role(admin1, "invalidrole", coursereg_id) + end + + test "fails when staff makes changes", %{st1: staff1, s1: %{id: coursereg_id}} do + assert {:error, {:forbidden, "User is not permitted to change others' roles"}} == + Accounts.update_role(staff1, "staff", coursereg_id) + end + end + + describe "delete_user" do + setup do + c1 = insert(:course, %{course_name: "c1"}) + c2 = insert(:course, %{course_name: "c2"}) + admin1 = insert(:course_registration, %{course: c1, role: :admin}) + admin2 = insert(:course_registration, %{course: c1, role: :admin}) + staff1 = insert(:course_registration, %{course: c1, role: :staff}) + student1 = insert(:course_registration, %{course: c1, role: :student}) + student2 = insert(:course_registration, %{course: c2, role: :student}) + + {:ok, %{a1: admin1, a2: admin2, s1: student1, s2: student2, st1: staff1}} + end + + test "successful when admin is admin of the course the user is in (student)", %{ + a1: admin1, + s1: %{id: coursereg_id} + } do + {:ok, deleted_entry} = Accounts.delete_user(admin1, coursereg_id) + assert deleted_entry.id == coursereg_id + end + + test "successful when admin is admin of the course the user is in (staff)", %{ + a1: admin1, + st1: %{id: coursereg_id} + } do + {:ok, deleted_entry} = Accounts.delete_user(admin1, coursereg_id) + assert deleted_entry.id == coursereg_id + end + + test "fails when staff tries to delete user", %{st1: staff1, s1: %{id: coursereg_id}} do + assert {:error, {:forbidden, "User is not permitted to delete other users"}} == + Accounts.delete_user(staff1, coursereg_id) + end + + test "fails when deleting own self", %{a1: %{id: coursereg_id} = admin1} do + assert {:error, {:bad_request, "Admin not allowed to delete ownself from course"}} == + Accounts.delete_user(admin1, coursereg_id) + end + + test "fails when user course registration does not exist", %{ + a1: admin1, + s2: %{id: coursereg_id} + } do + assert {:error, {:bad_request, "User course registration does not exist"}} == + Accounts.delete_user(admin1, coursereg_id + 1) + end + + test "fails when deleting an admin", %{a1: admin1, a2: %{id: coursereg_id}} do + assert {:error, {:bad_request, "Admins cannot be deleted"}} == + Accounts.delete_user(admin1, coursereg_id) + end + + test "fails when deleting a user from another course", %{a1: admin1, s2: %{id: coursereg_id}} do + assert {:error, {:forbidden, "Wrong course"}} == + Accounts.delete_user(admin1, coursereg_id) + end + end end diff --git a/test/cadet_web/admin_controllers/admin_user_controller_test.exs b/test/cadet_web/admin_controllers/admin_user_controller_test.exs index 74d56d6f8..10ffb0089 100644 --- a/test/cadet_web/admin_controllers/admin_user_controller_test.exs +++ b/test/cadet_web/admin_controllers/admin_user_controller_test.exs @@ -2,10 +2,12 @@ defmodule CadetWeb.AdminUserControllerTest do use CadetWeb.ConnCase import Cadet.Factory + import Ecto.Query alias CadetWeb.AdminUserController alias Cadet.Repo alias Cadet.Courses.Course + alias Cadet.Accounts.CourseRegistration test "swagger" do assert is_map(AdminUserController.swagger_definitions()) @@ -23,7 +25,7 @@ defmodule CadetWeb.AdminUserControllerTest do resp = conn - |> get(build_url(course_id)) + |> get(build_url_users(course_id)) |> json_response(200) assert 3 == Enum.count(resp) @@ -39,7 +41,7 @@ defmodule CadetWeb.AdminUserControllerTest do resp = conn - |> get(build_url(course_id) <> "?role=student") + |> get(build_url_users(course_id) <> "?role=student") |> json_response(200) assert 1 == Enum.count(resp) @@ -56,7 +58,7 @@ defmodule CadetWeb.AdminUserControllerTest do resp = conn - |> get(build_url(course_id) <> "?group=#{group.name}") + |> get(build_url_users(course_id) <> "?group=#{group.name}") |> json_response(200) assert 2 == Enum.count(resp) @@ -68,7 +70,7 @@ defmodule CadetWeb.AdminUserControllerTest do course_id = conn.assigns[:course_id] assert conn - |> get(build_url(course_id)) + |> get(build_url_users(course_id)) |> response(403) end @@ -80,5 +82,262 @@ defmodule CadetWeb.AdminUserControllerTest do # end end - defp build_url(course_id), do: "/v2/courses/#{course_id}/admin/users" + describe "PUT /v2/courses/{course_id}/admin/users/role" do + @tag authenticate: :admin + test "success (student to staff), when admin is admin of the course the user is in", %{ + conn: conn + } do + course_id = conn.assigns[:course_id] + course = Repo.get(Course, course_id) + user_course_reg = insert(:course_registration, %{role: :student, course: course}) + + params = %{ + "role" => "staff", + "crId" => user_course_reg.id + } + + resp = put(conn, build_url_users_role(course_id), params) + + assert response(resp, 200) == "OK" + updated_course_reg = Repo.get(CourseRegistration, user_course_reg.id) + assert updated_course_reg.role == :staff + end + + @tag authenticate: :admin + test "success (staff to student), when admin is admin of the course the user is in", %{ + conn: conn + } do + course_id = conn.assigns[:course_id] + course = Repo.get(Course, course_id) + user_course_reg = insert(:course_registration, %{role: :staff, course: course}) + + params = %{ + "role" => "student", + "crId" => user_course_reg.id + } + + resp = put(conn, build_url_users_role(course_id), params) + + assert response(resp, 200) == "OK" + updated_course_reg = Repo.get(CourseRegistration, user_course_reg.id) + assert updated_course_reg.role == :student + end + + @tag authenticate: :admin + test "success (admin to staff), when admin is admin of the course the user is in", %{ + conn: conn + } do + course_id = conn.assigns[:course_id] + course = Repo.get(Course, course_id) + user_course_reg = insert(:course_registration, %{role: :admin, course: course}) + + params = %{ + "role" => "staff", + "crId" => user_course_reg.id + } + + resp = put(conn, build_url_users_role(course_id), params) + + assert response(resp, 200) == "OK" + updated_course_reg = Repo.get(CourseRegistration, user_course_reg.id) + assert updated_course_reg.role == :staff + end + + @tag authenticate: :admin + test "fails, when course registration does not exist", %{conn: conn} do + course_id = conn.assigns[:course_id] + + params = %{ + "role" => "staff", + "crId" => 1 + } + + conn = put(conn, build_url_users_role(course_id), params) + + assert response(conn, 400) == "User course registration does not exist" + end + + @tag authenticate: :admin + test "fails, when admin is NOT admin of the course the user is in", %{conn: conn} do + course_id = conn.assigns[:course_id] + user_course_reg = insert(:course_registration, %{role: :student}) + + params = %{ + "role" => "staff", + "crId" => user_course_reg.id + } + + conn = put(conn, build_url_users_role(course_id), params) + + assert response(conn, 403) == "Wrong course" + unchanged_course_reg = Repo.get(CourseRegistration, user_course_reg.id) + assert unchanged_course_reg.role == :student + end + + @tag authenticate: :staff + test "fails, when staff attempts to make a role change", %{conn: conn} do + course_id = conn.assigns[:course_id] + course = Repo.get(Course, course_id) + user_course_reg = insert(:course_registration, %{role: :student, course: course}) + + params = %{ + "role" => "staff", + "crId" => user_course_reg.id + } + + conn = put(conn, build_url_users_role(course_id), params) + + assert response(conn, 403) == "User is not permitted to change others' roles" + unchanged_course_reg = Repo.get(CourseRegistration, user_course_reg.id) + assert unchanged_course_reg.role == :student + end + + @tag authenticate: :admin + test "fails, when invalid role is provided", %{conn: conn} do + course_id = conn.assigns[:course_id] + course = Repo.get(Course, course_id) + user_course_reg = insert(:course_registration, %{role: :student, course: course}) + + params = %{ + "role" => "avenger", + "crId" => user_course_reg.id + } + + conn = put(conn, build_url_users_role(course_id), params) + + assert response(conn, 400) == "role is invalid" + unchanged_course_reg = Repo.get(CourseRegistration, user_course_reg.id) + assert unchanged_course_reg.role == :student + end + end + + describe "DELETE /v2/courses/{course_id}/admin/users" do + @tag authenticate: :admin + test "success (delete student), when admin is admin of the course the user is in", %{ + conn: conn + } do + course_id = conn.assigns[:course_id] + course = Repo.get(Course, course_id) + user_course_reg = insert(:course_registration, %{role: :student, course: course}) + + params = %{ + "crId" => user_course_reg.id + } + + resp = delete(conn, build_url_users(course_id), params) + + assert response(resp, 200) == "OK" + assert Repo.get(CourseRegistration, user_course_reg.id) == nil + end + + @tag authenticate: :admin + test "success (delete staff), when admin is admin of the course the user is in", %{ + conn: conn + } do + course_id = conn.assigns[:course_id] + course = Repo.get(Course, course_id) + user_course_reg = insert(:course_registration, %{role: :staff, course: course}) + + params = %{ + "crId" => user_course_reg.id + } + + resp = delete(conn, build_url_users(course_id), params) + + assert response(resp, 200) == "OK" + assert Repo.get(CourseRegistration, user_course_reg.id) == nil + end + + @tag authenticate: :staff + test "fails when staff tries to delete user", %{ + conn: conn + } do + course_id = conn.assigns[:course_id] + course = Repo.get(Course, course_id) + user_course_reg = insert(:course_registration, %{role: :student, course: course}) + + params = %{ + "crId" => user_course_reg.id + } + + conn = delete(conn, build_url_users(course_id), params) + + assert response(conn, 403) == "User is not permitted to delete other users" + assert Repo.get(CourseRegistration, user_course_reg.id) != nil + end + + @tag authenticate: :admin + test "fails when admin tries to delete ownself", %{ + conn: conn + } do + course_id = conn.assigns[:course_id] + current_user = conn.assigns[:current_user] + + own_course_reg = + CourseRegistration + |> where(user_id: ^current_user.id) + |> where(course_id: ^course_id) + |> Repo.one() + + params = %{ + "crId" => own_course_reg.id + } + + conn = delete(conn, build_url_users(course_id), params) + + assert response(conn, 400) == "Admin not allowed to delete ownself from course" + assert Repo.get(CourseRegistration, own_course_reg.id) != nil + end + + @tag authenticate: :admin + test "fails when user course registration does not exist", %{ + conn: conn + } do + course_id = conn.assigns[:course_id] + + params = %{ + "crId" => 1 + } + + conn = delete(conn, build_url_users(course_id), params) + + assert response(conn, 400) == "User course registration does not exist" + end + + @tag authenticate: :admin + test "fails when deleting an admin", %{ + conn: conn + } do + course_id = conn.assigns[:course_id] + course = Repo.get(Course, course_id) + user_course_reg = insert(:course_registration, %{role: :admin, course: course}) + + params = %{ + "crId" => user_course_reg.id + } + + conn = delete(conn, build_url_users(course_id), params) + + assert response(conn, 400) == "Admins cannot be deleted" + end + + @tag authenticate: :admin + test "fails when deleting a user from another course", %{ + conn: conn + } do + course_id = conn.assigns[:course_id] + user_course_reg = insert(:course_registration, %{role: :student}) + + params = %{ + "crId" => user_course_reg.id + } + + conn = delete(conn, build_url_users(course_id), params) + + assert response(conn, 403) == "Wrong course" + end + end + + defp build_url_users(course_id), do: "/v2/courses/#{course_id}/admin/users" + defp build_url_users_role(course_id), do: build_url_users(course_id) <> "/role" end From b11f5a2c4f24081f61f8c2254d28f8f32885a4a8 Mon Sep 17 00:00:00 2001 From: YaleChen299 Date: Sat, 26 Jun 2021 17:44:23 +0800 Subject: [PATCH 082/174] tested reorder and mass_upsert_reorder --- lib/cadet/courses/assessment_config.ex | 4 +-- lib/cadet/courses/courses.ex | 26 ++++++++++++++----- test/cadet/courses/assessment_config_test.exs | 1 - test/cadet/courses/courses_test.exs | 26 +++++++++++++++++++ 4 files changed, 48 insertions(+), 9 deletions(-) diff --git a/lib/cadet/courses/assessment_config.ex b/lib/cadet/courses/assessment_config.ex index 4c425b730..1601fca8a 100644 --- a/lib/cadet/courses/assessment_config.ex +++ b/lib/cadet/courses/assessment_config.ex @@ -19,8 +19,8 @@ defmodule Cadet.Courses.AssessmentConfig do timestamps() end - @required_fields ~w(order course_id)a - @optional_fields ~w(type early_submission_xp hours_before_early_xp_decay is_graded)a + @required_fields ~w(course_id)a + @optional_fields ~w(order type early_submission_xp hours_before_early_xp_decay is_graded)a def changeset(assessment_config, params) do params = capitalize(params, :type) diff --git a/lib/cadet/courses/courses.ex b/lib/cadet/courses/courses.ex index 4c51bae69..c92e06679 100644 --- a/lib/cadet/courses/courses.ex +++ b/lib/cadet/courses/courses.ex @@ -78,8 +78,9 @@ defmodule Cadet.Courses do with true <- configs_length <= 8, true <- configs_length >= 1 do configs - |> Enum.each(fn elem -> - insert_or_update_assessment_config(course_id, elem) + |> Enum.map(fn elem -> + {:ok, config} = insert_or_update_assessment_config(course_id, elem) + Map.put(elem, :assessment_config_id, config.id) end) reorder_assessment_configs(course_id, configs) @@ -104,20 +105,33 @@ defmodule Cadet.Courses do |> Repo.insert_or_update() end + defp update_assessment_config( + course_id, + params = %{assessment_config_id: assessment_config_id} + ) do + AssessmentConfig + |> where(course_id: ^course_id) + |> where(id: ^assessment_config_id) + |> Repo.one() + |> case do + nil -> {:error, :no_such_entry} + at -> AssessmentConfig.changeset(at, params) |> Repo.update() + end + end + def reorder_assessment_configs(course_id, configs) do Repo.transaction(fn -> configs |> Enum.each(fn elem -> - insert_or_update_assessment_config(course_id, Map.put(elem, :order, nil)) + update_assessment_config(course_id, Map.put(elem, :order, nil)) end) configs |> Enum.with_index(1) |> Enum.each(fn {elem, idx} -> - insert_or_update_assessment_config(course_id, Map.put(elem, :order, idx)) + update_assessment_config(course_id, Map.put(elem, :order, idx)) end) - end - ) + end) end @spec delete_assessment_config(integer(), map()) :: diff --git a/test/cadet/courses/assessment_config_test.exs b/test/cadet/courses/assessment_config_test.exs index a5fc3db99..52533f8c2 100644 --- a/test/cadet/courses/assessment_config_test.exs +++ b/test/cadet/courses/assessment_config_test.exs @@ -14,7 +14,6 @@ defmodule Cadet.Courses.AssessmentConfigTest do test "invalid changeset missing required params" do assert_changeset(%{order: 1}, :invalid) - assert_changeset(%{course_id: 1}, :invalid) assert_changeset(%{order: 1, type: "Missions"}, :invalid) end diff --git a/test/cadet/courses/courses_test.exs b/test/cadet/courses/courses_test.exs index 5879d4c4d..63277bc1c 100644 --- a/test/cadet/courses/courses_test.exs +++ b/test/cadet/courses/courses_test.exs @@ -289,6 +289,32 @@ defmodule Cadet.CoursesTest do end end + describe "reorder_assessment_config" do + test "succeeds" do + course = insert(:course) + config1 = insert(:assessment_config, %{order: 1, type: "Missions", course: course}) + config3 = insert(:assessment_config, %{order: 2, type: "Paths", course: course}) + config2 = insert(:assessment_config, %{order: 3, type: "Quests", course: course}) + config4 = insert(:assessment_config, %{order: 4, type: "Others", course: course}) + old_configs = Courses.get_assessment_configs(course.id) + + params = [ + %{assessment_config_id: config1.id, type: "Paths"}, + %{assessment_config_id: config2.id, type: "Quests"}, + %{assessment_config_id: config3.id, type: "Missions"}, + %{assessment_config_id: config4.id, type: "Others"} + ] + + expected = ["Paths", "Quests", "Missions", "Others"] + + {:ok, _} = Courses.reorder_assessment_configs(course.id, params) + + new_configs = Courses.get_assessment_configs(course.id) + assert length(old_configs) == length(new_configs) + assert Enum.map(new_configs, & &1.type) == expected + end + end + describe "delete_assessment_config" do test "succeeds" do course = insert(:course) From c90471832feb68fc3b9eab29745288f6f9d12c16 Mon Sep 17 00:00:00 2001 From: chownces <53928333+chownces@users.noreply.github.com> Date: Sat, 26 Jun 2021 18:17:39 +0800 Subject: [PATCH 083/174] Refactor update_role and delete_course_registration --- lib/cadet/accounts/accounts.ex | 87 ------------ lib/cadet/accounts/course_registrations.ex | 65 +++++++-- lib/cadet/courses/courses.ex | 3 +- .../admin_user_controller.ex | 86 +++++++++-- test/cadet/accounts/accounts_test.exs | 123 ---------------- .../accounts/course_registration_test.exs | 134 ++++++++++++------ .../admin_user_controller_test.exs | 6 +- 7 files changed, 221 insertions(+), 283 deletions(-) diff --git a/lib/cadet/accounts/accounts.ex b/lib/cadet/accounts/accounts.ex index 6a6acbd46..c5ce87821 100644 --- a/lib/cadet/accounts/accounts.ex +++ b/lib/cadet/accounts/accounts.ex @@ -110,91 +110,4 @@ defmodule Cadet.Accounts do {:error, changeset} -> {:error, {:internal_server_error, full_error_messages(changeset)}} end end - - @update_role_roles ~w(admin)a - def update_role( - _admin_course_reg = %CourseRegistration{ - id: admin_course_reg_id, - course_id: admin_course_id, - role: admin_role - }, - role, - coursereg_id - ) do - with {:validate_role, true} <- {:validate_role, admin_role in @update_role_roles}, - {:validate_not_self, true} <- {:validate_not_self, admin_course_reg_id != coursereg_id}, - {:get_cr, user_course_reg} when not is_nil(user_course_reg) <- - {:get_cr, CourseRegistration |> where(id: ^coursereg_id) |> Repo.one()}, - {:validate_same_course, true} <- - {:validate_same_course, user_course_reg.course_id == admin_course_id}, - {:update_db, {:ok, _} = result} <- - {:update_db, - user_course_reg |> CourseRegistration.changeset(%{role: role}) |> Repo.update()} do - result - else - {:validate_role, false} -> - {:error, {:forbidden, "User is not permitted to change others' roles"}} - - {:validate_not_self, false} -> - {:error, {:bad_request, "Admin not allowed to downgrade own role"}} - - {:get_cr, _} -> - {:error, {:bad_request, "User course registration does not exist"}} - - {:validate_same_course, false} -> - {:error, {:forbidden, "Wrong course"}} - - {:update_db, {:error, changeset}} -> - {:error, {:bad_request, full_error_messages(changeset)}} - end - end - - @delete_user_roles ~w(admin)a - def delete_user( - _admin_course_reg = %CourseRegistration{ - id: admin_course_reg_id, - course_id: admin_course_id, - role: admin_role - }, - coursereg_id - ) do - with {:validate_role, true} <- {:validate_role, admin_role in @delete_user_roles}, - {:validate_not_self, true} <- {:validate_not_self, admin_course_reg_id != coursereg_id}, - {:get_cr, user_course_reg} when not is_nil(user_course_reg) <- - {:get_cr, CourseRegistration |> where(id: ^coursereg_id) |> Repo.one()}, - {:prevent_delete_admin, true} <- {:prevent_delete_admin, user_course_reg.role != :admin}, - {:validate_same_course, true} <- - {:validate_same_course, user_course_reg.course_id == admin_course_id} do - # TODO: Handle deletions of achievement entries, etc. too - - # Delete submissions and answers before deleting user - Submission - |> where(student_id: ^user_course_reg.id) - |> Repo.all() - |> Enum.each(fn x -> - Answer - |> where(submission_id: ^x.id) - |> Repo.delete_all() - - Repo.delete(x) - end) - - Repo.delete(user_course_reg) - else - {:validate_role, false} -> - {:error, {:forbidden, "User is not permitted to delete other users"}} - - {:validate_not_self, false} -> - {:error, {:bad_request, "Admin not allowed to delete ownself from course"}} - - {:get_cr, _} -> - {:error, {:bad_request, "User course registration does not exist"}} - - {:prevent_delete_admin, false} -> - {:error, {:bad_request, "Admins cannot be deleted"}} - - {:validate_same_course, false} -> - {:error, {:forbidden, "Wrong course"}} - end - end end diff --git a/lib/cadet/accounts/course_registrations.ex b/lib/cadet/accounts/course_registrations.ex index 6b66a6620..c5136191a 100644 --- a/lib/cadet/accounts/course_registrations.ex +++ b/lib/cadet/accounts/course_registrations.ex @@ -8,6 +8,7 @@ defmodule Cadet.Accounts.CourseRegistrations do alias Cadet.Repo alias Cadet.Accounts.{User, CourseRegistration} + alias Cadet.Assessments.{Answer, Submission} alias Cadet.Courses.AssessmentConfig # guide @@ -87,19 +88,20 @@ defmodule Cadet.Accounts.CourseRegistrations do |> Repo.insert_or_update() end - @spec delete_record(map()) :: - {:ok, Ecto.Schema.t()} | {:error, Ecto.Changeset.t()} | {:error, :no_such_enrty} - def delete_record(params = %{user_id: user_id, course_id: course_id}) - when is_ecto_id(user_id) and is_ecto_id(course_id) do - CourseRegistration - |> where(user_id: ^user_id) - |> where(course_id: ^course_id) - |> Repo.one() - |> case do - nil -> {:error, :no_such_enrty} - cr -> CourseRegistration.changeset(cr, params) |> Repo.delete() - end - end + # TODO: Remove eventually (duplicate of delete_course_registration) + # @spec delete_record(map()) :: + # {:ok, Ecto.Schema.t()} | {:error, Ecto.Changeset.t()} | {:error, :no_such_enrty} + # def delete_record(params = %{user_id: user_id, course_id: course_id}) + # when is_ecto_id(user_id) and is_ecto_id(course_id) do + # CourseRegistration + # |> where(user_id: ^user_id) + # |> where(course_id: ^course_id) + # |> Repo.one() + # |> case do + # nil -> {:error, :no_such_enrty} + # cr -> CourseRegistration.changeset(cr, params) |> Repo.delete() + # end + # end def update_game_states(cr = %CourseRegistration{}, new_game_state = %{}) do case cr @@ -109,4 +111,41 @@ defmodule Cadet.Accounts.CourseRegistrations do {:error, changeset} -> {:error, {:internal_server_error, full_error_messages(changeset)}} end end + + def update_role(role, coursereg_id) do + with {:get_cr, course_reg} when not is_nil(course_reg) <- + {:get_cr, CourseRegistration |> where(id: ^coursereg_id) |> Repo.one()} do + case course_reg + |> CourseRegistration.changeset(%{role: role}) + |> Repo.update() do + {:ok, _} = result -> result + {:error, changeset} -> {:error, {:bad_request, full_error_messages(changeset)}} + end + else + {:get_cr, nil} -> {:error, {:bad_request, "User course registration does not exist"}} + end + end + + def delete_course_registration(coursereg_id) do + # TODO: Handle deletions of achievement entries, etc. too + + with {:get_cr, course_reg} when not is_nil(course_reg) <- + {:get_cr, CourseRegistration |> where(id: ^coursereg_id) |> Repo.one()} do + # Delete submissions and answers before deleting user + Submission + |> where(student_id: ^course_reg.id) + |> Repo.all() + |> Enum.each(fn x -> + Answer + |> where(submission_id: ^x.id) + |> Repo.delete_all() + + Repo.delete(x) + end) + + Repo.delete(course_reg) + else + {:get_cr, nil} -> {:error, {:bad_request, "User course registration does not exist"}} + end + end end diff --git a/lib/cadet/courses/courses.ex b/lib/cadet/courses/courses.ex index 4c51bae69..e2042404d 100644 --- a/lib/cadet/courses/courses.ex +++ b/lib/cadet/courses/courses.ex @@ -116,8 +116,7 @@ defmodule Cadet.Courses do |> Enum.each(fn {elem, idx} -> insert_or_update_assessment_config(course_id, Map.put(elem, :order, idx)) end) - end - ) + end) end @spec delete_assessment_config(integer(), map()) :: diff --git a/lib/cadet_web/admin_controllers/admin_user_controller.ex b/lib/cadet_web/admin_controllers/admin_user_controller.ex index fb80a779c..9d29574f1 100644 --- a/lib/cadet_web/admin_controllers/admin_user_controller.ex +++ b/lib/cadet_web/admin_controllers/admin_user_controller.ex @@ -2,7 +2,11 @@ defmodule CadetWeb.AdminUserController do use CadetWeb, :controller use PhoenixSwagger + import Ecto.Query + + alias Cadet.Repo alias Cadet.Accounts + alias Cadet.Accounts.CourseRegistration # This controller is used to find all users of a course @@ -13,27 +17,79 @@ defmodule CadetWeb.AdminUserController do render(conn, "users.json", users: users) end + @update_role_roles ~w(admin)a def update_role(conn, %{"role" => role, "crId" => coursereg_id}) do - case Accounts.update_role(conn.assigns.course_reg, role, coursereg_id) do - {:ok, %{}} -> - text(conn, "OK") - - {:error, {status, message}} -> - conn - |> put_status(status) - |> text(message) + %{id: admin_course_reg_id, role: admin_role, course_id: admin_course_id} = + conn.assigns.course_reg + + with {:validate_role, true} <- {:validate_role, admin_role in @update_role_roles}, + {:validate_not_self, true} <- {:validate_not_self, admin_course_reg_id != coursereg_id}, + {:get_cr, user_course_reg} when not is_nil(user_course_reg) <- + {:get_cr, CourseRegistration |> where(id: ^coursereg_id) |> Repo.one()}, + {:validate_same_course, true} <- + {:validate_same_course, user_course_reg.course_id == admin_course_id} do + case Accounts.CourseRegistrations.update_role(role, coursereg_id) do + {:ok, %{}} -> + text(conn, "OK") + + {:error, {status, message}} -> + conn + |> put_status(status) + |> text(message) + end + else + {:validate_role, false} -> + conn |> put_status(:forbidden) |> text("User is not permitted to change others' roles") + + {:validate_not_self, false} -> + conn |> put_status(:bad_request) |> text("Admin not allowed to downgrade own role") + + {:get_cr, _} -> + conn |> put_status(:bad_request) |> text("User course registration does not exist") + + {:validate_same_course, false} -> + conn |> put_status(:forbidden) |> text("User is in a different course") end end + @delete_user_roles ~w(admin)a def delete_user(conn, %{"crId" => coursereg_id}) do - case Accounts.delete_user(conn.assigns.course_reg, coursereg_id) do - {:ok, %{}} -> - text(conn, "OK") - - {:error, {status, message}} -> + %{id: admin_course_reg_id, role: admin_role, course_id: admin_course_id} = + conn.assigns.course_reg + + with {:validate_role, true} <- {:validate_role, admin_role in @delete_user_roles}, + {:validate_not_self, true} <- {:validate_not_self, admin_course_reg_id != coursereg_id}, + {:get_cr, user_course_reg} when not is_nil(user_course_reg) <- + {:get_cr, CourseRegistration |> where(id: ^coursereg_id) |> Repo.one()}, + {:prevent_delete_admin, true} <- {:prevent_delete_admin, user_course_reg.role != :admin}, + {:validate_same_course, true} <- + {:validate_same_course, user_course_reg.course_id == admin_course_id} do + case Accounts.CourseRegistrations.delete_course_registration(coursereg_id) do + {:ok, %{}} -> + text(conn, "OK") + + {:error, {status, message}} -> + conn + |> put_status(status) + |> text(message) + end + else + {:validate_role, false} -> + conn |> put_status(:forbidden) |> text("User is not permitted to delete other users") + + {:validate_not_self, false} -> conn - |> put_status(status) - |> text(message) + |> put_status(:bad_request) + |> text("Admin not allowed to delete ownself from course") + + {:get_cr, _} -> + conn |> put_status(:bad_request) |> text("User course registration does not exist") + + {:prevent_delete_admin, false} -> + conn |> put_status(:bad_request) |> text("Admins cannot be deleted") + + {:validate_same_course, false} -> + conn |> put_status(:forbidden) |> text("User is in a different course") end end diff --git a/test/cadet/accounts/accounts_test.exs b/test/cadet/accounts/accounts_test.exs index 83dd511fe..aa75438b5 100644 --- a/test/cadet/accounts/accounts_test.exs +++ b/test/cadet/accounts/accounts_test.exs @@ -155,127 +155,4 @@ defmodule Cadet.AccountsTest do assert length(all_stu_in_c1g2) == 0 end end - - describe "update_role" do - setup do - c1 = insert(:course, %{course_name: "c1"}) - c2 = insert(:course, %{course_name: "c2"}) - admin1 = insert(:course_registration, %{course: c1, role: :admin}) - staff1 = insert(:course_registration, %{course: c1, role: :staff}) - student1 = insert(:course_registration, %{course: c1, role: :student}) - student2 = insert(:course_registration, %{course: c2, role: :student}) - - {:ok, %{a1: admin1, s1: student1, s2: student2, st1: staff1}} - end - - test "successful when admin is admin of the course the user is in (student)", %{ - a1: admin1, - s1: %{id: coursereg_id} - } do - {:ok, updated_coursereg} = Accounts.update_role(admin1, "student", coursereg_id) - assert updated_coursereg.role == :student - end - - test "successful when admin is admin of the course the user is in (staff)", %{ - a1: admin1, - s1: %{id: coursereg_id} - } do - {:ok, updated_coursereg} = Accounts.update_role(admin1, "staff", coursereg_id) - assert updated_coursereg.role == :staff - end - - test "successful when admin is admin of the course the user is in (admin)", %{ - a1: admin1, - s1: %{id: coursereg_id} - } do - {:ok, updated_coursereg} = Accounts.update_role(admin1, "admin", coursereg_id) - assert updated_coursereg.role == :admin - end - - test "fails when admin tries to downgrade own role", %{a1: %{id: coursereg_id} = admin1} do - assert {:error, {:bad_request, "Admin not allowed to downgrade own role"}} == - Accounts.update_role(admin1, "staff", coursereg_id) - end - - test "fails when user course registration does not exist", %{ - a1: admin1, - s2: %{id: coursereg_id} - } do - assert {:error, {:bad_request, "User course registration does not exist"}} == - Accounts.update_role(admin1, "staff", coursereg_id + 1) - end - - test "admin is not admin of the course the user is in", %{a1: admin1, s2: %{id: coursereg_id}} do - assert {:error, {:forbidden, "Wrong course"}} == - Accounts.update_role(admin1, "staff", coursereg_id) - end - - test "invalid role provided", %{a1: admin1, s1: %{id: coursereg_id}} do - assert {:error, {:bad_request, "role is invalid"}} == - Accounts.update_role(admin1, "invalidrole", coursereg_id) - end - - test "fails when staff makes changes", %{st1: staff1, s1: %{id: coursereg_id}} do - assert {:error, {:forbidden, "User is not permitted to change others' roles"}} == - Accounts.update_role(staff1, "staff", coursereg_id) - end - end - - describe "delete_user" do - setup do - c1 = insert(:course, %{course_name: "c1"}) - c2 = insert(:course, %{course_name: "c2"}) - admin1 = insert(:course_registration, %{course: c1, role: :admin}) - admin2 = insert(:course_registration, %{course: c1, role: :admin}) - staff1 = insert(:course_registration, %{course: c1, role: :staff}) - student1 = insert(:course_registration, %{course: c1, role: :student}) - student2 = insert(:course_registration, %{course: c2, role: :student}) - - {:ok, %{a1: admin1, a2: admin2, s1: student1, s2: student2, st1: staff1}} - end - - test "successful when admin is admin of the course the user is in (student)", %{ - a1: admin1, - s1: %{id: coursereg_id} - } do - {:ok, deleted_entry} = Accounts.delete_user(admin1, coursereg_id) - assert deleted_entry.id == coursereg_id - end - - test "successful when admin is admin of the course the user is in (staff)", %{ - a1: admin1, - st1: %{id: coursereg_id} - } do - {:ok, deleted_entry} = Accounts.delete_user(admin1, coursereg_id) - assert deleted_entry.id == coursereg_id - end - - test "fails when staff tries to delete user", %{st1: staff1, s1: %{id: coursereg_id}} do - assert {:error, {:forbidden, "User is not permitted to delete other users"}} == - Accounts.delete_user(staff1, coursereg_id) - end - - test "fails when deleting own self", %{a1: %{id: coursereg_id} = admin1} do - assert {:error, {:bad_request, "Admin not allowed to delete ownself from course"}} == - Accounts.delete_user(admin1, coursereg_id) - end - - test "fails when user course registration does not exist", %{ - a1: admin1, - s2: %{id: coursereg_id} - } do - assert {:error, {:bad_request, "User course registration does not exist"}} == - Accounts.delete_user(admin1, coursereg_id + 1) - end - - test "fails when deleting an admin", %{a1: admin1, a2: %{id: coursereg_id}} do - assert {:error, {:bad_request, "Admins cannot be deleted"}} == - Accounts.delete_user(admin1, coursereg_id) - end - - test "fails when deleting a user from another course", %{a1: admin1, s2: %{id: coursereg_id}} do - assert {:error, {:forbidden, "Wrong course"}} == - Accounts.delete_user(admin1, coursereg_id) - end - end end diff --git a/test/cadet/accounts/course_registration_test.exs b/test/cadet/accounts/course_registration_test.exs index 3bca7fa0d..d4c78d724 100644 --- a/test/cadet/accounts/course_registration_test.exs +++ b/test/cadet/accounts/course_registration_test.exs @@ -234,58 +234,112 @@ defmodule Cadet.Accounts.CourseRegistrationTest do end end - describe "delete course_registration" do - test "succeeds", %{course1: course1, user1: user1} do - assert length(CourseRegistrations.get_users(course1.id)) == 1 - - {:ok, _course_reg} = - CourseRegistrations.delete_record(%{ - user_id: user1.id, - course_id: course1.id, - role: :student - }) + # TODO: Remove eventually (duplicate of delete_course_registration) + # describe "delete record" do + # test "succeeds", %{course1: course1, user1: user1} do + # assert length(CourseRegistrations.get_users(course1.id)) == 1 + + # {:ok, _course_reg} = + # CourseRegistrations.delete_record(%{ + # user_id: user1.id, + # course_id: course1.id, + # role: :student + # }) + + # assert CourseRegistrations.get_users(course1.id) == [] + # end + + # test "failed due to repeated removal", %{course1: course1, user1: user1} do + # assert length(CourseRegistrations.get_users(course1.id)) == 1 + + # {:ok, _course_reg} = + # CourseRegistrations.delete_record(%{ + # user_id: user1.id, + # course_id: course1.id, + # role: :student + # }) + + # assert CourseRegistrations.get_users(course1.id) == [] + + # assert {:error, :no_such_enrty} == + # CourseRegistrations.delete_record(%{ + # user_id: user1.id, + # course_id: course1.id, + # role: :student + # }) + # end + + # test "failed due to non existing entry", %{course1: course1, user2: user2} do + # assert length(CourseRegistrations.get_users(course1.id)) == 1 + + # assert {:error, :no_such_enrty} == + # CourseRegistrations.delete_record(%{ + # user_id: user2.id, + # course_id: course1.id, + # role: :student + # }) + # end + + # test "failed due to invalid changeset", %{course1: course1, user2: user2} do + # assert length(CourseRegistrations.get_users(course1.id)) == 1 + + # {:error, :no_such_enrty} = + # CourseRegistrations.delete_record(%{user_id: user2.id, course_id: course1.id}) + + # assert length(CourseRegistrations.get_users(course1.id)) == 1 + # end + # end + + describe "update_role" do + setup do + student = insert(:course_registration, %{role: :student}) + staff = insert(:course_registration, %{role: :staff}) + admin = insert(:course_registration, %{role: :admin}) + + {:ok, %{student: student, staff: staff, admin: admin}} + end - assert CourseRegistrations.get_users(course1.id) == [] + test "succeeds for student to staff", %{student: %{id: coursereg_id}} do + {:ok, updated_coursereg} = CourseRegistrations.update_role("staff", coursereg_id) + assert updated_coursereg.role == :staff end - test "failed due to repeated removal", %{course1: course1, user1: user1} do - assert length(CourseRegistrations.get_users(course1.id)) == 1 + test "succeeds for student to admin", %{student: %{id: coursereg_id}} do + {:ok, updated_coursereg} = CourseRegistrations.update_role("admin", coursereg_id) + assert updated_coursereg.role == :admin + end - {:ok, _course_reg} = - CourseRegistrations.delete_record(%{ - user_id: user1.id, - course_id: course1.id, - role: :student - }) + test "succeeds for admin to staff", %{admin: %{id: coursereg_id}} do + {:ok, updated_coursereg} = CourseRegistrations.update_role("staff", coursereg_id) + assert updated_coursereg.role == :staff + end - assert CourseRegistrations.get_users(course1.id) == [] + test "fails when invalid role is provided", %{student: %{id: coursereg_id}} do + assert {:error, {:bad_request, "role is invalid"}} == + CourseRegistrations.update_role("invalidrole", coursereg_id) + end - assert {:error, :no_such_enrty} == - CourseRegistrations.delete_record(%{ - user_id: user1.id, - course_id: course1.id, - role: :student - }) + test "fails when course registration does not exist", %{} do + assert {:error, {:bad_request, "User course registration does not exist"}} == + CourseRegistrations.update_role("staff", 10_000) end + end - test "failed due to non existing entry", %{course1: course1, user2: user2} do - assert length(CourseRegistrations.get_users(course1.id)) == 1 + describe "delete_course_registration" do + setup do + student = insert(:course_registration, %{role: :student}) - assert {:error, :no_such_enrty} == - CourseRegistrations.delete_record(%{ - user_id: user2.id, - course_id: course1.id, - role: :student - }) + {:ok, %{student: student}} end - test "failed due to invalid changeset", %{course1: course1, user2: user2} do - assert length(CourseRegistrations.get_users(course1.id)) == 1 - - {:error, :no_such_enrty} = - CourseRegistrations.delete_record(%{user_id: user2.id, course_id: course1.id}) + test "succeeds", %{student: %{id: coursereg_id}} do + {:ok, deleted_coursereg} = CourseRegistrations.delete_course_registration(coursereg_id) + assert is_nil(CourseRegistration |> where(id: ^coursereg_id) |> Repo.one()) + end - assert length(CourseRegistrations.get_users(course1.id)) == 1 + test "fails when course registration does not exist", %{} do + assert {:error, {:bad_request, "User course registration does not exist"}} == + CourseRegistrations.delete_course_registration(10_000) end end end diff --git a/test/cadet_web/admin_controllers/admin_user_controller_test.exs b/test/cadet_web/admin_controllers/admin_user_controller_test.exs index 10ffb0089..a6c940d19 100644 --- a/test/cadet_web/admin_controllers/admin_user_controller_test.exs +++ b/test/cadet_web/admin_controllers/admin_user_controller_test.exs @@ -149,7 +149,7 @@ defmodule CadetWeb.AdminUserControllerTest do params = %{ "role" => "staff", - "crId" => 1 + "crId" => 10_000 } conn = put(conn, build_url_users_role(course_id), params) @@ -169,7 +169,7 @@ defmodule CadetWeb.AdminUserControllerTest do conn = put(conn, build_url_users_role(course_id), params) - assert response(conn, 403) == "Wrong course" + assert response(conn, 403) == "User is in a different course" unchanged_course_reg = Repo.get(CourseRegistration, user_course_reg.id) assert unchanged_course_reg.role == :student end @@ -334,7 +334,7 @@ defmodule CadetWeb.AdminUserControllerTest do conn = delete(conn, build_url_users(course_id), params) - assert response(conn, 403) == "Wrong course" + assert response(conn, 403) == "User is in a different course" end end From f9e313ac8ec265bef7069b324f79f8c66ba11b8f Mon Sep 17 00:00:00 2001 From: YaleChen299 Date: Sat, 26 Jun 2021 22:47:30 +0800 Subject: [PATCH 084/174] update test cases and fix some credo issues --- lib/cadet/accounts/course_registrations.ex | 87 +++++++++++++------ lib/cadet/assessments/assessments.ex | 3 +- lib/cadet/courses/course.ex | 3 +- lib/cadet/courses/courses.ex | 25 +++--- .../admin_user_controller.ex | 6 +- lib/cadet_web/router.ex | 2 +- test/cadet/accounts/accounts_test.exs | 6 +- .../accounts/course_registration_test.exs | 2 +- test/cadet/courses/courses_test.exs | 4 +- test/cadet/stories/stories_test.exs | 16 ++-- .../admin_courses_controller_test.exs | 6 +- .../sourcecast_controller_test.exs | 2 +- .../controllers/user_controller_test.exs | 2 + test/support/conn_case.ex | 3 +- 14 files changed, 104 insertions(+), 63 deletions(-) diff --git a/lib/cadet/accounts/course_registrations.ex b/lib/cadet/accounts/course_registrations.ex index c5136191a..1cc5fd8fe 100644 --- a/lib/cadet/accounts/course_registrations.ex +++ b/lib/cadet/accounts/course_registrations.ex @@ -113,39 +113,72 @@ defmodule Cadet.Accounts.CourseRegistrations do end def update_role(role, coursereg_id) do - with {:get_cr, course_reg} when not is_nil(course_reg) <- - {:get_cr, CourseRegistration |> where(id: ^coursereg_id) |> Repo.one()} do - case course_reg - |> CourseRegistration.changeset(%{role: role}) - |> Repo.update() do - {:ok, _} = result -> result - {:error, changeset} -> {:error, {:bad_request, full_error_messages(changeset)}} - end - else - {:get_cr, nil} -> {:error, {:bad_request, "User course registration does not exist"}} + # with {:get_cr, course_reg} when not is_nil(course_reg) <- + # {:get_cr, CourseRegistration |> where(id: ^coursereg_id) |> Repo.one()} do + # case course_reg + # |> CourseRegistration.changeset(%{role: role}) + # |> Repo.update() do + # {:ok, _} = result -> result + # {:error, changeset} -> {:error, {:bad_request, full_error_messages(changeset)}} + # end + # else + # {:get_cr, nil} -> {:error, {:bad_request, "User course registration does not exist"}} + # end + + case CourseRegistration |> where(id: ^coursereg_id) |> Repo.one() do + nil -> + {:error, {:bad_request, "User course registration does not exist"}} + + course_reg -> + case course_reg + |> CourseRegistration.changeset(%{role: role}) + |> Repo.update() do + {:ok, _} = result -> result + {:error, changeset} -> {:error, {:bad_request, full_error_messages(changeset)}} + end end end def delete_course_registration(coursereg_id) do # TODO: Handle deletions of achievement entries, etc. too - with {:get_cr, course_reg} when not is_nil(course_reg) <- - {:get_cr, CourseRegistration |> where(id: ^coursereg_id) |> Repo.one()} do - # Delete submissions and answers before deleting user - Submission - |> where(student_id: ^course_reg.id) - |> Repo.all() - |> Enum.each(fn x -> - Answer - |> where(submission_id: ^x.id) - |> Repo.delete_all() - - Repo.delete(x) - end) - - Repo.delete(course_reg) - else - {:get_cr, nil} -> {:error, {:bad_request, "User course registration does not exist"}} + # with {:get_cr, course_reg} when not is_nil(course_reg) <- + # {:get_cr, CourseRegistration |> where(id: ^coursereg_id) |> Repo.one()} do + # # Delete submissions and answers before deleting user + # Submission + # |> where(student_id: ^course_reg.id) + # |> Repo.all() + # |> Enum.each(fn x -> + # Answer + # |> where(submission_id: ^x.id) + # |> Repo.delete_all() + + # Repo.delete(x) + # end) + + # Repo.delete(course_reg) + # else + # {:get_cr, nil} -> {:error, {:bad_request, "User course registration does not exist"}} + # end + + case CourseRegistration |> where(id: ^coursereg_id) |> Repo.one() do + nil -> + {:error, {:bad_request, "User course registration does not exist"}} + + course_reg -> + # Delete submissions and answers before deleting user + Submission + |> where(student_id: ^course_reg.id) + |> Repo.all() + |> Enum.each(fn x -> + Answer + |> where(submission_id: ^x.id) + |> Repo.delete_all() + + Repo.delete(x) + end) + + Repo.delete(course_reg) end end end diff --git a/lib/cadet/assessments/assessments.ex b/lib/cadet/assessments/assessments.ex index 4b8946137..f32ce881d 100644 --- a/lib/cadet/assessments/assessments.ex +++ b/lib/cadet/assessments/assessments.ex @@ -278,7 +278,8 @@ defmodule Cadet.Assessments do |> select([s], [:assessment_id, :status]) assessments = - Query.all_assessments_with_aggregates(cr.course_id) + cr.course_id + |> Query.all_assessments_with_aggregates() |> subquery() |> join( :left, diff --git a/lib/cadet/courses/course.ex b/lib/cadet/courses/course.ex index 88290f71f..4ea190aa7 100644 --- a/lib/cadet/courses/course.ex +++ b/lib/cadet/courses/course.ex @@ -23,7 +23,8 @@ defmodule Cadet.Courses.Course do end @required_fields ~w(source_chapter source_variant)a - @optional_fields ~w(course_name course_short_name viewable enable_game enable_achievements enable_sourcecast module_help_text)a + @optional_fields ~w(course_name course_short_name viewable + enable_game enable_achievements enable_sourcecast module_help_text)a def changeset(course, params) do if Map.has_key?(params, :source_chapter) or Map.has_key?(params, :source_variant) do diff --git a/lib/cadet/courses/courses.ex b/lib/cadet/courses/courses.ex index c92e06679..9a13e468b 100644 --- a/lib/cadet/courses/courses.ex +++ b/lib/cadet/courses/courses.ex @@ -70,23 +70,24 @@ defmodule Cadet.Courses do end def mass_upsert_and_reorder_assessment_configs(course_id, configs) do - if not is_list(configs) do - {:error, {:bad_request, "Invalid parameter(s)"}} - else + if is_list(configs) do configs_length = configs |> length() with true <- configs_length <= 8, true <- configs_length >= 1 do - configs - |> Enum.map(fn elem -> - {:ok, config} = insert_or_update_assessment_config(course_id, elem) - Map.put(elem, :assessment_config_id, config.id) - end) - - reorder_assessment_configs(course_id, configs) + new_configs = + configs + |> Enum.map(fn elem -> + {:ok, config} = insert_or_update_assessment_config(course_id, elem) + Map.put(elem, :assessment_config_id, config.id) + end) + + reorder_assessment_configs(course_id, new_configs) else false -> {:error, {:bad_request, "Invalid parameter(s)"}} end + else + {:error, {:bad_request, "Invalid parameter(s)"}} end end @@ -115,7 +116,7 @@ defmodule Cadet.Courses do |> Repo.one() |> case do nil -> {:error, :no_such_entry} - at -> AssessmentConfig.changeset(at, params) |> Repo.update() + at -> at |> AssessmentConfig.changeset(params) |> Repo.update() end end @@ -143,7 +144,7 @@ defmodule Cadet.Courses do |> Repo.one() |> case do nil -> {:error, :no_such_enrty} - at -> AssessmentConfig.changeset(at, params) |> Repo.delete() + at -> at |> AssessmentConfig.changeset(params) |> Repo.delete() end end diff --git a/lib/cadet_web/admin_controllers/admin_user_controller.ex b/lib/cadet_web/admin_controllers/admin_user_controller.ex index 9d29574f1..755a02773 100644 --- a/lib/cadet_web/admin_controllers/admin_user_controller.ex +++ b/lib/cadet_web/admin_controllers/admin_user_controller.ex @@ -6,7 +6,7 @@ defmodule CadetWeb.AdminUserController do alias Cadet.Repo alias Cadet.Accounts - alias Cadet.Accounts.CourseRegistration + alias Cadet.Accounts.{CourseRegistrations, CourseRegistration} # This controller is used to find all users of a course @@ -28,7 +28,7 @@ defmodule CadetWeb.AdminUserController do {:get_cr, CourseRegistration |> where(id: ^coursereg_id) |> Repo.one()}, {:validate_same_course, true} <- {:validate_same_course, user_course_reg.course_id == admin_course_id} do - case Accounts.CourseRegistrations.update_role(role, coursereg_id) do + case CourseRegistrations.update_role(role, coursereg_id) do {:ok, %{}} -> text(conn, "OK") @@ -64,7 +64,7 @@ defmodule CadetWeb.AdminUserController do {:prevent_delete_admin, true} <- {:prevent_delete_admin, user_course_reg.role != :admin}, {:validate_same_course, true} <- {:validate_same_course, user_course_reg.course_id == admin_course_id} do - case Accounts.CourseRegistrations.delete_course_registration(coursereg_id) do + case CourseRegistrations.delete_course_registration(coursereg_id) do {:ok, %{}} -> text(conn, "OK") diff --git a/lib/cadet_web/router.ex b/lib/cadet_web/router.ex index 83f63901f..39d294d11 100644 --- a/lib/cadet_web/router.ex +++ b/lib/cadet_web/router.ex @@ -182,7 +182,7 @@ defmodule CadetWeb.Router do Cadet.Accounts.CourseRegistrations.get_user_record(conn.assigns.current_user.id, course_id) case course_reg do - nil -> send_resp(conn, 403, "Forbidden") |> halt() + nil -> conn |> send_resp(403, "Forbidden") |> halt() cr -> assign(conn, :course_reg, cr) end end diff --git a/test/cadet/accounts/accounts_test.exs b/test/cadet/accounts/accounts_test.exs index aa75438b5..cfd1a833e 100644 --- a/test/cadet/accounts/accounts_test.exs +++ b/test/cadet/accounts/accounts_test.exs @@ -132,14 +132,14 @@ defmodule Cadet.AccountsTest do all_stu_in_c1 = Accounts.get_users_by([role: :student], admin1) assert length(all_stu_in_c1) == 2 all_stu_in_c2 = Accounts.get_users_by([role: :student], admin2) - assert length(all_stu_in_c2) == 0 + assert all_stu_in_c2 == [] end test "get all users in a group in a course", %{a1: admin1, g1: g1, g2: g2} do all_in_c1g1 = Accounts.get_users_by([group: g1.name], admin1) assert length(all_in_c1g1) == 2 all_in_c1g2 = Accounts.get_users_by([group: g2.name], admin1) - assert length(all_in_c1g2) == 0 + assert all_in_c1g2 == [] end test "get all students in a group in a course", %{c1: c1, a1: admin1, g1: g1, g2: g2} do @@ -152,7 +152,7 @@ defmodule Cadet.AccountsTest do all_stu_in_c1g1 = Accounts.get_users_by([group: g1.name, role: :student], admin1) assert length(all_stu_in_c1g1) == 2 all_stu_in_c1g2 = Accounts.get_users_by([group: g2.name, role: :student], admin1) - assert length(all_stu_in_c1g2) == 0 + assert all_stu_in_c1g2 == [] end end end diff --git a/test/cadet/accounts/course_registration_test.exs b/test/cadet/accounts/course_registration_test.exs index d4c78d724..6489d43f2 100644 --- a/test/cadet/accounts/course_registration_test.exs +++ b/test/cadet/accounts/course_registration_test.exs @@ -339,7 +339,7 @@ defmodule Cadet.Accounts.CourseRegistrationTest do test "fails when course registration does not exist", %{} do assert {:error, {:bad_request, "User course registration does not exist"}} == - CourseRegistrations.delete_course_registration(10_000) + CourseRegistrations.delete_course_registration(10_000) end end end diff --git a/test/cadet/courses/courses_test.exs b/test/cadet/courses/courses_test.exs index 63277bc1c..e428943be 100644 --- a/test/cadet/courses/courses_test.exs +++ b/test/cadet/courses/courses_test.exs @@ -264,7 +264,7 @@ defmodule Cadet.CoursesTest do {:ok, updated_config} = Courses.insert_or_update_assessment_config(course.id, params) new_configs = Courses.get_assessment_configs(course.id) - assert length(old_configs) == 0 + assert old_configs == [] assert length(new_configs) == 1 assert updated_config.early_submission_xp == 100 assert updated_config.hours_before_early_xp_decay == 24 @@ -329,7 +329,7 @@ defmodule Cadet.CoursesTest do new_configs = Courses.get_assessment_configs(course.id) assert length(old_configs) == 1 - assert length(new_configs) == 0 + assert new_configs == [] end test "error" do diff --git a/test/cadet/stories/stories_test.exs b/test/cadet/stories/stories_test.exs index 36390838b..51f225859 100644 --- a/test/cadet/stories/stories_test.exs +++ b/test/cadet/stories/stories_test.exs @@ -36,8 +36,8 @@ defmodule Cadet.StoriesTest do describe "List stories" do test "All stories from own course" do course = insert(:course) - story1 = insert(:story, %{course: course}) |> remove_course_assoc() - story2 = insert(:story, %{course: course}) |> remove_course_assoc() + story1 = :story |> insert(%{course: course}) |> remove_course_assoc() + story2 = :story |> insert(%{course: course}) |> remove_course_assoc() assert Stories.list_stories(insert(:course_registration, %{course: course, role: :staff})) == [story1, story2] @@ -46,7 +46,7 @@ defmodule Cadet.StoriesTest do test "Does not list stories from other courses" do course = insert(:course) insert(:story) - story2 = insert(:story, %{course: course}) |> remove_course_assoc() + story2 = :story |> insert(%{course: course}) |> remove_course_assoc() assert Stories.list_stories(insert(:course_registration, %{course: course, role: :staff})) == [story2] @@ -61,10 +61,12 @@ defmodule Cadet.StoriesTest do insert(:story, %{Map.put(params, :course, course) | :open_at => one_week_ago}) published_open_story = - insert( - :story, - %{Map.put(params, :course, course) | :is_published => true, :open_at => one_week_ago} - ) + :story + |> insert(%{ + Map.put(params, :course, course) + | :is_published => true, + :open_at => one_week_ago + }) |> remove_course_assoc() assert Stories.list_stories(insert(:course_registration, %{course: course, role: :student})) == diff --git a/test/cadet_web/admin_controllers/admin_courses_controller_test.exs b/test/cadet_web/admin_controllers/admin_courses_controller_test.exs index a12d1b8cc..d7e940c62 100644 --- a/test/cadet_web/admin_controllers/admin_courses_controller_test.exs +++ b/test/cadet_web/admin_controllers/admin_courses_controller_test.exs @@ -200,7 +200,7 @@ defmodule CadetWeb.AdminCoursesControllerTest do course_id = conn.assigns[:course_id] config = insert(:assessment_config, %{course: Repo.get(Course, course_id)}) - old_configs = Courses.get_assessment_configs(course_id) |> Enum.map(& &1.type) + old_configs = course_id |> Courses.get_assessment_configs() |> Enum.map(& &1.type) params = %{ "assessmentConfigs" => [ @@ -230,7 +230,7 @@ defmodule CadetWeb.AdminCoursesControllerTest do assert resp == "OK" - new_configs = Courses.get_assessment_configs(course_id) |> Enum.map(& &1.type) + new_configs = course_id |> Courses.get_assessment_configs() |> Enum.map(& &1.type) refute old_configs == new_configs assert new_configs == ["Missions", "Paths"] end @@ -298,7 +298,7 @@ defmodule CadetWeb.AdminCoursesControllerTest do defp build_url_assessment_configs(course_id), do: "/v2/courses/#{course_id}/admin/config/assessment_configs" - defp to_map(schema), do: Map.from_struct(schema) |> Map.drop([:updated_at]) + defp to_map(schema), do: schema |> Map.from_struct() |> Map.drop([:updated_at]) defp update_map(map1, params), do: Map.merge(map1, to_snake_case_atom_keys(params), fn _k, _v1, v2 -> v2 end) diff --git a/test/cadet_web/controllers/sourcecast_controller_test.exs b/test/cadet_web/controllers/sourcecast_controller_test.exs index c8e03aa11..25e9d7bb2 100644 --- a/test/cadet_web/controllers/sourcecast_controller_test.exs +++ b/test/cadet_web/controllers/sourcecast_controller_test.exs @@ -359,7 +359,7 @@ defmodule CadetWeb.SourcecastControllerTest do end end - defp build_url(), do: "/v2/sourcecast/" + defp build_url, do: "/v2/sourcecast/" defp build_url(course_id), do: "/v2/courses/#{course_id}/sourcecast/" defp build_url(course_id, sourcecast_id), do: "#{build_url(course_id)}#{sourcecast_id}/" diff --git a/test/cadet_web/controllers/user_controller_test.exs b/test/cadet_web/controllers/user_controller_test.exs index 7ed280665..d2a745a9c 100644 --- a/test/cadet_web/controllers/user_controller_test.exs +++ b/test/cadet_web/controllers/user_controller_test.exs @@ -79,6 +79,7 @@ defmodule CadetWeb.UserControllerTest do ] }, "courseRegistration" => %{ + "crId" => cr.id, "courseId" => course.id, "role" => "#{cr.role}", "group" => nil, @@ -351,6 +352,7 @@ defmodule CadetWeb.UserControllerTest do expected = %{ "courseRegistration" => %{ + "crId" => cr.id, "courseId" => course.id, "role" => "#{cr.role}", "group" => nil, diff --git a/test/support/conn_case.ex b/test/support/conn_case.ex index 3fb6d3a3f..cd512433b 100644 --- a/test/support/conn_case.ex +++ b/test/support/conn_case.ex @@ -75,7 +75,8 @@ defmodule CadetWeb.ConnCase do # of the course_registration since we want the router plug to assign the course_registration # when actually accessing the endpoint during the test. conn = - sign_in(conn, course_registration.user) + conn + |> sign_in(course_registration.user) |> assign(:course_id, course_registration.course_id) {:ok, conn: conn} From 8c6f8544bb4f0dbc38fa6dbabeda13315824573e Mon Sep 17 00:00:00 2001 From: YaleChen299 Date: Sat, 26 Jun 2021 23:36:26 +0800 Subject: [PATCH 085/174] updated bonus_xp logic --- lib/cadet/assessments/assessments.ex | 18 +++++++++--------- test/cadet/accounts/user_test.exs | 6 +++--- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/lib/cadet/assessments/assessments.ex b/lib/cadet/assessments/assessments.ex index f32ce881d..903730268 100644 --- a/lib/cadet/assessments/assessments.ex +++ b/lib/cadet/assessments/assessments.ex @@ -797,7 +797,6 @@ defmodule Cadet.Assessments do end end - # :TODO update bonus logic @spec update_submission_status_and_xp_bonus(%Submission{}) :: {:ok, %Submission{}} | {:error, Ecto.Changeset.t()} defp update_submission_status_and_xp_bonus(submission = %Submission{}) do @@ -806,16 +805,17 @@ defmodule Cadet.Assessments do max_bonus_xp = assessment_conifg.early_submission_xp early_hours = assessment_conifg.hours_before_early_xp_decay - rate = assessment_conifg.decay_rate_points_per_hour xp_bonus = - cond do - Timex.before?(Timex.now(), Timex.shift(assessment.open_at, hours: early_hours)) -> - max_bonus_xp - - true -> - exceed_hours = Timex.diff(Timex.now(), assessment.open_at, :hours) - early_hours - Enum.max([0, max_bonus_xp - exceed_hours * rate]) + if Timex.before?(Timex.now(), Timex.shift(assessment.open_at, hours: early_hours)) do + max_bonus_xp + else + # This logic interpolates from max bonus at early hour to 0 bonus at close time + decaying_hours = Timex.diff(assessment.close_at, assessment.open_at, :hours) - early_hours + remaining_hours = Timex.diff(assessment.close_at, Timex.now(), :hours) + proportion = remaining_hours / decaying_hours + bonus_xp = (max_bonus_xp * proportion) |> round() + Enum.max([0, bonus_xp]) end submission diff --git a/test/cadet/accounts/user_test.exs b/test/cadet/accounts/user_test.exs index 932db6588..085d327db 100644 --- a/test/cadet/accounts/user_test.exs +++ b/test/cadet/accounts/user_test.exs @@ -5,12 +5,12 @@ defmodule Cadet.Accounts.UserTest do describe "Changesets" do test "valid changeset" do - assert_changeset(%{name: "happy people", role: :admin}, :valid) - assert_changeset(%{name: "happy", role: :student}, :valid) + assert_changeset(%{name: "happy people", username: "people"}, :valid) + assert_changeset(%{name: "happy", latest_viewed_id: 1}, :valid) end test "invalid changeset" do - assert_changeset(%{name: "people"}, :invalid) + assert_changeset(%{username: "people"}, :invalid) assert_changeset(%{role: :avenger}, :invalid) end end From eb78555f6af577d4b615123f06665377af4354cc Mon Sep 17 00:00:00 2001 From: YaleChen299 Date: Sun, 27 Jun 2021 02:12:50 +0800 Subject: [PATCH 086/174] update notifications with test --- lib/cadet/accounts/notification.ex | 10 ++-- lib/cadet/accounts/notifications.ex | 49 ++++++++-------- lib/cadet/assessments/assessments.ex | 8 +-- .../20210626164054_update_notification.exs | 10 ++++ test/cadet/accounts/notification_test.exs | 56 ++++++++++--------- test/cadet/courses/group_test.exs | 4 +- 6 files changed, 75 insertions(+), 62 deletions(-) create mode 100644 priv/repo/migrations/20210626164054_update_notification.exs diff --git a/lib/cadet/accounts/notification.ex b/lib/cadet/accounts/notification.ex index 9195fcd23..aa9de651f 100755 --- a/lib/cadet/accounts/notification.ex +++ b/lib/cadet/accounts/notification.ex @@ -1,13 +1,13 @@ defmodule Cadet.Accounts.Notification do @moduledoc """ The Notification entity represents a notification. - It stores information pertaining to the type of notification and who it belongs to. + It stores information pertaining to the type of notification and who in which course it belongs to. Each notification can have an assessment id or submission id, with optional question id. This will be used to pinpoint where the notification will be showed on the frontend. """ use Cadet, :model - alias Cadet.Accounts.{NotificationType, Role, User} + alias Cadet.Accounts.{NotificationType, Role, CourseRegistration} alias Cadet.Assessments.{Assessment, Submission} schema "notifications" do @@ -15,21 +15,21 @@ defmodule Cadet.Accounts.Notification do field(:read, :boolean, default: false) field(:role, Role, virtual: true) - belongs_to(:user, User) + belongs_to(:course_reg, CourseRegistration) belongs_to(:assessment, Assessment) belongs_to(:submission, Submission) timestamps() end - @required_fields ~w(type read role user_id assessment_id)a + @required_fields ~w(type read role course_reg_id assessment_id)a @optional_fields ~w(submission_id)a def changeset(answer, params) do answer |> cast(params, @required_fields ++ @optional_fields) |> validate_required(@required_fields) - |> foreign_key_constraint(:user) + |> foreign_key_constraint(:course_reg_id) |> foreign_key_constraint(:assessment_id) |> foreign_key_constraint(:submission_id) end diff --git a/lib/cadet/accounts/notifications.ex b/lib/cadet/accounts/notifications.ex index 8319f2801..f5ee3440f 100644 --- a/lib/cadet/accounts/notifications.ex +++ b/lib/cadet/accounts/notifications.ex @@ -8,18 +8,18 @@ defmodule Cadet.Accounts.Notifications do import Ecto.Query alias Cadet.Repo - alias Cadet.Accounts.{Notification, User, CourseRegistration} + alias Cadet.Accounts.{Notification, CourseRegistration, CourseRegistration} alias Cadet.Assessments.Submission alias Ecto.Multi @doc """ - Fetches all unread notifications belonging to a user as an array + Fetches all unread notifications belonging to a course_reg as an array """ - @spec fetch(%User{}) :: {:ok, {:array, Notification}} - def fetch(user = %User{}) do + @spec fetch(%CourseRegistration{}) :: {:ok, {:array, Notification}} + def fetch(course_reg = %CourseRegistration{}) do notifications = Notification - |> where(user_id: ^user.id) + |> where(course_reg_id: ^course_reg.id) |> where(read: false) |> preload(:assessment) |> Repo.all() @@ -43,13 +43,13 @@ defmodule Cadet.Accounts.Notifications do defp write_student( params = %{ - user_id: user_id, + course_reg_id: course_reg_id, assessment_id: assessment_id, type: type } ) do Notification - |> where(user_id: ^user_id) + |> where(course_reg_id: ^course_reg_id) |> where(assessment_id: ^assessment_id) |> where(type: ^type) |> Repo.one() @@ -71,13 +71,13 @@ defmodule Cadet.Accounts.Notifications do defp write_staff( params = %{ - user_id: user_id, + course_reg_id: course_reg_id, submission_id: submission_id, type: type } ) do Notification - |> where(user_id: ^user_id) + |> where(course_reg_id: ^course_reg_id) |> where(submission_id: ^submission_id) |> where(type: ^type) |> Repo.one() @@ -100,18 +100,18 @@ defmodule Cadet.Accounts.Notifications do @doc """ Changes read status of notification(s) from false to true. """ - @spec acknowledge({:array, :integer}, %User{}) :: + @spec acknowledge({:array, :integer}, %CourseRegistration{}) :: {:ok, Ecto.Schema.t()} | {:error, any} | {:error, Ecto.Multi.name(), any, %{Ecto.Multi.name() => any}} - def acknowledge(notification_ids, user = %User{}) when is_list(notification_ids) do + def acknowledge(notification_ids, course_reg = %CourseRegistration{}) when is_list(notification_ids) do Multi.new() |> Multi.run(:update_all, fn _repo, _ -> Enum.reduce_while(notification_ids, {:ok, nil}, fn n_id, acc -> # credo:disable-for-next-line case acc do {:ok, _} -> - {:cont, acknowledge(n_id, user)} + {:cont, acknowledge(n_id, course_reg)} _ -> {:halt, acc} @@ -122,8 +122,8 @@ defmodule Cadet.Accounts.Notifications do end @spec acknowledge(:integer, %CourseRegistration{}) :: {:ok, Ecto.Schema.t()} | {:error, any()} - def acknowledge(notification_id, cr = %CourseRegistration{}) do - notification = Repo.get_by(Notification, id: notification_id, user_id: cr.user_id) + def acknowledge(notification_id, course_reg = %CourseRegistration{}) do + notification = Repo.get_by(Notification, id: notification_id, course_reg_id: course_reg.id) case notification do nil -> @@ -131,7 +131,7 @@ defmodule Cadet.Accounts.Notifications do notification -> notification - |> Notification.changeset(%{role: cr.role, read: true}) + |> Notification.changeset(%{role: course_reg.role, read: true}) |> Repo.update() end end @@ -147,7 +147,7 @@ defmodule Cadet.Accounts.Notifications do # Add new notification :unsubmitted Notification - |> where(user_id: ^student.user_id) + |> where(course_reg_id: ^student.id) |> where(assessment_id: ^assessment_id) |> where([n], n.type in ^[:autograded, :graded]) |> Repo.delete_all() @@ -155,7 +155,7 @@ defmodule Cadet.Accounts.Notifications do write(%{ type: :unsubmitted, role: student.role, - user_id: student.user_id, + course_reg_id: student.id, assessment_id: assessment_id }) end @@ -175,7 +175,7 @@ defmodule Cadet.Accounts.Notifications do type: type, read: false, role: :student, - user_id: submission.student_id, + course_reg_id: submission.student_id, assessment_id: submission.assessment_id }) end @@ -183,12 +183,13 @@ defmodule Cadet.Accounts.Notifications do @doc """ Writes a notification to all students that a new assessment is available. """ - @spec write_notification_for_new_assessment(integer()) :: + @spec write_notification_for_new_assessment(integer(), integer()) :: {:ok, Ecto.Schema.t()} | {:error, Ecto.Changeset.t()} - def write_notification_for_new_assessment(assessment_id) when is_ecto_id(assessment_id) do + def write_notification_for_new_assessment(course_id, assessment_id) when is_ecto_id(assessment_id) and is_ecto_id(course_id) do Multi.new() |> Multi.run(:insert_all, fn _repo, _ -> - User + CourseRegistration + |> where(course_id: ^course_id) |> where(role: ^:student) |> Repo.all() |> Enum.reduce_while({:ok, nil}, fn student, acc -> @@ -200,7 +201,7 @@ defmodule Cadet.Accounts.Notifications do type: :new, read: false, role: :student, - user_id: student.id, + course_reg_id: student.id, assessment_id: assessment_id })} @@ -228,7 +229,7 @@ defmodule Cadet.Accounts.Notifications do type: :submitted, read: false, role: :staff, - user_id: avenger_id, + course_reg_id: avenger_id, assessment_id: submission.assessment_id, submission_id: submission.id }) @@ -236,7 +237,7 @@ defmodule Cadet.Accounts.Notifications do end defp get_avenger_id_of(student_id) when is_ecto_id(student_id) do - User + CourseRegistration |> Repo.get_by(id: student_id) |> Repo.preload(:group) |> Map.get(:group) diff --git a/lib/cadet/assessments/assessments.ex b/lib/cadet/assessments/assessments.ex index 903730268..9419d171e 100644 --- a/lib/cadet/assessments/assessments.ex +++ b/lib/cadet/assessments/assessments.ex @@ -870,12 +870,12 @@ defmodule Cadet.Assessments do end end - defp load_contest_voting_entries(questions, user_id) do + defp load_contest_voting_entries(questions, cr_id) do Enum.map( questions, fn q -> if q.type == :voting do - submission_votes = all_submission_votes_by_question_id_and_user_id(q.id, user_id) + submission_votes = all_submission_votes_by_question_id_and_cr_id(q.id, cr_id) # fetch top 10 contest voting entries with the contest question id question_id = fetch_associated_contest_question_id(q) @@ -901,9 +901,9 @@ defmodule Cadet.Assessments do ) end - defp all_submission_votes_by_question_id_and_user_id(question_id, user_id) do + defp all_submission_votes_by_question_id_and_cr_id(question_id, cr_id) do SubmissionVotes - |> where([v], v.user_id == ^user_id and v.question_id == ^question_id) + |> where([v], v.cr_id == ^cr_id and v.question_id == ^question_id) |> join(:inner, [v], s in assoc(v, :submission)) |> join(:inner, [v, s], a in assoc(s, :answers)) |> select([v, s, a], %{submission_id: v.submission_id, answer: a.answer, rank: v.rank}) diff --git a/priv/repo/migrations/20210626164054_update_notification.exs b/priv/repo/migrations/20210626164054_update_notification.exs new file mode 100644 index 000000000..be644b04d --- /dev/null +++ b/priv/repo/migrations/20210626164054_update_notification.exs @@ -0,0 +1,10 @@ +defmodule Cadet.Repo.Migrations.UpdateNotification do + use Ecto.Migration + + def change do + alter table(:notifications) do + remove(:user_id) + add(:course_reg_id, references(:course_registrations), null: false) + end + end +end diff --git a/test/cadet/accounts/notification_test.exs b/test/cadet/accounts/notification_test.exs index 13837aafd..7442c23af 100644 --- a/test/cadet/accounts/notification_test.exs +++ b/test/cadet/accounts/notification_test.exs @@ -3,19 +3,21 @@ defmodule Cadet.Accounts.NotificationTest do use Cadet.ChangesetCase, entity: Notification - @required_fields ~w(type role user_id)a + @required_fields ~w(type role course_reg_id)a setup do assessment = insert(:assessment, %{is_published: true}) - avenger = insert(:user, %{role: :staff}) - student = insert(:user, %{role: :student}) + avenger_user = insert(:user) + student_user = insert(:user) + avenger = insert(:course_registration, %{user: avenger_user, role: :staff}) + student = insert(:course_registration, %{user: student_user, role: :student}) submission = insert(:submission, %{student: student, assessment: assessment}) valid_params_for_student = %{ type: :new, read: false, role: student.role, - user_id: student.id, + course_reg_id: student.id, assessment_id: assessment.id } @@ -23,7 +25,7 @@ defmodule Cadet.Accounts.NotificationTest do type: :submitted, read: false, role: avenger.role, - user_id: avenger.id, + course_reg_id: avenger.id, assessment_id: assessment.id, submission_id: submission.id } @@ -74,14 +76,14 @@ defmodule Cadet.Accounts.NotificationTest do read: false, assessment_id: assessment.id, assessment: assessment, - user_id: student.id + course_reg_id: student.id }) - expected = Enum.sort(notifications, &(&1.id < &2.id)) + expected = notifications |> Enum.sort(&(&1.id < &2.id)) |> Enum.map(& Map.delete(&1, :assessment)) {:ok, notifications_db} = Notifications.fetch(student) - results = Enum.sort(notifications_db, &(&1.id < &2.id)) + results = notifications_db |> Enum.sort(&(&1.id < &2.id)) |> Enum.map(& Map.delete(&1, :assessment)) assert results == expected end @@ -90,7 +92,7 @@ defmodule Cadet.Accounts.NotificationTest do insert_list(3, :notification, %{ read: true, assessment_id: assessment.id, - user_id: student.id + course_reg_id: student.id }) {:ok, notifications_db} = Notifications.fetch(student) @@ -108,7 +110,7 @@ defmodule Cadet.Accounts.NotificationTest do type: :new, read: false, role: student.role, - user_id: student.id, + course_reg_id: student.id, assessment_id: assessment.id } @@ -116,7 +118,7 @@ defmodule Cadet.Accounts.NotificationTest do type: :submitted, read: false, role: avenger.role, - user_id: avenger.id, + course_reg_id: avenger.id, assessment_id: assessment.id, submission_id: submission.id } @@ -135,7 +137,7 @@ defmodule Cadet.Accounts.NotificationTest do type: :new, read: false, role: student.role, - user_id: student.id, + course_reg_id: student.id, assessment_id: assessment.id } @@ -143,7 +145,7 @@ defmodule Cadet.Accounts.NotificationTest do type: :submitted, read: false, role: avenger.role, - user_id: avenger.id, + course_reg_id: avenger.id, assessment_id: assessment.id, submission_id: submission.id } @@ -154,7 +156,7 @@ defmodule Cadet.Accounts.NotificationTest do assert Repo.one( from(n in Notification, where: - n.type == ^:new and n.user_id == ^student.id and + n.type == ^:new and n.course_reg_id == ^student.id and n.assessment_id == ^assessment.id ) ) @@ -165,7 +167,7 @@ defmodule Cadet.Accounts.NotificationTest do assert Repo.one( from(n in Notification, where: - n.type == ^:submitted and n.user_id == ^avenger.id and + n.type == ^:submitted and n.course_reg_id == ^avenger.id and n.submission_id == ^submission.id ) ) @@ -179,7 +181,7 @@ defmodule Cadet.Accounts.NotificationTest do type: :new, read: false, role: student.role, - user_id: student.id, + course_reg_id: student.id, assessment_id: assessment.id } @@ -200,7 +202,7 @@ defmodule Cadet.Accounts.NotificationTest do insert(:notification, %{ read: false, assessment_id: assessment.id, - user_id: student.id + course_reg_id: student.id }) Notifications.acknowledge([notification.id], student) @@ -218,7 +220,7 @@ defmodule Cadet.Accounts.NotificationTest do insert_list(3, :notification, %{ read: false, assessment_id: assessment.id, - user_id: student.id + course_reg_id: student.id }) notifications @@ -239,7 +241,7 @@ defmodule Cadet.Accounts.NotificationTest do insert(:notification, %{ read: false, assessment_id: assessment.id, - user_id: student.id + course_reg_id: student.id }) assert {:error, _} = Notifications.acknowledge(notification.id, avenger) @@ -256,16 +258,16 @@ defmodule Cadet.Accounts.NotificationTest do test "receives notification when submitted" do assessment = insert(:assessment, %{is_published: true}) - avenger = insert(:user, %{role: :staff}) + avenger = insert(:course_registration, %{role: :staff}) group = insert(:group, %{leader: avenger}) - student = insert(:user, %{role: :student, group: group}) + student = insert(:course_registration, %{role: :student, group: group}) submission = insert(:submission, %{student: student, assessment: assessment}) Notifications.write_notification_when_student_submits(submission) notification = Repo.get_by(Notification, - user_id: avenger.id, + course_reg_id: avenger.id, type: :submitted, submission_id: submission.id ) @@ -282,7 +284,7 @@ defmodule Cadet.Accounts.NotificationTest do notification = Repo.get_by(Notification, - user_id: student.id, + course_reg_id: student.id, type: :autograded, assessment_id: assessment.id ) @@ -298,7 +300,7 @@ defmodule Cadet.Accounts.NotificationTest do Notifications.write_notification_when_graded(submission.id, :graded) notification = - Repo.get_by(Notification, user_id: student.id, type: :graded, assessment_id: assessment.id) + Repo.get_by(Notification, course_reg_id: student.id, type: :graded, assessment_id: assessment.id) assert %{type: :graded} = notification end @@ -307,13 +309,13 @@ defmodule Cadet.Accounts.NotificationTest do assessment: assessment, student: student } do - students = [student | insert_list(3, :user, %{role: :student})] + students = [student | insert_list(3, :course_registration, %{course: student.course, role: :student})] - Notifications.write_notification_for_new_assessment(assessment.id) + Notifications.write_notification_for_new_assessment(student.course_id, assessment.id) for student <- students do notification = - Repo.get_by(Notification, user_id: student.id, type: :new, assessment_id: assessment.id) + Repo.get_by(Notification, course_reg_id: student.id, type: :new, assessment_id: assessment.id) assert %{type: :new} = notification end diff --git a/test/cadet/courses/group_test.exs b/test/cadet/courses/group_test.exs index d42d7f324..f3a7c470e 100644 --- a/test/cadet/courses/group_test.exs +++ b/test/cadet/courses/group_test.exs @@ -5,8 +5,8 @@ defmodule Cadet.Courses.GroupTest do describe "Changesets" do test "valid changeset" do - assert_changeset(%{}, :valid) - assert_changeset(%{name: "tst"}, :valid) + assert_changeset(%{name: "test", course_id: 1}, :valid) + assert_changeset(%{name: "tst"}, :invalid) end end end From 09bb647cd8d032c6e53fe7642d258da6f5ca3f4a Mon Sep 17 00:00:00 2001 From: chownces <53928333+chownces@users.noreply.github.com> Date: Sat, 26 Jun 2021 22:07:43 +0800 Subject: [PATCH 087/174] Added add_users endpoint and tests --- config/dev.secrets.exs.example | 2 +- lib/cadet/accounts/accounts.ex | 1 - lib/cadet/accounts/course_registrations.ex | 50 ++++- .../admin_user_controller.ex | 90 ++++++++ lib/cadet_web/router.ex | 1 + .../accounts/course_registration_test.exs | 58 ++++- .../admin_user_controller_test.exs | 198 ++++++++++++++++++ 7 files changed, 389 insertions(+), 11 deletions(-) diff --git a/config/dev.secrets.exs.example b/config/dev.secrets.exs.example index f27245f54..081434178 100644 --- a/config/dev.secrets.exs.example +++ b/config/dev.secrets.exs.example @@ -31,7 +31,7 @@ config :cadet, # {Cadet.Auth.Providers.GitHub, # # A map of GitHub client_id => client_secret # %{ - # "client_id": "client_secret" + # "client_id" => "client_secret" # }}, "test" => {Cadet.Auth.Providers.Config, diff --git a/lib/cadet/accounts/accounts.ex b/lib/cadet/accounts/accounts.ex index f0c145aa0..8284b7ecd 100644 --- a/lib/cadet/accounts/accounts.ex +++ b/lib/cadet/accounts/accounts.ex @@ -8,7 +8,6 @@ defmodule Cadet.Accounts do alias Cadet.Accounts.{Query, User, CourseRegistration} alias Cadet.Auth.Provider - alias Cadet.Assessments.{Answer, Submission} @doc """ Register new User entity using Cadet.Accounts.Form.Registration diff --git a/lib/cadet/accounts/course_registrations.ex b/lib/cadet/accounts/course_registrations.ex index c5136191a..cd1acdad4 100644 --- a/lib/cadet/accounts/course_registrations.ex +++ b/lib/cadet/accounts/course_registrations.ex @@ -7,6 +7,7 @@ defmodule Cadet.Accounts.CourseRegistrations do import Ecto.Query alias Cadet.Repo + alias Cadet.Accounts alias Cadet.Accounts.{User, CourseRegistration} alias Cadet.Assessments.{Answer, Submission} alias Cadet.Courses.AssessmentConfig @@ -66,9 +67,56 @@ defmodule Cadet.Accounts.CourseRegistrations do |> Repo.all() end + def add_users_to_course(usernames_and_roles, course_id) do + # Note: Usernames have already been namespaced in the controller + usernames_and_roles + |> Enum.reduce_while(nil, fn %{username: username, role: role}, _acc -> + add_users_to_course_helper(username, course_id, role) + end) + end + + defp add_users_to_course_helper(username, course_id, role) do + case User + |> where(username: ^username) + |> Repo.one() do + nil -> + with {:ok, _} <- Accounts.register(%{username: username}) do + add_users_to_course_helper(username, course_id, role) + else + {:error, changeset} -> + {:halt, {:error, {:bad_request, full_error_messages(changeset)}}} + end + + user -> + with {:ok, _} <- enroll_course(%{user_id: user.id, course_id: course_id, role: role}) do + {:cont, :ok} + else + {:error, changeset} -> + {:halt, {:error, {:bad_request, full_error_messages(changeset)}}} + end + end + end + + @doc """ + Enrolls the user into the specified course with the specified role, and updates the user's + latest viewed course id to this enrolled course. + """ def enroll_course(params = %{user_id: user_id, course_id: course_id, role: _role}) when is_ecto_id(user_id) and is_ecto_id(course_id) do - params |> insert_or_update_course_registration() + case params |> insert_or_update_course_registration() do + {:ok, _course_reg} = ok -> + # Ensures that the user has a latest_viewed_course + User + |> where(id: ^user_id) + |> Repo.one() + |> User.changeset(%{latest_viewed_id: course_id}) + |> Repo.update() + + ok + + {:error, _} = error -> + error + end end @spec insert_or_update_course_registration(map()) :: diff --git a/lib/cadet_web/admin_controllers/admin_user_controller.ex b/lib/cadet_web/admin_controllers/admin_user_controller.ex index 9d29574f1..9b0547100 100644 --- a/lib/cadet_web/admin_controllers/admin_user_controller.ex +++ b/lib/cadet_web/admin_controllers/admin_user_controller.ex @@ -7,6 +7,7 @@ defmodule CadetWeb.AdminUserController do alias Cadet.Repo alias Cadet.Accounts alias Cadet.Accounts.CourseRegistration + alias Cadet.Auth.Provider # This controller is used to find all users of a course @@ -17,6 +18,64 @@ defmodule CadetWeb.AdminUserController do render(conn, "users.json", users: users) end + @add_users_role ~w(admin)a + def add_users(conn, %{ + "course_id" => course_id, + "users" => usernames_and_roles, + "provider" => provider + }) do + %{role: admin_role} = conn.assigns.course_reg + + # Note: Usernames from frontend have not been namespaced yet + with {:validate_role, true} <- {:validate_role, admin_role in @add_users_role}, + {:validate_provider, true} <- + {:validate_provider, + Map.has_key?(Application.get_env(:cadet, :identity_providers, %{}), provider)}, + {:atomify_keys, usernames_and_roles} <- + {:atomify_keys, + Enum.map(usernames_and_roles, fn x -> + for({key, val} <- x, into: %{}, do: {String.to_atom(key), val}) + end)}, + {:validate_usernames, true} <- + {:validate_usernames, + Enum.reduce(usernames_and_roles, true, fn x, acc -> + acc and Map.has_key?(x, :username) and is_binary(x.username) and x.username != "" + end)}, + {:validate_roles, true} <- + {:validate_roles, + Enum.reduce(usernames_and_roles, true, fn x, acc -> + acc and Map.has_key?(x, :role) and + String.to_atom(x.role) in Cadet.Accounts.Role.__enums__() + end)}, + {:namespace, usernames_and_roles} <- + {:namespace, + Enum.map(usernames_and_roles, fn x -> + %{x | username: Provider.namespace(x.username, provider)} + end)} do + case Accounts.CourseRegistrations.add_users_to_course(usernames_and_roles, course_id) do + :ok -> + text(conn, "OK") + + {:error, {status, message}} -> + conn + |> put_status(status) + |> text(message) + end + else + {:validate_role, false} -> + conn |> put_status(:forbidden) |> text("User is not permitted to add users") + + {:validate_provider, false} -> + conn |> put_status(:bad_request) |> text("Invalid authentication provider") + + {:validate_usernames, false} -> + conn |> put_status(:bad_request) |> text("Invalid username(s) provided") + + {:validate_roles, false} -> + conn |> put_status(:bad_request) |> text("Invalid role(s) provided") + end + end + @update_role_roles ~w(admin)a def update_role(conn, %{"role" => role, "crId" => coursereg_id}) do %{id: admin_course_reg_id, role: admin_role, course_id: admin_course_id} = @@ -104,6 +163,27 @@ defmodule CadetWeb.AdminUserController do response(401, "Unauthorised") end + swagger_path :add_users do + put("/v2/courses/{course_id}/admin/users") + + summary("Adds the list of usernames and roles to the course") + security([%{JWT: []}]) + consumes("application/json") + + parameters do + course_id(:path, :integer, "Course ID", required: true) + users(:body, Schema.array(:UsernameAndRole), "Array of usernames and roles", required: true) + + provider(:body, :string, "The authentication provider linked to these usernames", + required: true + ) + end + + response(200, "OK") + response(400, "Bad Request. Invalid provider, username or role") + response(403, "Forbidden. You are not an admin") + end + swagger_path :update_role do put("/v2/courses/{course_id}/admin/users/role") @@ -175,6 +255,16 @@ defmodule CadetWeb.AdminUserController do "Group the user belongs to in this course. May be null if the user does not belong to any group" ) end + end, + UsernameAndRole: + swagger_schema do + title("Username and role") + description("Username and role of the user to add to this course") + + properties do + username(:string, "The user's username") + role(:role, "The user's role. Can be 'student', 'staff', or 'admin'") + end end } end diff --git a/lib/cadet_web/router.ex b/lib/cadet_web/router.ex index 83f63901f..1bdcee630 100644 --- a/lib/cadet_web/router.ex +++ b/lib/cadet_web/router.ex @@ -127,6 +127,7 @@ defmodule CadetWeb.Router do ) get("/users", AdminUserController, :index) + put("/users", AdminUserController, :add_users) put("/users/role", AdminUserController, :update_role) delete("/users", AdminUserController, :delete_user) post("/users/:userid/goals/:uuid/progress", AdminGoalsController, :update_progress) diff --git a/test/cadet/accounts/course_registration_test.exs b/test/cadet/accounts/course_registration_test.exs index d4c78d724..363fd5949 100644 --- a/test/cadet/accounts/course_registration_test.exs +++ b/test/cadet/accounts/course_registration_test.exs @@ -1,5 +1,5 @@ defmodule Cadet.Accounts.CourseRegistrationTest do - alias Cadet.Accounts.{CourseRegistration, CourseRegistrations} + alias Cadet.Accounts.{CourseRegistration, CourseRegistrations, User} use Cadet.ChangesetCase, entity: CourseRegistration @@ -154,12 +154,49 @@ defmodule Cadet.Accounts.CourseRegistrationTest do end end - describe "update course_registration" do - test "successful insert", %{course1: course1, user2: user2} do + describe "add_users_to_course" do + # Note: roles are already validated in the controller + test "successful", %{course2: course2} do + user = insert(:user, %{username: "existing-user"}) + insert(:course_registration, %{course: course2, user: user}) + assert length(CourseRegistrations.get_users(course2.id)) == 1 + + usernames_and_roles = [ + %{username: "existing-user", role: "admin"}, + %{username: "student1", role: "student"}, + %{username: "student2", role: "student"}, + %{username: "staff1", role: "staff"}, + %{username: "admin1", role: "admin"} + ] + + assert :ok == CourseRegistrations.add_users_to_course(usernames_and_roles, course2.id) + assert length(CourseRegistrations.get_users(course2.id)) == 5 + end + + test "successful when there are duplicate inputs in list", %{course2: course2} do + user = insert(:user, %{username: "existing-user"}) + insert(:course_registration, %{course: course2, user: user}) + assert length(CourseRegistrations.get_users(course2.id)) == 1 + + usernames_and_roles = [ + %{username: "existing-user", role: "admin"}, + %{username: "student1", role: "student"}, + %{username: "student1", role: "student"}, + %{username: "staff1", role: "staff"}, + %{username: "admin1", role: "admin"} + ] + + assert :ok == CourseRegistrations.add_users_to_course(usernames_and_roles, course2.id) + assert length(CourseRegistrations.get_users(course2.id)) == 4 + end + end + + describe "enroll course" do + test "successful enrollment", %{course1: course1, user2: user2} do assert length(CourseRegistrations.get_users(course1.id)) == 1 {:ok, course_reg} = - CourseRegistrations.insert_or_update_course_registration(%{ + CourseRegistrations.enroll_course(%{ user_id: user2.id, course_id: course1.id, role: :student @@ -168,13 +205,18 @@ defmodule Cadet.Accounts.CourseRegistrationTest do assert length(CourseRegistrations.get_users(course1.id)) == 2 assert course_reg.user_id == user2.id assert course_reg.course_id == course1.id + + assert User |> where(id: ^user2.id) |> Repo.one() |> Map.fetch!(:latest_viewed_id) == + course1.id end + end - test "successful insert through enroll_course", %{course1: course1, user2: user2} do + describe "update course_registration" do + test "successful insert", %{course1: course1, user2: user2} do assert length(CourseRegistrations.get_users(course1.id)) == 1 {:ok, course_reg} = - CourseRegistrations.enroll_course(%{ + CourseRegistrations.insert_or_update_course_registration(%{ user_id: user2.id, course_id: course1.id, role: :student @@ -333,13 +375,13 @@ defmodule Cadet.Accounts.CourseRegistrationTest do end test "succeeds", %{student: %{id: coursereg_id}} do - {:ok, deleted_coursereg} = CourseRegistrations.delete_course_registration(coursereg_id) + {:ok, _deleted_coursereg} = CourseRegistrations.delete_course_registration(coursereg_id) assert is_nil(CourseRegistration |> where(id: ^coursereg_id) |> Repo.one()) end test "fails when course registration does not exist", %{} do assert {:error, {:bad_request, "User course registration does not exist"}} == - CourseRegistrations.delete_course_registration(10_000) + CourseRegistrations.delete_course_registration(10_000) end end end diff --git a/test/cadet_web/admin_controllers/admin_user_controller_test.exs b/test/cadet_web/admin_controllers/admin_user_controller_test.exs index a6c940d19..34b443843 100644 --- a/test/cadet_web/admin_controllers/admin_user_controller_test.exs +++ b/test/cadet_web/admin_controllers/admin_user_controller_test.exs @@ -82,6 +82,204 @@ defmodule CadetWeb.AdminUserControllerTest do # end end + describe "PUT /v2/courses/{course_id}/admin/users" do + @tag authenticate: :admin + test "successfully namespaces and inserts users", %{conn: conn} do + course_id = conn.assigns[:course_id] + course = Repo.get(Course, course_id) + user = insert(:user, %{username: "test/existing-user"}) + insert(:course_registration, %{course: course, user: user}) + + assert CourseRegistration |> where(course_id: ^course_id) |> Repo.all() |> Enum.count() == 2 + + params = %{ + users: [ + %{"username" => "existing-user", "role" => "student"}, + %{"username" => "student1", "role" => "student"}, + %{"username" => "student2", "role" => "student"}, + %{"username" => "student3", "role" => "student"}, + %{"username" => "staff", "role" => "staff"}, + %{"username" => "admin", "role" => "admin"} + ], + provider: "test" + } + + resp = put(conn, build_url_users(course_id), params) + + assert response(resp, 200) == "OK" + + assert CourseRegistration |> where(course_id: ^course_id) |> Repo.all() |> Enum.count() == 7 + end + + @tag authenticate: :admin + test "successful with duplicate inputs", %{conn: conn} do + course_id = conn.assigns[:course_id] + course = Repo.get(Course, course_id) + user = insert(:user, %{username: "test/existing-user"}) + insert(:course_registration, %{course: course, user: user}) + + assert CourseRegistration |> where(course_id: ^course_id) |> Repo.all() |> Enum.count() == 2 + + params = %{ + users: [ + %{"username" => "existing-user", "role" => "student"}, + %{"username" => "student1", "role" => "student"}, + %{"username" => "student2", "role" => "student"}, + %{"username" => "student2", "role" => "student"}, + %{"username" => "staff", "role" => "staff"}, + %{"username" => "admin", "role" => "admin"} + ], + provider: "test" + } + + resp = put(conn, build_url_users(course_id), params) + + assert response(resp, 200) == "OK" + + assert CourseRegistration |> where(course_id: ^course_id) |> Repo.all() |> Enum.count() == 6 + end + + @tag authenticate: :staff + test "fails when not admin", %{conn: conn} do + course_id = conn.assigns[:course_id] + + params = %{ + users: [ + %{"username" => "student1", "role" => "student"}, + %{"username" => "student2", "role" => "student"}, + %{"username" => "student3", "role" => "student"}, + %{"username" => "staff", "role" => "staff"}, + %{"username" => "admin", "role" => "admin"} + ], + provider: "test" + } + + conn = put(conn, build_url_users(course_id), params) + + assert response(conn, 403) == "User is not permitted to add users" + end + + @tag authenticate: :admin + test "fails when invalid provider is specified", %{conn: conn} do + course_id = conn.assigns[:course_id] + + params = %{ + users: [ + %{"username" => "student1", "role" => "student"}, + %{"username" => "student2", "role" => "student"}, + %{"username" => "student3", "role" => "student"}, + %{"username" => "staff", "role" => "staff"}, + %{"username" => "admin", "role" => "admin"} + ], + provider: "invalid-provider" + } + + conn = put(conn, build_url_users(course_id), params) + + assert response(conn, 400) == "Invalid authentication provider" + end + + @tag authenticate: :admin + test "fails when no username is specified for at least one input", %{conn: conn} do + course_id = conn.assigns[:course_id] + + params = %{ + users: [ + %{"username" => "student1", "role" => "student"}, + %{"username" => "student2", "role" => "student"}, + %{"username" => "student3", "role" => "student"}, + %{"username" => "staff", "role" => "staff"}, + %{"role" => "admin"} + ], + provider: "test" + } + + conn = put(conn, build_url_users(course_id), params) + + assert response(conn, 400) == "Invalid username(s) provided" + end + + @tag authenticate: :admin + test "fails when invalid username is specified (not string)", %{conn: conn} do + course_id = conn.assigns[:course_id] + + params = %{ + users: [ + %{"username" => "student1", "role" => "student"}, + %{"username" => "student2", "role" => "student"}, + %{"username" => "student3", "role" => "student"}, + %{"username" => "staff", "role" => "staff"}, + %{"username" => nil, "role" => "admin"} + ], + provider: "test" + } + + conn = put(conn, build_url_users(course_id), params) + + assert response(conn, 400) == "Invalid username(s) provided" + end + + @tag authenticate: :admin + test "fails when invalid username is specified (empty string)", %{conn: conn} do + course_id = conn.assigns[:course_id] + + params = %{ + users: [ + %{"username" => "student1", "role" => "student"}, + %{"username" => "student2", "role" => "student"}, + %{"username" => "student3", "role" => "student"}, + %{"username" => "staff", "role" => "staff"}, + %{"username" => "", "role" => "admin"} + ], + provider: "test" + } + + conn = put(conn, build_url_users(course_id), params) + + assert response(conn, 400) == "Invalid username(s) provided" + end + + @tag authenticate: :admin + test "fails when no role is specified for at least one input", %{conn: conn} do + course_id = conn.assigns[:course_id] + + params = %{ + users: [ + %{"username" => "student1", "role" => "student"}, + %{"username" => "student2", "role" => "student"}, + %{"username" => "student3", "role" => "student"}, + %{"username" => "staff", "role" => "staff"}, + %{"username" => "admin"} + ], + provider: "test" + } + + conn = put(conn, build_url_users(course_id), params) + + assert response(conn, 400) == "Invalid role(s) provided" + end + + @tag authenticate: :admin + test "fails when invalid role is specified for at least one input", %{conn: conn} do + course_id = conn.assigns[:course_id] + + params = %{ + users: [ + %{"username" => "student1", "role" => "student"}, + %{"username" => "student2", "role" => "student"}, + %{"username" => "student3", "role" => "invalid-role"}, + %{"username" => "staff", "role" => "staff"}, + %{"username" => "admin", "role" => "admin"} + ], + provider: "test" + } + + conn = put(conn, build_url_users(course_id), params) + + assert response(conn, 400) == "Invalid role(s) provided" + end + end + describe "PUT /v2/courses/{course_id}/admin/users/role" do @tag authenticate: :admin test "success (student to staff), when admin is admin of the course the user is in", %{ From 8a76ce64f1ebacea57c568283a5b16890d231a0c Mon Sep 17 00:00:00 2001 From: chownces <53928333+chownces@users.noreply.github.com> Date: Sun, 27 Jun 2021 14:42:50 +0800 Subject: [PATCH 088/174] Format --- lib/cadet/accounts/notifications.ex | 6 ++++-- lib/cadet/jobs/autograder/lambda_worker.ex | 4 +--- test/cadet/accounts/notification_test.exs | 22 +++++++++++++++++----- 3 files changed, 22 insertions(+), 10 deletions(-) diff --git a/lib/cadet/accounts/notifications.ex b/lib/cadet/accounts/notifications.ex index f5ee3440f..b6b062808 100644 --- a/lib/cadet/accounts/notifications.ex +++ b/lib/cadet/accounts/notifications.ex @@ -104,7 +104,8 @@ defmodule Cadet.Accounts.Notifications do {:ok, Ecto.Schema.t()} | {:error, any} | {:error, Ecto.Multi.name(), any, %{Ecto.Multi.name() => any}} - def acknowledge(notification_ids, course_reg = %CourseRegistration{}) when is_list(notification_ids) do + def acknowledge(notification_ids, course_reg = %CourseRegistration{}) + when is_list(notification_ids) do Multi.new() |> Multi.run(:update_all, fn _repo, _ -> Enum.reduce_while(notification_ids, {:ok, nil}, fn n_id, acc -> @@ -185,7 +186,8 @@ defmodule Cadet.Accounts.Notifications do """ @spec write_notification_for_new_assessment(integer(), integer()) :: {:ok, Ecto.Schema.t()} | {:error, Ecto.Changeset.t()} - def write_notification_for_new_assessment(course_id, assessment_id) when is_ecto_id(assessment_id) and is_ecto_id(course_id) do + def write_notification_for_new_assessment(course_id, assessment_id) + when is_ecto_id(assessment_id) and is_ecto_id(course_id) do Multi.new() |> Multi.run(:insert_all, fn _repo, _ -> CourseRegistration diff --git a/lib/cadet/jobs/autograder/lambda_worker.ex b/lib/cadet/jobs/autograder/lambda_worker.ex index a6c8563f2..b96435de5 100644 --- a/lib/cadet/jobs/autograder/lambda_worker.ex +++ b/lib/cadet/jobs/autograder/lambda_worker.ex @@ -44,9 +44,7 @@ defmodule Cadet.Autograder.LambdaWorker do def on_failure(%{answer: answer = %Answer{}, question: %Question{}}, error) do error_message = - "Failed to get autograder result. answer_id: #{answer.id}, error: #{ - inspect(error, pretty: true) - }" + "Failed to get autograder result. answer_id: #{answer.id}, error: #{inspect(error, pretty: true)}" Logger.error(error_message) Sentry.capture_message(error_message) diff --git a/test/cadet/accounts/notification_test.exs b/test/cadet/accounts/notification_test.exs index 7442c23af..689e440cf 100644 --- a/test/cadet/accounts/notification_test.exs +++ b/test/cadet/accounts/notification_test.exs @@ -79,11 +79,13 @@ defmodule Cadet.Accounts.NotificationTest do course_reg_id: student.id }) - expected = notifications |> Enum.sort(&(&1.id < &2.id)) |> Enum.map(& Map.delete(&1, :assessment)) + expected = + notifications |> Enum.sort(&(&1.id < &2.id)) |> Enum.map(&Map.delete(&1, :assessment)) {:ok, notifications_db} = Notifications.fetch(student) - results = notifications_db |> Enum.sort(&(&1.id < &2.id)) |> Enum.map(& Map.delete(&1, :assessment)) + results = + notifications_db |> Enum.sort(&(&1.id < &2.id)) |> Enum.map(&Map.delete(&1, :assessment)) assert results == expected end @@ -300,7 +302,11 @@ defmodule Cadet.Accounts.NotificationTest do Notifications.write_notification_when_graded(submission.id, :graded) notification = - Repo.get_by(Notification, course_reg_id: student.id, type: :graded, assessment_id: assessment.id) + Repo.get_by(Notification, + course_reg_id: student.id, + type: :graded, + assessment_id: assessment.id + ) assert %{type: :graded} = notification end @@ -309,13 +315,19 @@ defmodule Cadet.Accounts.NotificationTest do assessment: assessment, student: student } do - students = [student | insert_list(3, :course_registration, %{course: student.course, role: :student})] + students = [ + student | insert_list(3, :course_registration, %{course: student.course, role: :student}) + ] Notifications.write_notification_for_new_assessment(student.course_id, assessment.id) for student <- students do notification = - Repo.get_by(Notification, course_reg_id: student.id, type: :new, assessment_id: assessment.id) + Repo.get_by(Notification, + course_reg_id: student.id, + type: :new, + assessment_id: assessment.id + ) assert %{type: :new} = notification end From d3ec484aa4caa3523a6b28bee997fc9bed146978 Mon Sep 17 00:00:00 2001 From: chownces <53928333+chownces@users.noreply.github.com> Date: Sun, 27 Jun 2021 16:47:01 +0800 Subject: [PATCH 089/174] Added create course endpoint and tests --- lib/cadet/courses/courses.ex | 26 ++++++- .../controllers/courses_controller.ex | 56 ++++++++++++++- lib/cadet_web/router.ex | 2 + test/cadet/courses/courses_test.exs | 38 +++++++++- .../controllers/courses_controller_test.exs | 72 +++++++++++++++++++ 5 files changed, 190 insertions(+), 4 deletions(-) diff --git a/lib/cadet/courses/courses.ex b/lib/cadet/courses/courses.ex index 9a13e468b..8a17bfa60 100644 --- a/lib/cadet/courses/courses.ex +++ b/lib/cadet/courses/courses.ex @@ -6,8 +6,9 @@ defmodule Cadet.Courses do use Cadet, [:context, :display] import Ecto.Query + alias Ecto.Multi - alias Cadet.Accounts.CourseRegistration + alias Cadet.Accounts.{CourseRegistration, User} alias Cadet.Courses.{ AssessmentConfig, @@ -17,6 +18,29 @@ defmodule Cadet.Courses do SourcecastUpload } + @doc """ + Creates a new course configuration, course registration, and sets + the user's latest course id to the newly created course. + """ + def create_course_config(params, user) do + Multi.new() + |> Multi.insert(:course, Course.changeset(%Course{}, params)) + |> Multi.insert(:course_reg, fn %{course: course} -> + CourseRegistration.changeset(%CourseRegistration{}, %{ + course_id: course.id, + user_id: user.id, + role: :admin + }) + end) + |> Multi.update(:latest_viewed_id, fn %{course: course} -> + User + |> where(id: ^user.id) + |> Repo.one() + |> User.changeset(%{latest_viewed_id: course.id}) + end) + |> Repo.transaction() + end + @doc """ Returns the course configuration for the specified course. """ diff --git a/lib/cadet_web/controllers/courses_controller.ex b/lib/cadet_web/controllers/courses_controller.ex index 1ff82efae..21f18ca2e 100644 --- a/lib/cadet_web/controllers/courses_controller.ex +++ b/lib/cadet_web/controllers/courses_controller.ex @@ -12,9 +12,61 @@ defmodule CadetWeb.CoursesController do end end - # def create_course(conn, _) do + def create(conn, params) do + user = conn.assigns.current_user + params = params |> to_snake_case_atom_keys() - # end + required_keys = [ + :course_name, + :course_short_name, + :viewable, + :enable_game, + :enable_achievements, + :enable_sourcecast, + :source_chapter, + :source_variant, + :module_help_text + ] + + if Enum.reduce(required_keys, true, fn x, acc -> acc and Map.has_key?(params, x) end) do + case Courses.create_course_config(params, user) do + {:ok, _} -> + text(conn, "OK") + + {:error, _, _, _} -> + conn + |> put_status(:bad_request) + |> text("Invalid parameter(s)") + end + else + send_resp(conn, :bad_request, "Missing parameter(s)") + end + end + + swagger_path :create do + post("/v2/config/create") + + summary("Creates a new course") + + security([%{JWT: []}]) + consumes("application/json") + + parameters do + course_name(:body, :string, "Course name", required: true) + course_short_name(:body, :string, "Course module code", required: true) + viewable(:body, :boolean, "Course viewability", required: true) + enable_game(:body, :boolean, "Enable game", required: true) + enable_achievements(:body, :boolean, "Enable achievements", required: true) + enable_sourcecast(:body, :boolean, "Enable sourcecast", required: true) + source_chapter(:body, :number, "Default source chapter", required: true) + + source_variant(:body, Schema.ref(:SourceVariant), "Default source variant name", + required: true + ) + + module_help_text(:body, :string, "Module help text", required: true) + end + end swagger_path :get_course_config do get("/v2/courses/{course_id}/config") diff --git a/lib/cadet_web/router.ex b/lib/cadet_web/router.ex index cab338c6f..4897888a9 100644 --- a/lib/cadet_web/router.ex +++ b/lib/cadet_web/router.ex @@ -56,6 +56,8 @@ defmodule CadetWeb.Router do get("/user", UserController, :index) get("/user/latest_viewed", UserController, :get_latest_viewed) put("/user/latest_viewed", UserController, :update_latest_viewed) + + post("/config/create", CoursesController, :create) end # Authenticated Pages with course diff --git a/test/cadet/courses/courses_test.exs b/test/cadet/courses/courses_test.exs index e428943be..db127006f 100644 --- a/test/cadet/courses/courses_test.exs +++ b/test/cadet/courses/courses_test.exs @@ -2,7 +2,43 @@ defmodule Cadet.CoursesTest do use Cadet.DataCase alias Cadet.{Courses, Repo} - alias Cadet.Courses.{Sourcecast, SourcecastUpload} + alias Cadet.Accounts.{CourseRegistration, User} + alias Cadet.Courses.{Course, Sourcecast, SourcecastUpload} + + describe "create course config" do + test "succeeds" do + user = insert(:user) + + # Course precreated in User factory + assert Course |> Repo.all() |> length() == 1 + + params = %{ + course_name: "CS1101S Programming Methodology (AY20/21 Sem 1)", + course_short_name: "CS1101S", + viewable: true, + enable_game: true, + enable_achievements: true, + enable_sourcecast: true, + source_chapter: 1, + source_variant: "default", + module_help_text: "Help Text" + } + + Courses.create_course_config(params, user) + + # New course created + assert Course |> Repo.all() |> length() == 2 + + # New admin course registration for user + course_regs = CourseRegistration |> where(user_id: ^user.id) |> Repo.all() + assert length(course_regs) == 1 + assert Enum.at(course_regs, 0).role == :admin + + # User's latest_viewed course is updated + assert User |> where(id: ^user.id) |> Repo.one() |> Map.fetch!(:latest_viewed_id) == + Enum.at(course_regs, 0).course_id + end + end describe "get course config" do test "succeeds" do diff --git a/test/cadet_web/controllers/courses_controller_test.exs b/test/cadet_web/controllers/courses_controller_test.exs index cb6daf1e7..7e8873297 100644 --- a/test/cadet_web/controllers/courses_controller_test.exs +++ b/test/cadet_web/controllers/courses_controller_test.exs @@ -1,7 +1,10 @@ defmodule CadetWeb.CoursesControllerTest do use CadetWeb.ConnCase + import Ecto.Query + alias Cadet.Repo + alias Cadet.Accounts.CourseRegistration alias Cadet.Courses.Course alias CadetWeb.CoursesController @@ -10,6 +13,74 @@ defmodule CadetWeb.CoursesControllerTest do CoursesController.swagger_path_get_course_config(nil) end + describe "POST /v2/config/create" do + @tag authenticate: :student + test "succeeds", %{conn: conn} do + user = conn.assigns.current_user + assert CourseRegistration |> where(user_id: ^user.id) |> Repo.all() |> length() == 1 + + params = %{ + "course_name" => "CS1101S Programming Methodology (AY20/21 Sem 1)", + "course_short_name" => "CS1101S", + "viewable" => "true", + "enable_game" => "true", + "enable_achievements" => "true", + "enable_sourcecast" => "true", + "source_chapter" => "1", + "source_variant" => "default", + "module_help_text" => "Help Text" + } + + resp = post(conn, build_url_create(), params) + + assert response(resp, 200) == "OK" + assert CourseRegistration |> where(user_id: ^user.id) |> Repo.all() |> length() == 2 + end + + @tag authenticate: :student + test "fails when there are missing parameters", %{conn: conn} do + user = conn.assigns.current_user + assert CourseRegistration |> where(user_id: ^user.id) |> Repo.all() |> length() == 1 + + params = %{ + "course_name" => "CS1101S Programming Methodology (AY20/21 Sem 1)", + "course_short_name" => "CS1101S", + "viewable" => "true", + "enable_achievements" => "true", + "enable_sourcecast" => "true", + "source_chapter" => "1", + "source_variant" => "default", + "module_help_text" => "Help Text" + } + + conn = post(conn, build_url_create(), params) + + assert response(conn, 400) == "Missing parameter(s)" + end + + @tag authenticate: :student + test "fails when there are invalid parameters", %{conn: conn} do + user = conn.assigns.current_user + assert CourseRegistration |> where(user_id: ^user.id) |> Repo.all() |> length() == 1 + + params = %{ + "course_name" => "CS1101S Programming Methodology (AY20/21 Sem 1)", + "course_short_name" => "CS1101S", + "viewable" => "boolean", + "enable_game" => "true", + "enable_achievements" => "true", + "enable_sourcecast" => "true", + "source_chapter" => "1", + "source_variant" => "default", + "module_help_text" => "Help Text" + } + + conn = post(conn, build_url_create(), params) + + assert response(conn, 400) == "Invalid parameter(s)" + end + end + describe "GET /v2/courses/course_id/config, unauthenticated" do test "unauthorized", %{conn: conn} do course = insert(:course) @@ -58,5 +129,6 @@ defmodule CadetWeb.CoursesControllerTest do end end + defp build_url_create(), do: "/v2/config/create" defp build_url_config(course_id), do: "/v2/courses/#{course_id}/config" end From 2d4aeb6c804b4c3e131b0def04465950e491374e Mon Sep 17 00:00:00 2001 From: YaleChen299 Date: Sun, 27 Jun 2021 18:07:04 +0800 Subject: [PATCH 090/174] updated notification controller with test --- lib/cadet/accounts/notifications.ex | 3 +- .../controllers/notifications_controller.ex | 8 +-- lib/cadet_web/views/notifications_view.ex | 13 +++-- .../20210608085548_update_assessments.exs | 2 + test/cadet/accounts/accounts_test.exs | 2 +- test/cadet/assessments/answer_test.exs | 38 ++------------ .../assessments/submission_votes_test.exs | 18 +++---- .../notifications_controller_test.exs | 50 +++++++++++-------- 8 files changed, 57 insertions(+), 77 deletions(-) diff --git a/lib/cadet/accounts/notifications.ex b/lib/cadet/accounts/notifications.ex index f5ee3440f..f9c781100 100644 --- a/lib/cadet/accounts/notifications.ex +++ b/lib/cadet/accounts/notifications.ex @@ -21,7 +21,8 @@ defmodule Cadet.Accounts.Notifications do Notification |> where(course_reg_id: ^course_reg.id) |> where(read: false) - |> preload(:assessment) + |> join(:inner, [n], a in assoc(n, :assessment)) + |> preload([n, a], assessment: {a, :config}) |> Repo.all() {:ok, notifications} diff --git a/lib/cadet_web/controllers/notifications_controller.ex b/lib/cadet_web/controllers/notifications_controller.ex index 5e82d3c63..5989f3336 100644 --- a/lib/cadet_web/controllers/notifications_controller.ex +++ b/lib/cadet_web/controllers/notifications_controller.ex @@ -9,7 +9,7 @@ defmodule CadetWeb.NotificationsController do alias Cadet.Accounts.Notifications def index(conn, _) do - {:ok, notifications} = Notifications.fetch(conn.assigns.current_user) + {:ok, notifications} = Notifications.fetch(conn.assigns.course_reg) render( conn, @@ -21,7 +21,7 @@ defmodule CadetWeb.NotificationsController do def acknowledge(conn, %{"notificationIds" => notification_ids}) do case Notifications.acknowledge( notification_ids, - conn.assigns.current_user + conn.assigns.course_reg ) do {:ok, _nil} -> text(conn, "OK") @@ -39,7 +39,7 @@ defmodule CadetWeb.NotificationsController do end swagger_path :index do - get("/notifications") + get("/v2/courses/{course_id}/notifications") summary("Get the unread notifications belonging to a user") @@ -52,7 +52,7 @@ defmodule CadetWeb.NotificationsController do end swagger_path :acknowledge do - post("/notifications/acknowledge") + post("/v2/courses/{course_id}/notifications/acknowledge") summary("Acknowledge notification(s)") security([%{JWT: []}]) diff --git a/lib/cadet_web/views/notifications_view.ex b/lib/cadet_web/views/notifications_view.ex index 09171d542..88e7b578a 100644 --- a/lib/cadet_web/views/notifications_view.ex +++ b/lib/cadet_web/views/notifications_view.ex @@ -11,11 +11,14 @@ defmodule CadetWeb.NotificationsView do type: :type, assessment_id: :assessment_id, submission_id: :submission_id, - assessment: - &transform_map_for_view(&1.assessment, %{ - type: :type, - title: :title - }) + assessment: &render_notification_assessment/1 + }) + end + + defp render_notification_assessment(notification) do + transform_map_for_view(notification.assessment, %{ + type: &(&1.config.type), + title: :title }) end end diff --git a/priv/repo/migrations/20210608085548_update_assessments.exs b/priv/repo/migrations/20210608085548_update_assessments.exs index 848198cab..d1df9ec68 100644 --- a/priv/repo/migrations/20210608085548_update_assessments.exs +++ b/priv/repo/migrations/20210608085548_update_assessments.exs @@ -36,5 +36,7 @@ defmodule Cadet.Repo.Migrations.UpdateAssessments do remove(:user_id) add(:voter_id, references(:course_registrations), null: false) end + + create(unique_index(:submission_votes, [:voter_id, :question_id, :rank], name: :unique_score)) end end diff --git a/test/cadet/accounts/accounts_test.exs b/test/cadet/accounts/accounts_test.exs index 162ebbb61..65434a69e 100644 --- a/test/cadet/accounts/accounts_test.exs +++ b/test/cadet/accounts/accounts_test.exs @@ -46,7 +46,7 @@ defmodule Cadet.AccountsTest do end test "pre-created user during first login" do - user = insert(:user, %{username: "student", name: nil}) + insert(:user, %{username: "student", name: nil}) {:ok, _user} = Accounts.sign_in("student", "student_token", "test") user = Repo.one(Query.username("student")) assert user.username == "student" diff --git a/test/cadet/assessments/answer_test.exs b/test/cadet/assessments/answer_test.exs index 669d4146d..b091ef449 100644 --- a/test/cadet/assessments/answer_test.exs +++ b/test/cadet/assessments/answer_test.exs @@ -7,7 +7,7 @@ defmodule Cadet.Assessments.AnswerTest do setup do assessment = insert(:assessment, %{is_published: true}) - student = insert(:user, %{role: :student}) + student = insert(:course_registration, %{role: :student}) submission = insert(:submission, %{student: student, assessment: assessment}) mcq_question = insert(:mcq_question, %{assessment: assessment}) programming_question = insert(:programming_question, %{assessment: assessment}) @@ -121,43 +121,11 @@ defmodule Cadet.Assessments.AnswerTest do |> Map.put(:question_id, new_mcq_question.id) |> assert_changeset_db(:invalid) end - end - - describe "grading_changeset" do - test "invalid changeset total grade < 0", %{ - valid_mcq_params: valid_mcq_params, - mcq_question: mcq_question, - valid_programming_params: valid_programming_params, - programming_question: programming_question - } do - for {question, params} <- [ - {mcq_question, valid_mcq_params}, - {programming_question, valid_programming_params} - ] do - answer = insert(:answer, %{params | question_id: question.id, grade: 1}) - - refute Answer.grading_changeset(answer, %{adjustment: -2}).valid? - end - end - - test "invalid changeset total grade > max_grade", %{ - valid_mcq_params: valid_mcq_params, - mcq_question: mcq_question, - valid_programming_params: valid_programming_params, - programming_question: programming_question - } do - for {question, params} <- [ - {mcq_question, valid_mcq_params}, - {programming_question, valid_programming_params} - ] do - answer = insert(:answer, %{params | question_id: question.id, grade: question.max_grade}) - - refute Answer.grading_changeset(answer, %{adjustment: 1}).valid? - end - end test "invalid changeset without question_id" do assert_changeset(%{}, :invalid, :grading_changeset) end end + + end diff --git a/test/cadet/assessments/submission_votes_test.exs b/test/cadet/assessments/submission_votes_test.exs index bfb7a4076..3840436c2 100644 --- a/test/cadet/assessments/submission_votes_test.exs +++ b/test/cadet/assessments/submission_votes_test.exs @@ -3,16 +3,16 @@ defmodule Cadet.Assessments.SubmissionVotesTest do use Cadet.ChangesetCase, entity: SubmissionVotes - @required_fields ~w(user_id submission_id question_id)a + @required_fields ~w(voter_id submission_id question_id)a setup do question = insert(:question) - user = insert(:user) + voter = insert(:course_registration) submission = insert(:submission) - valid_params = %{user_id: user.id, submission_id: submission.id, question_id: question.id} + valid_params = %{voter_id: voter.id, submission_id: submission.id, question_id: question.id} - {:ok, %{question: question, user: user, submission: submission, valid_params: valid_params}} + {:ok, %{question: question, voter: voter, submission: submission, valid_params: valid_params}} end describe "Changesets" do @@ -22,10 +22,10 @@ defmodule Cadet.Assessments.SubmissionVotesTest do test "converts valid params with models into ids", %{ question: question, - user: user, + voter: voter, submission: submission } do - assert_changeset_db(%{question: question, user: user, submission: submission}, :valid) + assert_changeset_db(%{question: question, voter: voter, submission: submission}, :valid) end test "invalid changeset missing params", %{valid_params: params} do @@ -38,15 +38,15 @@ defmodule Cadet.Assessments.SubmissionVotesTest do test "invalid changeset foreign key constraint", %{ question: question, - user: user, + voter: voter, submission: submission, valid_params: params } do - {:ok, _} = Repo.delete(user) + {:ok, _} = Repo.delete(voter) assert_changeset_db(params, :invalid) - new_user = insert(:user) + new_user = insert(:course_registration) {:ok, _} = Repo.delete(question) params diff --git a/test/cadet_web/controllers/notifications_controller_test.exs b/test/cadet_web/controllers/notifications_controller_test.exs index e4fd10794..813690eef 100644 --- a/test/cadet_web/controllers/notifications_controller_test.exs +++ b/test/cadet_web/controllers/notifications_controller_test.exs @@ -10,25 +10,27 @@ defmodule CadetWeb.NotificationsControllerTest do end setup do - assessment = insert(:assessment, %{is_published: true}) - avenger = insert(:user, %{role: :staff}) - student = insert(:user, %{role: :student}) + course = insert(:course) + assessment = insert(:assessment, %{is_published: true, course: course}) + avenger = insert(:course_registration, %{role: :staff, course: course}) + student = insert(:course_registration, %{role: :student, course: course}) submission = insert(:submission, %{student: student, assessment: assessment}) notifications = insert_list(3, :notification, %{ read: false, assessment_id: assessment.id, - user_id: student.id + course_reg_id: student.id }) ++ insert_list(3, :notification, %{ read: true, assessment_id: assessment.id, - user_id: student.id + course_reg_id: student.id }) {:ok, %{ + course: course, assessment: assessment, avenger: avenger, student: student, @@ -38,16 +40,16 @@ defmodule CadetWeb.NotificationsControllerTest do end describe "GET /, unauthenticated" do - test "/notifications", %{conn: conn} do - conn = get(conn, build_url()) + test "/notifications", %{course: course, conn: conn} do + conn = get(conn, build_url(course.id)) assert response(conn, 401) =~ "Unauthorised" end end describe "POST /, unaunthenticated" do - test "/notifications/acknowledge", %{conn: conn} do + test "/notifications/acknowledge", %{course: course,conn: conn} do conn = - post(conn, build_acknowledge_url(), %{ + post(conn, build_acknowledge_url(course.id), %{ "notificationIds" => [1] }) @@ -58,6 +60,7 @@ defmodule CadetWeb.NotificationsControllerTest do describe "GET /notifications" do test "student fetches unread notifications", %{ conn: conn, + course: course, student: student, assessment: assessment, notifications: notifications @@ -73,7 +76,7 @@ defmodule CadetWeb.NotificationsControllerTest do "submission_id" => nil, "type" => Atom.to_string(&1.type), "assessment" => %{ - "type" => assessment.type, + "type" => assessment.config.type, "title" => assessment.title } } @@ -81,8 +84,8 @@ defmodule CadetWeb.NotificationsControllerTest do results = conn - |> sign_in(student) - |> get(build_url()) + |> sign_in(student.user) + |> get(build_url(course.id)) |> json_response(200) |> Enum.sort(&(&1["id"] < &2["id"])) @@ -91,6 +94,7 @@ defmodule CadetWeb.NotificationsControllerTest do test "avenger fetches unread notifications", %{ conn: conn, + course: course, avenger: avenger, assessment: assessment, submission: submission @@ -101,7 +105,7 @@ defmodule CadetWeb.NotificationsControllerTest do read: false, assessment_id: assessment.id, submission_id: submission.id, - user_id: avenger.id + course_reg_id: avenger.id }) expected = @@ -115,7 +119,7 @@ defmodule CadetWeb.NotificationsControllerTest do "submission_id" => &1.submission_id, "type" => Atom.to_string(&1.type), "assessment" => %{ - "type" => assessment.type, + "type" => assessment.config.type, "title" => assessment.title } } @@ -123,8 +127,8 @@ defmodule CadetWeb.NotificationsControllerTest do results = conn - |> sign_in(avenger) - |> get(build_url()) + |> sign_in(avenger.user) + |> get(build_url(course.id)) |> json_response(200) |> Enum.sort(&(&1["id"] < &2["id"])) @@ -135,13 +139,14 @@ defmodule CadetWeb.NotificationsControllerTest do describe "POST /notifications/acknowledge" do test "student acknowledges own notification", %{ conn: conn, + course: course, student: student, notifications: notifications } do conn = conn - |> sign_in(student) - |> post(build_acknowledge_url(), %{ + |> sign_in(student.user) + |> post(build_acknowledge_url(course.id), %{ "notificationIds" => [Enum.random(notifications).id] }) @@ -150,13 +155,14 @@ defmodule CadetWeb.NotificationsControllerTest do test "other user not allowed to acknowledge notification that is not theirs", %{ conn: conn, + course: course, avenger: avenger, notifications: notifications } do conn = conn - |> sign_in(avenger) - |> post(build_acknowledge_url(), %{ + |> sign_in(avenger.user) + |> post(build_acknowledge_url(course.id), %{ "notificationIds" => [Enum.random(notifications).id] }) @@ -164,6 +170,6 @@ defmodule CadetWeb.NotificationsControllerTest do end end - defp build_url, do: "/v2/notifications" - defp build_acknowledge_url, do: "/v2/notifications/acknowledge" + defp build_url(course_id), do: "/v2/courses/#{course_id}/notifications" + defp build_acknowledge_url(course_id), do: "/v2/courses/#{course_id}/notifications/acknowledge" end From 3fd8e74c26c469f094ab25155e8b6b080ef5d0c6 Mon Sep 17 00:00:00 2001 From: YaleChen299 Date: Sun, 27 Jun 2021 19:58:01 +0800 Subject: [PATCH 091/174] updated assessments context functions with test(skipping contest) --- test/cadet/assessments/assessments_test.exs | 61 +++++++++++++-------- test/cadet/assessments/query_test.exs | 12 ++-- 2 files changed, 44 insertions(+), 29 deletions(-) diff --git a/test/cadet/assessments/assessments_test.exs b/test/cadet/assessments/assessments_test.exs index 456f3e6c1..9e86dba01 100644 --- a/test/cadet/assessments/assessments_test.exs +++ b/test/cadet/assessments/assessments_test.exs @@ -5,20 +5,22 @@ defmodule Cadet.AssessmentsTest do alias Cadet.Assessments.{Assessment, Question, SubmissionVotes} test "create assessments of all types" do - for type <- Assessment.assessment_types() do - title_string = type - - {_res, assessment} = - Assessments.create_assessment(%{ - title: title_string, - type: type, - number: "#{type |> String.upcase()}#{Enum.random(0..10)}", - open_at: Timex.now(), - close_at: Timex.shift(Timex.now(), days: 7) - }) - - assert %{title: ^title_string, type: ^type} = assessment - end + course = insert(:course) + config = insert(:assessment_config, %{type: "Test", course: course}) + course_id = course.id + config_id = config.id + + {_res, assessment} = + Assessments.create_assessment(%{ + course_id: course_id, + title: "test", + config_id: config_id, + number: "#{config.type |> String.upcase()}#{Enum.random(0..10)}", + open_at: Timex.now(), + close_at: Timex.shift(Timex.now(), days: 7) + }) + + assert %{title: "test", config_id: ^config_id, course_id: ^course_id} = assessment end test "create programming question" do @@ -108,14 +110,18 @@ defmodule Cadet.AssessmentsTest do end test "publish assessment" do - assessment = insert(:assessment, is_published: false) + course = insert(:course) + config = insert(:assessment_config, %{course: course}) + assessment = insert(:assessment, %{is_published: false, course: course, config: config}) {:ok, assessment} = Assessments.publish_assessment(assessment.id) assert assessment.is_published == true end test "update assessment" do - assessment = insert(:assessment, title: "assessment") + course = insert(:course) + config = insert(:assessment_config, %{course: course}) + assessment = insert(:assessment, %{title: "assessment", course: course, config: config}) Assessments.update_assessment(assessment.id, %{title: "changed_assessment"}) @@ -138,15 +144,17 @@ defmodule Cadet.AssessmentsTest do end describe "contest voting" do + @tag :skip test "inserts votes into submission_votes table" do contest_question = insert(:programming_question) question = insert(:voting_question) - users = Enum.map(0..5, fn _x -> insert(:user, role: "student") end) + # users = Enum.map(0..5, fn _x -> insert(:user, role: "student") end) + students = insert_list(5, :course_registration, %{role: :student}) - Enum.map(users, fn user -> + Enum.map(students, fn student -> submission = insert(:submission, - student: user, + student: student, assessment: contest_question.assessment, status: "submitted" ) @@ -158,7 +166,7 @@ defmodule Cadet.AssessmentsTest do ) end) - unattempted_student = insert(:user, role: "student") + unattempted_student = insert(:course_registration, role: :student) # unattempted submission will automatically be submitted after the assessment closes. unattempted_submission = @@ -183,6 +191,7 @@ defmodule Cadet.AssessmentsTest do assert length(Repo.all(SubmissionVotes, question_id: question.id)) == 6 * 5 + 6 end + @tag :skip test "create voting parameters with invalid contest number" do question = insert(:voting_question) @@ -191,16 +200,17 @@ defmodule Cadet.AssessmentsTest do assert status == :error end + @tag :skip test "deletes submission_votes when assessment is deleted" do contest_question = insert(:programming_question) voting_assessment = insert(:assessment, type: "practical") question = insert(:voting_question, assessment: voting_assessment) - users = Enum.map(0..5, fn _x -> insert(:user, role: "student") end) + students = insert_list(5, :course_registration, %{role: :student}) - Enum.map(users, fn user -> + Enum.map(students, fn student -> submission = insert(:submission, - student: user, + student: student, assessment: contest_question.assessment, status: "submitted" ) @@ -283,6 +293,7 @@ defmodule Cadet.AssessmentsTest do %{answers: ans_list, question_id: voting_question.id, student_list: student_list} end + @tag :skip test "computes correct relative_score with lexing/penalty and fetches highest x relative_score correctly", %{answers: _answers, question_id: question_id, student_list: _student_list} do Assessments.compute_relative_score(question_id) @@ -492,6 +503,7 @@ defmodule Cadet.AssessmentsTest do } end + @tag :skip test "fetch_voting_questions_due_yesterday only fetching voting questions closed yesterday", %{ yesterday_question: yesterday_question, @@ -502,6 +514,7 @@ defmodule Cadet.AssessmentsTest do get_question_ids(Assessments.fetch_voting_questions_due_yesterday()) end + @tag :skip test "fetch_active_voting_questions only fetches active voting questions", %{ yesterday_question: _yesterday_question, @@ -512,6 +525,7 @@ defmodule Cadet.AssessmentsTest do get_question_ids(Assessments.fetch_active_voting_questions()) end + @tag :skip test "update_final_contest_leaderboards correctly updates leaderboards that voting closed yesterday", %{ @@ -535,6 +549,7 @@ defmodule Cadet.AssessmentsTest do ) == [99.0, 89.0, 79.0, 69.0, 59.0] end + @tag :skip test "update_rolling_contest_leaderboards correcly updates leaderboards which voting is active", %{ diff --git a/test/cadet/assessments/query_test.exs b/test/cadet/assessments/query_test.exs index ab314dc06..5f01b6e0e 100644 --- a/test/cadet/assessments/query_test.exs +++ b/test/cadet/assessments/query_test.exs @@ -5,27 +5,27 @@ defmodule Cadet.Assessments.QueryTest do test "all_assessments_with_max_grade" do assessment = insert(:assessment) - insert_list(5, :question, assessment: assessment, max_grade: 200) + insert_list(5, :question, assessment: assessment, max_xp: 200) result = - Query.all_assessments_with_max_grade() + Query.all_assessments_with_max_xp() |> where(id: ^assessment.id) |> Repo.one() assessment_id = assessment.id - assert %{max_grade: 1000, id: ^assessment_id} = result + assert %{max_xp: 1000, id: ^assessment_id} = result end test "assessments_max_grade" do assessment = insert(:assessment) - insert_list(5, :question, assessment: assessment, max_grade: 200) + insert_list(5, :question, assessment: assessment, max_xp: 200) result = - Query.assessments_max_grade() + Query.assessments_max_xp() |> Repo.all() |> Enum.find(&(&1.assessment_id == assessment.id)) - assert result.max_grade == 1000 + assert result.max_xp == 1000 end end From 0dda3c765c53caf5d4aade819b1b282027254381 Mon Sep 17 00:00:00 2001 From: YaleChen299 Date: Mon, 28 Jun 2021 02:34:16 +0800 Subject: [PATCH 092/174] updated answer controller with test --- lib/cadet/accounts/notifications.ex | 6 +- lib/cadet/assessments/assessments.ex | 90 +++++----- .../controllers/answer_controller.ex | 10 +- lib/cadet_web/views/notifications_view.ex | 2 +- test/cadet/accounts/notification_test.exs | 22 ++- test/cadet/assessments/answer_test.exs | 2 - .../controllers/answer_controller_test.exs | 157 ++++++++++-------- .../notifications_controller_test.exs | 2 +- test/support/conn_case.ex | 1 + 9 files changed, 166 insertions(+), 126 deletions(-) diff --git a/lib/cadet/accounts/notifications.ex b/lib/cadet/accounts/notifications.ex index f9c781100..cec15a064 100644 --- a/lib/cadet/accounts/notifications.ex +++ b/lib/cadet/accounts/notifications.ex @@ -105,7 +105,8 @@ defmodule Cadet.Accounts.Notifications do {:ok, Ecto.Schema.t()} | {:error, any} | {:error, Ecto.Multi.name(), any, %{Ecto.Multi.name() => any}} - def acknowledge(notification_ids, course_reg = %CourseRegistration{}) when is_list(notification_ids) do + def acknowledge(notification_ids, course_reg = %CourseRegistration{}) + when is_list(notification_ids) do Multi.new() |> Multi.run(:update_all, fn _repo, _ -> Enum.reduce_while(notification_ids, {:ok, nil}, fn n_id, acc -> @@ -186,7 +187,8 @@ defmodule Cadet.Accounts.Notifications do """ @spec write_notification_for_new_assessment(integer(), integer()) :: {:ok, Ecto.Schema.t()} | {:error, Ecto.Changeset.t()} - def write_notification_for_new_assessment(course_id, assessment_id) when is_ecto_id(assessment_id) and is_ecto_id(course_id) do + def write_notification_for_new_assessment(course_id, assessment_id) + when is_ecto_id(assessment_id) and is_ecto_id(course_id) do Multi.new() |> Multi.run(:insert_all, fn _repo, _ -> CourseRegistration diff --git a/lib/cadet/assessments/assessments.ex b/lib/cadet/assessments/assessments.ex index 9419d171e..a6b60733c 100644 --- a/lib/cadet/assessments/assessments.ex +++ b/lib/cadet/assessments/assessments.ex @@ -22,44 +22,44 @@ defmodule Cadet.Assessments do # submitted answers @bypass_closed_roles ~w(staff admin)a - # def delete_assessment(id) do - # assessment = Repo.get(Assessment, id) - - # Submission - # |> where(assessment_id: ^id) - # |> delete_submission_assocation(id) - - # Question - # |> where(assessment_id: ^id) - # |> Repo.all() - # |> Enum.each(fn q -> - # delete_submission_votes_association(q) - # end) - - # Repo.delete(assessment) - # end - - # defp delete_submission_votes_association(question) do - # SubmissionVotes - # |> where(question_id: ^question.id) - # |> Repo.delete_all() - # end - - # defp delete_submission_assocation(submissions, assessment_id) do - # submissions - # |> Repo.all() - # |> Enum.each(fn submission -> - # Answer - # |> where(submission_id: ^submission.id) - # |> Repo.delete_all() - # end) - - # Notification - # |> where(assessment_id: ^assessment_id) - # |> Repo.delete_all() - - # Repo.delete_all(submissions) - # end + def delete_assessment(id) do + assessment = Repo.get(Assessment, id) + + Submission + |> where(assessment_id: ^id) + |> delete_submission_assocation(id) + + Question + |> where(assessment_id: ^id) + |> Repo.all() + |> Enum.each(fn q -> + delete_submission_votes_association(q) + end) + + Repo.delete(assessment) + end + + defp delete_submission_votes_association(question) do + SubmissionVotes + |> where(question_id: ^question.id) + |> Repo.delete_all() + end + + defp delete_submission_assocation(submissions, assessment_id) do + submissions + |> Repo.all() + |> Enum.each(fn submission -> + Answer + |> where(submission_id: ^submission.id) + |> Repo.delete_all() + end) + + Notification + |> where(assessment_id: ^assessment_id) + |> Repo.delete_all() + + Repo.delete_all(submissions) + end @spec user_max_xp(%CourseRegistration{}) :: integer() def user_max_xp(%CourseRegistration{id: cr_id}) do @@ -109,7 +109,6 @@ defmodule Cadet.Assessments do end end - # :TODO to check how this story works def user_current_story(cr = %CourseRegistration{}) do {:ok, %{result: story}} = Multi.new() @@ -861,7 +860,7 @@ defmodule Cadet.Assessments do not_nil_entries = SubmissionVotes |> where(question_id: ^question.id) - |> where(user_id: ^submission.student_id) + |> where(voter_id: ^submission.student_id) |> where([sv], is_nil(sv.rank)) |> Repo.exists?() @@ -1419,12 +1418,12 @@ defmodule Cadet.Assessments do submission = %Submission{}, question = %Question{}, raw_answer, - user_id + course_reg_id ) do answer_content = build_answer_content(raw_answer, question.type) if question.type == :voting do - insert_or_update_voting_answer(submission.id, user_id, question.id, answer_content) + insert_or_update_voting_answer(submission.id, course_reg_id, question.id, answer_content) else answer_changeset = %Answer{} @@ -1443,10 +1442,11 @@ defmodule Cadet.Assessments do end end - def insert_or_update_voting_answer(submission_id, user_id, question_id, answer_content) do + # :TODO contest + check voting answer content + def insert_or_update_voting_answer(submission_id, course_reg_id, question_id, answer_content) do set_rank_to_nil = SubmissionVotes - |> where(user_id: ^user_id, question_id: ^question_id) + |> where(voter_id: ^course_reg_id, question_id: ^question_id) voting_multi = Multi.new() @@ -1459,7 +1459,7 @@ defmodule Cadet.Assessments do |> Multi.run("update#{index}", fn _repo, _ -> SubmissionVotes |> Repo.get_by( - user_id: user_id, + voter_id: course_reg_id, submission_id: entry.submission_id ) |> SubmissionVotes.changeset(%{rank: entry.rank}) diff --git a/lib/cadet_web/controllers/answer_controller.ex b/lib/cadet_web/controllers/answer_controller.ex index 3c744cd32..5a5b22cb0 100644 --- a/lib/cadet_web/controllers/answer_controller.ex +++ b/lib/cadet_web/controllers/answer_controller.ex @@ -5,20 +5,20 @@ defmodule CadetWeb.AnswerController do alias Cadet.Assessments - # These roles can save and finalise answers for closed assessments and - # submitted answers + # These roles can save and finalise answers for + # closed assessments and submitted answers @bypass_closed_roles ~w(staff admin)a def submit(conn, %{"questionid" => question_id, "answer" => answer}) when is_ecto_id(question_id) do - user = conn.assigns[:current_user] - can_bypass? = user.role in @bypass_closed_roles + course_reg = conn.assigns[:course_reg] + can_bypass? = course_reg.role in @bypass_closed_roles with {:question, question} when not is_nil(question) <- {:question, Assessments.get_question(question_id)}, {:is_open?, true} <- {:is_open?, can_bypass? or Assessments.is_open?(question.assessment)}, - {:ok, _nil} <- Assessments.answer_question(question, user, answer, can_bypass?) do + {:ok, _nil} <- Assessments.answer_question(question, course_reg, answer, can_bypass?) do text(conn, "OK") else {:question, nil} -> diff --git a/lib/cadet_web/views/notifications_view.ex b/lib/cadet_web/views/notifications_view.ex index 88e7b578a..64c4a8cf0 100644 --- a/lib/cadet_web/views/notifications_view.ex +++ b/lib/cadet_web/views/notifications_view.ex @@ -17,7 +17,7 @@ defmodule CadetWeb.NotificationsView do defp render_notification_assessment(notification) do transform_map_for_view(notification.assessment, %{ - type: &(&1.config.type), + type: & &1.config.type, title: :title }) end diff --git a/test/cadet/accounts/notification_test.exs b/test/cadet/accounts/notification_test.exs index 7442c23af..689e440cf 100644 --- a/test/cadet/accounts/notification_test.exs +++ b/test/cadet/accounts/notification_test.exs @@ -79,11 +79,13 @@ defmodule Cadet.Accounts.NotificationTest do course_reg_id: student.id }) - expected = notifications |> Enum.sort(&(&1.id < &2.id)) |> Enum.map(& Map.delete(&1, :assessment)) + expected = + notifications |> Enum.sort(&(&1.id < &2.id)) |> Enum.map(&Map.delete(&1, :assessment)) {:ok, notifications_db} = Notifications.fetch(student) - results = notifications_db |> Enum.sort(&(&1.id < &2.id)) |> Enum.map(& Map.delete(&1, :assessment)) + results = + notifications_db |> Enum.sort(&(&1.id < &2.id)) |> Enum.map(&Map.delete(&1, :assessment)) assert results == expected end @@ -300,7 +302,11 @@ defmodule Cadet.Accounts.NotificationTest do Notifications.write_notification_when_graded(submission.id, :graded) notification = - Repo.get_by(Notification, course_reg_id: student.id, type: :graded, assessment_id: assessment.id) + Repo.get_by(Notification, + course_reg_id: student.id, + type: :graded, + assessment_id: assessment.id + ) assert %{type: :graded} = notification end @@ -309,13 +315,19 @@ defmodule Cadet.Accounts.NotificationTest do assessment: assessment, student: student } do - students = [student | insert_list(3, :course_registration, %{course: student.course, role: :student})] + students = [ + student | insert_list(3, :course_registration, %{course: student.course, role: :student}) + ] Notifications.write_notification_for_new_assessment(student.course_id, assessment.id) for student <- students do notification = - Repo.get_by(Notification, course_reg_id: student.id, type: :new, assessment_id: assessment.id) + Repo.get_by(Notification, + course_reg_id: student.id, + type: :new, + assessment_id: assessment.id + ) assert %{type: :new} = notification end diff --git a/test/cadet/assessments/answer_test.exs b/test/cadet/assessments/answer_test.exs index b091ef449..746bb0f3a 100644 --- a/test/cadet/assessments/answer_test.exs +++ b/test/cadet/assessments/answer_test.exs @@ -126,6 +126,4 @@ defmodule Cadet.Assessments.AnswerTest do assert_changeset(%{}, :invalid, :grading_changeset) end end - - end diff --git a/test/cadet_web/controllers/answer_controller_test.exs b/test/cadet_web/controllers/answer_controller_test.exs index f541f0650..4bd8c8162 100644 --- a/test/cadet_web/controllers/answer_controller_test.exs +++ b/test/cadet_web/controllers/answer_controller_test.exs @@ -13,7 +13,9 @@ defmodule CadetWeb.AnswerControllerTest do end setup do - assessment = insert(:assessment, %{is_published: true}) + course = insert(:course) + config = insert(:assessment_config, %{course: course}) + assessment = insert(:assessment, %{is_published: true, course: course, config: config}) mcq_question = insert(:mcq_question, %{assessment: assessment}) programming_question = insert(:programming_question, %{assessment: assessment}) voting_question = insert(:voting_question, %{assessment: assessment}) @@ -28,7 +30,8 @@ defmodule CadetWeb.AnswerControllerTest do describe "POST /assessments/question/{questionId}/answer/, Unauthenticated" do test "is disallowed", %{conn: conn, mcq_question: question} do - conn = post(conn, build_url(question.id), %{answer: 5}) + course = insert(:course) + conn = post(conn, build_url(course.id, question.id), %{answer: 5}) assert response(conn, 401) =~ "Unauthorised" end @@ -44,34 +47,35 @@ defmodule CadetWeb.AnswerControllerTest do programming_question: programming_question, voting_question: voting_question } do - user = conn.assigns.current_user - mcq_conn = post(conn, build_url(mcq_question.id), %{answer: 5}) + course_reg = conn.assigns.test_cr + course_id = conn.assigns.course_id + mcq_conn = post(conn, build_url(course_id, mcq_question.id), %{answer: 5}) assert response(mcq_conn, 200) =~ "OK" - assert get_answer_value(mcq_question, assessment, user) == 5 + assert get_answer_value(mcq_question, assessment, course_reg) == 5 programming_conn = - post(conn, build_url(programming_question.id), %{answer: "hello world"}) + post(conn, build_url(course_id, programming_question.id), %{answer: "hello world"}) assert response(programming_conn, 200) =~ "OK" - assert get_answer_value(programming_question, assessment, user) == "hello world" + assert get_answer_value(programming_question, assessment, course_reg) == "hello world" contest_submission = insert(:submission) Repo.insert(%SubmissionVotes{ - user_id: user.id, + voter_id: course_reg.id, question_id: voting_question.id, submission_id: contest_submission.id }) voting_conn = - post(conn, build_url(voting_question.id), %{ + post(conn, build_url(course_id, voting_question.id), %{ answer: [ %{"answer" => "hello world", "submission_id" => contest_submission.id, "rank" => 3} ] }) assert response(voting_conn, 200) =~ "OK" - rank = get_rank_for_submission_vote(voting_question, user, contest_submission) + rank = get_rank_for_submission_vote(voting_question, course_reg, contest_submission) assert rank == 3 end @@ -83,37 +87,38 @@ defmodule CadetWeb.AnswerControllerTest do programming_question: programming_question, voting_question: voting_question } do - user = conn.assigns.current_user - mcq_conn = post(conn, build_url(mcq_question.id), %{answer: 5}) + course_reg = conn.assigns.test_cr + course_id = conn.assigns.course_id + mcq_conn = post(conn, build_url(course_id, mcq_question.id), %{answer: 5}) assert response(mcq_conn, 200) =~ "OK" - assert get_answer_value(mcq_question, assessment, user) == 5 + assert get_answer_value(mcq_question, assessment, course_reg) == 5 - updated_mcq_conn = post(conn, build_url(mcq_question.id), %{answer: 6}) + updated_mcq_conn = post(conn, build_url(course_id, mcq_question.id), %{answer: 6}) assert response(updated_mcq_conn, 200) =~ "OK" - assert get_answer_value(mcq_question, assessment, user) == 6 + assert get_answer_value(mcq_question, assessment, course_reg) == 6 programming_conn = - post(conn, build_url(programming_question.id), %{answer: "hello world"}) + post(conn, build_url(course_id, programming_question.id), %{answer: "hello world"}) assert response(programming_conn, 200) =~ "OK" - assert get_answer_value(programming_question, assessment, user) == "hello world" + assert get_answer_value(programming_question, assessment, course_reg) == "hello world" updated_programming_conn = - post(conn, build_url(programming_question.id), %{answer: "hello_world"}) + post(conn, build_url(course_id, programming_question.id), %{answer: "hello_world"}) assert response(updated_programming_conn, 200) =~ "OK" - assert get_answer_value(programming_question, assessment, user) == "hello_world" + assert get_answer_value(programming_question, assessment, course_reg) == "hello_world" contest_submission = insert(:submission) Repo.insert(%SubmissionVotes{ - user_id: user.id, + voter_id: course_reg.id, question_id: voting_question.id, submission_id: contest_submission.id }) voting_conn = - post(conn, build_url(voting_question.id), %{ + post(conn, build_url(course_id, voting_question.id), %{ answer: [ %{"answer" => "hello world", "submission_id" => contest_submission.id, "rank" => 3} ] @@ -122,7 +127,7 @@ defmodule CadetWeb.AnswerControllerTest do assert response(voting_conn, 200) =~ "OK" updated_voting_conn = - post(conn, build_url(voting_question.id), %{ + post(conn, build_url(course_id, voting_question.id), %{ answer: [ %{"answer" => "hello world", "submission_id" => contest_submission.id, "rank" => 5} ] @@ -130,7 +135,7 @@ defmodule CadetWeb.AnswerControllerTest do assert response(updated_voting_conn, 200) =~ "OK" - rank = get_rank_for_submission_vote(voting_question, user, contest_submission) + rank = get_rank_for_submission_vote(voting_question, course_reg, contest_submission) assert rank == 5 end @@ -142,18 +147,19 @@ defmodule CadetWeb.AnswerControllerTest do programming_question: programming_question, voting_question: voting_question } do - user = conn.assigns.current_user - post(conn, build_url(mcq_question.id), %{answer: 5}) - post(conn, build_url(programming_question.id), %{answer: "hello world"}) + course_reg = conn.assigns.test_cr + course_id = conn.assigns.course_id + post(conn, build_url(course_id, mcq_question.id), %{answer: 5}) + post(conn, build_url(course_id, programming_question.id), %{answer: "hello world"}) contest_submission = insert(:submission) Repo.insert(%SubmissionVotes{ - user_id: user.id, + voter_id: course_reg.id, question_id: voting_question.id, submission_id: contest_submission.id }) - post(conn, build_url(voting_question.id), %{ + post(conn, build_url(course_id, voting_question.id), %{ answer: [ %{"answer" => "hello world", "submission_id" => contest_submission.id, "rank" => 3} ] @@ -163,14 +169,14 @@ defmodule CadetWeb.AnswerControllerTest do submission = Submission - |> where(student_id: ^user.id) + |> where(student_id: ^course_reg.id) |> where(assessment_id: ^assessment.id) |> Repo.one!() assert submission.status == :attempted # should not affect submission changes - conn = post(conn, build_url(mcq_question.id), %{answer: 5}) + conn = post(conn, build_url(course_id, mcq_question.id), %{answer: 5}) assert response(conn, 200) =~ "OK" end @@ -181,10 +187,11 @@ defmodule CadetWeb.AnswerControllerTest do assessment: assessment, mcq_question: mcq_question } do - user = conn.assigns.current_user + course_reg = conn.assigns.test_cr + course_id = conn.assigns.course_id - insert(:submission, %{assessment: assessment, student: user, status: :submitted}) - conn = post(conn, build_url(mcq_question.id), %{answer: 5}) + insert(:submission, %{assessment: assessment, student: course_reg, status: :submitted}) + conn = post(conn, build_url(course_id, mcq_question.id), %{answer: 5}) assert response(conn, 403) == "Assessment submission already finalised" end @@ -195,10 +202,11 @@ defmodule CadetWeb.AnswerControllerTest do assessment: assessment, mcq_question: mcq_question } do - user = conn.assigns.current_user + course_reg = conn.assigns.test_cr + course_id = conn.assigns.course_id - insert(:submission, %{assessment: assessment, student: user, status: :submitted}) - conn = post(conn, build_url(mcq_question.id), %{answer: 5}) + insert(:submission, %{assessment: assessment, student: course_reg, status: :submitted}) + conn = post(conn, build_url(course_id, mcq_question.id), %{answer: 5}) assert response(conn, 200) == "OK" end @@ -212,29 +220,35 @@ defmodule CadetWeb.AnswerControllerTest do programming_question: programming_question, voting_question: voting_question } do - user = conn.assigns.current_user - missing_answer_conn = post(conn, build_url(programming_question.id), %{answ: 5}) + course_reg = conn.assigns.test_cr + course_id = conn.assigns.course_id + + missing_answer_conn = + post(conn, build_url(course_id, programming_question.id), %{answ: 5}) + assert response(missing_answer_conn, 400) == "Missing or invalid parameter(s)" - assert is_nil(get_answer_value(mcq_question, assessment, user)) + assert is_nil(get_answer_value(mcq_question, assessment, course_reg)) - mcq_conn = post(conn, build_url(programming_question.id), %{answer: 5}) + mcq_conn = post(conn, build_url(course_id, programming_question.id), %{answer: 5}) assert response(mcq_conn, 400) == "Missing or invalid parameter(s)" - assert is_nil(get_answer_value(mcq_question, assessment, user)) + assert is_nil(get_answer_value(mcq_question, assessment, course_reg)) + + programming_conn = + post(conn, build_url(course_id, mcq_question.id), %{answer: "hello world"}) - programming_conn = post(conn, build_url(mcq_question.id), %{answer: "hello world"}) assert response(programming_conn, 400) == "Missing or invalid parameter(s)" - assert is_nil(get_answer_value(programming_question, assessment, user)) + assert is_nil(get_answer_value(programming_question, assessment, course_reg)) contest_submission = insert(:submission) Repo.insert(%SubmissionVotes{ - user_id: user.id, + voter_id: course_reg.id, question_id: voting_question.id, submission_id: contest_submission.id }) voting_conn = - post(conn, build_url(voting_question.id), %{ + post(conn, build_url(course_id, voting_question.id), %{ answer: [ %{ "answer" => "hello world", @@ -253,27 +267,28 @@ defmodule CadetWeb.AnswerControllerTest do conn: conn, voting_question: voting_question } do - user = conn.assigns.current_user + course_reg = conn.assigns.test_cr + course_id = conn.assigns.course_id first_contest_submission = insert(:submission) second_contest_submission = insert(:submission) Repo.insert(%SubmissionVotes{ - user_id: user.id, + voter_id: course_reg.id, question_id: voting_question.id, submission_id: first_contest_submission.id, rank: 1 }) Repo.insert(%SubmissionVotes{ - user_id: user.id, + voter_id: course_reg.id, question_id: voting_question.id, submission_id: second_contest_submission.id, rank: 2 }) voting_conn = - post(conn, build_url(voting_question.id), %{ + post(conn, build_url(course_id, voting_question.id), %{ answer: [ %{ "answer" => "hello world", @@ -300,17 +315,19 @@ defmodule CadetWeb.AnswerControllerTest do assessment: assessment, mcq_question: mcq_question } do - user = conn.assigns.current_user + course_reg = conn.assigns.test_cr + course_id = conn.assigns.course_id {:ok, _} = Repo.delete(mcq_question) - conn = post(conn, build_url(mcq_question.id), %{answer: 5}) + conn = post(conn, build_url(course_id, mcq_question.id), %{answer: 5}) assert response(conn, 404) == "Question not found" - assert is_nil(get_answer_value(mcq_question, assessment, user)) + assert is_nil(get_answer_value(mcq_question, assessment, course_reg)) end @tag authenticate: :student test "invalid params not open submission is unsuccessful", %{conn: conn} do - user = conn.assigns.current_user + course_reg = conn.assigns.test_cr + course_id = conn.assigns.course_id before_open_at_assessment = insert(:assessment, %{ @@ -320,9 +337,14 @@ defmodule CadetWeb.AnswerControllerTest do before_open_at_question = insert(:mcq_question, %{assessment: before_open_at_assessment}) - before_open_at_conn = post(conn, build_url(before_open_at_question.id), %{answer: 5}) + before_open_at_conn = + post(conn, build_url(course_id, before_open_at_question.id), %{answer: 5}) + assert response(before_open_at_conn, 403) == "Assessment not open" - assert is_nil(get_answer_value(before_open_at_question, before_open_at_assessment, user)) + + assert is_nil( + get_answer_value(before_open_at_question, before_open_at_assessment, course_reg) + ) after_close_at_assessment = insert(:assessment, %{ @@ -332,29 +354,34 @@ defmodule CadetWeb.AnswerControllerTest do after_close_at_question = insert(:mcq_question, %{assessment: after_close_at_assessment}) - after_close_at_conn = post(conn, build_url(after_close_at_question.id), %{answer: 5}) + after_close_at_conn = + post(conn, build_url(course_id, after_close_at_question.id), %{answer: 5}) + assert response(after_close_at_conn, 403) == "Assessment not open" - assert is_nil(get_answer_value(after_close_at_question, after_close_at_assessment, user)) + + assert is_nil( + get_answer_value(after_close_at_question, after_close_at_assessment, course_reg) + ) unpublished_assessment = insert(:assessment, %{is_published: false}) unpublished_question = insert(:mcq_question, %{assessment: unpublished_assessment}) - unpublished_conn = post(conn, build_url(unpublished_question.id), %{answer: 5}) + unpublished_conn = post(conn, build_url(course_id, unpublished_question.id), %{answer: 5}) assert response(unpublished_conn, 403) == "Assessment not open" - assert is_nil(get_answer_value(unpublished_question, unpublished_assessment, user)) + assert is_nil(get_answer_value(unpublished_question, unpublished_assessment, course_reg)) end - defp build_url(question_id) do - "/v2/assessments/question/#{question_id}/answer/" + defp build_url(course_id, question_id) do + "/v2/courses/#{course_id}/assessments/question/#{question_id}/answer/" end - defp get_answer_value(question, assessment, user) do + defp get_answer_value(question, assessment, course_reg) do answer = Answer |> where(question_id: ^question.id) |> join(:inner, [a], s in assoc(a, :submission)) - |> where([a, s], s.student_id == ^user.id) + |> where([a, s], s.student_id == ^course_reg.id) |> where([a, s], s.assessment_id == ^assessment.id) |> Repo.one() @@ -366,10 +393,10 @@ defmodule CadetWeb.AnswerControllerTest do end end - defp get_rank_for_submission_vote(question, user, submission) do + defp get_rank_for_submission_vote(question, course_reg, submission) do SubmissionVotes |> where(question_id: ^question.id) - |> where(user_id: ^user.id) + |> where(voter_id: ^course_reg.id) |> where(submission_id: ^submission.id) |> select([sv], sv.rank) |> Repo.one() diff --git a/test/cadet_web/controllers/notifications_controller_test.exs b/test/cadet_web/controllers/notifications_controller_test.exs index 813690eef..b55665d7d 100644 --- a/test/cadet_web/controllers/notifications_controller_test.exs +++ b/test/cadet_web/controllers/notifications_controller_test.exs @@ -47,7 +47,7 @@ defmodule CadetWeb.NotificationsControllerTest do end describe "POST /, unaunthenticated" do - test "/notifications/acknowledge", %{course: course,conn: conn} do + test "/notifications/acknowledge", %{course: course, conn: conn} do conn = post(conn, build_acknowledge_url(course.id), %{ "notificationIds" => [1] diff --git a/test/support/conn_case.ex b/test/support/conn_case.ex index cd512433b..dda40a8f1 100644 --- a/test/support/conn_case.ex +++ b/test/support/conn_case.ex @@ -78,6 +78,7 @@ defmodule CadetWeb.ConnCase do conn |> sign_in(course_registration.user) |> assign(:course_id, course_registration.course_id) + |> assign(:test_cr, course_registration) {:ok, conn: conn} else From bbbd9097b07020bb3bf72e1cff56f1df29be0f36 Mon Sep 17 00:00:00 2001 From: YaleChen299 Date: Mon, 28 Jun 2021 17:52:57 +0800 Subject: [PATCH 093/174] added delete assessment_config route with test --- .../admin_courses_controller.ex | 23 ++++++ lib/cadet_web/router.ex | 1 + .../admin_courses_controller_test.exs | 82 +++++++++++++++++++ 3 files changed, 106 insertions(+) diff --git a/lib/cadet_web/admin_controllers/admin_courses_controller.ex b/lib/cadet_web/admin_controllers/admin_courses_controller.ex index 0bc810668..9b39e0e65 100644 --- a/lib/cadet_web/admin_controllers/admin_courses_controller.ex +++ b/lib/cadet_web/admin_controllers/admin_courses_controller.ex @@ -60,6 +60,29 @@ defmodule CadetWeb.AdminCoursesController do send_resp(conn, :bad_request, "Missing List parameter(s)") end + def delete_assessment_config(conn, %{ + "course_id" => course_id, + "assessmentConfig" => assessment_config + }) + when is_ecto_id(course_id) and is_map(assessment_config) do + config = assessment_config |> to_snake_case_atom_keys() + + case Courses.delete_assessment_config(course_id, config) do + {:ok, _} -> + text(conn, "OK") + + {:error, message} -> + conn + |> put_status(:bad_request) + |> text(message) + end + + end + + def delete_assessment_config(conn, _) do + send_resp(conn, :bad_request, "Missing Map parameter(s)") + end + swagger_path :update_course_config do put("/v2/courses/{course_id}/admin/onfig") diff --git a/lib/cadet_web/router.ex b/lib/cadet_web/router.ex index 4897888a9..9774d5b88 100644 --- a/lib/cadet_web/router.ex +++ b/lib/cadet_web/router.ex @@ -146,6 +146,7 @@ defmodule CadetWeb.Router do put("/config", AdminCoursesController, :update_course_config) get("/config/assessment_configs", AdminCoursesController, :get_assessment_configs) put("/config/assessment_configs", AdminCoursesController, :update_assessment_configs) + delete("/config/assessment_config", AdminCoursesController, :delete_assessment_config) end # Other scopes may use custom stacks. diff --git a/test/cadet_web/admin_controllers/admin_courses_controller_test.exs b/test/cadet_web/admin_controllers/admin_courses_controller_test.exs index d7e940c62..b4d8e5ab0 100644 --- a/test/cadet_web/admin_controllers/admin_courses_controller_test.exs +++ b/test/cadet_web/admin_controllers/admin_courses_controller_test.exs @@ -293,11 +293,93 @@ defmodule CadetWeb.AdminCoursesControllerTest do end end + describe "DELETE /v2/courses/{course_id}/admin/config/assessment_config" do + @tag authenticate: :admin + test "succeeds", %{conn: conn} do + course_id = conn.assigns[:course_id] + course = Repo.get(Course, course_id) + config1 = insert(:assessment_config, %{order: 1, course: course, type: "Missions"}) + config2 = insert(:assessment_config, %{order: 2, course: course, type: "Paths"}) + + old_configs = course_id |> Courses.get_assessment_configs() |> Enum.map(& &1.type) + + params = %{ + "assessmentConfig" => %{ + "AssessmentConfigId" => config1.id, + "courseId" => course_id, + "type" => "Missions", + "earlySubmissionXp" => 100, + "hoursBeforeEarlyXpDecay" => 24, + "decayRatePointsPerHour" => 1 + } + } + + resp = + conn + |> delete(build_url_assessment_config(course_id), params) + |> response(200) + + assert resp == "OK" + + new_configs = course_id |> Courses.get_assessment_configs() |> Enum.map(& &1.type) + refute old_configs == new_configs + assert new_configs == ["Paths"] + end + + @tag authenticate: :student + test "rejects forbidden request for non-staff users", %{conn: conn} do + course_id = conn.assigns[:course_id] + + conn = + delete(conn, build_url_assessment_config(course_id), %{ + "assessmentConfig" => %{} + }) + + assert response(conn, 403) == "Forbidden" + end + + @tag authenticate: :staff + test "rejects request if user is not in specified course", %{conn: conn} do + course_id = conn.assigns[:course_id] + + conn = + delete(conn, build_url_assessment_config(course_id + 1), %{ + "assessmentConfig" => %{} + }) + + assert response(conn, 403) == "Forbidden" + end + + @tag authenticate: :staff + test "rejects requests with invalid params 1", %{conn: conn} do + course_id = conn.assigns[:course_id] + + conn = + delete(conn, build_url_assessment_config(course_id), %{ + "assessmentConfigs" => "Missions" + }) + + assert response(conn, 400) == "Missing Map parameter(s)" + end + + @tag authenticate: :staff + test "rejects requests with missing params", %{conn: conn} do + course_id = conn.assigns[:course_id] + + conn = delete(conn, build_url_assessment_config(course_id), %{}) + + assert response(conn, 400) == "Missing Map parameter(s)" + end + end + defp build_url_course_config(course_id), do: "/v2/courses/#{course_id}/admin/config" defp build_url_assessment_configs(course_id), do: "/v2/courses/#{course_id}/admin/config/assessment_configs" + defp build_url_assessment_config(course_id), + do: "/v2/courses/#{course_id}/admin/config/assessment_config" + defp to_map(schema), do: schema |> Map.from_struct() |> Map.drop([:updated_at]) defp update_map(map1, params), From 434a3d7a1b4cf82dcbcad89a8d86813975b9aa1d Mon Sep 17 00:00:00 2001 From: chownces <53928333+chownces@users.noreply.github.com> Date: Tue, 29 Jun 2021 16:30:31 +0800 Subject: [PATCH 094/174] Updated provider tests --- config/dev.secrets.exs.example | 8 +- lib/cadet/auth/providers/github.ex | 24 +++-- test/cadet/auth/guardian_test.exs | 5 +- test/cadet/auth/provider_test.exs | 1 - test/cadet/auth/providers/config_test.exs | 3 +- test/cadet/auth/providers/github_test.exs | 105 ++++++++++++++++++++++ test/cadet/auth/providers/openid_test.exs | 3 +- test/test_helper.exs | 16 ++++ 8 files changed, 150 insertions(+), 15 deletions(-) create mode 100644 test/cadet/auth/providers/github_test.exs diff --git a/config/dev.secrets.exs.example b/config/dev.secrets.exs.example index f27245f54..2c59d3780 100644 --- a/config/dev.secrets.exs.example +++ b/config/dev.secrets.exs.example @@ -29,9 +29,13 @@ config :cadet, # # To use authentication with GitHub # "github" => # {Cadet.Auth.Providers.GitHub, - # # A map of GitHub client_id => client_secret # %{ - # "client_id": "client_secret" + # # A map of GitHub client_id => client_secret + # clients: %{ + # "client_id" => "client_secret" + # }, + # token_url: "https://github.com/login/oauth/access_token", + # user_api: "https://api.github.com/user" # }}, "test" => {Cadet.Auth.Providers.Config, diff --git a/lib/cadet/auth/providers/github.ex b/lib/cadet/auth/providers/github.ex index a15353f10..1a1fbb341 100644 --- a/lib/cadet/auth/providers/github.ex +++ b/lib/cadet/auth/providers/github.ex @@ -6,10 +6,11 @@ defmodule Cadet.Auth.Providers.GitHub do @behaviour Provider - @type config :: %{client_secret: String.t()} - - @token_url "https://github.com/login/oauth/access_token" - @user_api "https://api.github.com/user" + @type config :: %{ + clients: %{}, + token_url: String.t(), + user_api: String.t() + } @spec authorise(config(), Provider.code(), Provider.client_id(), Provider.redirect_uri()) :: {:ok, %{token: Provider.token(), username: String.t()}} @@ -20,8 +21,11 @@ defmodule Cadet.Auth.Providers.GitHub do {"Accept", "application/json"} ] + token_url = config.token_url + user_api = config.user_api + with {:validate_client, {:ok, client_secret}} <- - {:validate_client, Map.fetch(config, client_id)}, + {:validate_client, Map.fetch(config.clients, client_id)}, {:token_query, token_query} <- {:token_query, URI.encode_query(%{ @@ -31,9 +35,9 @@ defmodule Cadet.Auth.Providers.GitHub do redirect_uri: redirect_uri })}, {:token, {:ok, %{body: body, status_code: 200}}} <- - {:token, HTTPoison.post(@token_url, token_query, token_headers)}, + {:token, HTTPoison.post(token_url, token_query, token_headers)}, {:token_response, %{"access_token" => token}} <- {:token_response, Jason.decode!(body)}, - {:user, {:ok, %{"login" => username}}} <- {:user, api_call(@user_api, token)} do + {:user, {:ok, %{"login" => username}}} <- {:user, api_call(user_api, token)} do {:ok, %{token: token, username: Provider.namespace(username, "github")}} else {:validate_client, :error} -> @@ -52,8 +56,10 @@ defmodule Cadet.Auth.Providers.GitHub do @spec get_name(config(), Provider.token()) :: {:ok, String.t()} | {:error, Provider.error(), String.t()} - def get_name(_, token) do - case api_call(@user_api, token) do + def get_name(config, token) do + user_api = config.user_api + + case api_call(user_api, token) do {:ok, %{"name" => name}} -> {:ok, name} diff --git a/test/cadet/auth/guardian_test.exs b/test/cadet/auth/guardian_test.exs index 90211f252..e445a96e2 100644 --- a/test/cadet/auth/guardian_test.exs +++ b/test/cadet/auth/guardian_test.exs @@ -1,6 +1,7 @@ defmodule Cadet.Auth.GuardianTest do use Cadet.DataCase + import Cadet.TestHelper alias Cadet.Auth.Guardian test "token subject is user id" do @@ -19,7 +20,9 @@ defmodule Cadet.Auth.GuardianTest do "sub" => "2000" } - assert Guardian.resource_from_claims(good_claims) == {:ok, user} + assert Guardian.resource_from_claims(good_claims) == + {:ok, remove_preload(user, :latest_viewed)} + assert Guardian.resource_from_claims(bad_claims) == {:error, :not_found} end end diff --git a/test/cadet/auth/provider_test.exs b/test/cadet/auth/provider_test.exs index 7836323b7..3d59ae01e 100644 --- a/test/cadet/auth/provider_test.exs +++ b/test/cadet/auth/provider_test.exs @@ -10,7 +10,6 @@ defmodule Cadet.Auth.ProviderTest do test "with valid provider" do assert {:ok, _} = Provider.authorise("test", "student_code", nil, nil) assert {:ok, _} = Provider.get_name("test", "student_token") - assert {:ok, _} = Provider.get_role("test", "student_token") end test "with invalid provider" do diff --git a/test/cadet/auth/providers/config_test.exs b/test/cadet/auth/providers/config_test.exs index b2ad33aef..c75ffa3a4 100644 --- a/test/cadet/auth/providers/config_test.exs +++ b/test/cadet/auth/providers/config_test.exs @@ -7,6 +7,7 @@ defmodule Cadet.Auth.Providers.ConfigTest do @token "token" @name "Test Name" @username "testusername" + @namespaced_username "test/testusername" @role :student @config [ @@ -21,7 +22,7 @@ defmodule Cadet.Auth.Providers.ConfigTest do describe "authorise" do test "successfully" do - assert {:ok, %{token: @token, username: @username}} = + assert {:ok, %{token: @token, username: @namespaced_username}} = Config.authorise(@config, @code, nil, nil) end diff --git a/test/cadet/auth/providers/github_test.exs b/test/cadet/auth/providers/github_test.exs new file mode 100644 index 000000000..582a183dd --- /dev/null +++ b/test/cadet/auth/providers/github_test.exs @@ -0,0 +1,105 @@ +defmodule Cadet.Auth.Providers.GitHubTest do + use ExUnit.Case, async: false + + alias Cadet.Auth.Providers.GitHub + alias Plug.Conn, as: PlugConn + + @username "username" + @namespaced_username "github/username" + @name "name" + + @dummy_access_token "dummy_access_token" + + setup_all do + Application.ensure_all_started(:bypass) + bypass = Bypass.open() + + {:ok, bypass: bypass} + end + + defp config(bypass) do + %{ + clients: %{"dummy_client_id" => "dummy_client_secret"}, + token_url: "http://localhost:#{bypass.port}/login/oauth/access_token", + user_api: "http://localhost:#{bypass.port}/user" + } + end + + defp bypass_return_token(bypass) do + Bypass.stub(bypass, "POST", "login/oauth/access_token", fn conn -> + conn + |> PlugConn.put_resp_header("content-type", "application/json") + |> PlugConn.resp(200, ~s({"access_token":"#{@dummy_access_token}"})) + end) + end + + defp bypass_api_call(bypass) do + Bypass.stub(bypass, "GET", "user", fn conn -> + conn + |> PlugConn.put_resp_header("content-type", "application/json") + |> PlugConn.resp(200, ~s({"login":"#{@username}","name":"#{@name}"})) + end) + end + + test "successful", %{bypass: bypass} do + bypass_return_token(bypass) + bypass_api_call(bypass) + + assert {:ok, %{token: @dummy_access_token, username: @namespaced_username}} == + GitHub.authorise(config(bypass), "", "dummy_client_id", "") + end + + test "invalid github client id", %{bypass: bypass} do + bypass_return_token(bypass) + bypass_api_call(bypass) + + assert {:error, :invalid_credentials, "Invalid client id"} == + GitHub.authorise(config(bypass), "", "invalid_client_id", "") + end + + test "non-successful HTTP status (access token)", %{bypass: bypass} do + Bypass.stub(bypass, "POST", "login/oauth/access_token", fn conn -> + PlugConn.resp(conn, 403, "") + end) + + assert {:error, :upstream, "Status code 403 from GitHub"} == + GitHub.authorise(config(bypass), "", "dummy_client_id", "") + end + + test "error token response", %{bypass: bypass} do + Bypass.stub(bypass, "POST", "login/oauth/access_token", fn conn -> + conn + |> PlugConn.put_resp_header("content-type", "application/json") + |> PlugConn.resp(200, ~s({"error":"bad_verification_code"})) + + assert {:error, :invalid_credentials, "Error from GitHub: bad_verification_code"} == + GitHub.authorise(config(bypass), "", "dummy_client_id", "") + end) + end + + test "non-successful HTTP status (user api call)", %{bypass: bypass} do + bypass_return_token(bypass) + + Bypass.stub(bypass, "GET", "user", fn conn -> + PlugConn.resp(conn, 401, "") + end) + + assert {:error, :upstream, "Status code 401 from GitHub"} + GitHub.authorise(config(bypass), "", "dummy_client_id", "") + end + + test "get_name successful", %{bypass: bypass} do + bypass_api_call(bypass) + + assert {:ok, @name} == GitHub.get_name(config(bypass), @dummy_access_token) + end + + test "get_name non-successful HTTP status", %{bypass: bypass} do + Bypass.stub(bypass, "GET", "user", fn conn -> + PlugConn.resp(conn, 401, "") + end) + + assert {:error, :upstream, "Status code 401 from GitHub"} == + GitHub.get_name(config(bypass), "invalid_access_token") + end +end diff --git a/test/cadet/auth/providers/openid_test.exs b/test/cadet/auth/providers/openid_test.exs index 27d0aed70..6526a395b 100644 --- a/test/cadet/auth/providers/openid_test.exs +++ b/test/cadet/auth/providers/openid_test.exs @@ -33,6 +33,7 @@ defmodule Cadet.Auth.Providers.OpenIDTest do """ @username "username" + @namespaced_username "test/username" @role :admin @openid_provider_name :test @@ -103,7 +104,7 @@ defmodule Cadet.Auth.Providers.OpenIDTest do bypass_return_token(bypass, @okay_token) - assert {:ok, %{token: @okay_token, username: @username}} = + assert {:ok, %{token: @okay_token, username: @namespaced_username}} = OpenID.authorise(@config, "dummy_code", "", "") assert {:ok, @username} == OpenID.get_name(@config, @okay_token) diff --git a/test/test_helper.exs b/test/test_helper.exs index 30e668048..2a7b91426 100644 --- a/test/test_helper.exs +++ b/test/test_helper.exs @@ -6,3 +6,19 @@ ExUnit.start() Faker.start() Ecto.Adapters.SQL.Sandbox.mode(Cadet.Repo, :manual) + +defmodule Cadet.TestHelper do + @doc """ + Removes a preloaded Ecto association. + """ + def remove_preload(struct, field, cardinality \\ :one) do + %{ + struct + | field => %Ecto.Association.NotLoaded{ + __field__: field, + __owner__: struct.__struct__, + __cardinality__: cardinality + } + } + end +end From 9bf0cbf1c4facde7e013ad7b12b858b7ce63e818 Mon Sep 17 00:00:00 2001 From: chownces <53928333+chownces@users.noreply.github.com> Date: Tue, 29 Jun 2021 16:38:10 +0800 Subject: [PATCH 095/174] Format --- lib/cadet/courses/courses.ex | 7 +++++-- .../admin_courses_controller.ex | 19 +++++++++---------- .../admin_courses_controller_test.exs | 14 +++++++------- 3 files changed, 21 insertions(+), 19 deletions(-) diff --git a/lib/cadet/courses/courses.ex b/lib/cadet/courses/courses.ex index 8a17bfa60..1b4358c4d 100644 --- a/lib/cadet/courses/courses.ex +++ b/lib/cadet/courses/courses.ex @@ -124,8 +124,11 @@ defmodule Cadet.Courses do |> where(id: ^assessment_config_id) |> Repo.one() |> case do - nil -> AssessmentConfig.changeset(%AssessmentConfig{course_id: course_id}, params) - at -> AssessmentConfig.changeset(at, params) + nil -> + AssessmentConfig.changeset(%AssessmentConfig{}, Map.put(params, :course_id, course_id)) + + at -> + AssessmentConfig.changeset(at, params) end |> Repo.insert_or_update() end diff --git a/lib/cadet_web/admin_controllers/admin_courses_controller.ex b/lib/cadet_web/admin_controllers/admin_courses_controller.ex index 9b39e0e65..fda2aa9fe 100644 --- a/lib/cadet_web/admin_controllers/admin_courses_controller.ex +++ b/lib/cadet_web/admin_controllers/admin_courses_controller.ex @@ -65,18 +65,17 @@ defmodule CadetWeb.AdminCoursesController do "assessmentConfig" => assessment_config }) when is_ecto_id(course_id) and is_map(assessment_config) do - config = assessment_config |> to_snake_case_atom_keys() + config = assessment_config |> to_snake_case_atom_keys() - case Courses.delete_assessment_config(course_id, config) do - {:ok, _} -> - text(conn, "OK") - - {:error, message} -> - conn - |> put_status(:bad_request) - |> text(message) - end + case Courses.delete_assessment_config(course_id, config) do + {:ok, _} -> + text(conn, "OK") + {:error, message} -> + conn + |> put_status(:bad_request) + |> text(message) + end end def delete_assessment_config(conn, _) do diff --git a/test/cadet_web/admin_controllers/admin_courses_controller_test.exs b/test/cadet_web/admin_controllers/admin_courses_controller_test.exs index b4d8e5ab0..aa05e5673 100644 --- a/test/cadet_web/admin_controllers/admin_courses_controller_test.exs +++ b/test/cadet_web/admin_controllers/admin_courses_controller_test.exs @@ -305,13 +305,13 @@ defmodule CadetWeb.AdminCoursesControllerTest do params = %{ "assessmentConfig" => %{ - "AssessmentConfigId" => config1.id, - "courseId" => course_id, - "type" => "Missions", - "earlySubmissionXp" => 100, - "hoursBeforeEarlyXpDecay" => 24, - "decayRatePointsPerHour" => 1 - } + "AssessmentConfigId" => config1.id, + "courseId" => course_id, + "type" => "Missions", + "earlySubmissionXp" => 100, + "hoursBeforeEarlyXpDecay" => 24, + "decayRatePointsPerHour" => 1 + } } resp = From d2013cf8360c3cecfac29995d4b4aeca537b824e Mon Sep 17 00:00:00 2001 From: chownces <53928333+chownces@users.noreply.github.com> Date: Wed, 30 Jun 2021 11:25:30 +0800 Subject: [PATCH 096/174] Temp updates to migration file --- ...0210531155751_add_course_configuration.exs | 195 +++++++++++++++++- 1 file changed, 184 insertions(+), 11 deletions(-) diff --git a/priv/repo/migrations/20210531155751_add_course_configuration.exs b/priv/repo/migrations/20210531155751_add_course_configuration.exs index b361514de..e1be5d67a 100644 --- a/priv/repo/migrations/20210531155751_add_course_configuration.exs +++ b/priv/repo/migrations/20210531155751_add_course_configuration.exs @@ -1,15 +1,14 @@ defmodule Cadet.Repo.Migrations.AddCourseConfiguration do use Ecto.Migration + import Ecto.Query, only: [from: 2, where: 2] - alias Cadet.Accounts.Role + alias Cadet.Accounts.{CourseRegistration, Role, User} + alias Cadet.Courses.{AssessmentConfig, Course, Group, Sourcecast} + alias Cadet.Repo + alias Cadet.Stories.Story def change do - alter table(:users) do - remove(:role) - remove(:group_id) - remove(:game_states) - end - + # Tracks course configurations create table(:courses) do add(:course_name, :string, null: false) add(:course_short_name, :string) @@ -23,6 +22,7 @@ defmodule Cadet.Repo.Migrations.AddCourseConfiguration do timestamps() end + # Tracks assessment configurations per assessment type in a course create table(:assessment_configs) do add(:order, :integer, null: true) add(:type, :string, null: false) @@ -33,8 +33,7 @@ defmodule Cadet.Repo.Migrations.AddCourseConfiguration do timestamps() end - create(unique_index(:assessment_configs, [:course_id, :order])) - + # Tracks course registrations (many-to-many r/s between users and courses) create table(:course_registrations) do add(:role, :role, null: false) add(:game_states, :map, default: %{}) @@ -44,20 +43,194 @@ defmodule Cadet.Repo.Migrations.AddCourseConfiguration do timestamps() end + # Enforce that users cannot be enrolled twice in a course create( unique_index(:course_registrations, [:user_id, :course_id], name: :course_registrations_user_id_course_id_index ) ) - drop_if_exists(table(:sublanguages)) + # latest_viewed_id to track which course to load after the user logs in. + # name and username modifications to allow for names to be nullable as accounts can + # now be precreated by any course instructor by specifying the username used in the + # respective auth provider. + alter table(:users) do + add(:latest_viewed_id, references(:courses), null: true) + modify(:name, :string, null: true) + modify(:username, :string, null: false) + end + + # Prep for migration of leader_id and mentor_id from User entity to CourseRegistration entity. + # Also make groups associated with a course. + rename(table(:groups), :leader_id, to: :temp_leader_id) + rename(table(:groups), :mentor_id, to: :temp_mentor_id) + drop(constraint(:groups, "groups_leader_id_fkey")) + drop(constraint(:groups, "groups_mentor_id_fkey")) + alter table(:groups) do + add(:leader_id, references(:course_registrations)) + add(:mentor_id, references(:course_registrations)) + add(:course_id, references(:courses)) + end + + # Sourcecasts to be associated with a course alter table(:sourcecasts) do add(:course_id, references(:courses)) end + # Stories to be associated with a course alter table(:stories) do - add(:course_id, references(:courses), null: false) + add(:course_id, references(:courses)) + end + + # Sublanguage is now being tracked under course configuration, and can be different depending on course + drop_if_exists(table(:sublanguages)) + + # Manual data entry and manipulation to migrate data from Source Academy Knight --> Rook. + # Note that in Knight, there was only 1 course running at a time, so it is okay to assume + # that all existing data belongs to that course. + execute( + fn -> + # Create the new course for migration + {:ok, course} = + %Course{} + |> Course.changeset(%{ + course_name: "CS1101S Programming Methodology (AY21/22 Sem 1)", + course_short_name: "CS1101S", + viewable: true, + enable_game: true, + enable_achievments: true, + enable_sourcecast: true, + source_chapter: 1, + source_variant: "default" + }) + |> Repo.insert() + + # Create course registrations for existing users + from(u in "users", select: {u.id, u.role, u.group_id, u.game_states}) + |> Repo.all() + |> Enum.each(fn user -> + %CourseRegistration{} + |> CourseRegistration.changeset(%{ + user_id: elem(user, 0), + role: elem(user, 1), + group_id: elem(user, 2), + game_states: elem(user, 3), + course_id: course.id + }) + |> Repo.insert() + end) + + # Add latest_viewed_id to existing users + User + |> Repo.all() + |> Enum.each(fn user -> + user + |> User.changeset(%{latest_viewed_id: course.id}) + |> Repo.update() + end) + + # Create Assessment Configurations based on Source Academy Knight + ["Missions", "Quests", "Paths", "Contests", "Others"] + |> Enum.each(fn assessment_type -> + %AssessmentConfig{} + |> AssessmentConfig.changeset(%{ + type: assessment_type, + course_id: course.id, + is_graded: true, + early_submission_xp: 200, + hours_before_early_xp_decay: 48 + }) + |> Repo.insert() + + # TODO: Link these to the new assessments/ submissions/ answers when they are done + end) + + # Handle groups (adding course_id, and updating leader_id and mentor_id to course registrations) + from(g in "groups", select: {g.id, g.temp_leader_id, g.temp_mentor_id}) + |> Repo.all() + |> Enum.each(fn group -> + leader_id = + case elem(group, 1) do + # leader_id is now going to be non-nullable. if it was previously nil, we will just + # assign a staff to be the leader_id during migration + nil -> + CourseRegistration + |> where(role: :staff) + |> Repo.one() + + Map.fetch!(:id) + + id -> + CourseRegistration + |> where(user_id: ^id) + |> Repo.one() + |> Map.fetch!(:id) + end + + mentor_id = + case elem(group, 2) do + nil -> + nil + + id -> + CourseRegistration + |> where(user_id: ^id) + |> Repo.one() + |> Map.fetch!(:id) + end + + Group + |> where(id: ^elem(group, 0)) + |> Repo.one() + |> Group.changeset(%{leader_id: leader_id, mentor_id: mentor_id, course_id: course.id}) + |> Repo.update() + end) + + # Add course id to all Sourcecasts + Sourcecast + |> Repo.all() + |> Enum.each(fn x -> + x + |> Sourcecast.changeset(%{course_id: course.id}) + |> Repo.update() + end) + + # Add course id to all Stories + Story + |> Repo.all() + |> Enum.each(fn x -> + x + |> Story.changeset(%{course_id: course.id}) + |> Repo.update() + end) + end, + fn -> nil end + ) + + # Cleanup users table after data migration + alter table(:users) do + remove(:role) + remove(:group_id) + remove(:game_states) + end + + # Cleanup groups table, and make course_id and leader_id non-nullable + alter table(:groups) do + remove(:temp_leader_id) + remove(:temp_mentor_id) + + modify(:leader_id, references(:course_registrations), + null: false, + from: references(:course_registrations) + ) + + modify(:course_id, references(:courses), null: false, from: references(:courses)) + end + + # Set course_id to be non-nullable + alter table(:stories) do + modify(:course_id, references(:courses), null: false, from: references(:courses)) end end end From b980e65f41522db759d610ea823dc832a1b32a79 Mon Sep 17 00:00:00 2001 From: chownces <53928333+chownces@users.noreply.github.com> Date: Wed, 30 Jun 2021 12:14:32 +0800 Subject: [PATCH 097/174] Complete migration file --- ...=> 20210531155751_multitenant_upgrade.exs} | 199 ++++++++++++++++-- .../20210608085548_update_assessments.exs | 42 ---- ...20210617084452_remove_assessment_grade.exs | 14 -- ...055445_alter_user_table_for_onboarding.exs | 17 -- .../20210626164054_update_notification.exs | 10 - 5 files changed, 181 insertions(+), 101 deletions(-) rename priv/repo/migrations/{20210531155751_add_course_configuration.exs => 20210531155751_multitenant_upgrade.exs} (59%) delete mode 100644 priv/repo/migrations/20210608085548_update_assessments.exs delete mode 100644 priv/repo/migrations/20210617084452_remove_assessment_grade.exs delete mode 100644 priv/repo/migrations/20210626055445_alter_user_table_for_onboarding.exs delete mode 100644 priv/repo/migrations/20210626164054_update_notification.exs diff --git a/priv/repo/migrations/20210531155751_add_course_configuration.exs b/priv/repo/migrations/20210531155751_multitenant_upgrade.exs similarity index 59% rename from priv/repo/migrations/20210531155751_add_course_configuration.exs rename to priv/repo/migrations/20210531155751_multitenant_upgrade.exs index e1be5d67a..86300d3cb 100644 --- a/priv/repo/migrations/20210531155751_add_course_configuration.exs +++ b/priv/repo/migrations/20210531155751_multitenant_upgrade.exs @@ -1,8 +1,9 @@ -defmodule Cadet.Repo.Migrations.AddCourseConfiguration do +defmodule Cadet.Repo.Migrations.MultitenantUpgrade do use Ecto.Migration import Ecto.Query, only: [from: 2, where: 2] - alias Cadet.Accounts.{CourseRegistration, Role, User} + alias Cadet.Accounts.{CourseRegistration, Notification, Role, User} + alias Cadet.Assessments.{Assessment, Submission, SubmissionVotes} alias Cadet.Courses.{AssessmentConfig, Course, Group, Sourcecast} alias Cadet.Repo alias Cadet.Stories.Story @@ -73,6 +74,42 @@ defmodule Cadet.Repo.Migrations.AddCourseConfiguration do add(:course_id, references(:courses)) end + # Make assessments related to an assessment config and a course + alter table(:assessments) do + add(:config_id, references(:assessment_configs)) + add(:course_id, references(:courses)) + end + + # Prep for migration of student_id and unsubmitted_by_id from User entity to CourseRegistration entity. + rename(table(:submissions), :student_id, to: :temp_student_id) + rename(table(:submissions), :unsubmitted_by_id, to: :temp_unsubmitted_by_id) + drop(constraint(:submissions, "submissions_student_id_fkey")) + drop(constraint(:submissions, "submissions_unsubmitted_by_id_fkey")) + + alter table(:submissions) do + add(:student_id, references(:course_registrations)) + add(:unsubmitted_by_id, references(:course_registrations)) + end + + alter table(:submission_votes) do + add(:voter_id, references(:course_registrations)) + end + + # Remove grade metric from backend + alter table(:answers) do + remove(:grade) + remove(:adjustment) + end + + alter table(:questions) do + remove(:max_grade) + end + + # Update notifications + alter table(:notifications) do + add(:course_reg_id, references(:course_registrations)) + end + # Sourcecasts to be associated with a course alter table(:sourcecasts) do add(:course_id, references(:courses)) @@ -130,22 +167,6 @@ defmodule Cadet.Repo.Migrations.AddCourseConfiguration do |> Repo.update() end) - # Create Assessment Configurations based on Source Academy Knight - ["Missions", "Quests", "Paths", "Contests", "Others"] - |> Enum.each(fn assessment_type -> - %AssessmentConfig{} - |> AssessmentConfig.changeset(%{ - type: assessment_type, - course_id: course.id, - is_graded: true, - early_submission_xp: 200, - hours_before_early_xp_decay: 48 - }) - |> Repo.insert() - - # TODO: Link these to the new assessments/ submissions/ answers when they are done - end) - # Handle groups (adding course_id, and updating leader_id and mentor_id to course registrations) from(g in "groups", select: {g.id, g.temp_leader_id, g.temp_mentor_id}) |> Repo.all() @@ -187,6 +208,106 @@ defmodule Cadet.Repo.Migrations.AddCourseConfiguration do |> Repo.update() end) + # Create Assessment Configurations based on Source Academy Knight + ["Missions", "Quests", "Paths", "Contests", "Others"] + |> Enum.each(fn assessment_type -> + %AssessmentConfig{} + |> AssessmentConfig.changeset(%{ + type: assessment_type, + course_id: course.id, + is_graded: true, + early_submission_xp: 200, + hours_before_early_xp_decay: 48 + }) + |> Repo.insert() + end) + + # Link existing assessments to an assessment config and course + from(a in "assessments", select: {a.id, a.type}) + |> Repo.all() + |> Enum.each(fn assessment -> + assessment_type = + case elem(assessment, 1) do + "mission" -> "Missions" + "sidequest" -> "Quests" + "path" -> "Paths" + "contest" -> "Contests" + "practical" -> "Others" + end + + assessment_config = + AssessmentConfig + |> where(type: ^assessment_type) + |> Repo.one() + + Assessment + |> where(id: ^elem(assessment, 0)) + |> Repo.one() + |> Assessment.changeset(%{config_id: assessment_config.id, course_id: course.id}) + |> Repo.update() + end) + + # Updating student_id and unsubmitted_by_id from User to CourseRegistration + from(s in "submissions", select: {s.id, s.temp_student_id, s.temp_unsubmitted_by_id}) + |> Repo.all() + |> Enum.each(fn submission -> + student_id = + CourseRegistration + |> where(user_id: ^elem(submission, 1)) + |> Repo.one() + |> Map.fetch!(:id) + + unsubmitted_by_id = + case elem(submission, 2) do + nil -> + nil + + id -> + CourseRegistration + |> where(user_id: ^id) + |> Repo.one() + |> Map.fetch!(:id) + end + + Submission + |> where(id: ^elem(submission, 0)) + |> Repo.one() + |> Submission.changeset(%{student_id: student_id, unsubmitted_by_id: unsubmitted_by_id}) + |> Repo.update() + end) + + from(s in "submission_votes", select: {s.id, s.user_id}) + |> Repo.all() + |> Enum.each(fn vote -> + voter_id = + CourseRegistration + |> where(user_id: ^elem(vote, 1)) + |> Repo.one() + |> Map.fetch!(:id) + + SubmissionVotes + |> where(id: ^elem(vote, 0)) + |> Repo.one() + |> SubmissionVotes.changeset(%{voter_id: voter_id}) + |> Repo.update() + end) + + from(n in "notifications", select: {n.id, n.user_id}) + |> Repo.all() + |> Enum.each(fn notification -> + course_reg_id = + CourseRegistration + |> where(user_id: ^elem(notification, 1)) + |> Repo.one() + |> Map.fetch!(:id) + + Notification + |> where(id: ^elem(notification, 0)) + |> Repo.one() + |> Notification.changeset(%{course_reg_id: course_reg_id}) + |> Repo.update() + end) + # Add course id to all Sourcecasts Sourcecast |> Repo.all() @@ -228,6 +349,48 @@ defmodule Cadet.Repo.Migrations.AddCourseConfiguration do modify(:course_id, references(:courses), null: false, from: references(:courses)) end + create(unique_index(:groups, [:name, :course_id])) + + # Cleanup assessments table, and make config_id and course_id non-nullable + alter table(:assessments) do + remove(:type) + modify(:config_id, references(:assessment_configs), null: false, from: references(:courses)) + modify(:course_id, references(:courses), null: false, from: references(:courses)) + end + + alter table(:submissions) do + remove(:temp_student_id) + remove(:temp_unsubmitted_by_id) + + modify(:student_id, references(:course_registrations), + null: false, + from: references(:course_registrations) + ) + end + + create(index(:submissions, :student_id)) + create(unique_index(:submissions, [:assessment_id, :student_id])) + + alter table(:submission_votes) do + remove(:user_id) + + modify(:voter_id, references(:course_registrations), + null: false, + from: references(:course_registrations) + ) + end + + create(unique_index(:submission_votes, [:voter_id, :question_id, :rank], name: :unique_score)) + + alter table(:notifications) do + remove(:user_id) + + modify(:course_reg_id, references(:course_registrations), + null: false, + from: references(:course_registrations) + ) + end + # Set course_id to be non-nullable alter table(:stories) do modify(:course_id, references(:courses), null: false, from: references(:courses)) diff --git a/priv/repo/migrations/20210608085548_update_assessments.exs b/priv/repo/migrations/20210608085548_update_assessments.exs deleted file mode 100644 index d1df9ec68..000000000 --- a/priv/repo/migrations/20210608085548_update_assessments.exs +++ /dev/null @@ -1,42 +0,0 @@ -defmodule Cadet.Repo.Migrations.UpdateAssessments do - use Ecto.Migration - - def change do - alter table(:assessments) do - remove(:type) - add(:config_id, references(:assessment_configs), null: false) - add(:course_id, references(:courses), null: false) - end - - alter table(:submissions) do - remove(:student_id) - add(:student_id, references(:course_registrations), null: false) - remove(:unsubmitted_by_id) - add(:unsubmitted_by_id, references(:course_registrations), null: true) - end - - create(index(:submissions, :student_id)) - create(unique_index(:submissions, [:assessment_id, :student_id])) - - alter table(:groups) do - remove(:leader_id) - add(:leader_id, references(:course_registrations), null: false) - remove(:mentor_id) - add(:mentor_id, references(:course_registrations), null: true) - add(:course_id, references(:courses), null: false) - end - - create(unique_index(:groups, [:name, :course_id])) - - alter table(:users) do - add(:latest_viewed_id, references(:courses), null: true) - end - - alter table(:submission_votes) do - remove(:user_id) - add(:voter_id, references(:course_registrations), null: false) - end - - create(unique_index(:submission_votes, [:voter_id, :question_id, :rank], name: :unique_score)) - end -end diff --git a/priv/repo/migrations/20210617084452_remove_assessment_grade.exs b/priv/repo/migrations/20210617084452_remove_assessment_grade.exs deleted file mode 100644 index eef3ec8f8..000000000 --- a/priv/repo/migrations/20210617084452_remove_assessment_grade.exs +++ /dev/null @@ -1,14 +0,0 @@ -defmodule Cadet.Repo.Migrations.RemoveAssessmentGrade do - use Ecto.Migration - - def change do - alter table(:answers) do - remove(:grade) - remove(:adjustment) - end - - alter table(:questions) do - remove(:max_grade) - end - end -end diff --git a/priv/repo/migrations/20210626055445_alter_user_table_for_onboarding.exs b/priv/repo/migrations/20210626055445_alter_user_table_for_onboarding.exs deleted file mode 100644 index 6c4056d37..000000000 --- a/priv/repo/migrations/20210626055445_alter_user_table_for_onboarding.exs +++ /dev/null @@ -1,17 +0,0 @@ -defmodule Cadet.Repo.Migrations.AlterUserTableForOnboarding do - use Ecto.Migration - - def up do - alter table(:users) do - modify(:name, :string, null: true) - modify(:username, :string, null: false) - end - end - - def down do - alter table(:users) do - modify(:name, :string, null: false) - modify(:username, :string, null: true) - end - end -end diff --git a/priv/repo/migrations/20210626164054_update_notification.exs b/priv/repo/migrations/20210626164054_update_notification.exs deleted file mode 100644 index be644b04d..000000000 --- a/priv/repo/migrations/20210626164054_update_notification.exs +++ /dev/null @@ -1,10 +0,0 @@ -defmodule Cadet.Repo.Migrations.UpdateNotification do - use Ecto.Migration - - def change do - alter table(:notifications) do - remove(:user_id) - add(:course_reg_id, references(:course_registrations), null: false) - end - end -end From f36cf1b4a7a42189e75e7022959664ee4a674f62 Mon Sep 17 00:00:00 2001 From: YaleChen299 Date: Wed, 30 Jun 2021 12:41:53 +0800 Subject: [PATCH 098/174] remove required in notification --- lib/cadet/accounts/notification.ex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/cadet/accounts/notification.ex b/lib/cadet/accounts/notification.ex index aa9de651f..e6e552d1b 100755 --- a/lib/cadet/accounts/notification.ex +++ b/lib/cadet/accounts/notification.ex @@ -22,7 +22,7 @@ defmodule Cadet.Accounts.Notification do timestamps() end - @required_fields ~w(type read role course_reg_id assessment_id)a + @required_fields ~w(type read course_reg_id assessment_id)a @optional_fields ~w(submission_id)a def changeset(answer, params) do From 8e544a0023f128a22bdbc505f59656b0a6ceeed0 Mon Sep 17 00:00:00 2001 From: YaleChen299 Date: Wed, 30 Jun 2021 13:04:42 +0800 Subject: [PATCH 099/174] fixing test seed and update with migration --- lib/cadet/assessments/assessments.ex | 2 +- lib/cadet/courses/courses.ex | 2 +- lib/cadet_web/views/assessments_helpers.ex | 9 ++- test/cadet/accounts/notification_test.exs | 2 +- .../admin_courses_controller_test.exs | 2 +- .../courses/assessment_config_factory.ex | 2 +- test/support/seeds.ex | 78 ++++++++++++++----- 7 files changed, 69 insertions(+), 28 deletions(-) diff --git a/lib/cadet/assessments/assessments.ex b/lib/cadet/assessments/assessments.ex index a6b60733c..9f2016045 100644 --- a/lib/cadet/assessments/assessments.ex +++ b/lib/cadet/assessments/assessments.ex @@ -800,7 +800,7 @@ defmodule Cadet.Assessments do {:ok, %Submission{}} | {:error, Ecto.Changeset.t()} defp update_submission_status_and_xp_bonus(submission = %Submission{}) do assessment = submission.assessment - assessment_conifg = Repo.get_by(AssessmentConfig, assessment_type_id: assessment.type_id) + assessment_conifg = Repo.get_by(AssessmentConfig, id: assessment.config_id) max_bonus_xp = assessment_conifg.early_submission_xp early_hours = assessment_conifg.hours_before_early_xp_decay diff --git a/lib/cadet/courses/courses.ex b/lib/cadet/courses/courses.ex index 1b4358c4d..51521eef1 100644 --- a/lib/cadet/courses/courses.ex +++ b/lib/cadet/courses/courses.ex @@ -67,7 +67,7 @@ defmodule Cadet.Courses do Updates the general course configuration for the specified course """ @spec update_course_config(integer, %{}) :: - {:ok, %Course{}} | {:error, {:bad_request, String.t()} | {:error, Ecto.Changeset.t()}} + {:ok, %Course{}} | {:error, {:bad_request, String.t()}} | {:error, Ecto.Changeset.t()} def update_course_config(course_id, params) when is_ecto_id(course_id) do case retrieve_course(course_id) do nil -> diff --git a/lib/cadet_web/views/assessments_helpers.ex b/lib/cadet_web/views/assessments_helpers.ex index 2f48ec64b..d779a131d 100644 --- a/lib/cadet_web/views/assessments_helpers.ex +++ b/lib/cadet_web/views/assessments_helpers.ex @@ -173,7 +173,7 @@ defmodule CadetWeb.AssessmentsHelpers do }) end - defp build_testcases(%{assessment_type: assessment_type}, all_testcases?) do + defp build_testcases(%{assessment_config: assessment_config}, all_testcases?) do cond do all_testcases? -> &Enum.concat( @@ -181,7 +181,8 @@ defmodule CadetWeb.AssessmentsHelpers do Enum.map(&1["private"], fn testcase -> build_testcase(testcase, "private") end) ) - assessment_type == "path" -> + # :TODO another indicator for whether to build all testcases + assessment_config == "path" -> &Enum.concat( Enum.map(&1["public"], fn testcase -> build_testcase(testcase, "public") end), Enum.map(&1["private"], fn testcase -> build_testcase(testcase, "hidden") end) @@ -192,8 +193,8 @@ defmodule CadetWeb.AssessmentsHelpers do end end - defp build_postpend(%{assessment_type: assessment_type}, all_testcases?) do - case {all_testcases?, assessment_type} do + defp build_postpend(%{assessment_config: assessment_config}, all_testcases?) do + case {all_testcases?, assessment_config} do {true, _} -> & &1["postpend"] {_, "path"} -> & &1["postpend"] # Create a 1-arity function to return an empty postpend for non-paths diff --git a/test/cadet/accounts/notification_test.exs b/test/cadet/accounts/notification_test.exs index 689e440cf..ba65bc7f2 100644 --- a/test/cadet/accounts/notification_test.exs +++ b/test/cadet/accounts/notification_test.exs @@ -3,7 +3,7 @@ defmodule Cadet.Accounts.NotificationTest do use Cadet.ChangesetCase, entity: Notification - @required_fields ~w(type role course_reg_id)a + @required_fields ~w(type course_reg_id)a setup do assessment = insert(:assessment, %{is_published: true}) diff --git a/test/cadet_web/admin_controllers/admin_courses_controller_test.exs b/test/cadet_web/admin_controllers/admin_courses_controller_test.exs index aa05e5673..725b62e7e 100644 --- a/test/cadet_web/admin_controllers/admin_courses_controller_test.exs +++ b/test/cadet_web/admin_controllers/admin_courses_controller_test.exs @@ -299,7 +299,7 @@ defmodule CadetWeb.AdminCoursesControllerTest do course_id = conn.assigns[:course_id] course = Repo.get(Course, course_id) config1 = insert(:assessment_config, %{order: 1, course: course, type: "Missions"}) - config2 = insert(:assessment_config, %{order: 2, course: course, type: "Paths"}) + _config2 = insert(:assessment_config, %{order: 2, course: course, type: "Paths"}) old_configs = course_id |> Courses.get_assessment_configs() |> Enum.map(& &1.type) diff --git a/test/factories/courses/assessment_config_factory.ex b/test/factories/courses/assessment_config_factory.ex index 80fab667a..d422d0ad0 100644 --- a/test/factories/courses/assessment_config_factory.ex +++ b/test/factories/courses/assessment_config_factory.ex @@ -10,7 +10,7 @@ defmodule Cadet.Courses.AssessmentConfigFactory do def assessment_config_factory do %AssessmentConfig{ order: 1, - type: "Missions", + type: Faker.Pokemon.En.name(), early_submission_xp: 200, hours_before_early_xp_decay: 48, course: build(:course) diff --git a/test/support/seeds.ex b/test/support/seeds.ex index 22c8ef0ad..9f35003e3 100644 --- a/test/support/seeds.ex +++ b/test/support/seeds.ex @@ -38,40 +38,80 @@ defmodule Cadet.Test.Seeds do def assessments do if Cadet.Env.env() == :test do - # User and Group - avenger = insert(:user, %{name: "avenger", role: :staff}) - mentor = insert(:user, %{name: "mentor", role: :staff}) - group = insert(:group, %{leader: avenger, mentor: mentor}) - students = insert_list(5, :student, %{group: group}) - admin = insert(:user, %{name: "admin", role: :admin}) + # # User and Group + # avenger = insert(:user, %{name: "avenger", role: :staff}) + # mentor = insert(:user, %{name: "mentor", role: :staff}) + # group = insert(:group, %{leader: avenger, mentor: mentor}) + # students = insert_list(5, :student, %{group: group}) + # admin = insert(:user, %{name: "admin", role: :admin}) + # Course + course1 = insert(:course) + course2 = insert(:course, %{course_name: "Algorithm", course_short_name: "CS2040S"}) + # Users + avenger1 = insert(:user, %{name: "avenger", latest_viewed: course1}) + mentor1 = insert(:user, %{name: "mentor", latest_viewed: course1}) + admin1 = insert(:user, %{name: "admin", latest_viewed: course1}) + + studenta1admin2 = insert(:user, %{name: "student a", latest_viewed: course1}) + + studentb1 = insert(:user, %{latest_viewed: course1}) + studentc1 = insert(:user, %{latest_viewed: course1}) + # CourseRegistration and Group + avenger1_cr = insert(:course_registration, %{user: avenger1, course: course1, role: :staff}) + mentor1_cr = insert(:course_registration, %{user: mentor1, course: course1, role: :staff}) + admin1_cr = insert(:course_registration, %{user: admin1, course: course1, role: :admin}) + group = insert(:group, %{leader: avenger1_cr, mentor: mentor1_cr}) + + student1a_cr = + insert(:course_registration, %{ + user: studenta1admin2, + course: course1, + role: :student, + group: group + }) + + student1b_cr = + insert(:course_registration, %{user: studentb1, course: course1, role: :student, group: group}) + + student1c_cr = + insert(:course_registration, %{user: studentc1, course: course1, role: :student, group: group}) + + students = [student1a_cr, student1b_cr, student1c_cr] + + _admin2cr = insert(:course_registration, %{user: studenta1admin2, course: course2, role: :admin}) assessments = - Enum.reduce( - Cadet.Assessments.Assessment.assessment_types(), - %{}, - fn type, acc -> Map.put(acc, type, insert_assessments(type, students)) end - ) + 1..5 + |> Enum.map(&insert(:assessment_config, %{course: course1, order: &1})) + |> Enum.reduce( + %{}, + fn config, acc -> Map.put(acc, config.type, insert_assessments(config, students, course1)) end + ) %{ + courses: %{ + course1: course1, + course2: course2 + }, accounts: %{ - avenger: avenger, - mentor: mentor, + avenger1_cr: avenger1_cr, + mentor1_cr: mentor1_cr, group: group, students: students, - admin: admin + admin1_cr: admin1_cr }, users: %{ - staff: avenger, - student: List.first(students), - admin: admin + staff: avenger1, + student: studenta1admin2, + admin: admin1 }, assessments: assessments } end end - defp insert_assessments(assessment_type, students) do - assessment = insert(:assessment, %{type: assessment_type, is_published: true}) + defp insert_assessments(assessment_config, students, course) do + assessment = insert(:assessment, %{course: course, config: assessment_config, is_published: true}) programming_questions = Enum.map(1..3, fn id -> From a33fd00807979741f724a7721bb4d23f9730adca Mon Sep 17 00:00:00 2001 From: YaleChen299 Date: Thu, 1 Jul 2021 13:12:07 +0800 Subject: [PATCH 100/174] update assessment controller with test + alter assessment config table --- lib/cadet/accounts/course_registrations.ex | 14 +- lib/cadet/assessments/assessments.ex | 34 +- lib/cadet/assessments/submission.ex | 4 +- lib/cadet/courses/assessment_config.ex | 9 +- lib/cadet/jobs/autograder/lambda_worker.ex | 4 +- lib/cadet/jobs/autograder/utilities.ex | 7 +- .../admin_views/admin_courses_view.ex | 4 +- lib/cadet_web/views/assessments_helpers.ex | 11 +- lib/cadet_web/views/assessments_view.ex | 3 +- .../20210531155751_multitenant_upgrade.exs | 8 +- test/cadet/courses/courses_test.exs | 5 +- test/cadet/jobs/autograder/utilities_test.exs | 2 +- .../admin_courses_controller_test.exs | 19 +- .../assessments_controller_test.exs | 557 ++++++++++-------- .../controllers/courses_controller_test.exs | 2 +- test/support/seeds.ex | 69 ++- 16 files changed, 447 insertions(+), 305 deletions(-) diff --git a/lib/cadet/accounts/course_registrations.ex b/lib/cadet/accounts/course_registrations.ex index 42669cab0..df2562865 100644 --- a/lib/cadet/accounts/course_registrations.ex +++ b/lib/cadet/accounts/course_registrations.ex @@ -80,17 +80,19 @@ defmodule Cadet.Accounts.CourseRegistrations do |> where(username: ^username) |> Repo.one() do nil -> - with {:ok, _} <- Accounts.register(%{username: username}) do - add_users_to_course_helper(username, course_id, role) - else + case Accounts.register(%{username: username}) do + {:ok, _} -> + add_users_to_course_helper(username, course_id, role) + {:error, changeset} -> {:halt, {:error, {:bad_request, full_error_messages(changeset)}}} end user -> - with {:ok, _} <- enroll_course(%{user_id: user.id, course_id: course_id, role: role}) do - {:cont, :ok} - else + case enroll_course(%{user_id: user.id, course_id: course_id, role: role}) do + {:ok, _} -> + {:cont, :ok} + {:error, changeset} -> {:halt, {:error, {:bad_request, full_error_messages(changeset)}}} end diff --git a/lib/cadet/assessments/assessments.ex b/lib/cadet/assessments/assessments.ex index 9f2016045..8dc09c639 100644 --- a/lib/cadet/assessments/assessments.ex +++ b/lib/cadet/assessments/assessments.ex @@ -203,11 +203,13 @@ defmodule Cadet.Assessments do if role in @open_all_assessment_roles do Assessment |> where(id: ^id) + |> preload(:config) |> Repo.one() else Assessment |> where(id: ^id) |> where(is_published: true) + |> preload(:config) |> Repo.one() end @@ -220,13 +222,13 @@ defmodule Cadet.Assessments do def assessment_with_questions_and_answers( assessment = %Assessment{id: id}, - cr = %CourseRegistration{role: role} + course_reg = %CourseRegistration{role: role} ) do if Timex.compare(Timex.now(), assessment.open_at) >= 0 or role in @open_all_assessment_roles do answer_query = Answer |> join(:inner, [a], s in assoc(a, :submission)) - |> where([_, s], s.student_id == ^cr.id) + |> where([_, s], s.student_id == ^course_reg.id) questions = Question @@ -240,10 +242,9 @@ defmodule Cadet.Assessments do {q, nil, _} -> %{q | answer: %Answer{grader: nil}} {q, a, g} -> %{q | answer: %Answer{a | grader: g}} end) + |> load_contest_voting_entries(course_reg.id) - # |> load_contest_voting_entries(cr.id) - - assessment = Map.put(assessment, :questions, questions) + assessment = assessment |> Map.put(:questions, questions) {:ok, assessment} else {:error, {:unauthorized, "Assessment not open"}} @@ -811,9 +812,9 @@ defmodule Cadet.Assessments do else # This logic interpolates from max bonus at early hour to 0 bonus at close time decaying_hours = Timex.diff(assessment.close_at, assessment.open_at, :hours) - early_hours - remaining_hours = Timex.diff(assessment.close_at, Timex.now(), :hours) - proportion = remaining_hours / decaying_hours - bonus_xp = (max_bonus_xp * proportion) |> round() + remaining_hours = Enum.max([0, Timex.diff(assessment.close_at, Timex.now(), :hours)]) + proportion = if(decaying_hours > 0, do: remaining_hours / decaying_hours, else: 1) + bonus_xp = round(max_bonus_xp * proportion) Enum.max([0, bonus_xp]) end @@ -857,24 +858,24 @@ defmodule Cadet.Assessments do end defp update_contest_voting_submission_status(submission = %Submission{}, question = %Question{}) do - not_nil_entries = + has_nil_entries = SubmissionVotes |> where(question_id: ^question.id) |> where(voter_id: ^submission.student_id) |> where([sv], is_nil(sv.rank)) |> Repo.exists?() - unless not_nil_entries do + unless has_nil_entries do submission |> Submission.changeset(%{status: :attempted}) |> Repo.update() end end - defp load_contest_voting_entries(questions, cr_id) do + defp load_contest_voting_entries(questions, voter_id) do Enum.map( questions, fn q -> if q.type == :voting do - submission_votes = all_submission_votes_by_question_id_and_cr_id(q.id, cr_id) + submission_votes = all_submission_votes_by_question_id_and_voter_id(q.id, voter_id) # fetch top 10 contest voting entries with the contest question id question_id = fetch_associated_contest_question_id(q) @@ -900,9 +901,9 @@ defmodule Cadet.Assessments do ) end - defp all_submission_votes_by_question_id_and_cr_id(question_id, cr_id) do + defp all_submission_votes_by_question_id_and_voter_id(question_id, voter_id) do SubmissionVotes - |> where([v], v.cr_id == ^cr_id and v.question_id == ^question_id) + |> where([v], v.voter_id == ^voter_id and v.question_id == ^question_id) |> join(:inner, [v], s in assoc(v, :submission)) |> join(:inner, [v, s], a in assoc(s, :answers)) |> select([v, s, a], %{submission_id: v.submission_id, answer: a.answer, rank: v.rank}) @@ -936,11 +937,12 @@ defmodule Cadet.Assessments do |> order_by(desc: :relative_score) |> join(:left, [a], s in assoc(a, :submission)) |> join(:left, [a, s], student in assoc(s, :student)) - |> select([a, s, student], %{ + |> join(:inner, [a, s, student], student_user in assoc(student, :user)) + |> select([a, s, student, student_user], %{ submission_id: a.submission_id, answer: a.answer, relative_score: a.relative_score, - student_name: student.name + student_name: student_user.name }) |> limit(^number_of_answers) |> Repo.all() diff --git a/lib/cadet/assessments/submission.ex b/lib/cadet/assessments/submission.ex index e00414858..5d789f334 100644 --- a/lib/cadet/assessments/submission.ex +++ b/lib/cadet/assessments/submission.ex @@ -26,15 +26,13 @@ defmodule Cadet.Assessments.Submission do @required_fields ~w(student_id assessment_id status)a @optional_fields ~w(xp_bonus unsubmitted_by_id unsubmitted_at)a - @xp_early_submission_max_bonus 100 def changeset(submission, params) do submission |> cast(params, @required_fields ++ @optional_fields) |> validate_number( :xp_bonus, - greater_than_or_equal_to: 0, - less_than_or_equal_to: @xp_early_submission_max_bonus + greater_than_or_equal_to: 0 ) |> add_belongs_to_id_from_model([:student, :assessment, :unsubmitted_by], params) |> validate_required(@required_fields) diff --git a/lib/cadet/courses/assessment_config.ex b/lib/cadet/courses/assessment_config.ex index 1601fca8a..a5661828d 100644 --- a/lib/cadet/courses/assessment_config.ex +++ b/lib/cadet/courses/assessment_config.ex @@ -10,7 +10,11 @@ defmodule Cadet.Courses.AssessmentConfig do schema "assessment_configs" do field(:order, :integer) field(:type, :string) - field(:is_graded, :boolean, default: true) + field(:build_solution, :boolean, default: false) + # a graded assessment type will not build solutions to the frontend + field(:build_hidden, :boolean, default: false) + # backend will build public testcases with hidden private testcases and will build postpend. + field(:is_contest, :boolean, default: false) field(:early_submission_xp, :integer, default: 0) field(:hours_before_early_xp_decay, :integer, default: 0) @@ -20,7 +24,8 @@ defmodule Cadet.Courses.AssessmentConfig do end @required_fields ~w(course_id)a - @optional_fields ~w(order type early_submission_xp hours_before_early_xp_decay is_graded)a + @optional_fields ~w(order type early_submission_xp + hours_before_early_xp_decay build_solution build_hidden is_contest)a def changeset(assessment_config, params) do params = capitalize(params, :type) diff --git a/lib/cadet/jobs/autograder/lambda_worker.ex b/lib/cadet/jobs/autograder/lambda_worker.ex index b96435de5..a6c8563f2 100644 --- a/lib/cadet/jobs/autograder/lambda_worker.ex +++ b/lib/cadet/jobs/autograder/lambda_worker.ex @@ -44,7 +44,9 @@ defmodule Cadet.Autograder.LambdaWorker do def on_failure(%{answer: answer = %Answer{}, question: %Question{}}, error) do error_message = - "Failed to get autograder result. answer_id: #{answer.id}, error: #{inspect(error, pretty: true)}" + "Failed to get autograder result. answer_id: #{answer.id}, error: #{ + inspect(error, pretty: true) + }" Logger.error(error_message) Sentry.capture_message(error_message) diff --git a/lib/cadet/jobs/autograder/utilities.ex b/lib/cadet/jobs/autograder/utilities.ex index 4f4b40401..b491c58c3 100644 --- a/lib/cadet/jobs/autograder/utilities.ex +++ b/lib/cadet/jobs/autograder/utilities.ex @@ -42,9 +42,10 @@ defmodule Cadet.Autograder.Utilities do Assessment |> where(is_published: true) |> where([a], a.close_at < ^Timex.now() and a.close_at >= ^Timex.shift(Timex.now(), days: -1)) - |> where([a], a.type != "contest") - |> join(:inner, [a], q in assoc(a, :questions)) - |> preload([_, q], questions: q) + |> join(:inner, [a], c in assoc(a, :config)) + |> where([a, c], not c.is_contest) + |> join(:inner, [a, c], q in assoc(a, :questions)) + |> preload([_, _, q], questions: q) |> Repo.all() |> Enum.map(&sort_assessment_questions(&1)) end diff --git a/lib/cadet_web/admin_views/admin_courses_view.ex b/lib/cadet_web/admin_views/admin_courses_view.ex index fb7bb72f4..ce1f6d63d 100644 --- a/lib/cadet_web/admin_views/admin_courses_view.ex +++ b/lib/cadet_web/admin_views/admin_courses_view.ex @@ -9,7 +9,9 @@ defmodule CadetWeb.AdminCoursesView do transform_map_for_view(config, %{ AssessmentConfigId: :id, type: :type, - isGraded: :is_graded, + buildHidden: :build_hidden, + buildSolution: :build_solution, + isContest: :is_contest, earlySubmissionXp: :early_submission_xp, hoursBeforeEarlyXpDecay: :hours_before_early_xp_decay }) diff --git a/lib/cadet_web/views/assessments_helpers.ex b/lib/cadet_web/views/assessments_helpers.ex index d779a131d..a2147e327 100644 --- a/lib/cadet_web/views/assessments_helpers.ex +++ b/lib/cadet_web/views/assessments_helpers.ex @@ -42,7 +42,7 @@ defmodule CadetWeb.AssessmentsHelpers do components = [ build_question_by_assessment_config(%{ question: question, - assessment_config: assessment.config.type + assessment_config: assessment.config }), build_answer_fields_by_question_type(%{question: question}), build_solution_if_ungraded_by_config(%{question: question, assessment: assessment}) @@ -66,7 +66,7 @@ defmodule CadetWeb.AssessmentsHelpers do question: %{question: question, type: question_type}, assessment: %{config: assessment_config} }) do - if !assessment_config.is_graded do + if assessment_config.build_solution do solution_getter = case question_type do :programming -> &Map.get(&1, "solution") @@ -182,7 +182,8 @@ defmodule CadetWeb.AssessmentsHelpers do ) # :TODO another indicator for whether to build all testcases - assessment_config == "path" -> + # assessment_config == "path" -> + assessment_config.build_hidden -> &Enum.concat( Enum.map(&1["public"], fn testcase -> build_testcase(testcase, "public") end), Enum.map(&1["private"], fn testcase -> build_testcase(testcase, "hidden") end) @@ -194,9 +195,9 @@ defmodule CadetWeb.AssessmentsHelpers do end defp build_postpend(%{assessment_config: assessment_config}, all_testcases?) do - case {all_testcases?, assessment_config} do + case {all_testcases?, assessment_config.build_hidden} do {true, _} -> & &1["postpend"] - {_, "path"} -> & &1["postpend"] + {_, true} -> & &1["postpend"] # Create a 1-arity function to return an empty postpend for non-paths _ -> fn _question -> "" end end diff --git a/lib/cadet_web/views/assessments_view.ex b/lib/cadet_web/views/assessments_view.ex index 6518e5026..d475537e8 100644 --- a/lib/cadet_web/views/assessments_view.ex +++ b/lib/cadet_web/views/assessments_view.ex @@ -36,8 +36,9 @@ defmodule CadetWeb.AssessmentsView do assessment, %{ id: :id, + courseId: :course_id, title: :title, - config: & &1.config.type, + type: & &1.config.type, story: :story, number: :number, reading: :reading, diff --git a/priv/repo/migrations/20210531155751_multitenant_upgrade.exs b/priv/repo/migrations/20210531155751_multitenant_upgrade.exs index 86300d3cb..5f7f80fbe 100644 --- a/priv/repo/migrations/20210531155751_multitenant_upgrade.exs +++ b/priv/repo/migrations/20210531155751_multitenant_upgrade.exs @@ -28,7 +28,9 @@ defmodule Cadet.Repo.Migrations.MultitenantUpgrade do add(:order, :integer, null: true) add(:type, :string, null: false) add(:course_id, references(:courses), null: false) - add(:is_graded, :boolean, null: false) + add(:build_solution, :boolean, null: false, default: false) + add(:is_contest, :boolean, null: false, default: false) + add(:build_hidden, :boolean, null: false, default: false) add(:early_submission_xp, :integer, null: false) add(:hours_before_early_xp_decay, :integer, null: false) timestamps() @@ -215,7 +217,9 @@ defmodule Cadet.Repo.Migrations.MultitenantUpgrade do |> AssessmentConfig.changeset(%{ type: assessment_type, course_id: course.id, - is_graded: true, + build_solution: assessment_type in ["Paths", "Others"], + build_hidden: assessment_type == "Paths", + is_contest: assessment_type == "Contests", early_submission_xp: 200, hours_before_early_xp_decay: 48 }) diff --git a/test/cadet/courses/courses_test.exs b/test/cadet/courses/courses_test.exs index db127006f..16dfff449 100644 --- a/test/cadet/courses/courses_test.exs +++ b/test/cadet/courses/courses_test.exs @@ -10,7 +10,7 @@ defmodule Cadet.CoursesTest do user = insert(:user) # Course precreated in User factory - assert Course |> Repo.all() |> length() == 1 + old_courses = Course |> Repo.all() |> length() params = %{ course_name: "CS1101S Programming Methodology (AY20/21 Sem 1)", @@ -27,7 +27,8 @@ defmodule Cadet.CoursesTest do Courses.create_course_config(params, user) # New course created - assert Course |> Repo.all() |> length() == 2 + new_courses = Course |> Repo.all() |> length() + assert new_courses - old_courses == 1 # New admin course registration for user course_regs = CourseRegistration |> where(user_id: ^user.id) |> Repo.all() diff --git a/test/cadet/jobs/autograder/utilities_test.exs b/test/cadet/jobs/autograder/utilities_test.exs index ab0f57fff..6619eb87b 100644 --- a/test/cadet/jobs/autograder/utilities_test.exs +++ b/test/cadet/jobs/autograder/utilities_test.exs @@ -53,7 +53,7 @@ defmodule Cadet.Autograder.UtilitiesTest do insert_list(2, :programming_question, %{assessment: assessment}) end - assert get_assessments_ids(Enum.filter(assessments, &(&1.type != "contest"))) == + assert get_assessments_ids(Enum.filter(assessments, &(!&1.config.is_contest))) == get_assessments_ids(Utilities.fetch_assessments_due_yesterday()) end diff --git a/test/cadet_web/admin_controllers/admin_courses_controller_test.exs b/test/cadet_web/admin_controllers/admin_courses_controller_test.exs index 725b62e7e..121115033 100644 --- a/test/cadet_web/admin_controllers/admin_courses_controller_test.exs +++ b/test/cadet_web/admin_controllers/admin_courses_controller_test.exs @@ -150,7 +150,12 @@ defmodule CadetWeb.AdminCoursesControllerTest do config3 = insert(:assessment_config, %{order: 3, type: "Mission3", course: course}) config2 = - insert(:assessment_config, %{is_graded: false, order: 2, type: "Mission2", course: course}) + insert(:assessment_config, %{ + build_solution: true, + order: 2, + type: "Mission2", + course: course + }) resp = conn @@ -161,21 +166,27 @@ defmodule CadetWeb.AdminCoursesControllerTest do %{ "earlySubmissionXp" => 200, "hoursBeforeEarlyXpDecay" => 48, - "isGraded" => true, + "buildSolution" => false, + "buildHidden" => false, + "isContest" => false, "type" => "Mission1", "AssessmentConfigId" => config1.id }, %{ "earlySubmissionXp" => 200, "hoursBeforeEarlyXpDecay" => 48, - "isGraded" => false, + "buildSolution" => true, + "buildHidden" => false, + "isContest" => false, "type" => "Mission2", "AssessmentConfigId" => config2.id }, %{ "earlySubmissionXp" => 200, "hoursBeforeEarlyXpDecay" => 48, - "isGraded" => true, + "buildSolution" => false, + "buildHidden" => false, + "isContest" => false, "type" => "Mission3", "AssessmentConfigId" => config3.id } diff --git a/test/cadet_web/controllers/assessments_controller_test.exs b/test/cadet_web/controllers/assessments_controller_test.exs index 320b0277b..f55a1c46b 100644 --- a/test/cadet_web/controllers/assessments_controller_test.exs +++ b/test/cadet_web/controllers/assessments_controller_test.exs @@ -6,7 +6,7 @@ defmodule CadetWeb.AssessmentsControllerTest do import Mock alias Cadet.{Assessments, Repo} - alias Cadet.Accounts.{Role, User} + alias Cadet.Accounts.{Role, CourseRegistration} alias Cadet.Assessments.{Assessment, Submission, SubmissionStatus} alias Cadet.Autograder.GradingJob alias CadetWeb.AssessmentsController @@ -23,9 +23,6 @@ defmodule CadetWeb.AssessmentsControllerTest do Cadet.Test.Seeds.assessments() end - @xp_early_submission_max_bonus 100 - @xp_bonus_assessment_type ~w(mission sidequest) - test "swagger" do AssessmentsController.swagger_definitions() AssessmentsController.swagger_path_index(nil) @@ -35,23 +32,28 @@ defmodule CadetWeb.AssessmentsControllerTest do end describe "GET /, unauthenticated" do - test "unauthorized", %{conn: conn} do - conn = get(conn, build_url()) + test "unauthorized", %{conn: conn, courses: %{course1: course1}} do + conn = get(conn, build_url(course1.id)) assert response(conn, 401) =~ "Unauthorised" end end describe "GET /:assessment_id, unauthenticated" do - test "unauthorized", %{conn: conn} do - conn = get(conn, build_url(1)) + test "unauthorized", %{conn: conn, courses: %{course1: course1}} do + conn = get(conn, build_url(course1.id, 1)) assert response(conn, 401) =~ "Unauthorised" end end # All roles should see almost the same overview describe "GET /, all roles" do - test "renders assessments overview", %{conn: conn, users: users, assessments: assessments} do - for {_role, user} <- users do + test "renders assessments overview", %{ + conn: conn, + courses: %{course1: course1}, + role_crs: role_crs, + assessments: assessments + } do + for {_role, course_reg} <- role_crs do expected = assessments |> Map.values() @@ -59,6 +61,7 @@ defmodule CadetWeb.AssessmentsControllerTest do |> Enum.sort(&open_at_asc_comparator/2) |> Enum.map( &%{ + "courseId" => &1.course_id, "id" => &1.id, "title" => &1.title, "shortSummary" => &1.summary_short, @@ -67,11 +70,10 @@ defmodule CadetWeb.AssessmentsControllerTest do "reading" => &1.reading, "openAt" => format_datetime(&1.open_at), "closeAt" => format_datetime(&1.close_at), - "type" => &1.type, + "type" => &1.config.type, "coverImage" => &1.cover_picture, - "maxGrade" => 750, "maxXp" => 4800, - "status" => get_assessment_status(user, &1), + "status" => get_assessment_status(course_reg, &1), "private" => false, "isPublished" => &1.is_published, "gradedCount" => 0, @@ -81,11 +83,10 @@ defmodule CadetWeb.AssessmentsControllerTest do resp = conn - |> sign_in(user) - |> get(build_url()) + |> sign_in(course_reg.user) + |> get(build_url(course1.id)) |> json_response(200) |> Enum.map(&Map.delete(&1, "xp")) - |> Enum.map(&Map.delete(&1, "grade")) assert expected == resp end @@ -93,11 +94,13 @@ defmodule CadetWeb.AssessmentsControllerTest do test "render password protected assessments properly", %{ conn: conn, - users: users, + courses: %{course1: course1}, + role_crs: role_crs, + assessment_configs: configs, assessments: assessments } do - for {_role, user} <- users do - mission = assessments["mission"] + for {_role, course_reg} <- role_crs do + mission = assessments[hd(configs).type] {:ok, _} = mission.assessment @@ -106,10 +109,10 @@ defmodule CadetWeb.AssessmentsControllerTest do resp = conn - |> sign_in(user) - |> get(build_url()) + |> sign_in(course_reg.user) + |> get(build_url(course1.id)) |> json_response(200) - |> Enum.find(&(&1["type"] == "mission")) + |> Enum.find(&(&1["type"] == hd(configs).type)) |> Map.get("private") assert resp == true @@ -120,10 +123,12 @@ defmodule CadetWeb.AssessmentsControllerTest do describe "GET /, student only" do test "does not render unpublished assessments", %{ conn: conn, - users: %{student: student}, + courses: %{course1: course1}, + role_crs: %{student: student}, + assessment_configs: configs, assessments: assessments } do - mission = assessments["mission"] + mission = assessments[hd(configs).type] {:ok, _} = mission.assessment @@ -132,12 +137,13 @@ defmodule CadetWeb.AssessmentsControllerTest do expected = assessments - |> Map.delete("mission") + |> Map.delete(hd(configs).type) |> Map.values() |> Enum.map(fn a -> a.assessment end) |> Enum.sort(&open_at_asc_comparator/2) |> Enum.map( &%{ + "courseId" => &1.course_id, "id" => &1.id, "title" => &1.title, "shortSummary" => &1.summary_short, @@ -146,9 +152,8 @@ defmodule CadetWeb.AssessmentsControllerTest do "reading" => &1.reading, "openAt" => format_datetime(&1.open_at), "closeAt" => format_datetime(&1.close_at), - "type" => &1.type, + "type" => &1.config.type, "coverImage" => &1.cover_picture, - "maxGrade" => 750, "maxXp" => 4800, "status" => get_assessment_status(student, &1), "private" => false, @@ -160,22 +165,23 @@ defmodule CadetWeb.AssessmentsControllerTest do resp = conn - |> sign_in(student) - |> get(build_url()) + |> sign_in(student.user) + |> get(build_url(course1.id)) |> json_response(200) |> Enum.map(&Map.delete(&1, "xp")) - |> Enum.map(&Map.delete(&1, "grade")) assert expected == resp end test "renders student submission status in overview", %{ conn: conn, - users: %{student: student}, + courses: %{course1: course1}, + role_crs: %{student: student}, + assessment_configs: configs, assessments: assessments } do - assessment = assessments["mission"].assessment - [submission | _] = assessments["mission"].submissions + assessment = assessments[hd(configs).type].assessment + [submission | _] = assessments[hd(configs).type].submissions for status <- SubmissionStatus.__enum_map__() do submission @@ -184,8 +190,8 @@ defmodule CadetWeb.AssessmentsControllerTest do resp = conn - |> sign_in(student) - |> get(build_url()) + |> sign_in(student.user) + |> get(build_url(course1.id)) |> json_response(200) |> Enum.find(&(&1["id"] == assessment.id)) |> Map.get("status") @@ -196,50 +202,36 @@ defmodule CadetWeb.AssessmentsControllerTest do test "renders xp for students", %{ conn: conn, - users: %{student: student}, + courses: %{course1: course1}, + role_crs: %{student: student}, + assessment_configs: configs, assessments: assessments } do - assessment = assessments["mission"].assessment + assessment = assessments[hd(configs).type].assessment resp = conn - |> sign_in(student) - |> get(build_url()) + |> sign_in(student.user) + |> get(build_url(course1.id)) |> json_response(200) |> Enum.find(&(&1["id"] == assessment.id)) |> Map.get("xp") assert resp == 1000 * 3 + 500 * 3 + 100 * 3 end - - test "renders grade for students", %{ - conn: conn, - users: %{student: student}, - assessments: assessments - } do - assessment = assessments["mission"].assessment - - resp = - conn - |> sign_in(student) - |> get(build_url()) - |> json_response(200) - |> Enum.find(&(&1["id"] == assessment.id)) - |> Map.get("grade") - - assert resp == 200 * 3 + 40 * 3 + 10 * 3 - end end describe "GET /, non-students" do test "renders unpublished assessments", %{ conn: conn, - users: users, + courses: %{course1: course1}, + role_crs: role_crs, + assessment_configs: configs, assessments: assessments } do for role <- ~w(staff admin)a do - user = Map.get(users, role) - mission = assessments["mission"] + course_reg = Map.get(role_crs, role) + mission = assessments[hd(configs).type] {:ok, _} = mission.assessment @@ -248,11 +240,10 @@ defmodule CadetWeb.AssessmentsControllerTest do resp = conn - |> sign_in(user) - |> get(build_url()) + |> sign_in(course_reg.user) + |> get(build_url(course1.id)) |> json_response(200) |> Enum.map(&Map.delete(&1, "xp")) - |> Enum.map(&Map.delete(&1, "grade")) expected = assessments @@ -262,6 +253,7 @@ defmodule CadetWeb.AssessmentsControllerTest do |> Enum.map( &%{ "id" => &1.id, + "courseId" => &1.course_id, "title" => &1.title, "shortSummary" => &1.summary_short, "story" => &1.story, @@ -269,16 +261,15 @@ defmodule CadetWeb.AssessmentsControllerTest do "reading" => &1.reading, "openAt" => format_datetime(&1.open_at), "closeAt" => format_datetime(&1.close_at), - "type" => &1.type, + "type" => &1.config.type, "coverImage" => &1.cover_picture, - "maxGrade" => 750, "maxXp" => 4800, - "status" => get_assessment_status(user, &1), + "status" => get_assessment_status(course_reg, &1), "private" => false, "gradedCount" => 0, "questionCount" => 9, "isPublished" => - if &1.type == "mission" do + if &1.config.type == hd(configs).type do false else &1.is_published @@ -294,17 +285,19 @@ defmodule CadetWeb.AssessmentsControllerTest do describe "GET /assessment_id, all roles" do test "it renders assessment details", %{ conn: conn, - users: users, + courses: %{course1: course1}, + role_crs: role_crs, assessments: assessments } do for role <- Role.__enum_map__() do - user = Map.get(users, role) + course_reg = Map.get(role_crs, role) - for {_type, %{assessment: assessment}} <- assessments do + for {type, %{assessment: assessment}} <- assessments do expected_assessments = %{ + "courseId" => assessment.course_id, "id" => assessment.id, "title" => assessment.title, - "type" => "#{assessment.type}", + "type" => type, "story" => assessment.story, "number" => assessment.number, "reading" => assessment.reading, @@ -314,8 +307,8 @@ defmodule CadetWeb.AssessmentsControllerTest do resp_assessments = conn - |> sign_in(user) - |> get(build_url(assessment.id)) + |> sign_in(course_reg.user) + |> get(build_url(course1.id, assessment.id)) |> json_response(200) |> Map.delete("questions") @@ -326,11 +319,12 @@ defmodule CadetWeb.AssessmentsControllerTest do test "it renders assessment questions", %{ conn: conn, - users: users, + courses: %{course1: course1}, + role_crs: role_crs, assessments: assessments } do for role <- Role.__enum_map__() do - user = Map.get(users, role) + course_reg = Map.get(role_crs, role) for {_type, %{ @@ -350,7 +344,7 @@ defmodule CadetWeb.AssessmentsControllerTest do "solutionTemplate" => &1.question.template, "prepend" => &1.question.prepend, "postpend" => - if assessment.type == "path" do + if assessment.config.build_hidden do &1.question.postpend else "" @@ -364,7 +358,7 @@ defmodule CadetWeb.AssessmentsControllerTest do do: {Atom.to_string(k), v} end ) ++ - if assessment.type == "path" do + if assessment.config.build_hidden do Enum.map( &1.question.private, fn testcase -> @@ -430,7 +424,11 @@ defmodule CadetWeb.AssessmentsControllerTest do |> Enum.zip(contests_submissions) |> Enum.map(fn {question, contest_submissions} -> Enum.map(contest_submissions, fn submission -> - insert(:submission_vote, %{user: user, submission: submission, question: question}) + insert(:submission_vote, %{ + voter: course_reg, + submission: submission, + question: question + }) end) end) @@ -458,17 +456,15 @@ defmodule CadetWeb.AssessmentsControllerTest do resp_questions = conn - |> sign_in(user) - |> get(build_url(assessment.id)) + |> sign_in(course_reg.user) + |> get(build_url(course1.id, assessment.id)) |> json_response(200) |> Map.get("questions", []) |> Enum.map(&Map.delete(&1, "answer")) |> Enum.map(&Map.delete(&1, "solution")) |> Enum.map(&Map.delete(&1, "library")) |> Enum.map(&Map.delete(&1, "xp")) - |> Enum.map(&Map.delete(&1, "grade")) |> Enum.map(&Map.delete(&1, "maxXp")) - |> Enum.map(&Map.delete(&1, "maxGrade")) |> Enum.map(&Map.delete(&1, "grader")) |> Enum.map(&Map.delete(&1, "gradedAt")) |> Enum.map(&Map.delete(&1, "autogradingResults")) @@ -482,11 +478,12 @@ defmodule CadetWeb.AssessmentsControllerTest do test "it renders contest leaderboards", %{ conn: conn, - accounts: accounts, - users: users, + course_regs: course_regs, + courses: %{course1: course1}, + role_crs: role_crs, assessments: assessments } do - voting_question = assessments["contest"].voting_questions |> List.first() + voting_question = assessments["practical"].voting_questions |> List.first() contest_assessment_number = voting_question.question.contest_number contest_assessment = Repo.get_by(Assessment, number: contest_assessment_number) @@ -496,20 +493,18 @@ defmodule CadetWeb.AssessmentsControllerTest do insert(:programming_question, %{ display_order: 1, assessment: contest_assessment, - max_grade: 0, max_xp: 1000 }) # insert contest submissions and answers contest_submissions = - for student <- Enum.take(accounts.students, 2) do + for student <- Enum.take(course_regs.students, 2) do insert(:submission, %{assessment: contest_assessment, student: student}) end contest_answers = for {submission, score} <- Enum.with_index(contest_submissions, 1) do insert(:answer, %{ - grade: 0, xp: 1000, question: contest_question, submission: submission, @@ -523,19 +518,19 @@ defmodule CadetWeb.AssessmentsControllerTest do %{ "answer" => %{"code" => answer.answer.code}, "score" => answer.relative_score, - "student_name" => answer.submission.student.name, + "student_name" => answer.submission.student.user.name, "submission_id" => answer.submission.id } end |> Enum.sort_by(& &1["score"], &>=/2) for role <- Role.__enum_map__() do - user = Map.get(users, role) + course_reg = Map.get(role_crs, role) resp_leaderboard = conn - |> sign_in(user) - |> get(build_url(voting_question.assessment.id)) + |> sign_in(course_reg.user) + |> get(build_url(course1.id, voting_question.assessment.id)) |> json_response(200) |> Map.get("questions", []) |> Enum.find(&(&1["id"] == voting_question.id)) @@ -547,11 +542,12 @@ defmodule CadetWeb.AssessmentsControllerTest do test "it renders assessment question libraries", %{ conn: conn, - users: users, + courses: %{course1: course1}, + role_crs: role_crs, assessments: assessments } do for role <- Role.__enum_map__() do - user = Map.get(users, role) + course_reg = Map.get(role_crs, role) for {_type, %{ @@ -578,8 +574,8 @@ defmodule CadetWeb.AssessmentsControllerTest do resp_libraries = conn - |> sign_in(user) - |> get(build_url(assessment.id)) + |> sign_in(course_reg.user) + |> get(build_url(course1.id, assessment.id)) |> json_response(200) |> Map.get("questions", []) |> Enum.map(&Map.get(&1, "library")) @@ -591,11 +587,12 @@ defmodule CadetWeb.AssessmentsControllerTest do test "it renders solutions for ungraded assessments (path)", %{ conn: conn, - users: users, + courses: %{course1: course1}, + role_crs: role_crs, assessments: assessments } do for role <- Role.__enum_map__() do - user = Map.get(users, role) + course_reg = Map.get(role_crs, role) %{ assessment: assessment, @@ -604,6 +601,8 @@ defmodule CadetWeb.AssessmentsControllerTest do voting_questions: voting_questions } = assessments["path"] + # This is the case cuz the seed set "path" to build_soultion = true + # Seeds set solution as 0 expected_mcq_solutions = Enum.map(mcq_questions, fn _ -> %{"solution" => 0} end) @@ -620,8 +619,8 @@ defmodule CadetWeb.AssessmentsControllerTest do resp_solutions = conn - |> sign_in(user) - |> get(build_url(assessment.id)) + |> sign_in(course_reg.user) + |> get(build_url(course1.id, assessment.id)) |> json_response(200) |> Map.get("questions", []) |> Enum.map(&Map.take(&1, ["solution"])) @@ -633,11 +632,12 @@ defmodule CadetWeb.AssessmentsControllerTest do test "it renders xp, grade for students", %{ conn: conn, - users: users, + courses: %{course1: course1}, + role_crs: role_crs, assessments: assessments } do for role <- Role.__enum_map__() do - user = Map.get(users, role) + course_reg = Map.get(role_crs, role) for {_type, %{ @@ -651,12 +651,11 @@ defmodule CadetWeb.AssessmentsControllerTest do Enum.map( programming_answers ++ mcq_answers ++ voting_answers, &%{ - "xp" => &1.xp + &1.xp_adjustment, - "grade" => &1.grade + &1.adjustment + "xp" => &1.xp + &1.xp_adjustment } ) else - fn -> %{"xp" => 0, "grade" => 0} end + fn -> %{"xp" => 0} end |> Stream.repeatedly() |> Enum.take( length(programming_answers) + length(mcq_answers) + length(voting_answers) @@ -665,11 +664,11 @@ defmodule CadetWeb.AssessmentsControllerTest do resp = conn - |> sign_in(user) - |> get(build_url(assessment.id)) + |> sign_in(course_reg.user) + |> get(build_url(course1.id, assessment.id)) |> json_response(200) |> Map.get("questions", []) - |> Enum.map(&Map.take(&1, ~w(xp grade))) + |> Enum.map(&Map.take(&1, ~w(xp))) assert expected == resp end @@ -678,11 +677,12 @@ defmodule CadetWeb.AssessmentsControllerTest do test "it does not render solutions for ungraded assessments (path)", %{ conn: conn, - users: users, + courses: %{course1: course1}, + role_crs: role_crs, assessments: assessments } do for role <- Role.__enum_map__() do - user = Map.get(users, role) + course_reg = Map.get(role_crs, role) for {_type, %{ @@ -690,8 +690,8 @@ defmodule CadetWeb.AssessmentsControllerTest do }} <- Map.delete(assessments, "path") do resp_solutions = conn - |> sign_in(user) - |> get(build_url(assessment.id)) + |> sign_in(course_reg.user) + |> get(build_url(course1.id, assessment.id)) |> json_response(200) |> Map.get("questions", []) |> Enum.map(&Map.get(&1, ["solution"])) @@ -705,7 +705,8 @@ defmodule CadetWeb.AssessmentsControllerTest do describe "GET /assessment_id, student" do test "it renders previously submitted answers", %{ conn: conn, - users: %{student: student}, + courses: %{course1: course1}, + role_crs: %{student: student}, assessments: assessments } do for {_type, @@ -729,8 +730,8 @@ defmodule CadetWeb.AssessmentsControllerTest do resp_answers = conn - |> sign_in(student) - |> get(build_url(assessment.id)) + |> sign_in(student.user) + |> get(build_url(course1.id, assessment.id)) |> json_response(200) |> Map.get("questions", []) |> Enum.map(&Map.take(&1, ["answer"])) @@ -741,7 +742,8 @@ defmodule CadetWeb.AssessmentsControllerTest do test "it does not permit access to not yet open assessments", %{ conn: conn, - users: %{student: student}, + courses: %{course1: course1}, + role_crs: %{student: student}, assessments: %{"mission" => mission} } do mission.assessment @@ -753,15 +755,16 @@ defmodule CadetWeb.AssessmentsControllerTest do conn = conn - |> sign_in(student) - |> get(build_url(mission.assessment.id)) + |> sign_in(student.user) + |> get(build_url(course1.id, mission.assessment.id)) assert response(conn, 401) == "Assessment not open" end test "it does not permit access to unpublished assessments", %{ conn: conn, - users: %{student: student}, + courses: %{course1: course1}, + role_crs: %{student: student}, assessments: %{"mission" => mission} } do {:ok, _} = @@ -771,8 +774,8 @@ defmodule CadetWeb.AssessmentsControllerTest do conn = conn - |> sign_in(student) - |> get(build_url(mission.assessment.id)) + |> sign_in(student.user) + |> get(build_url(course1.id, mission.assessment.id)) assert response(conn, 400) == "Assessment not found" end @@ -781,17 +784,18 @@ defmodule CadetWeb.AssessmentsControllerTest do describe "GET /assessment_id, non-students" do test "it renders empty answers", %{ conn: conn, - users: users, + courses: %{course1: course1}, + role_crs: role_crs, assessments: assessments } do for role <- ~w(staff admin)a do - user = Map.get(users, role) + course_reg = Map.get(role_crs, role) for {_type, %{assessment: assessment}} <- assessments do resp_answers = conn - |> sign_in(user) - |> get(build_url(assessment.id)) + |> sign_in(course_reg.user) + |> get(build_url(course1.id, assessment.id)) |> json_response(200) |> Map.get("questions", []) |> Enum.map(&Map.get(&1, ["answer"])) @@ -803,11 +807,12 @@ defmodule CadetWeb.AssessmentsControllerTest do test "it permits access to not yet open assessments", %{ conn: conn, - users: users, + courses: %{course1: course1}, + role_crs: role_crs, assessments: %{"mission" => mission} } do for role <- ~w(staff admin)a do - user = Map.get(users, role) + course_reg = Map.get(role_crs, role) mission.assessment |> Assessment.changeset(%{ @@ -818,8 +823,8 @@ defmodule CadetWeb.AssessmentsControllerTest do resp = conn - |> sign_in(user) - |> get(build_url(mission.assessment.id)) + |> sign_in(course_reg.user) + |> get(build_url(course1.id, mission.assessment.id)) |> json_response(200) assert resp["id"] == mission.assessment.id @@ -828,11 +833,12 @@ defmodule CadetWeb.AssessmentsControllerTest do test "it permits access to unpublished assessments", %{ conn: conn, - users: users, + courses: %{course1: course1}, + role_crs: role_crs, assessments: %{"mission" => mission} } do for role <- ~w(staff admin)a do - user = Map.get(users, role) + course_reg = Map.get(role_crs, role) {:ok, _} = mission.assessment @@ -841,8 +847,8 @@ defmodule CadetWeb.AssessmentsControllerTest do resp = conn - |> sign_in(user) - |> get(build_url(mission.assessment.id)) + |> sign_in(course_reg.user) + |> get(build_url(course1.id, mission.assessment.id)) |> json_response(200) assert resp["id"] == mission.assessment.id @@ -851,8 +857,8 @@ defmodule CadetWeb.AssessmentsControllerTest do end describe "GET /assessment_id/submit unauthenticated" do - test "is not permitted", %{conn: conn, assessments: %{"mission" => %{assessment: assessment}}} do - conn = post(conn, build_url_submit(assessment.id)) + test "is not permitted", %{conn: conn, courses: %{course1: course1}, assessments: %{"mission" => %{assessment: assessment}}} do + conn = post(conn, build_url_submit(course1.id, assessment.id)) assert response(conn, 401) == "Unauthorised" end end @@ -862,21 +868,23 @@ defmodule CadetWeb.AssessmentsControllerTest do @tag role: role test "is successful for attempted assessments for #{role}", %{ conn: conn, + courses: %{course1: course1}, assessments: %{"mission" => %{assessment: assessment}}, + role_crs: role_crs, role: role } do with_mock GradingJob, force_grade_individual_submission: fn _ -> nil end do - group = if(role == :student, do: insert(:group), else: nil) - user = insert(:user, %{role: role, group: group}) + group = if(role == :student, do: insert(:group, %{course: course1, leader: role_crs.staff}), else: nil) + course_reg = insert(:course_registration, %{role: role, group: group, course: course1}) submission = - insert(:submission, %{student: user, assessment: assessment, status: :attempted}) + insert(:submission, %{student: course_reg, assessment: assessment, status: :attempted}) conn = conn - |> sign_in(user) - |> post(build_url_submit(assessment.id)) + |> sign_in(course_reg.user) + |> post(build_url_submit(course1.id, assessment.id)) assert response(conn, 200) == "OK" @@ -890,67 +898,85 @@ defmodule CadetWeb.AssessmentsControllerTest do end end - test "submission of answer within 2 days of opening grants full XP bonus", %{conn: conn} do + test "submission of answer within early hours(seeded 48) of opening grants full XP bonus", %{ + conn: conn, + courses: %{course1: course1}, + role_crs: role_crs, + } do with_mock GradingJob, force_grade_individual_submission: fn _ -> nil end do - for type <- @xp_bonus_assessment_type do - assessment = - insert( - :assessment, - open_at: Timex.shift(Timex.now(), hours: -40), - close_at: Timex.shift(Timex.now(), days: 7), - is_published: true, - type: type - ) + assessment_config = insert( + :assessment_config, + early_submission_xp: 100, + hours_before_early_xp_decay: 48, + course: course1 + ) + assessment = + insert( + :assessment, + open_at: Timex.shift(Timex.now(), hours: -40), + close_at: Timex.shift(Timex.now(), days: 7), + is_published: true, + config: assessment_config, + course: course1 + ) - question = insert(:programming_question, assessment: assessment) + question = insert(:programming_question, assessment: assessment) - group = insert(:group) - user = insert(:user, %{role: :student, group: group}) + group = insert(:group, leader: role_crs.staff) + course_reg = insert(:course_registration, %{role: :student, group: group, course: course1}) - submission = - insert(:submission, assessment: assessment, student: user, status: :attempted) + submission = + insert(:submission, assessment: assessment, student: course_reg, status: :attempted) - insert( - :answer, - submission: submission, - question: question, - answer: %{code: "f => f(f);"} - ) + insert( + :answer, + submission: submission, + question: question, + answer: %{code: "f => f(f);"} + ) - conn - |> sign_in(user) - |> post(build_url_submit(assessment.id)) - |> response(200) + conn + |> sign_in(course_reg.user) + |> post(build_url_submit(course1.id, assessment.id)) + |> response(200) - submission_db = Repo.get(Submission, submission.id) + submission_db = Repo.get(Submission, submission.id) - assert submission_db.status == :submitted - assert submission_db.xp_bonus == @xp_early_submission_max_bonus - end + assert submission_db.status == :submitted + assert submission_db.xp_bonus == 100 end end - test "submission of answer after 2 days within the next 100 hours of opening grants decaying XP bonus", - %{conn: conn} do + test "submission of answer after early hours before deadline get decaying XP bonus", %{ + conn: conn, + courses: %{course1: course1}, + role_crs: role_crs, + } do with_mock GradingJob, force_grade_individual_submission: fn _ -> nil end do - for hours_after <- 48..148, - type <- @xp_bonus_assessment_type do + for hours_after <- 48..148 do + assessment_config = insert( + :assessment_config, + early_submission_xp: 100, + hours_before_early_xp_decay: 48, + course: course1 + ) assessment = insert( :assessment, open_at: Timex.shift(Timex.now(), hours: -hours_after), - close_at: Timex.shift(Timex.now(), hours: 500), + close_at: Timex.shift(Timex.now(), hours: 100), is_published: true, - type: type + config: assessment_config, + course: course1 ) question = insert(:programming_question, assessment: assessment) - group = insert(:group) - user = insert(:user, %{role: :student, group: group}) + group = insert(:group, leader: role_crs.staff) + course_reg = insert(:course_registration, %{role: :student, group: group, course: course1}) submission = - insert(:submission, assessment: assessment, student: user, status: :attempted) + insert(:submission, assessment: assessment, student: course_reg, status: :attempted) insert( :answer, @@ -960,39 +986,48 @@ defmodule CadetWeb.AssessmentsControllerTest do ) conn - |> sign_in(user) - |> post(build_url_submit(assessment.id)) + |> sign_in(course_reg.user) + |> post(build_url_submit(course1.id, assessment.id)) |> response(200) submission_db = Repo.get(Submission, submission.id) - + proportion = Timex.diff(assessment.close_at, Timex.now(), :hours) / (100 + hours_after - 48) assert submission_db.status == :submitted - assert submission_db.xp_bonus == @xp_early_submission_max_bonus - (hours_after - 48) + assert submission_db.xp_bonus == round(proportion * 100) end end end - test "submission of answer after 2 days and after the next 100 hours yield 0 XP bonus", %{ - conn: conn + test "submission of answer at the last hour yield 0 XP bonus", %{ + conn: conn, + courses: %{course1: course1}, + role_crs: role_crs, } do with_mock GradingJob, force_grade_individual_submission: fn _ -> nil end do - for type <- @xp_bonus_assessment_type do + for hours_after <- 48..148 do + assessment_config = insert( + :assessment_config, + early_submission_xp: 100, + hours_before_early_xp_decay: 48, + course: course1 + ) assessment = insert( :assessment, - open_at: Timex.shift(Timex.now(), hours: -150), - close_at: Timex.shift(Timex.now(), days: 7), + open_at: Timex.shift(Timex.now(), hours: -hours_after), + close_at: Timex.shift(Timex.now(), hours: 1), is_published: true, - type: type + config: assessment_config, + course: course1 ) question = insert(:programming_question, assessment: assessment) - group = insert(:group) - user = insert(:user, %{role: :student, group: group}) + group = insert(:group, leader: role_crs.staff) + course_reg = insert(:course_registration, %{role: :student, group: group, course: course1}) submission = - insert(:submission, assessment: assessment, student: user, status: :attempted) + insert(:submission, assessment: assessment, student: course_reg, status: :attempted) insert( :answer, @@ -1002,8 +1037,8 @@ defmodule CadetWeb.AssessmentsControllerTest do ) conn - |> sign_in(user) - |> post(build_url_submit(assessment.id)) + |> sign_in(course_reg.user) + |> post(build_url_submit(course1.id, assessment.id)) |> response(200) submission_db = Repo.get(Submission, submission.id) @@ -1014,29 +1049,36 @@ defmodule CadetWeb.AssessmentsControllerTest do end end - test "does not give bonus for non-bonus eligible assessment types", %{conn: conn} do + test "give 0 bonus for configs with 0 max", %{ + conn: conn, courses: %{course1: course1}, role_crs: role_crs,} do with_mock GradingJob, force_grade_individual_submission: fn _ -> nil end do - non_eligible_types = - Enum.filter(Assessment.assessment_types(), &(&1 not in @xp_bonus_assessment_type)) - for hours_after <- 0..148, - type <- non_eligible_types do + + for hours_after <- 0..148 do + assessment_config = insert( + :assessment_config, + early_submission_xp: 0, + hours_before_early_xp_decay: 48, + course: course1 + ) + assessment = insert( :assessment, open_at: Timex.shift(Timex.now(), hours: -hours_after), close_at: Timex.shift(Timex.now(), days: 7), is_published: true, - type: type + config: assessment_config, + course: course1 ) question = insert(:programming_question, assessment: assessment) - group = insert(:group) - user = insert(:user, %{role: :student, group: group}) + group = insert(:group, leader: role_crs.staff) + course_reg = insert(:course_registration, %{role: :student, group: group, course: course1}) submission = - insert(:submission, assessment: assessment, student: user, status: :attempted) + insert(:submission, assessment: assessment, student: course_reg, status: :attempted) insert( :answer, @@ -1046,8 +1088,8 @@ defmodule CadetWeb.AssessmentsControllerTest do ) conn - |> sign_in(user) - |> post(build_url_submit(assessment.id)) + |> sign_in(course_reg.user) + |> post(build_url_submit(course1.id, assessment.id)) |> response(200) submission_db = Repo.get(Submission, submission.id) @@ -1062,77 +1104,103 @@ defmodule CadetWeb.AssessmentsControllerTest do # answered. test "is not permitted for unattempted assessments", %{ conn: conn, - assessments: %{"mission" => %{assessment: assessment}} + courses: %{course1: course1}, + assessments: %{"mission" => %{assessment: assessment}}, } do - user = insert(:user, %{role: :student}) + course_reg = insert(:course_registration, %{role: :student, course: course1}) conn = conn - |> sign_in(user) - |> post(build_url_submit(assessment.id)) + |> sign_in(course_reg.user) + |> post(build_url_submit(course1.id, assessment.id)) assert response(conn, 404) == "Submission not found" end test "is not permitted for incomplete assessments", %{ conn: conn, + courses: %{course1: course1}, assessments: %{"mission" => %{assessment: assessment}} } do - user = insert(:user, %{role: :student}) - insert(:submission, %{student: user, assessment: assessment, status: :attempting}) + course_reg = insert(:course_registration, %{role: :student, course: course1}) + insert(:submission, %{student: course_reg, assessment: assessment, status: :attempting}) conn = conn - |> sign_in(user) - |> post(build_url_submit(assessment.id)) + |> sign_in(course_reg.user) + |> post(build_url_submit(course1.id, assessment.id)) assert response(conn, 400) == "Some questions have not been attempted" end test "is not permitted for already submitted assessments", %{ conn: conn, + courses: %{course1: course1}, assessments: %{"mission" => %{assessment: assessment}} } do - user = insert(:user, %{role: :student}) - insert(:submission, %{student: user, assessment: assessment, status: :submitted}) + course_reg = insert(:course_registration, %{role: :student, course: course1}) + insert(:submission, %{student: course_reg, assessment: assessment, status: :submitted}) conn = conn - |> sign_in(user) - |> post(build_url_submit(assessment.id)) + |> sign_in(course_reg.user) + |> post(build_url_submit(course1.id, assessment.id)) assert response(conn, 403) == "Assessment has already been submitted" end - test "is not permitted for closed assessments", %{conn: conn} do - user = insert(:user, %{role: :student}) + test "is not permitted for closed assessments", %{conn: conn, courses: %{course1: course1},} do + course_reg = insert(:course_registration, %{role: :student, course: course1}) # Only check for after-closing because submission shouldn't exist if unpublished or # before opening and would fall under "Submission not found" after_close_at_assessment = insert(:assessment, %{ open_at: Timex.shift(Timex.now(), days: -10), - close_at: Timex.shift(Timex.now(), days: -5) + close_at: Timex.shift(Timex.now(), days: -5), + course: course1 }) insert(:submission, %{ - student: user, + student: course_reg, assessment: after_close_at_assessment, status: :attempted }) conn = conn - |> sign_in(user) - |> post(build_url_submit(after_close_at_assessment.id)) + |> sign_in(course_reg.user) + |> post(build_url_submit(course1.id, after_close_at_assessment.id)) assert response(conn, 403) == "Assessment not open" end + + test "not found if not in same course", %{conn: conn, courses: %{course2: course2}, role_crs: %{student: student}, assessments: %{"mission" => %{assessment: assessment}}} do + # user is in both course, but assessment belongs to a course and no submission will be found + conn = + conn + |> sign_in(student.user) + |> post(build_url_submit(course2.id, assessment.id)) + + assert response(conn, 404) == "Submission not found" + end + + test "forbidden if not in course", %{conn: conn, courses: %{course2: course2}, course_regs: %{students: students}, assessments: %{"mission" => %{assessment: assessment}}} do + # user is not in the course + student2 = hd(tl(students)) + conn = + conn + |> sign_in(student2.user) + |> post(build_url_submit(course2.id, assessment.id)) + + assert response(conn, 403) == "Forbidden" + end end + @tag :skip test "graded count is updated when assessment is graded", %{ conn: conn, - users: %{staff: avenger} + role_crs: %{staff: avenger} } do assessment = insert( @@ -1183,10 +1251,11 @@ defmodule CadetWeb.AssessmentsControllerTest do end describe "Password protected assessments render properly" do + @tag :skip test "returns 403 when trying to access a password protected assessment without a password", %{ conn: conn, - users: users + role_crs: role_crs } do assessment = insert(:assessment, %{type: "practical", is_published: true}) @@ -1198,15 +1267,16 @@ defmodule CadetWeb.AssessmentsControllerTest do }) |> Repo.update!() - for {_role, user} <- users do + for {_role, user} <- role_crs do conn = conn |> sign_in(user) |> get(build_url(assessment.id)) assert response(conn, 403) == "Missing Password." end end + @tag :skip test "returns 403 when password is wrong/invalid", %{ conn: conn, - users: users + role_crs: role_crs } do assessment = insert(:assessment, %{type: "practical", is_published: true}) @@ -1218,7 +1288,7 @@ defmodule CadetWeb.AssessmentsControllerTest do }) |> Repo.update!() - for {_role, user} <- users do + for {_role, user} <- role_crs do conn = conn |> sign_in(user) @@ -1228,10 +1298,11 @@ defmodule CadetWeb.AssessmentsControllerTest do end end - test "allow users with preexisting submission to access private assessment without a password", + @tag :skip + test "allow role_crs with preexisting submission to access private assessment without a password", %{ conn: conn, - users: %{student: student} + role_crs: %{student: student} } do assessment = insert(:assessment, %{type: "practical", is_published: true}) @@ -1248,14 +1319,15 @@ defmodule CadetWeb.AssessmentsControllerTest do assert response(conn, 200) end + @tag :skip test "ignore password when assessment is not password protected", %{ conn: conn, - users: users, + role_crs: role_crs, assessments: assessments } do assessment = assessments["mission"].assessment - for {_role, user} <- users do + for {_role, user} <- role_crs do conn = conn |> sign_in(user) @@ -1266,9 +1338,10 @@ defmodule CadetWeb.AssessmentsControllerTest do end end + @tag :skip test "render assessment when password is correct", %{ conn: conn, - users: users, + role_crs: role_crs, assessments: assessments } do assessment = assessments["mission"].assessment @@ -1278,7 +1351,7 @@ defmodule CadetWeb.AssessmentsControllerTest do |> Assessment.changeset(%{password: "mysupersecretpassword"}) |> Repo.update() - for {_role, user} <- users do + for {_role, user} <- role_crs do conn = conn |> sign_in(user) @@ -1289,9 +1362,10 @@ defmodule CadetWeb.AssessmentsControllerTest do end end + @tag :skip test "permit global access to private assessment after closed", %{ conn: conn, - users: %{student: student}, + role_crs: %{student: student}, assessments: %{"mission" => mission} } do mission.assessment @@ -1311,16 +1385,25 @@ defmodule CadetWeb.AssessmentsControllerTest do end defp build_url, do: "/v2/assessments/" - defp build_url(assessment_id), do: "/v2/assessments/#{assessment_id}" defp build_url_submit(assessment_id), do: "/v2/assessments/#{assessment_id}/submit" defp build_url_unlock(assessment_id), do: "/v2/assessments/#{assessment_id}/unlock" + defp build_url(course_id), do: "/v2/courses/#{course_id}/assessments/" + + defp build_url(course_id, assessment_id), + do: "/v2/courses/#{course_id}/assessments/#{assessment_id}" + + defp build_url_submit(course_id, assessment_id), + do: "/v2/courses/#{course_id}/assessments/#{assessment_id}/submit" + + defp build_url_unlock(course_id, assessment_id), + do: "/v2/courses/#{course_id}/assessments/#{assessment_id}/unlock" defp open_at_asc_comparator(x, y), do: Timex.before?(x.open_at, y.open_at) - defp get_assessment_status(user = %User{}, assessment = %Assessment{}) do + defp get_assessment_status(course_reg = %CourseRegistration{}, assessment = %Assessment{}) do submission = Submission - |> where(student_id: ^user.id) + |> where(student_id: ^course_reg.id) |> where(assessment_id: ^assessment.id) |> Repo.one() diff --git a/test/cadet_web/controllers/courses_controller_test.exs b/test/cadet_web/controllers/courses_controller_test.exs index 7e8873297..22939d622 100644 --- a/test/cadet_web/controllers/courses_controller_test.exs +++ b/test/cadet_web/controllers/courses_controller_test.exs @@ -129,6 +129,6 @@ defmodule CadetWeb.CoursesControllerTest do end end - defp build_url_create(), do: "/v2/config/create" + defp build_url_create, do: "/v2/config/create" defp build_url_config(course_id), do: "/v2/courses/#{course_id}/config" end diff --git a/test/support/seeds.ex b/test/support/seeds.ex index 9f35003e3..600526e20 100644 --- a/test/support/seeds.ex +++ b/test/support/seeds.ex @@ -71,54 +71,88 @@ defmodule Cadet.Test.Seeds do }) student1b_cr = - insert(:course_registration, %{user: studentb1, course: course1, role: :student, group: group}) + insert(:course_registration, %{ + user: studentb1, + course: course1, + role: :student, + group: group + }) student1c_cr = - insert(:course_registration, %{user: studentc1, course: course1, role: :student, group: group}) + insert(:course_registration, %{ + user: studentc1, + course: course1, + role: :student, + group: group + }) students = [student1a_cr, student1b_cr, student1c_cr] - _admin2cr = insert(:course_registration, %{user: studenta1admin2, course: course2, role: :admin}) + _admin2cr = + insert(:course_registration, %{user: studenta1admin2, course: course2, role: :admin}) + + assessment_configs = [ + insert(:assessment_config, %{course: course1, order: 1, type: "mission"}), + insert(:assessment_config, %{course: course1, order: 2}), + insert(:assessment_config, %{ + course: course1, + order: 3, + build_hidden: true, + build_solution: true, + type: "path" + }), + insert(:assessment_config, %{course: course1, order: 4, is_contest: true}), + insert(:assessment_config, %{ + course: course1, + order: 5, + build_solution: true, + type: "practical" + }) + ] + + # 1..5 |> Enum.map(&insert(:assessment_config, %{course: course1, order: &1})) assessments = - 1..5 - |> Enum.map(&insert(:assessment_config, %{course: course1, order: &1})) + assessment_configs |> Enum.reduce( - %{}, - fn config, acc -> Map.put(acc, config.type, insert_assessments(config, students, course1)) end - ) + %{}, + fn config, acc -> + Map.put(acc, config.type, insert_assessments(config, students, course1)) + end + ) %{ courses: %{ course1: course1, course2: course2 }, - accounts: %{ + course_regs: %{ avenger1_cr: avenger1_cr, mentor1_cr: mentor1_cr, group: group, students: students, admin1_cr: admin1_cr }, - users: %{ - staff: avenger1, - student: studenta1admin2, - admin: admin1 + role_crs: %{ + staff: avenger1_cr, + student: student1a_cr, + admin: admin1_cr }, + assessment_configs: assessment_configs, assessments: assessments } end end defp insert_assessments(assessment_config, students, course) do - assessment = insert(:assessment, %{course: course, config: assessment_config, is_published: true}) + assessment = + insert(:assessment, %{course: course, config: assessment_config, is_published: true}) programming_questions = Enum.map(1..3, fn id -> insert(:programming_question, %{ display_order: id, assessment: assessment, - max_grade: 200, max_xp: 1000 }) end) @@ -128,7 +162,6 @@ defmodule Cadet.Test.Seeds do insert(:mcq_question, %{ display_order: id, assessment: assessment, - max_grade: 40, max_xp: 500 }) end) @@ -138,7 +171,6 @@ defmodule Cadet.Test.Seeds do insert(:voting_question, %{ display_order: id, assessment: assessment, - max_grade: 10, max_xp: 100 }) end) @@ -153,7 +185,6 @@ defmodule Cadet.Test.Seeds do Enum.map(submissions, fn submission -> Enum.map(programming_questions, fn question -> insert(:answer, %{ - grade: 200, xp: 1000, question: question, submission: submission, @@ -166,7 +197,6 @@ defmodule Cadet.Test.Seeds do Enum.map(submissions, fn submission -> Enum.map(mcq_questions, fn question -> insert(:answer, %{ - grade: 40, xp: 500, question: question, submission: submission, @@ -179,7 +209,6 @@ defmodule Cadet.Test.Seeds do Enum.map(submissions, fn submission -> Enum.map(voting_questions, fn question -> insert(:answer, %{ - grade: 10, xp: 100, question: question, submission: submission, From 58486acbac44d99ca14b120fd95ffb053e3e8799 Mon Sep 17 00:00:00 2001 From: YaleChen299 Date: Thu, 1 Jul 2021 15:29:06 +0800 Subject: [PATCH 101/174] finalise assessment controller with test --- lib/cadet/assessments/assessments.ex | 4 +- .../20210531155751_multitenant_upgrade.exs | 2 + .../assessments_controller_test.exs | 119 +++++++----------- 3 files changed, 52 insertions(+), 73 deletions(-) diff --git a/lib/cadet/assessments/assessments.ex b/lib/cadet/assessments/assessments.ex index 8dc09c639..ea79b3827 100644 --- a/lib/cadet/assessments/assessments.ex +++ b/lib/cadet/assessments/assessments.ex @@ -1215,14 +1215,14 @@ defmodule Cadet.Assessments do @spec update_grading_info( %{submission_id: integer() | String.t(), question_id: integer() | String.t()}, %{}, - %User{} + %CourseRegistration{} ) :: {:ok, nil} | {:error, {:unauthorized | :bad_request | :internal_server_error, String.t()}} def update_grading_info( %{submission_id: submission_id, question_id: question_id}, attrs, - %User{id: grader_id} + %CourseRegistration{id: grader_id} ) when is_ecto_id(submission_id) and is_ecto_id(question_id) do attrs = Map.put(attrs, "grader_id", grader_id) diff --git a/priv/repo/migrations/20210531155751_multitenant_upgrade.exs b/priv/repo/migrations/20210531155751_multitenant_upgrade.exs index 5f7f80fbe..fad340a8f 100644 --- a/priv/repo/migrations/20210531155751_multitenant_upgrade.exs +++ b/priv/repo/migrations/20210531155751_multitenant_upgrade.exs @@ -101,6 +101,8 @@ defmodule Cadet.Repo.Migrations.MultitenantUpgrade do alter table(:answers) do remove(:grade) remove(:adjustment) + remove(:grader_id) + add(:grader_id, references(:course_registrations), null: true) end alter table(:questions) do diff --git a/test/cadet_web/controllers/assessments_controller_test.exs b/test/cadet_web/controllers/assessments_controller_test.exs index f55a1c46b..b7d0774a3 100644 --- a/test/cadet_web/controllers/assessments_controller_test.exs +++ b/test/cadet_web/controllers/assessments_controller_test.exs @@ -1197,9 +1197,10 @@ defmodule CadetWeb.AssessmentsControllerTest do end end - @tag :skip test "graded count is updated when assessment is graded", %{ conn: conn, + courses: %{course1: course1}, + assessment_configs: [config| _], role_crs: %{staff: avenger} } do assessment = @@ -1208,14 +1209,15 @@ defmodule CadetWeb.AssessmentsControllerTest do open_at: Timex.shift(Timex.now(), hours: -2), close_at: Timex.shift(Timex.now(), days: 7), is_published: true, - type: "mission" + config: config, + course: course1 ) [question_one, question_two] = insert_list(2, :programming_question, assessment: assessment) - user = insert(:user, role: :student) + course_reg = insert(:course_registration, role: :student, course: course1) - submission = insert(:submission, assessment: assessment, student: user, status: :submitted) + submission = insert(:submission, assessment: assessment, student: course_reg, status: :submitted) Enum.each( [question_one, question_two], @@ -1224,8 +1226,8 @@ defmodule CadetWeb.AssessmentsControllerTest do get_graded_count = fn -> conn - |> sign_in(user) - |> get(build_url()) + |> sign_in(course_reg.user) + |> get(build_url(course1.id)) |> json_response(200) |> Enum.find(&(&1["id"] == assessment.id)) |> Map.get("gradedCount") @@ -1234,7 +1236,7 @@ defmodule CadetWeb.AssessmentsControllerTest do grade_question = fn question -> Assessments.update_grading_info( %{submission_id: submission.id, question_id: question.id}, - %{"adjustment" => 0}, + %{"xp_adjustment" => 0}, avenger ) end @@ -1251,13 +1253,8 @@ defmodule CadetWeb.AssessmentsControllerTest do end describe "Password protected assessments render properly" do - @tag :skip - test "returns 403 when trying to access a password protected assessment without a password", - %{ - conn: conn, - role_crs: role_crs - } do - assessment = insert(:assessment, %{type: "practical", is_published: true}) + setup %{courses: %{course1: course1}, assessment_configs: configs,} do + assessment = insert(:assessment, %{config: Enum.at(configs, 4), course: course1, is_published: true}) assessment |> Assessment.changeset(%{ @@ -1267,104 +1264,87 @@ defmodule CadetWeb.AssessmentsControllerTest do }) |> Repo.update!() - for {_role, user} <- role_crs do - conn = conn |> sign_in(user) |> get(build_url(assessment.id)) + {:ok, protected_assessment: assessment} + end + + test "returns 403 when trying to access a password protected assessment without a password", %{ + conn: conn, + courses: %{course1: course1}, + protected_assessment: protected_assessment, + role_crs: role_crs + } do + for {_role, course_reg} <- role_crs do + conn = conn |> sign_in(course_reg.user) |> get(build_url(course1.id, protected_assessment.id)) assert response(conn, 403) == "Missing Password." end end - @tag :skip test "returns 403 when password is wrong/invalid", %{ conn: conn, + courses: %{course1: course1}, + protected_assessment: protected_assessment, role_crs: role_crs } do - assessment = insert(:assessment, %{type: "practical", is_published: true}) - - assessment - |> Assessment.changeset(%{ - password: "mysupersecretpassword", - open_at: Timex.shift(Timex.now(), days: -2), - close_at: Timex.shift(Timex.now(), days: +1) - }) - |> Repo.update!() - - for {_role, user} <- role_crs do + for {_role, course_reg} <- role_crs do conn = conn - |> sign_in(user) - |> post(build_url_unlock(assessment.id), %{:password => "wrong"}) + |> sign_in(course_reg.user) + |> post(build_url_unlock(course1.id, protected_assessment.id), %{:password => "wrong"}) assert response(conn, 403) == "Invalid Password." end end - @tag :skip - test "allow role_crs with preexisting submission to access private assessment without a password", - %{ - conn: conn, - role_crs: %{student: student} - } do - assessment = insert(:assessment, %{type: "practical", is_published: true}) - - assessment - |> Assessment.changeset(%{ - password: "mysupersecretpassword", - open_at: Timex.shift(Timex.now(), days: -2), - close_at: Timex.shift(Timex.now(), days: +1) - }) - |> Repo.update!() - - insert(:submission, %{assessment: assessment, student: student}) - conn = conn |> sign_in(student) |> get(build_url(assessment.id)) + test "allow role_crs with preexisting submission to access private assessment without a password", %{ + conn: conn, + courses: %{course1: course1}, + protected_assessment: protected_assessment, + role_crs: %{student: student} + } do + insert(:submission, %{assessment: protected_assessment, student: student}) + conn = conn |> sign_in(student.user) |> get(build_url(course1.id, protected_assessment.id)) assert response(conn, 200) end - @tag :skip test "ignore password when assessment is not password protected", %{ conn: conn, + courses: %{course1: course1}, role_crs: role_crs, assessments: assessments } do assessment = assessments["mission"].assessment - for {_role, user} <- role_crs do + for {_role, course_reg} <- role_crs do conn = conn - |> sign_in(user) - |> post(build_url_unlock(assessment.id), %{:password => "wrong"}) + |> sign_in(course_reg.user) + |> post(build_url_unlock(course1.id, assessment.id), %{:password => "wrong"}) |> json_response(200) assert conn["id"] == assessment.id end end - @tag :skip test "render assessment when password is correct", %{ conn: conn, + courses: %{course1: course1}, + protected_assessment: protected_assessment, role_crs: role_crs, - assessments: assessments } do - assessment = assessments["mission"].assessment - - {:ok, _} = - assessment - |> Assessment.changeset(%{password: "mysupersecretpassword"}) - |> Repo.update() - - for {_role, user} <- role_crs do + for {_role, course_reg} <- role_crs do conn = conn - |> sign_in(user) - |> post(build_url_unlock(assessment.id), %{:password => "mysupersecretpassword"}) + |> sign_in(course_reg.user) + |> post(build_url_unlock(course1.id, protected_assessment.id), %{:password => "mysupersecretpassword"}) |> json_response(200) - assert conn["id"] == assessment.id + assert conn["id"] == protected_assessment.id end end - @tag :skip test "permit global access to private assessment after closed", %{ conn: conn, + courses: %{course1: course1}, role_crs: %{student: student}, assessments: %{"mission" => mission} } do @@ -1377,16 +1357,13 @@ defmodule CadetWeb.AssessmentsControllerTest do conn = conn - |> sign_in(student) - |> get(build_url(mission.assessment.id)) + |> sign_in(student.user) + |> get(build_url(course1.id, mission.assessment.id)) assert response(conn, 200) end end - defp build_url, do: "/v2/assessments/" - defp build_url_submit(assessment_id), do: "/v2/assessments/#{assessment_id}/submit" - defp build_url_unlock(assessment_id), do: "/v2/assessments/#{assessment_id}/unlock" defp build_url(course_id), do: "/v2/courses/#{course_id}/assessments/" defp build_url(course_id, assessment_id), From d810fc20f535b942a7b8df10d92a0e1fb3ffa155 Mon Sep 17 00:00:00 2001 From: chownces <53928333+chownces@users.noreply.github.com> Date: Fri, 2 Jul 2021 18:29:44 +0800 Subject: [PATCH 102/174] Updated assessment config json to proper camelCase --- lib/cadet_web/admin_views/admin_courses_view.ex | 2 +- .../admin_courses_controller_test.exs | 12 ++++++------ 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/lib/cadet_web/admin_views/admin_courses_view.ex b/lib/cadet_web/admin_views/admin_courses_view.ex index ce1f6d63d..982b40974 100644 --- a/lib/cadet_web/admin_views/admin_courses_view.ex +++ b/lib/cadet_web/admin_views/admin_courses_view.ex @@ -7,7 +7,7 @@ defmodule CadetWeb.AdminCoursesView do def render("config.json", %{config: config}) do transform_map_for_view(config, %{ - AssessmentConfigId: :id, + assessmentConfigId: :id, type: :type, buildHidden: :build_hidden, buildSolution: :build_solution, diff --git a/test/cadet_web/admin_controllers/admin_courses_controller_test.exs b/test/cadet_web/admin_controllers/admin_courses_controller_test.exs index 121115033..1bb18cb3e 100644 --- a/test/cadet_web/admin_controllers/admin_courses_controller_test.exs +++ b/test/cadet_web/admin_controllers/admin_courses_controller_test.exs @@ -170,7 +170,7 @@ defmodule CadetWeb.AdminCoursesControllerTest do "buildHidden" => false, "isContest" => false, "type" => "Mission1", - "AssessmentConfigId" => config1.id + "assessmentConfigId" => config1.id }, %{ "earlySubmissionXp" => 200, @@ -179,7 +179,7 @@ defmodule CadetWeb.AdminCoursesControllerTest do "buildHidden" => false, "isContest" => false, "type" => "Mission2", - "AssessmentConfigId" => config2.id + "assessmentConfigId" => config2.id }, %{ "earlySubmissionXp" => 200, @@ -188,7 +188,7 @@ defmodule CadetWeb.AdminCoursesControllerTest do "buildHidden" => false, "isContest" => false, "type" => "Mission3", - "AssessmentConfigId" => config3.id + "assessmentConfigId" => config3.id } ] @@ -216,7 +216,7 @@ defmodule CadetWeb.AdminCoursesControllerTest do params = %{ "assessmentConfigs" => [ %{ - "AssessmentConfigId" => config.id, + "assessmentConfigId" => config.id, "courseId" => course_id, "type" => "Missions", "earlySubmissionXp" => 100, @@ -224,7 +224,7 @@ defmodule CadetWeb.AdminCoursesControllerTest do "decayRatePointsPerHour" => 1 }, %{ - "AssessmentConfigId" => -1, + "assessmentConfigId" => -1, "courseId" => course_id, "type" => "Paths", "earlySubmissionXp" => 100, @@ -316,7 +316,7 @@ defmodule CadetWeb.AdminCoursesControllerTest do params = %{ "assessmentConfig" => %{ - "AssessmentConfigId" => config1.id, + "assessmentConfigId" => config1.id, "courseId" => course_id, "type" => "Missions", "earlySubmissionXp" => 100, From a20db4931bae66315b8d689aa8081c8786b3b7dd Mon Sep 17 00:00:00 2001 From: chownces <53928333+chownces@users.noreply.github.com> Date: Fri, 2 Jul 2021 18:30:37 +0800 Subject: [PATCH 103/174] Remove capitalisation of assessment types inside assessment_config changeset --- lib/cadet/courses/assessment_config.ex | 6 ------ 1 file changed, 6 deletions(-) diff --git a/lib/cadet/courses/assessment_config.ex b/lib/cadet/courses/assessment_config.ex index a5661828d..78370ffe8 100644 --- a/lib/cadet/courses/assessment_config.ex +++ b/lib/cadet/courses/assessment_config.ex @@ -28,8 +28,6 @@ defmodule Cadet.Courses.AssessmentConfig do hours_before_early_xp_decay build_solution build_hidden is_contest)a def changeset(assessment_config, params) do - params = capitalize(params, :type) - assessment_config |> cast(params, @required_fields ++ @optional_fields) |> validate_required(@required_fields) @@ -39,8 +37,4 @@ defmodule Cadet.Courses.AssessmentConfig do |> validate_number(:hours_before_early_xp_decay, greater_than_or_equal_to: 0) |> unique_constraint([:order, :course_id]) end - - defp capitalize(params, field) do - Map.update(params, field, nil, &String.capitalize/1) - end end From 2543309bd99554e1b135ff438d24b69bf05ac567 Mon Sep 17 00:00:00 2001 From: YaleChen299 Date: Sat, 3 Jul 2021 02:31:27 +0800 Subject: [PATCH 104/174] update /user call --- lib/cadet/courses/courses.ex | 2 +- lib/cadet_web/views/user_view.ex | 17 +++++++++++++---- .../controllers/user_controller_test.exs | 14 ++++++++------ 3 files changed, 22 insertions(+), 11 deletions(-) diff --git a/lib/cadet/courses/courses.ex b/lib/cadet/courses/courses.ex index 51521eef1..d2994fa54 100644 --- a/lib/cadet/courses/courses.ex +++ b/lib/cadet/courses/courses.ex @@ -67,7 +67,7 @@ defmodule Cadet.Courses do Updates the general course configuration for the specified course """ @spec update_course_config(integer, %{}) :: - {:ok, %Course{}} | {:error, {:bad_request, String.t()}} | {:error, Ecto.Changeset.t()} + {:ok, %Course{}} | {:error, Ecto.Changeset.t()} | {:error, {:bad_request, String.t()}} def update_course_config(course_id, params) when is_ecto_id(course_id) do case retrieve_course(course_id) do nil -> diff --git a/lib/cadet_web/views/user_view.ex b/lib/cadet_web/views/user_view.ex index 15c5c24b9..8f3577bde 100644 --- a/lib/cadet_web/views/user_view.ex +++ b/lib/cadet_web/views/user_view.ex @@ -22,7 +22,12 @@ defmodule CadetWeb.UserView do xp: xp, story: story }), - courseConfiguration: render_config(latest) + courseConfiguration: render_config(latest), + assessmentConfigs: + case latest do + nil -> nil + latest -> Enum.map(latest.course.assessment_config, fn x -> x.type end) + end } end @@ -40,7 +45,12 @@ defmodule CadetWeb.UserView do xp: xp, story: story }), - courseConfiguration: render_config(latest) + courseConfiguration: render_config(latest), + assessmentConfigs: + case latest do + nil -> nil + latest -> Enum.map(latest.course.assessment_config, fn x -> x.type end) + end } end @@ -100,8 +110,7 @@ defmodule CadetWeb.UserView do enableSourcecast: :enable_sourcecast, sourceChapter: :source_chapter, sourceVariant: :source_variant, - moduleHelpText: :module_help_text, - assessmentTypes: &Enum.map(&1.assessment_config, fn x -> x.type end) + moduleHelpText: :module_help_text }) end end diff --git a/test/cadet_web/controllers/user_controller_test.exs b/test/cadet_web/controllers/user_controller_test.exs index d2a745a9c..7c44a5a64 100644 --- a/test/cadet_web/controllers/user_controller_test.exs +++ b/test/cadet_web/controllers/user_controller_test.exs @@ -89,7 +89,6 @@ defmodule CadetWeb.UserControllerTest do "story" => nil }, "courseConfiguration" => %{ - "assessmentTypes" => ["test type 1", "test type 2", "test type 3"], "enableAchievements" => true, "enableGame" => true, "enableSourcecast" => true, @@ -99,7 +98,8 @@ defmodule CadetWeb.UserControllerTest do "sourceChapter" => 1, "sourceVariant" => "default", "viewable" => true - } + }, + "assessmentConfigs" => ["test type 1", "test type 2", "test type 3"], } assert expected == resp @@ -121,7 +121,8 @@ defmodule CadetWeb.UserControllerTest do "courses" => [] }, "courseRegistration" => nil, - "courseConfiguration" => nil + "courseConfiguration" => nil, + "assessmentConfigs" => nil, } assert expected == resp @@ -362,7 +363,6 @@ defmodule CadetWeb.UserControllerTest do "story" => nil }, "courseConfiguration" => %{ - "assessmentTypes" => [], "enableAchievements" => true, "enableGame" => true, "enableSourcecast" => true, @@ -372,7 +372,8 @@ defmodule CadetWeb.UserControllerTest do "sourceChapter" => 1, "sourceVariant" => "default", "viewable" => true - } + }, + "assessmentConfigs" => [] } assert expected == resp @@ -387,7 +388,8 @@ defmodule CadetWeb.UserControllerTest do expected = %{ "courseRegistration" => nil, - "courseConfiguration" => nil + "courseConfiguration" => nil, + "assessmentConfigs" => nil } assert expected == resp From abb8c8546b88aee076df7bd54d965c563cd4d79c Mon Sep 17 00:00:00 2001 From: YaleChen299 Date: Sat, 3 Jul 2021 03:55:14 +0800 Subject: [PATCH 105/174] update xml parser for assessment with test --- lib/cadet/assessments/assessments.ex | 4 +- lib/cadet/jobs/xml_parser.ex | 32 ++-- .../admin_assessments_controller.ex | 8 +- test/cadet/updater/xml_parser_test.exs | 100 +++++++---- .../assessments_controller_test.exs | 167 +++++++++++------- test/support/xml_generator.ex | 15 +- 6 files changed, 195 insertions(+), 131 deletions(-) diff --git a/lib/cadet/assessments/assessments.ex b/lib/cadet/assessments/assessments.ex index ea79b3827..cdc9c5605 100644 --- a/lib/cadet/assessments/assessments.ex +++ b/lib/cadet/assessments/assessments.ex @@ -509,9 +509,9 @@ defmodule Cadet.Assessments do contest_submission_ids_length = Enum.count(contest_submission_ids) user_ids = - User + CourseRegistration |> where(role: "student") - |> select([u], u.id) + |> select([cr], cr.id) |> Repo.all() votes_per_user = min(contest_submission_ids_length, 10) diff --git a/lib/cadet/jobs/xml_parser.ex b/lib/cadet/jobs/xml_parser.ex index 0152f56a5..c07795b40 100644 --- a/lib/cadet/jobs/xml_parser.ex +++ b/lib/cadet/jobs/xml_parser.ex @@ -15,10 +15,10 @@ defmodule Cadet.Updater.XMLParser do quote do: is_list(unquote(term)) and unquote(term) != [] end - @spec parse_xml(String.t(), boolean()) :: + @spec parse_xml(String.t(), integer(), integer(), boolean()) :: :ok | {:ok, String.t()} | {:error, {atom(), String.t()}} - def parse_xml(xml, force_update \\ false) do - with {:ok, assessment_params} <- process_assessment(xml), + def parse_xml(xml, course_id, assessment_config_id, force_update \\ false) do + with {:ok, assessment_params} <- process_assessment(xml, course_id, assessment_config_id), {:ok, questions_params} <- process_questions(xml), {:ok, %{assessment: assessment}} <- Assessments.insert_or_update_assessments_and_questions( @@ -69,8 +69,8 @@ defmodule Cadet.Updater.XMLParser do |> List.foldr("", fn x, acc -> "#{acc <> x} " end) end - @spec process_assessment(String.t()) :: {:ok, map()} | {:error, String.t()} - defp process_assessment(xml) do + @spec process_assessment(String.t(), integer(), integer()) :: {:ok, map()} | {:error, String.t()} + defp process_assessment(xml, course_id, assessment_config_id) do open_at = Timex.now() |> Timex.beginning_of_day() @@ -84,7 +84,7 @@ defmodule Cadet.Updater.XMLParser do |> xpath( ~x"//TASK"e, access: ~x"./@access"s |> transform_by(&process_access/1), - type: ~x"./@kind"s |> transform_by(&change_quest_to_sidequest/1), + # type: ~x"./@kind"s |> transform_by(&change_quest_to_sidequest/1), title: ~x"./@title"s, number: ~x"./@number"s, story: ~x"./@story"s, @@ -97,6 +97,8 @@ defmodule Cadet.Updater.XMLParser do |> Map.put(:is_published, false) |> Map.put(:open_at, open_at) |> Map.put(:close_at, close_at) + |> Map.put(:course_id, course_id) + |> Map.put(:config_id, assessment_config_id) if assessment_params.access === "public" do _ = Map.put(assessment_params, :password, nil) @@ -121,14 +123,14 @@ defmodule Cadet.Updater.XMLParser do "public" end - @spec change_quest_to_sidequest(String.t()) :: String.t() - defp change_quest_to_sidequest("quest") do - "sidequest" - end + # @spec change_quest_to_sidequest(String.t()) :: String.t() + # defp change_quest_to_sidequest("quest") do + # "sidequest" + # end - defp change_quest_to_sidequest(type) when is_binary(type) do - type - end + # defp change_quest_to_sidequest(type) when is_binary(type) do + # type + # end @spec process_questions(String.t()) :: {:ok, [map()]} | {:error, String.t()} defp process_questions(xml) do @@ -140,13 +142,13 @@ defmodule Cadet.Updater.XMLParser do |> xpath( ~x"//PROBLEMS/PROBLEM"el, type: ~x"./@type"o |> transform_by(&process_charlist/1), - max_grade: ~x"./@maxgrade"oi, + # max_grade: ~x"./@maxgrade"oi, max_xp: ~x"./@maxxp"oi, entity: ~x"." ) |> Enum.map(fn param -> with {:no_missing_attr?, true} <- - {:no_missing_attr?, not is_nil(param[:type]) and not is_nil(param[:max_grade])}, + {:no_missing_attr?, not is_nil(param[:type]) and not is_nil(param[:max_xp])}, question when is_map(question) <- process_question_by_question_type(param), question when is_map(question) <- process_question_library(question, default_library, default_grading_library), diff --git a/lib/cadet_web/admin_controllers/admin_assessments_controller.ex b/lib/cadet_web/admin_controllers/admin_assessments_controller.ex index 256ec5832..0e741b75a 100644 --- a/lib/cadet_web/admin_controllers/admin_assessments_controller.ex +++ b/lib/cadet_web/admin_controllers/admin_assessments_controller.ex @@ -4,17 +4,17 @@ defmodule CadetWeb.AdminAssessmentsController do use PhoenixSwagger alias Cadet.Assessments - import Cadet.Updater.XMLParser, only: [parse_xml: 2] + import Cadet.Updater.XMLParser, only: [parse_xml: 4] - def create(conn, %{"assessment" => assessment, "forceUpdate" => force_update}) do + def create(conn, %{"course_id" => course_id, "assessment" => assessment, "forceUpdate" => force_update, "assessmentConfigId" => assessment_config_id}) do file = assessment["file"].path |> File.read!() result = case force_update do - "true" -> parse_xml(file, true) - "false" -> parse_xml(file, false) + "true" -> parse_xml(file, course_id, assessment_config_id, true) + "false" -> parse_xml(file, course_id, assessment_config_id, false) end case result do diff --git a/test/cadet/updater/xml_parser_test.exs b/test/cadet/updater/xml_parser_test.exs index 2d44cc02d..e13e3bc64 100644 --- a/test/cadet/updater/xml_parser_test.exs +++ b/test/cadet/updater/xml_parser_test.exs @@ -9,29 +9,51 @@ defmodule Cadet.Updater.XMLParserTest do import ExUnit.CaptureLog setup do + course = insert(:course) + assessment_configs = [ + insert(:assessment_config, %{course: course, order: 1, type: "mission"}), + insert(:assessment_config, %{course: course, order: 2}), + insert(:assessment_config, %{ + course: course, + order: 3, + build_hidden: true, + build_solution: true, + type: "path" + }), + insert(:assessment_config, %{course: course, order: 4, is_contest: true}), + insert(:assessment_config, %{ + course: course, + order: 5, + build_solution: true, + type: "practical" + }) + ] + assessments = Enum.map( - Assessment.assessment_types(), - &build(:assessment, type: &1, is_published: true) + assessment_configs, + &build(:assessment, course_id: course.id, course: course, config: &1, config_id: &1.id, is_published: true) ) - assessments_with_type = Enum.into(assessments, %{}, &{&1.type, &1}) + assessments_with_config = Enum.into(assessments, %{}, &{&1, &1.config}) questions = build_list(5, :question, assessment: nil) %{ assessments: assessments, questions: questions, - assessments_with_type: assessments_with_type + course: course, + assessment_configs: assessment_configs, + assessments_with_config: assessments_with_config } end describe "Pure XML Parser" do - test "XML Parser happy path", %{assessments: assessments, questions: questions} do - for assessment <- assessments do + test "XML Parser happy path", %{questions: questions, course: course, assessments_with_config: assessments_with_config} do + for {assessment, assessment_config} <- assessments_with_config do xml = XMLGenerator.generate_xml_for(assessment, questions) - assert XMLParser.parse_xml(xml) == :ok + assert XMLParser.parse_xml(xml, course.id, assessment_config.id) == :ok number = assessment.number @@ -53,11 +75,13 @@ defmodule Cadet.Updater.XMLParserTest do |> Map.put(:open_at, open_at) |> Map.put(:close_at, close_at) |> Map.put(:is_published, false) + |> Map.put(:course_id, course.id) + |> Map.put(:config_id, assessment_config.id) assert_map_keys( Map.from_struct(expected_assesment), Map.from_struct(assessment_db), - ~w(title is_published type summary_short summary_long open_at close_at)a ++ + ~w(title is_published config_id course_id summary_short summary_long open_at close_at)a ++ ~w(number story reading password)a ) @@ -80,10 +104,9 @@ defmodule Cadet.Updater.XMLParserTest do end test "happy path existing still closed assessment", %{ - assessments: assessments, - questions: questions + questions: questions, course: course, assessments_with_config: assessments_with_config } do - for assessment <- assessments do + for {assessment, assessment_config} <- assessments_with_config do still_closed_assessment = Map.from_struct(%{ assessment @@ -97,18 +120,18 @@ defmodule Cadet.Updater.XMLParserTest do xml = XMLGenerator.generate_xml_for(assessment, questions) - assert XMLParser.parse_xml(xml) == :ok + assert XMLParser.parse_xml(xml, course.id, assessment_config.id) == :ok end end - test "PROBLEM with missing type", %{assessments: assessments, questions: questions} do - for assessment <- assessments do + test "PROBLEM with missing type", %{questions: questions, course: course, assessments_with_config: assessments_with_config} do + for {assessment, assessment_config} <- assessments_with_config do xml = - XMLGenerator.generate_xml_for(assessment, questions, problem_permit_keys: ~w(maxgrade)a) + XMLGenerator.generate_xml_for(assessment, questions, problem_permit_keys: ~w(maxxp)a) assert capture_log(fn -> assert( - XMLParser.parse_xml(xml) == + XMLParser.parse_xml(xml, course.id, assessment_config.id) == {:error, {:bad_request, "Missing attribute(s) on PROBLEM"}} ) end) =~ @@ -116,13 +139,13 @@ defmodule Cadet.Updater.XMLParserTest do end end - test "PROBLEM with missing maxgrade", %{assessments: assessments, questions: questions} do - for assessment <- assessments do + test "PROBLEM with missing maxxp", %{questions: questions, course: course, assessments_with_config: assessments_with_config} do + for {assessment, assessment_config} <- assessments_with_config do xml = XMLGenerator.generate_xml_for(assessment, questions, problem_permit_keys: ~w(type)a) assert capture_log(fn -> assert( - XMLParser.parse_xml(xml) == + XMLParser.parse_xml(xml, course.id, assessment_config.id) == {:error, {:bad_request, "Missing attribute(s) on PROBLEM"}} ) end) =~ @@ -130,21 +153,21 @@ defmodule Cadet.Updater.XMLParserTest do end end - test "Invalid question type", %{assessments: assessments, questions: questions} do - for assessment <- assessments do + test "Invalid question type", %{questions: questions, course: course, assessments_with_config: assessments_with_config} do + for {assessment, assessment_config} <- assessments_with_config do xml = XMLGenerator.generate_xml_for(assessment, questions, override_type: "anu") assert capture_log(fn -> assert( - XMLParser.parse_xml(xml) == {:error, {:bad_request, "Invalid question type."}} + XMLParser.parse_xml(xml, course.id, assessment_config.id) == {:error, {:bad_request, "Invalid question type."}} ) end) =~ "Invalid question type." end end - test "Invalid question changeset", %{assessments: assessments, questions: questions} do - for assessment <- assessments do + test "Invalid question changeset", %{questions: questions, course: course, assessments_with_config: assessments_with_config} do + for {assessment, assessment_config} <- assessments_with_config do questions_without_content = Enum.map(questions, &%{&1 | question: %{&1.question | content: ""}}) @@ -152,27 +175,27 @@ defmodule Cadet.Updater.XMLParserTest do # the error message can be quite convoluted assert capture_log(fn -> - assert({:error, {:bad_request, _error_message}} = XMLParser.parse_xml(xml)) + assert({:error, {:bad_request, _error_message}} = XMLParser.parse_xml(xml, course.id, assessment_config.id)) end) =~ ~r/Invalid \b.*\b changeset\./ end end - test "missing DEPLOYMENT", %{assessments: assessments, questions: questions} do - for assessment <- assessments do + test "missing DEPLOYMENT", %{questions: questions, course: course, assessments_with_config: assessments_with_config} do + for {assessment, assessment_config} <- assessments_with_config do xml = XMLGenerator.generate_xml_for(assessment, questions, no_deployment: true) assert capture_log(fn -> assert( - XMLParser.parse_xml(xml) == {:error, {:bad_request, "Missing DEPLOYMENT"}} + XMLParser.parse_xml(xml, course.id, assessment_config.id) == {:error, {:bad_request, "Missing DEPLOYMENT"}} ) end) =~ "Missing DEPLOYMENT" end end - test "existing assessment with submissions", %{assessments: assessments, questions: questions} do - for assessment <- assessments do + test "existing assessment with submissions", %{questions: questions, course: course, assessments_with_config: assessments_with_config} do + for {assessment, assessment_config} <- assessments_with_config do already_open_assessment = Map.from_struct(%{ assessment @@ -197,7 +220,7 @@ defmodule Cadet.Updater.XMLParserTest do xml = XMLGenerator.generate_xml_for(assessment, questions) assert capture_log(fn -> - assert XMLParser.parse_xml(xml) == + assert XMLParser.parse_xml(xml, course.id, assessment_config.id) == {:ok, "Assessment has submissions, ignoring..."} end) =~ "Assessment has submissions, ignoring..." @@ -207,22 +230,21 @@ defmodule Cadet.Updater.XMLParserTest do describe "XML file processing" do test "happy path", %{ - assessments_with_type: assessments_with_type, - questions: questions + questions: questions, course: course, assessments_with_config: assessments_with_config } do - for {_, assessment} <- assessments_with_type do + for {assessment, assessment_config} <- assessments_with_config do xml = XMLGenerator.generate_xml_for(assessment, questions) - assert :ok == XMLParser.parse_xml(xml) + assert :ok == XMLParser.parse_xml(xml, course.id, assessment_config.id) end end - test "empty xml file" do + test "empty xml file", %{assessment_configs: [config| _], course: course} do assert capture_log(fn -> - assert {:error, {:bad_request, _}} = XMLParser.parse_xml("") + assert {:error, {:bad_request, _}} = XMLParser.parse_xml("", course.id, config.id) end) =~ ":expected_element_start_tag" end - test "valid xml file but invalid assessment xml" do + test "valid xml file but invalid assessment xml", %{assessment_configs: [config| _], course: course} do xml = """ Best markup language! @@ -233,7 +255,7 @@ defmodule Cadet.Updater.XMLParserTest do """ assert capture_log(fn -> - {:error, {:bad_request, "Missing TASK"}} == XMLParser.parse_xml(xml) + {:error, {:bad_request, "Missing TASK"}} == XMLParser.parse_xml(xml, course.id, config.id) end) =~ "Missing TASK" end end diff --git a/test/cadet_web/controllers/assessments_controller_test.exs b/test/cadet_web/controllers/assessments_controller_test.exs index b7d0774a3..29d53ca48 100644 --- a/test/cadet_web/controllers/assessments_controller_test.exs +++ b/test/cadet_web/controllers/assessments_controller_test.exs @@ -857,7 +857,11 @@ defmodule CadetWeb.AssessmentsControllerTest do end describe "GET /assessment_id/submit unauthenticated" do - test "is not permitted", %{conn: conn, courses: %{course1: course1}, assessments: %{"mission" => %{assessment: assessment}}} do + test "is not permitted", %{ + conn: conn, + courses: %{course1: course1}, + assessments: %{"mission" => %{assessment: assessment}} + } do conn = post(conn, build_url_submit(course1.id, assessment.id)) assert response(conn, 401) == "Unauthorised" end @@ -875,7 +879,12 @@ defmodule CadetWeb.AssessmentsControllerTest do } do with_mock GradingJob, force_grade_individual_submission: fn _ -> nil end do - group = if(role == :student, do: insert(:group, %{course: course1, leader: role_crs.staff}), else: nil) + group = + if(role == :student, + do: insert(:group, %{course: course1, leader: role_crs.staff}), + else: nil + ) + course_reg = insert(:course_registration, %{role: role, group: group, course: course1}) submission = @@ -901,15 +910,17 @@ defmodule CadetWeb.AssessmentsControllerTest do test "submission of answer within early hours(seeded 48) of opening grants full XP bonus", %{ conn: conn, courses: %{course1: course1}, - role_crs: role_crs, + role_crs: role_crs } do with_mock GradingJob, force_grade_individual_submission: fn _ -> nil end do - assessment_config = insert( - :assessment_config, - early_submission_xp: 100, - hours_before_early_xp_decay: 48, - course: course1 - ) + assessment_config = + insert( + :assessment_config, + early_submission_xp: 100, + hours_before_early_xp_decay: 48, + course: course1 + ) + assessment = insert( :assessment, @@ -923,7 +934,9 @@ defmodule CadetWeb.AssessmentsControllerTest do question = insert(:programming_question, assessment: assessment) group = insert(:group, leader: role_crs.staff) - course_reg = insert(:course_registration, %{role: :student, group: group, course: course1}) + + course_reg = + insert(:course_registration, %{role: :student, group: group, course: course1}) submission = insert(:submission, assessment: assessment, student: course_reg, status: :attempted) @@ -950,16 +963,18 @@ defmodule CadetWeb.AssessmentsControllerTest do test "submission of answer after early hours before deadline get decaying XP bonus", %{ conn: conn, courses: %{course1: course1}, - role_crs: role_crs, + role_crs: role_crs } do with_mock GradingJob, force_grade_individual_submission: fn _ -> nil end do for hours_after <- 48..148 do - assessment_config = insert( - :assessment_config, - early_submission_xp: 100, - hours_before_early_xp_decay: 48, - course: course1 - ) + assessment_config = + insert( + :assessment_config, + early_submission_xp: 100, + hours_before_early_xp_decay: 48, + course: course1 + ) + assessment = insert( :assessment, @@ -973,7 +988,9 @@ defmodule CadetWeb.AssessmentsControllerTest do question = insert(:programming_question, assessment: assessment) group = insert(:group, leader: role_crs.staff) - course_reg = insert(:course_registration, %{role: :student, group: group, course: course1}) + + course_reg = + insert(:course_registration, %{role: :student, group: group, course: course1}) submission = insert(:submission, assessment: assessment, student: course_reg, status: :attempted) @@ -991,7 +1008,10 @@ defmodule CadetWeb.AssessmentsControllerTest do |> response(200) submission_db = Repo.get(Submission, submission.id) - proportion = Timex.diff(assessment.close_at, Timex.now(), :hours) / (100 + hours_after - 48) + + proportion = + Timex.diff(assessment.close_at, Timex.now(), :hours) / (100 + hours_after - 48) + assert submission_db.status == :submitted assert submission_db.xp_bonus == round(proportion * 100) end @@ -1001,16 +1021,18 @@ defmodule CadetWeb.AssessmentsControllerTest do test "submission of answer at the last hour yield 0 XP bonus", %{ conn: conn, courses: %{course1: course1}, - role_crs: role_crs, + role_crs: role_crs } do with_mock GradingJob, force_grade_individual_submission: fn _ -> nil end do for hours_after <- 48..148 do - assessment_config = insert( - :assessment_config, - early_submission_xp: 100, - hours_before_early_xp_decay: 48, - course: course1 - ) + assessment_config = + insert( + :assessment_config, + early_submission_xp: 100, + hours_before_early_xp_decay: 48, + course: course1 + ) + assessment = insert( :assessment, @@ -1024,7 +1046,9 @@ defmodule CadetWeb.AssessmentsControllerTest do question = insert(:programming_question, assessment: assessment) group = insert(:group, leader: role_crs.staff) - course_reg = insert(:course_registration, %{role: :student, group: group, course: course1}) + + course_reg = + insert(:course_registration, %{role: :student, group: group, course: course1}) submission = insert(:submission, assessment: assessment, student: course_reg, status: :attempted) @@ -1050,17 +1074,19 @@ defmodule CadetWeb.AssessmentsControllerTest do end test "give 0 bonus for configs with 0 max", %{ - conn: conn, courses: %{course1: course1}, role_crs: role_crs,} do + conn: conn, + courses: %{course1: course1}, + role_crs: role_crs + } do with_mock GradingJob, force_grade_individual_submission: fn _ -> nil end do - - for hours_after <- 0..148 do - assessment_config = insert( - :assessment_config, - early_submission_xp: 0, - hours_before_early_xp_decay: 48, - course: course1 - ) + assessment_config = + insert( + :assessment_config, + early_submission_xp: 0, + hours_before_early_xp_decay: 48, + course: course1 + ) assessment = insert( @@ -1075,7 +1101,9 @@ defmodule CadetWeb.AssessmentsControllerTest do question = insert(:programming_question, assessment: assessment) group = insert(:group, leader: role_crs.staff) - course_reg = insert(:course_registration, %{role: :student, group: group, course: course1}) + + course_reg = + insert(:course_registration, %{role: :student, group: group, course: course1}) submission = insert(:submission, assessment: assessment, student: course_reg, status: :attempted) @@ -1105,7 +1133,7 @@ defmodule CadetWeb.AssessmentsControllerTest do test "is not permitted for unattempted assessments", %{ conn: conn, courses: %{course1: course1}, - assessments: %{"mission" => %{assessment: assessment}}, + assessments: %{"mission" => %{assessment: assessment}} } do course_reg = insert(:course_registration, %{role: :student, course: course1}) @@ -1149,7 +1177,7 @@ defmodule CadetWeb.AssessmentsControllerTest do assert response(conn, 403) == "Assessment has already been submitted" end - test "is not permitted for closed assessments", %{conn: conn, courses: %{course1: course1},} do + test "is not permitted for closed assessments", %{conn: conn, courses: %{course1: course1}} do course_reg = insert(:course_registration, %{role: :student, course: course1}) # Only check for after-closing because submission shouldn't exist if unpublished or @@ -1175,7 +1203,12 @@ defmodule CadetWeb.AssessmentsControllerTest do assert response(conn, 403) == "Assessment not open" end - test "not found if not in same course", %{conn: conn, courses: %{course2: course2}, role_crs: %{student: student}, assessments: %{"mission" => %{assessment: assessment}}} do + test "not found if not in same course", %{ + conn: conn, + courses: %{course2: course2}, + role_crs: %{student: student}, + assessments: %{"mission" => %{assessment: assessment}} + } do # user is in both course, but assessment belongs to a course and no submission will be found conn = conn @@ -1185,9 +1218,15 @@ defmodule CadetWeb.AssessmentsControllerTest do assert response(conn, 404) == "Submission not found" end - test "forbidden if not in course", %{conn: conn, courses: %{course2: course2}, course_regs: %{students: students}, assessments: %{"mission" => %{assessment: assessment}}} do + test "forbidden if not in course", %{ + conn: conn, + courses: %{course2: course2}, + course_regs: %{students: students}, + assessments: %{"mission" => %{assessment: assessment}} + } do # user is not in the course student2 = hd(tl(students)) + conn = conn |> sign_in(student2.user) @@ -1200,7 +1239,7 @@ defmodule CadetWeb.AssessmentsControllerTest do test "graded count is updated when assessment is graded", %{ conn: conn, courses: %{course1: course1}, - assessment_configs: [config| _], + assessment_configs: [config | _], role_crs: %{staff: avenger} } do assessment = @@ -1217,7 +1256,8 @@ defmodule CadetWeb.AssessmentsControllerTest do course_reg = insert(:course_registration, role: :student, course: course1) - submission = insert(:submission, assessment: assessment, student: course_reg, status: :submitted) + submission = + insert(:submission, assessment: assessment, student: course_reg, status: :submitted) Enum.each( [question_one, question_two], @@ -1253,8 +1293,9 @@ defmodule CadetWeb.AssessmentsControllerTest do end describe "Password protected assessments render properly" do - setup %{courses: %{course1: course1}, assessment_configs: configs,} do - assessment = insert(:assessment, %{config: Enum.at(configs, 4), course: course1, is_published: true}) + setup %{courses: %{course1: course1}, assessment_configs: configs} do + assessment = + insert(:assessment, %{config: Enum.at(configs, 4), course: course1, is_published: true}) assessment |> Assessment.changeset(%{ @@ -1267,14 +1308,17 @@ defmodule CadetWeb.AssessmentsControllerTest do {:ok, protected_assessment: assessment} end - test "returns 403 when trying to access a password protected assessment without a password", %{ - conn: conn, - courses: %{course1: course1}, - protected_assessment: protected_assessment, - role_crs: role_crs - } do + test "returns 403 when trying to access a password protected assessment without a password", + %{ + conn: conn, + courses: %{course1: course1}, + protected_assessment: protected_assessment, + role_crs: role_crs + } do for {_role, course_reg} <- role_crs do - conn = conn |> sign_in(course_reg.user) |> get(build_url(course1.id, protected_assessment.id)) + conn = + conn |> sign_in(course_reg.user) |> get(build_url(course1.id, protected_assessment.id)) + assert response(conn, 403) == "Missing Password." end end @@ -1295,12 +1339,13 @@ defmodule CadetWeb.AssessmentsControllerTest do end end - test "allow role_crs with preexisting submission to access private assessment without a password", %{ - conn: conn, - courses: %{course1: course1}, - protected_assessment: protected_assessment, - role_crs: %{student: student} - } do + test "allow role_crs with preexisting submission to access private assessment without a password", + %{ + conn: conn, + courses: %{course1: course1}, + protected_assessment: protected_assessment, + role_crs: %{student: student} + } do insert(:submission, %{assessment: protected_assessment, student: student}) conn = conn |> sign_in(student.user) |> get(build_url(course1.id, protected_assessment.id)) assert response(conn, 200) @@ -1329,13 +1374,15 @@ defmodule CadetWeb.AssessmentsControllerTest do conn: conn, courses: %{course1: course1}, protected_assessment: protected_assessment, - role_crs: role_crs, + role_crs: role_crs } do for {_role, course_reg} <- role_crs do conn = conn |> sign_in(course_reg.user) - |> post(build_url_unlock(course1.id, protected_assessment.id), %{:password => "mysupersecretpassword"}) + |> post(build_url_unlock(course1.id, protected_assessment.id), %{ + :password => "mysupersecretpassword" + }) |> json_response(200) assert conn["id"] == protected_assessment.id diff --git a/test/support/xml_generator.ex b/test/support/xml_generator.ex index 3e07b4e9b..0b56b6409 100644 --- a/test/support/xml_generator.ex +++ b/test/support/xml_generator.ex @@ -40,7 +40,6 @@ defmodule Cadet.Test.XMLGenerator do task( map_convert_keys(assessment, %{ access: :access, - type: :kind, number: :number, open_at: :startdate, close_at: :duedate, @@ -85,8 +84,8 @@ defmodule Cadet.Test.XMLGenerator do type = override_type || question.type map_permit_keys( - %{type: type, maxgrade: question.max_grade}, - permit_keys || ~w(type maxgrade)a + %{type: type, maxxp: question.max_xp}, + permit_keys || ~w(type maxxp)a ) end @@ -224,13 +223,7 @@ defmodule Cadet.Test.XMLGenerator do end defp task(raw_attrs, children) do - attrs = - Map.update!(raw_attrs, :kind, fn - "sidequest" -> "quest" - type -> type - end) - - {"TASK", map_permit_keys(attrs, ~w(kind number startdate duedate title story access)a), + {"TASK", map_permit_keys(raw_attrs, ~w(number startdate duedate title story access)a), children} end @@ -251,7 +244,7 @@ defmodule Cadet.Test.XMLGenerator do end defp problem(raw_attrs, children) do - {"PROBLEM", map_permit_keys(raw_attrs, ~w(maxgrade type)a), children} + {"PROBLEM", map_permit_keys(raw_attrs, ~w(maxxp type)a), children} end defp text(content) do From a5474b81d1a560dccf07c73cfca0971bc710a621 Mon Sep 17 00:00:00 2001 From: YaleChen299 Date: Sat, 3 Jul 2021 16:21:51 +0800 Subject: [PATCH 106/174] updated assessment config booleans --- lib/cadet/courses/assessment_config.ex | 14 +-- lib/cadet/jobs/xml_parser.ex | 3 +- .../admin_assessments_controller.ex | 7 +- .../admin_views/admin_courses_view.ex | 6 +- lib/cadet_web/views/assessments_helpers.ex | 11 ++- .../20210531155751_multitenant_upgrade.exs | 12 +-- test/cadet/updater/xml_parser_test.exs | 88 ++++++++++++++----- .../admin_assessments_controller_test.exs | 6 +- .../admin_courses_controller_test.exs | 20 ++--- .../assessments_controller_test.exs | 4 +- .../controllers/user_controller_test.exs | 4 +- test/support/seeds.ex | 8 +- 12 files changed, 120 insertions(+), 63 deletions(-) diff --git a/lib/cadet/courses/assessment_config.ex b/lib/cadet/courses/assessment_config.ex index 78370ffe8..81f526ce7 100644 --- a/lib/cadet/courses/assessment_config.ex +++ b/lib/cadet/courses/assessment_config.ex @@ -10,11 +10,13 @@ defmodule Cadet.Courses.AssessmentConfig do schema "assessment_configs" do field(:order, :integer) field(:type, :string) - field(:build_solution, :boolean, default: false) - # a graded assessment type will not build solutions to the frontend - field(:build_hidden, :boolean, default: false) - # backend will build public testcases with hidden private testcases and will build postpend. - field(:is_contest, :boolean, default: false) + field(:is_graded, :boolean, default: true) + + # a graded assessment type will not build solutions and private testcases as hidden to the frontend + field(:skippable, :boolean, default: true) + # only for frontend to determine if a student can go to next question without attempting + field(:is_autograded, :boolean, default: true) + # assessment will be autograded a day after due day field(:early_submission_xp, :integer, default: 0) field(:hours_before_early_xp_decay, :integer, default: 0) @@ -25,7 +27,7 @@ defmodule Cadet.Courses.AssessmentConfig do @required_fields ~w(course_id)a @optional_fields ~w(order type early_submission_xp - hours_before_early_xp_decay build_solution build_hidden is_contest)a + hours_before_early_xp_decay is_graded skippable is_autograded)a def changeset(assessment_config, params) do assessment_config diff --git a/lib/cadet/jobs/xml_parser.ex b/lib/cadet/jobs/xml_parser.ex index c07795b40..76026af96 100644 --- a/lib/cadet/jobs/xml_parser.ex +++ b/lib/cadet/jobs/xml_parser.ex @@ -69,7 +69,8 @@ defmodule Cadet.Updater.XMLParser do |> List.foldr("", fn x, acc -> "#{acc <> x} " end) end - @spec process_assessment(String.t(), integer(), integer()) :: {:ok, map()} | {:error, String.t()} + @spec process_assessment(String.t(), integer(), integer()) :: + {:ok, map()} | {:error, String.t()} defp process_assessment(xml, course_id, assessment_config_id) do open_at = Timex.now() diff --git a/lib/cadet_web/admin_controllers/admin_assessments_controller.ex b/lib/cadet_web/admin_controllers/admin_assessments_controller.ex index 0e741b75a..c39f23819 100644 --- a/lib/cadet_web/admin_controllers/admin_assessments_controller.ex +++ b/lib/cadet_web/admin_controllers/admin_assessments_controller.ex @@ -6,7 +6,12 @@ defmodule CadetWeb.AdminAssessmentsController do alias Cadet.Assessments import Cadet.Updater.XMLParser, only: [parse_xml: 4] - def create(conn, %{"course_id" => course_id, "assessment" => assessment, "forceUpdate" => force_update, "assessmentConfigId" => assessment_config_id}) do + def create(conn, %{ + "course_id" => course_id, + "assessment" => assessment, + "forceUpdate" => force_update, + "assessmentConfigId" => assessment_config_id + }) do file = assessment["file"].path |> File.read!() diff --git a/lib/cadet_web/admin_views/admin_courses_view.ex b/lib/cadet_web/admin_views/admin_courses_view.ex index 982b40974..b22cefee1 100644 --- a/lib/cadet_web/admin_views/admin_courses_view.ex +++ b/lib/cadet_web/admin_views/admin_courses_view.ex @@ -9,9 +9,9 @@ defmodule CadetWeb.AdminCoursesView do transform_map_for_view(config, %{ assessmentConfigId: :id, type: :type, - buildHidden: :build_hidden, - buildSolution: :build_solution, - isContest: :is_contest, + skippable: :skippable, + isGraded: :is_graded, + isAutograded: :is_autograded, earlySubmissionXp: :early_submission_xp, hoursBeforeEarlyXpDecay: :hours_before_early_xp_decay }) diff --git a/lib/cadet_web/views/assessments_helpers.ex b/lib/cadet_web/views/assessments_helpers.ex index a2147e327..b8e0d030f 100644 --- a/lib/cadet_web/views/assessments_helpers.ex +++ b/lib/cadet_web/views/assessments_helpers.ex @@ -66,7 +66,7 @@ defmodule CadetWeb.AssessmentsHelpers do question: %{question: question, type: question_type}, assessment: %{config: assessment_config} }) do - if assessment_config.build_solution do + if !assessment_config.is_graded do solution_getter = case question_type do :programming -> &Map.get(&1, "solution") @@ -181,9 +181,8 @@ defmodule CadetWeb.AssessmentsHelpers do Enum.map(&1["private"], fn testcase -> build_testcase(testcase, "private") end) ) - # :TODO another indicator for whether to build all testcases - # assessment_config == "path" -> - assessment_config.build_hidden -> + # build hidden testcases if ungraded + !assessment_config.is_graded -> &Enum.concat( Enum.map(&1["public"], fn testcase -> build_testcase(testcase, "public") end), Enum.map(&1["private"], fn testcase -> build_testcase(testcase, "hidden") end) @@ -195,9 +194,9 @@ defmodule CadetWeb.AssessmentsHelpers do end defp build_postpend(%{assessment_config: assessment_config}, all_testcases?) do - case {all_testcases?, assessment_config.build_hidden} do + case {all_testcases?, assessment_config.is_graded} do {true, _} -> & &1["postpend"] - {_, true} -> & &1["postpend"] + {_, false} -> & &1["postpend"] # Create a 1-arity function to return an empty postpend for non-paths _ -> fn _question -> "" end end diff --git a/priv/repo/migrations/20210531155751_multitenant_upgrade.exs b/priv/repo/migrations/20210531155751_multitenant_upgrade.exs index fad340a8f..8db6e2515 100644 --- a/priv/repo/migrations/20210531155751_multitenant_upgrade.exs +++ b/priv/repo/migrations/20210531155751_multitenant_upgrade.exs @@ -28,9 +28,9 @@ defmodule Cadet.Repo.Migrations.MultitenantUpgrade do add(:order, :integer, null: true) add(:type, :string, null: false) add(:course_id, references(:courses), null: false) - add(:build_solution, :boolean, null: false, default: false) - add(:is_contest, :boolean, null: false, default: false) - add(:build_hidden, :boolean, null: false, default: false) + add(:is_graded, :boolean, null: false, default: true) + add(:is_autograded, :boolean, null: false, default: true) + add(:skippable, :boolean, null: false, default: true) add(:early_submission_xp, :integer, null: false) add(:hours_before_early_xp_decay, :integer, null: false) timestamps() @@ -219,9 +219,9 @@ defmodule Cadet.Repo.Migrations.MultitenantUpgrade do |> AssessmentConfig.changeset(%{ type: assessment_type, course_id: course.id, - build_solution: assessment_type in ["Paths", "Others"], - build_hidden: assessment_type == "Paths", - is_contest: assessment_type == "Contests", + is_graded: assessment_type in ["Missions", "Quests", "Contests"], + skippable: assessment_type == "Paths", + is_autograded: assessment_type != "Contests", early_submission_xp: 200, hours_before_early_xp_decay: 48 }) diff --git a/test/cadet/updater/xml_parser_test.exs b/test/cadet/updater/xml_parser_test.exs index e13e3bc64..6198baee1 100644 --- a/test/cadet/updater/xml_parser_test.exs +++ b/test/cadet/updater/xml_parser_test.exs @@ -10,21 +10,22 @@ defmodule Cadet.Updater.XMLParserTest do setup do course = insert(:course) + assessment_configs = [ insert(:assessment_config, %{course: course, order: 1, type: "mission"}), insert(:assessment_config, %{course: course, order: 2}), insert(:assessment_config, %{ course: course, order: 3, - build_hidden: true, - build_solution: true, + skippable: false, + is_graded: false, type: "path" }), - insert(:assessment_config, %{course: course, order: 4, is_contest: true}), + insert(:assessment_config, %{course: course, order: 4, is_autograded: false}), insert(:assessment_config, %{ course: course, order: 5, - build_solution: true, + is_graded: false, type: "practical" }) ] @@ -32,7 +33,13 @@ defmodule Cadet.Updater.XMLParserTest do assessments = Enum.map( assessment_configs, - &build(:assessment, course_id: course.id, course: course, config: &1, config_id: &1.id, is_published: true) + &build(:assessment, + course_id: course.id, + course: course, + config: &1, + config_id: &1.id, + is_published: true + ) ) assessments_with_config = Enum.into(assessments, %{}, &{&1, &1.config}) @@ -49,7 +56,11 @@ defmodule Cadet.Updater.XMLParserTest do end describe "Pure XML Parser" do - test "XML Parser happy path", %{questions: questions, course: course, assessments_with_config: assessments_with_config} do + test "XML Parser happy path", %{ + questions: questions, + course: course, + assessments_with_config: assessments_with_config + } do for {assessment, assessment_config} <- assessments_with_config do xml = XMLGenerator.generate_xml_for(assessment, questions) @@ -104,7 +115,9 @@ defmodule Cadet.Updater.XMLParserTest do end test "happy path existing still closed assessment", %{ - questions: questions, course: course, assessments_with_config: assessments_with_config + questions: questions, + course: course, + assessments_with_config: assessments_with_config } do for {assessment, assessment_config} <- assessments_with_config do still_closed_assessment = @@ -124,7 +137,11 @@ defmodule Cadet.Updater.XMLParserTest do end end - test "PROBLEM with missing type", %{questions: questions, course: course, assessments_with_config: assessments_with_config} do + test "PROBLEM with missing type", %{ + questions: questions, + course: course, + assessments_with_config: assessments_with_config + } do for {assessment, assessment_config} <- assessments_with_config do xml = XMLGenerator.generate_xml_for(assessment, questions, problem_permit_keys: ~w(maxxp)a) @@ -139,7 +156,11 @@ defmodule Cadet.Updater.XMLParserTest do end end - test "PROBLEM with missing maxxp", %{questions: questions, course: course, assessments_with_config: assessments_with_config} do + test "PROBLEM with missing maxxp", %{ + questions: questions, + course: course, + assessments_with_config: assessments_with_config + } do for {assessment, assessment_config} <- assessments_with_config do xml = XMLGenerator.generate_xml_for(assessment, questions, problem_permit_keys: ~w(type)a) @@ -153,20 +174,29 @@ defmodule Cadet.Updater.XMLParserTest do end end - test "Invalid question type", %{questions: questions, course: course, assessments_with_config: assessments_with_config} do + test "Invalid question type", %{ + questions: questions, + course: course, + assessments_with_config: assessments_with_config + } do for {assessment, assessment_config} <- assessments_with_config do xml = XMLGenerator.generate_xml_for(assessment, questions, override_type: "anu") assert capture_log(fn -> assert( - XMLParser.parse_xml(xml, course.id, assessment_config.id) == {:error, {:bad_request, "Invalid question type."}} + XMLParser.parse_xml(xml, course.id, assessment_config.id) == + {:error, {:bad_request, "Invalid question type."}} ) end) =~ "Invalid question type." end end - test "Invalid question changeset", %{questions: questions, course: course, assessments_with_config: assessments_with_config} do + test "Invalid question changeset", %{ + questions: questions, + course: course, + assessments_with_config: assessments_with_config + } do for {assessment, assessment_config} <- assessments_with_config do questions_without_content = Enum.map(questions, &%{&1 | question: %{&1.question | content: ""}}) @@ -175,26 +205,38 @@ defmodule Cadet.Updater.XMLParserTest do # the error message can be quite convoluted assert capture_log(fn -> - assert({:error, {:bad_request, _error_message}} = XMLParser.parse_xml(xml, course.id, assessment_config.id)) + assert( + {:error, {:bad_request, _error_message}} = + XMLParser.parse_xml(xml, course.id, assessment_config.id) + ) end) =~ ~r/Invalid \b.*\b changeset\./ end end - test "missing DEPLOYMENT", %{questions: questions, course: course, assessments_with_config: assessments_with_config} do + test "missing DEPLOYMENT", %{ + questions: questions, + course: course, + assessments_with_config: assessments_with_config + } do for {assessment, assessment_config} <- assessments_with_config do xml = XMLGenerator.generate_xml_for(assessment, questions, no_deployment: true) assert capture_log(fn -> assert( - XMLParser.parse_xml(xml, course.id, assessment_config.id) == {:error, {:bad_request, "Missing DEPLOYMENT"}} + XMLParser.parse_xml(xml, course.id, assessment_config.id) == + {:error, {:bad_request, "Missing DEPLOYMENT"}} ) end) =~ "Missing DEPLOYMENT" end end - test "existing assessment with submissions", %{questions: questions, course: course, assessments_with_config: assessments_with_config} do + test "existing assessment with submissions", %{ + questions: questions, + course: course, + assessments_with_config: assessments_with_config + } do for {assessment, assessment_config} <- assessments_with_config do already_open_assessment = Map.from_struct(%{ @@ -230,7 +272,9 @@ defmodule Cadet.Updater.XMLParserTest do describe "XML file processing" do test "happy path", %{ - questions: questions, course: course, assessments_with_config: assessments_with_config + questions: questions, + course: course, + assessments_with_config: assessments_with_config } do for {assessment, assessment_config} <- assessments_with_config do xml = XMLGenerator.generate_xml_for(assessment, questions) @@ -238,13 +282,16 @@ defmodule Cadet.Updater.XMLParserTest do end end - test "empty xml file", %{assessment_configs: [config| _], course: course} do + test "empty xml file", %{assessment_configs: [config | _], course: course} do assert capture_log(fn -> assert {:error, {:bad_request, _}} = XMLParser.parse_xml("", course.id, config.id) end) =~ ":expected_element_start_tag" end - test "valid xml file but invalid assessment xml", %{assessment_configs: [config| _], course: course} do + test "valid xml file but invalid assessment xml", %{ + assessment_configs: [config | _], + course: course + } do xml = """ Best markup language! @@ -255,7 +302,8 @@ defmodule Cadet.Updater.XMLParserTest do """ assert capture_log(fn -> - {:error, {:bad_request, "Missing TASK"}} == XMLParser.parse_xml(xml, course.id, config.id) + {:error, {:bad_request, "Missing TASK"}} == + XMLParser.parse_xml(xml, course.id, config.id) end) =~ "Missing TASK" end end diff --git a/test/cadet_web/admin_controllers/admin_assessments_controller_test.exs b/test/cadet_web/admin_controllers/admin_assessments_controller_test.exs index af331a334..81bde05eb 100644 --- a/test/cadet_web/admin_controllers/admin_assessments_controller_test.exs +++ b/test/cadet_web/admin_controllers/admin_assessments_controller_test.exs @@ -389,6 +389,8 @@ defmodule CadetWeb.AdminAssessmentsControllerTest do end end - defp build_url, do: "/v2/admin/assessments/" - defp build_url(assessment_id), do: "/v2/admin/assessments/#{assessment_id}" + defp build_url(course_id), do: "/v2/courses/#{course_id}/admin/assessments/" + + defp build_url(course_id, assessment_id), + do: "/v2/courses/#{course_id}/admin/assessments/#{assessment_id}" end diff --git a/test/cadet_web/admin_controllers/admin_courses_controller_test.exs b/test/cadet_web/admin_controllers/admin_courses_controller_test.exs index 1bb18cb3e..778207035 100644 --- a/test/cadet_web/admin_controllers/admin_courses_controller_test.exs +++ b/test/cadet_web/admin_controllers/admin_courses_controller_test.exs @@ -151,7 +151,7 @@ defmodule CadetWeb.AdminCoursesControllerTest do config2 = insert(:assessment_config, %{ - build_solution: true, + is_graded: false, order: 2, type: "Mission2", course: course @@ -166,27 +166,27 @@ defmodule CadetWeb.AdminCoursesControllerTest do %{ "earlySubmissionXp" => 200, "hoursBeforeEarlyXpDecay" => 48, - "buildSolution" => false, - "buildHidden" => false, - "isContest" => false, + "isAutograded" => true, + "isGraded" => true, + "skippable" => true, "type" => "Mission1", "assessmentConfigId" => config1.id }, %{ "earlySubmissionXp" => 200, "hoursBeforeEarlyXpDecay" => 48, - "buildSolution" => true, - "buildHidden" => false, - "isContest" => false, + "isAutograded" => true, + "isGraded" => false, + "skippable" => true, "type" => "Mission2", "assessmentConfigId" => config2.id }, %{ "earlySubmissionXp" => 200, "hoursBeforeEarlyXpDecay" => 48, - "buildSolution" => false, - "buildHidden" => false, - "isContest" => false, + "isAutograded" => true, + "isGraded" => true, + "skippable" => true, "type" => "Mission3", "assessmentConfigId" => config3.id } diff --git a/test/cadet_web/controllers/assessments_controller_test.exs b/test/cadet_web/controllers/assessments_controller_test.exs index 29d53ca48..95bec94b2 100644 --- a/test/cadet_web/controllers/assessments_controller_test.exs +++ b/test/cadet_web/controllers/assessments_controller_test.exs @@ -344,7 +344,7 @@ defmodule CadetWeb.AssessmentsControllerTest do "solutionTemplate" => &1.question.template, "prepend" => &1.question.prepend, "postpend" => - if assessment.config.build_hidden do + if not assessment.config.is_graded do &1.question.postpend else "" @@ -358,7 +358,7 @@ defmodule CadetWeb.AssessmentsControllerTest do do: {Atom.to_string(k), v} end ) ++ - if assessment.config.build_hidden do + if not assessment.config.is_graded do Enum.map( &1.question.private, fn testcase -> diff --git a/test/cadet_web/controllers/user_controller_test.exs b/test/cadet_web/controllers/user_controller_test.exs index 7c44a5a64..7377077de 100644 --- a/test/cadet_web/controllers/user_controller_test.exs +++ b/test/cadet_web/controllers/user_controller_test.exs @@ -99,7 +99,7 @@ defmodule CadetWeb.UserControllerTest do "sourceVariant" => "default", "viewable" => true }, - "assessmentConfigs" => ["test type 1", "test type 2", "test type 3"], + "assessmentConfigs" => ["test type 1", "test type 2", "test type 3"] } assert expected == resp @@ -122,7 +122,7 @@ defmodule CadetWeb.UserControllerTest do }, "courseRegistration" => nil, "courseConfiguration" => nil, - "assessmentConfigs" => nil, + "assessmentConfigs" => nil } assert expected == resp diff --git a/test/support/seeds.ex b/test/support/seeds.ex index 600526e20..aa7b1ee6b 100644 --- a/test/support/seeds.ex +++ b/test/support/seeds.ex @@ -97,15 +97,15 @@ defmodule Cadet.Test.Seeds do insert(:assessment_config, %{ course: course1, order: 3, - build_hidden: true, - build_solution: true, + skippable: false, + is_graded: false, type: "path" }), - insert(:assessment_config, %{course: course1, order: 4, is_contest: true}), + insert(:assessment_config, %{course: course1, order: 4, is_autograded: false}), insert(:assessment_config, %{ course: course1, order: 5, - build_solution: true, + is_graded: false, type: "practical" }) ] From 93367467f5d9b5748a375b8272fb3bc46b0f3b2c Mon Sep 17 00:00:00 2001 From: YaleChen299 Date: Sat, 3 Jul 2021 16:41:21 +0800 Subject: [PATCH 107/174] rename /user view field --- lib/cadet_web/views/user_view.ex | 4 ++-- test/cadet_web/controllers/user_controller_test.exs | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/lib/cadet_web/views/user_view.ex b/lib/cadet_web/views/user_view.ex index 8f3577bde..985356379 100644 --- a/lib/cadet_web/views/user_view.ex +++ b/lib/cadet_web/views/user_view.ex @@ -23,7 +23,7 @@ defmodule CadetWeb.UserView do story: story }), courseConfiguration: render_config(latest), - assessmentConfigs: + assessmentConfigurations: case latest do nil -> nil latest -> Enum.map(latest.course.assessment_config, fn x -> x.type end) @@ -46,7 +46,7 @@ defmodule CadetWeb.UserView do story: story }), courseConfiguration: render_config(latest), - assessmentConfigs: + assessmentConfigurations: case latest do nil -> nil latest -> Enum.map(latest.course.assessment_config, fn x -> x.type end) diff --git a/test/cadet_web/controllers/user_controller_test.exs b/test/cadet_web/controllers/user_controller_test.exs index 7377077de..02bec6cbc 100644 --- a/test/cadet_web/controllers/user_controller_test.exs +++ b/test/cadet_web/controllers/user_controller_test.exs @@ -99,7 +99,7 @@ defmodule CadetWeb.UserControllerTest do "sourceVariant" => "default", "viewable" => true }, - "assessmentConfigs" => ["test type 1", "test type 2", "test type 3"] + "assessmentConfigurations" => ["test type 1", "test type 2", "test type 3"] } assert expected == resp @@ -122,7 +122,7 @@ defmodule CadetWeb.UserControllerTest do }, "courseRegistration" => nil, "courseConfiguration" => nil, - "assessmentConfigs" => nil + "assessmentConfigurations" => nil } assert expected == resp @@ -373,7 +373,7 @@ defmodule CadetWeb.UserControllerTest do "sourceVariant" => "default", "viewable" => true }, - "assessmentConfigs" => [] + "assessmentConfigurations" => [] } assert expected == resp @@ -389,7 +389,7 @@ defmodule CadetWeb.UserControllerTest do expected = %{ "courseRegistration" => nil, "courseConfiguration" => nil, - "assessmentConfigs" => nil + "assessmentConfigurations" => nil } assert expected == resp From 9da4ae9cdbeaddcf39add39e2629a2e4b355b0e0 Mon Sep 17 00:00:00 2001 From: YaleChen299 Date: Sat, 3 Jul 2021 17:22:17 +0800 Subject: [PATCH 108/174] updated /user endpoint with skippable sent --- lib/cadet/courses/assessment_config.ex | 3 +-- lib/cadet_web/views/user_view.ex | 24 +++++++++++-------- .../controllers/user_controller_test.exs | 6 ++++- 3 files changed, 20 insertions(+), 13 deletions(-) diff --git a/lib/cadet/courses/assessment_config.ex b/lib/cadet/courses/assessment_config.ex index 81f526ce7..524073d34 100644 --- a/lib/cadet/courses/assessment_config.ex +++ b/lib/cadet/courses/assessment_config.ex @@ -11,8 +11,7 @@ defmodule Cadet.Courses.AssessmentConfig do field(:order, :integer) field(:type, :string) field(:is_graded, :boolean, default: true) - - # a graded assessment type will not build solutions and private testcases as hidden to the frontend + # a graded type will not build solutions and will build private testcases as hidden field(:skippable, :boolean, default: true) # only for frontend to determine if a student can go to next question without attempting field(:is_autograded, :boolean, default: true) diff --git a/lib/cadet_web/views/user_view.ex b/lib/cadet_web/views/user_view.ex index 985356379..4be4e34c2 100644 --- a/lib/cadet_web/views/user_view.ex +++ b/lib/cadet_web/views/user_view.ex @@ -23,11 +23,7 @@ defmodule CadetWeb.UserView do story: story }), courseConfiguration: render_config(latest), - assessmentConfigurations: - case latest do - nil -> nil - latest -> Enum.map(latest.course.assessment_config, fn x -> x.type end) - end + assessmentConfigurations: render_assessment_configs(latest) } end @@ -46,11 +42,7 @@ defmodule CadetWeb.UserView do story: story }), courseConfiguration: render_config(latest), - assessmentConfigurations: - case latest do - nil -> nil - latest -> Enum.map(latest.course.assessment_config, fn x -> x.type end) - end + assessmentConfigurations: render_assessment_configs(latest) } end @@ -114,4 +106,16 @@ defmodule CadetWeb.UserView do }) end end + + defp render_assessment_configs(latest) do + case latest do + nil -> + nil + + latest -> + Enum.map(latest.course.assessment_config, fn config -> + transform_map_for_view(config, %{type: :type, skippable: :skippable}) + end) + end + end end diff --git a/test/cadet_web/controllers/user_controller_test.exs b/test/cadet_web/controllers/user_controller_test.exs index 02bec6cbc..03e9b65b1 100644 --- a/test/cadet_web/controllers/user_controller_test.exs +++ b/test/cadet_web/controllers/user_controller_test.exs @@ -99,7 +99,11 @@ defmodule CadetWeb.UserControllerTest do "sourceVariant" => "default", "viewable" => true }, - "assessmentConfigurations" => ["test type 1", "test type 2", "test type 3"] + "assessmentConfigurations" => [ + %{"skippable" => true, "type" => "test type 1"}, + %{"skippable" => true, "type" => "test type 2"}, + %{"skippable" => true, "type" => "test type 3"} + ] } assert expected == resp From 417b4ac18ddc9ec0238c1ed85658ed55712e7897 Mon Sep 17 00:00:00 2001 From: YaleChen299 Date: Sat, 3 Jul 2021 18:47:16 +0800 Subject: [PATCH 109/174] updated admin assessment controller with test --- .../admin_assessments_controller_test.exs | 196 ++++++++++++++---- 1 file changed, 160 insertions(+), 36 deletions(-) diff --git a/test/cadet_web/admin_controllers/admin_assessments_controller_test.exs b/test/cadet_web/admin_controllers/admin_assessments_controller_test.exs index 81bde05eb..0f310020d 100644 --- a/test/cadet_web/admin_controllers/admin_assessments_controller_test.exs +++ b/test/cadet_web/admin_controllers/admin_assessments_controller_test.exs @@ -29,14 +29,26 @@ defmodule CadetWeb.AdminAssessmentsControllerTest do end describe "POST /, unauthenticated" do - test "unauthorized", %{conn: conn} do - assessment = build(:assessment, type: "mission", is_published: true) + test "unauthorized", %{ + conn: conn, + courses: %{course1: course1}, + assessment_configs: [config | _] + } do + assessment = + build(:assessment, + course_id: course1.id, + course: course1, + config: config, + config_id: config.id, + is_published: true + ) + questions = build_list(5, :question, assessment: nil) xml = XMLGenerator.generate_xml_for(assessment, questions) file = File.write("test/fixtures/local_repo/test.xml", xml) force_update = "false" body = %{assessment: file, forceUpdate: force_update} - conn = post(conn, build_url(), body) + conn = post(conn, build_url(course1.id), body) assert response(conn, 401) =~ "Unauthorised" end end @@ -44,12 +56,25 @@ defmodule CadetWeb.AdminAssessmentsControllerTest do describe "POST /, student only" do @tag authenticate: :student test "unauthorized", %{conn: conn} do - assessment = build(:assessment, type: "mission", is_published: true) + test_cr = conn.assigns.test_cr + course = test_cr.course + config = insert(:assessment_config, %{course: course}) + + assessment = + build(:assessment, + course: course, + course_id: course.id, + config: config, + config_id: config.id, + is_published: true + ) + questions = build_list(5, :question, assessment: nil) + xml = XMLGenerator.generate_xml_for(assessment, questions) force_update = "false" - body = %{assessment: xml, forceUpdate: force_update} - conn = post(conn, build_url(), body) + body = %{assessment: xml, forceUpdate: force_update, assessmentConfigId: config.id} + conn = post(conn, build_url(course.id), body) assert response(conn, 403) == "Forbidden" end end @@ -57,7 +82,19 @@ defmodule CadetWeb.AdminAssessmentsControllerTest do describe "POST /, staff only" do @tag authenticate: :staff test "successful", %{conn: conn} do - assessment = build(:assessment, type: "mission", is_published: true) + test_cr = conn.assigns.test_cr + course = test_cr.course + config = insert(:assessment_config, %{course: course}) + + assessment = + build(:assessment, + course: course, + course_id: course.id, + config: config, + config_id: config.id, + is_published: true + ) + questions = build_list(5, :question, assessment: nil) xml = XMLGenerator.generate_xml_for(assessment, questions) @@ -74,8 +111,13 @@ defmodule CadetWeb.AdminAssessmentsControllerTest do path: location } - body = %{assessment: %{file: formdata}, forceUpdate: force_update} - conn = post(conn, build_url(), body) + body = %{ + assessment: %{file: formdata}, + forceUpdate: force_update, + assessmentConfigId: config.id + } + + conn = post(conn, build_url(course.id), body) number = assessment.number expected_assessment = @@ -89,6 +131,10 @@ defmodule CadetWeb.AdminAssessmentsControllerTest do @tag authenticate: :staff test "upload empty xml", %{conn: conn} do + test_cr = conn.assigns.test_cr + course = test_cr.course + config = insert(:assessment_config, %{course: course}) + xml = "" force_update = "true" path = Path.join(@local_name, "connTest") @@ -103,13 +149,17 @@ defmodule CadetWeb.AdminAssessmentsControllerTest do path: location } - body = %{assessment: %{file: formdata}, forceUpdate: force_update} + body = %{ + assessment: %{file: formdata}, + forceUpdate: force_update, + assessmentConfigId: config.id + } err_msg = "Invalid XML fatal expected_element_start_tag file file_name_unknown line 1 col 1 " assert capture_log(fn -> - conn = post(conn, build_url(), body) + conn = post(conn, build_url(course.id), body) assert(response(conn, 400) == err_msg) end) =~ ~r/.*fatal: :expected_element_start_tag.*/ end @@ -118,7 +168,7 @@ defmodule CadetWeb.AdminAssessmentsControllerTest do describe "DELETE /:assessment_id, unauthenticated" do test "unauthorized", %{conn: conn} do assessment = insert(:assessment) - conn = delete(conn, build_url(assessment.id)) + conn = delete(conn, build_url(assessment.course.id, assessment.id)) assert response(conn, 401) =~ "Unauthorised" end end @@ -126,8 +176,11 @@ defmodule CadetWeb.AdminAssessmentsControllerTest do describe "DELETE /:assessment_id, student only" do @tag authenticate: :student test "unauthorized", %{conn: conn} do - assessment = insert(:assessment) - conn = delete(conn, build_url(assessment.id)) + test_cr = conn.assigns.test_cr + course = test_cr.course + config = insert(:assessment_config, %{course: course}) + assessment = insert(:assessment, %{course: course, config: config}) + conn = delete(conn, build_url(course.id, assessment.id)) assert response(conn, 403) == "Forbidden" end end @@ -135,8 +188,11 @@ defmodule CadetWeb.AdminAssessmentsControllerTest do describe "DELETE /:assessment_id, staff only" do @tag authenticate: :staff test "successful", %{conn: conn} do - assessment = insert(:assessment) - conn = delete(conn, build_url(assessment.id)) + test_cr = conn.assigns.test_cr + course = test_cr.course + config = insert(:assessment_config, %{course: course}) + assessment = insert(:assessment, %{course: course, config: config}) + conn = delete(conn, build_url(course.id, assessment.id)) assert response(conn, 200) == "OK" assert is_nil(Repo.get(Assessment, assessment.id)) end @@ -145,7 +201,7 @@ defmodule CadetWeb.AdminAssessmentsControllerTest do describe "POST /:assessment_id, unauthenticated, publish" do test "unauthorized", %{conn: conn} do assessment = insert(:assessment) - conn = post(conn, build_url(assessment.id), %{isPublished: true}) + conn = post(conn, build_url(assessment.course.id, assessment.id), %{isPublished: true}) assert response(conn, 401) =~ "Unauthorised" end end @@ -153,8 +209,11 @@ defmodule CadetWeb.AdminAssessmentsControllerTest do describe "POST /:assessment_id, student only, publish" do @tag authenticate: :student test "forbidden", %{conn: conn} do - assessment = insert(:assessment) - conn = post(conn, build_url(assessment.id), %{isPublished: true}) + test_cr = conn.assigns.test_cr + course = test_cr.course + config = insert(:assessment_config, %{course: course}) + assessment = insert(:assessment, %{course: course, config: config}) + conn = post(conn, build_url(course.id, assessment.id), %{isPublished: true}) assert response(conn, 403) == "Forbidden" end end @@ -162,8 +221,11 @@ defmodule CadetWeb.AdminAssessmentsControllerTest do describe "POST /:assessment_id, staff only, publish" do @tag authenticate: :staff test "successful toggle from published to unpublished", %{conn: conn} do - assessment = insert(:assessment, is_published: true) - conn = post(conn, build_url(assessment.id), %{isPublished: false}) + test_cr = conn.assigns.test_cr + course = test_cr.course + config = insert(:assessment_config, %{course: course}) + assessment = insert(:assessment, %{course: course, config: config}) + conn = post(conn, build_url(course.id, assessment.id), %{isPublished: false}) expected = Repo.get(Assessment, assessment.id).is_published assert response(conn, 200) == "OK" refute expected @@ -171,8 +233,11 @@ defmodule CadetWeb.AdminAssessmentsControllerTest do @tag authenticate: :staff test "successful toggle from unpublished to published", %{conn: conn} do - assessment = insert(:assessment, is_published: false) - conn = post(conn, build_url(assessment.id), %{isPublished: true}) + test_cr = conn.assigns.test_cr + course = test_cr.course + config = insert(:assessment_config, %{course: course}) + assessment = insert(:assessment, %{course: course, config: config, is_published: false}) + conn = post(conn, build_url(course.id, assessment.id), %{isPublished: true}) expected = Repo.get(Assessment, assessment.id).is_published assert response(conn, 200) == "OK" assert expected @@ -182,7 +247,7 @@ defmodule CadetWeb.AdminAssessmentsControllerTest do describe "POST /:assessment_id, unauthenticated" do test "unauthorized", %{conn: conn} do assessment = insert(:assessment) - conn = post(conn, build_url(assessment.id)) + conn = post(conn, build_url(assessment.course.id, assessment.id)) assert response(conn, 401) =~ "Unauthorised" end end @@ -190,6 +255,11 @@ defmodule CadetWeb.AdminAssessmentsControllerTest do describe "POST /:assessment_id, student only" do @tag authenticate: :student test "forbidden", %{conn: conn} do + test_cr = conn.assigns.test_cr + course = test_cr.course + config = insert(:assessment_config, %{course: course}) + assessment = insert(:assessment, %{course: course, config: config}) + new_open_at = Timex.now() |> Timex.beginning_of_day() @@ -207,8 +277,7 @@ defmodule CadetWeb.AdminAssessmentsControllerTest do |> Timex.format!("{ISO:Extended}") new_dates = %{openAt: new_open_at_string, closeAt: new_close_at_string} - assessment = insert(:assessment) - conn = post(conn, build_url(assessment.id), new_dates) + conn = post(conn, build_url(course.id, assessment.id), new_dates) assert response(conn, 403) == "Forbidden" end end @@ -216,6 +285,10 @@ defmodule CadetWeb.AdminAssessmentsControllerTest do describe "POST /:assessment_id, staff only" do @tag authenticate: :staff test "successful", %{conn: conn} do + test_cr = conn.assigns.test_cr + course = test_cr.course + config = insert(:assessment_config, %{course: course}) + open_at = Timex.now() |> Timex.beginning_of_day() @@ -223,7 +296,14 @@ defmodule CadetWeb.AdminAssessmentsControllerTest do |> Timex.shift(hours: 4) close_at = Timex.shift(open_at, days: 7) - assessment = insert(:assessment, %{open_at: open_at, close_at: close_at}) + + assessment = + insert(:assessment, %{ + course: course, + config: config, + open_at: open_at, + close_at: close_at + }) new_open_at = open_at @@ -245,7 +325,7 @@ defmodule CadetWeb.AdminAssessmentsControllerTest do conn = conn - |> post(build_url(assessment.id), new_dates) + |> post(build_url(course.id, assessment.id), new_dates) assessment = Repo.get(Assessment, assessment.id) assert response(conn, 200) == "OK" @@ -254,6 +334,10 @@ defmodule CadetWeb.AdminAssessmentsControllerTest do @tag authenticate: :staff test "allowed to change open time of opened assessments", %{conn: conn} do + test_cr = conn.assigns.test_cr + course = test_cr.course + config = insert(:assessment_config, %{course: course}) + open_at = Timex.now() |> Timex.beginning_of_day() @@ -261,7 +345,14 @@ defmodule CadetWeb.AdminAssessmentsControllerTest do |> Timex.shift(hours: 4) close_at = Timex.shift(open_at, days: 7) - assessment = insert(:assessment, %{open_at: open_at, close_at: close_at}) + + assessment = + insert(:assessment, %{ + course: course, + config: config, + open_at: open_at, + close_at: close_at + }) new_open_at = open_at @@ -279,7 +370,7 @@ defmodule CadetWeb.AdminAssessmentsControllerTest do conn = conn - |> post(build_url(assessment.id), new_dates) + |> post(build_url(course.id, assessment.id), new_dates) assessment = Repo.get(Assessment, assessment.id) assert response(conn, 200) == "OK" @@ -288,6 +379,10 @@ defmodule CadetWeb.AdminAssessmentsControllerTest do @tag authenticate: :staff test "not allowed to set close time to before open time", %{conn: conn} do + test_cr = conn.assigns.test_cr + course = test_cr.course + config = insert(:assessment_config, %{course: course}) + open_at = Timex.now() |> Timex.beginning_of_day() @@ -295,7 +390,14 @@ defmodule CadetWeb.AdminAssessmentsControllerTest do |> Timex.shift(hours: 4) close_at = Timex.shift(open_at, days: 7) - assessment = insert(:assessment, %{open_at: open_at, close_at: close_at}) + + assessment = + insert(:assessment, %{ + course: course, + config: config, + open_at: open_at, + close_at: close_at + }) new_close_at = open_at @@ -313,7 +415,7 @@ defmodule CadetWeb.AdminAssessmentsControllerTest do conn = conn - |> post(build_url(assessment.id), new_dates) + |> post(build_url(course.id, assessment.id), new_dates) assessment = Repo.get(Assessment, assessment.id) assert response(conn, 400) == "New end date should occur after new opening date" @@ -322,6 +424,10 @@ defmodule CadetWeb.AdminAssessmentsControllerTest do @tag authenticate: :staff test "successful, set close time to before current time", %{conn: conn} do + test_cr = conn.assigns.test_cr + course = test_cr.course + config = insert(:assessment_config, %{course: course}) + open_at = Timex.now() |> Timex.beginning_of_day() @@ -329,7 +435,14 @@ defmodule CadetWeb.AdminAssessmentsControllerTest do |> Timex.shift(hours: 4) close_at = Timex.shift(open_at, days: 7) - assessment = insert(:assessment, %{open_at: open_at, close_at: close_at}) + + assessment = + insert(:assessment, %{ + course: course, + config: config, + open_at: open_at, + close_at: close_at + }) new_close_at = Timex.now() @@ -347,7 +460,7 @@ defmodule CadetWeb.AdminAssessmentsControllerTest do conn = conn - |> post(build_url(assessment.id), new_dates) + |> post(build_url(course.id, assessment.id), new_dates) assessment = Repo.get(Assessment, assessment.id) assert response(conn, 200) == "OK" @@ -356,6 +469,10 @@ defmodule CadetWeb.AdminAssessmentsControllerTest do @tag authenticate: :staff test "successful, set open time to before current time", %{conn: conn} do + test_cr = conn.assigns.test_cr + course = test_cr.course + config = insert(:assessment_config, %{course: course}) + open_at = Timex.now() |> Timex.beginning_of_day() @@ -363,7 +480,14 @@ defmodule CadetWeb.AdminAssessmentsControllerTest do |> Timex.shift(hours: 4) close_at = Timex.shift(open_at, days: 7) - assessment = insert(:assessment, %{open_at: open_at, close_at: close_at}) + + assessment = + insert(:assessment, %{ + course: course, + config: config, + open_at: open_at, + close_at: close_at + }) new_open_at = Timex.now() @@ -381,7 +505,7 @@ defmodule CadetWeb.AdminAssessmentsControllerTest do conn = conn - |> post(build_url(assessment.id), new_dates) + |> post(build_url(course.id, assessment.id), new_dates) assessment = Repo.get(Assessment, assessment.id) assert response(conn, 200) == "OK" From aa81d40e10021379f73847726e87e95f68864380 Mon Sep 17 00:00:00 2001 From: YaleChen299 Date: Sat, 3 Jul 2021 22:39:53 +0800 Subject: [PATCH 110/174] update /user call and remove settings test --- lib/cadet_web/views/user_view.ex | 9 +++- test/cadet/settings/settings_test.exs | 45 ------------------- test/cadet/settings/sublanguage_test.exs | 30 ------------- .../controllers/user_controller_test.exs | 27 +++++++++-- .../factories/settings/sublanguage_factory.ex | 18 -------- 5 files changed, 32 insertions(+), 97 deletions(-) delete mode 100644 test/cadet/settings/settings_test.exs delete mode 100644 test/cadet/settings/sublanguage_test.exs delete mode 100644 test/factories/settings/sublanguage_factory.ex diff --git a/lib/cadet_web/views/user_view.ex b/lib/cadet_web/views/user_view.ex index 4be4e34c2..8468aed94 100644 --- a/lib/cadet_web/views/user_view.ex +++ b/lib/cadet_web/views/user_view.ex @@ -114,7 +114,14 @@ defmodule CadetWeb.UserView do latest -> Enum.map(latest.course.assessment_config, fn config -> - transform_map_for_view(config, %{type: :type, skippable: :skippable}) + transform_map_for_view(config, %{ + type: :type, + skippable: :skippable, + isGraded: :is_graded, + isAutograded: :is_autograded, + EarlySubmissionXp: :early_submission_xp, + HoursBeforeEarlyXpDecay: :hours_before_early_xp_decay + }) end) end end diff --git a/test/cadet/settings/settings_test.exs b/test/cadet/settings/settings_test.exs deleted file mode 100644 index ec3a4c575..000000000 --- a/test/cadet/settings/settings_test.exs +++ /dev/null @@ -1,45 +0,0 @@ -defmodule Cadet.SettingsTest do - use Cadet.DataCase - - alias Cadet.Settings - - describe "get sublanguage" do - test "succeeds" do - insert(:sublanguage, %{chapter: 3, variant: "concurrent"}) - {:ok, sublanguage} = Settings.get_sublanguage() - assert sublanguage.chapter == 3 - assert sublanguage.variant == "concurrent" - end - - test "returns default if no existing entry exists" do - {:ok, sublanguage} = Settings.get_sublanguage() - assert sublanguage.chapter == 1 - assert sublanguage.variant == "default" - end - end - - describe "update sublanguage" do - test "succeeds" do - insert(:sublanguage) - new_chapter = Enum.random(1..4) - {:ok, sublanguage} = Settings.update_sublanguage(new_chapter, "default") - assert sublanguage.chapter == new_chapter - assert sublanguage.variant == "default" - end - - test "succeeds if no existing entry exists" do - new_chapter = Enum.random(1..4) - {:ok, sublanguage} = Settings.update_sublanguage(new_chapter, "default") - assert sublanguage.chapter == new_chapter - assert sublanguage.variant == "default" - end - - test "returns with error for failed updates" do - assert {:error, changeset} = Settings.update_sublanguage(0, "default") - assert %{chapter: ["is invalid"]} = errors_on(changeset) - - assert {:error, changeset} = Settings.update_sublanguage(2, "gpu") - assert %{variant: ["is invalid"]} = errors_on(changeset) - end - end -end diff --git a/test/cadet/settings/sublanguage_test.exs b/test/cadet/settings/sublanguage_test.exs deleted file mode 100644 index f5c845105..000000000 --- a/test/cadet/settings/sublanguage_test.exs +++ /dev/null @@ -1,30 +0,0 @@ -defmodule Cadet.Settings.SublanguageTest do - alias Cadet.Settings.Sublanguage - - use Cadet.ChangesetCase, entity: Sublanguage - - describe "Changesets" do - test "valid changesets" do - assert_changeset(%{chapter: 1, variant: "wasm"}, :valid) - assert_changeset(%{chapter: 2, variant: "lazy"}, :valid) - assert_changeset(%{chapter: 3, variant: "non-det"}, :valid) - assert_changeset(%{chapter: 4, variant: "default"}, :valid) - end - - test "invalid changeset missing required params" do - assert_changeset(%{chapter: 2}, :invalid) - end - - test "invalid changeset with invalid chapter" do - assert_changeset(%{chapter: 5, variant: "default"}, :invalid) - end - - test "invalid changeset with invalid variant" do - assert_changeset(%{chapter: Enum.random(1..4), variant: "error"}, :invalid) - end - - test "invalid changeset with invalid chapter-variant combination" do - assert_changeset(%{chapter: 4, variant: "lazy"}, :invalid) - end - end -end diff --git a/test/cadet_web/controllers/user_controller_test.exs b/test/cadet_web/controllers/user_controller_test.exs index 03e9b65b1..2d7782ba2 100644 --- a/test/cadet_web/controllers/user_controller_test.exs +++ b/test/cadet_web/controllers/user_controller_test.exs @@ -100,9 +100,30 @@ defmodule CadetWeb.UserControllerTest do "viewable" => true }, "assessmentConfigurations" => [ - %{"skippable" => true, "type" => "test type 1"}, - %{"skippable" => true, "type" => "test type 2"}, - %{"skippable" => true, "type" => "test type 3"} + %{ + "skippable" => true, + "type" => "test type 1", + "EarlySubmissionXp" => 200, + "HoursBeforeEarlyXpDecay" => 48, + "isAutograded" => true, + "isGraded" => true + }, + %{ + "skippable" => true, + "type" => "test type 2", + "EarlySubmissionXp" => 200, + "HoursBeforeEarlyXpDecay" => 48, + "isAutograded" => true, + "isGraded" => true + }, + %{ + "skippable" => true, + "type" => "test type 3", + "EarlySubmissionXp" => 200, + "HoursBeforeEarlyXpDecay" => 48, + "isAutograded" => true, + "isGraded" => true + } ] } diff --git a/test/factories/settings/sublanguage_factory.ex b/test/factories/settings/sublanguage_factory.ex deleted file mode 100644 index 21987b96c..000000000 --- a/test/factories/settings/sublanguage_factory.ex +++ /dev/null @@ -1,18 +0,0 @@ -defmodule Cadet.Settings.SublanguageFactory do - @moduledoc """ - Factory for the Sublanguage entity - """ - - defmacro __using__(_opts) do - quote do - alias Cadet.Settings.Sublanguage - - def sublanguage_factory do - %Sublanguage{ - chapter: 1, - variant: "default" - } - end - end - end -end From 57b89c9b9c974b1d0ddb75285479a365516d1535 Mon Sep 17 00:00:00 2001 From: YaleChen299 Date: Sat, 3 Jul 2021 22:53:57 +0800 Subject: [PATCH 111/174] finalised /user call --- lib/cadet_web/views/user_view.ex | 5 ++-- .../controllers/user_controller_test.exs | 27 ++++++++++--------- 2 files changed, 18 insertions(+), 14 deletions(-) diff --git a/lib/cadet_web/views/user_view.ex b/lib/cadet_web/views/user_view.ex index 8468aed94..d62a7a1d4 100644 --- a/lib/cadet_web/views/user_view.ex +++ b/lib/cadet_web/views/user_view.ex @@ -115,12 +115,13 @@ defmodule CadetWeb.UserView do latest -> Enum.map(latest.course.assessment_config, fn config -> transform_map_for_view(config, %{ + assessmentConfigId: :id, type: :type, skippable: :skippable, isGraded: :is_graded, isAutograded: :is_autograded, - EarlySubmissionXp: :early_submission_xp, - HoursBeforeEarlyXpDecay: :hours_before_early_xp_decay + earlySubmissionXp: :early_submission_xp, + hoursBeforeEarlyXpDecay: :hours_before_early_xp_decay }) end) end diff --git a/test/cadet_web/controllers/user_controller_test.exs b/test/cadet_web/controllers/user_controller_test.exs index 2d7782ba2..93e133afa 100644 --- a/test/cadet_web/controllers/user_controller_test.exs +++ b/test/cadet_web/controllers/user_controller_test.exs @@ -18,9 +18,9 @@ defmodule CadetWeb.UserControllerTest do test "success, student non-story fields", %{conn: conn} do user = conn.assigns.current_user course = user.latest_viewed - insert(:assessment_config, %{order: 2, type: "test type 2", course: course}) - insert(:assessment_config, %{order: 3, type: "test type 3", course: course}) - insert(:assessment_config, %{order: 1, type: "test type 1", course: course}) + config2 = insert(:assessment_config, %{order: 2, type: "test type 2", course: course}) + config3 = insert(:assessment_config, %{order: 3, type: "test type 3", course: course}) + config1 = insert(:assessment_config, %{order: 1, type: "test type 1", course: course}) cr = Repo.get_by(CourseRegistration, course_id: course.id, user_id: user.id) another_cr = insert(:course_registration, %{user: user}) assessment = insert(:assessment, %{is_published: true, course: course}) @@ -103,26 +103,29 @@ defmodule CadetWeb.UserControllerTest do %{ "skippable" => true, "type" => "test type 1", - "EarlySubmissionXp" => 200, - "HoursBeforeEarlyXpDecay" => 48, "isAutograded" => true, - "isGraded" => true + "isGraded" => true, + "assessmentConfigId" => config1.id, + "earlySubmissionXp" => 200, + "hoursBeforeEarlyXpDecay" => 48 }, %{ "skippable" => true, "type" => "test type 2", - "EarlySubmissionXp" => 200, - "HoursBeforeEarlyXpDecay" => 48, "isAutograded" => true, - "isGraded" => true + "isGraded" => true, + "assessmentConfigId" => config2.id, + "earlySubmissionXp" => 200, + "hoursBeforeEarlyXpDecay" => 48 }, %{ "skippable" => true, "type" => "test type 3", - "EarlySubmissionXp" => 200, - "HoursBeforeEarlyXpDecay" => 48, "isAutograded" => true, - "isGraded" => true + "isGraded" => true, + "assessmentConfigId" => config3.id, + "earlySubmissionXp" => 200, + "hoursBeforeEarlyXpDecay" => 48 } ] } From 10360bb06e08b25481601cc9f53bdbe3f6ba7c00 Mon Sep 17 00:00:00 2001 From: YaleChen299 Date: Sat, 3 Jul 2021 23:39:11 +0800 Subject: [PATCH 112/174] fix auth_controller_test --- test/cadet_web/controllers/auth_controller_test.exs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/test/cadet_web/controllers/auth_controller_test.exs b/test/cadet_web/controllers/auth_controller_test.exs index 1b2295903..1a4a9a45e 100644 --- a/test/cadet_web/controllers/auth_controller_test.exs +++ b/test/cadet_web/controllers/auth_controller_test.exs @@ -63,7 +63,8 @@ defmodule CadetWeb.AuthControllerTest do "client_id" => "" }) - assert response(conn, 400) == "Unable to retrieve token from ADFS: Upstream error" + assert response(conn, 400) == + "Unable to retrieve token from authentication provider: Upstream error" end test_with_mock "unknown error from Provider.authorise", From 2951c7724383888cc54cef52b147e2f9be372834 Mon Sep 17 00:00:00 2001 From: YaleChen299 Date: Sun, 4 Jul 2021 01:48:36 +0800 Subject: [PATCH 113/174] updated contest test --- test/cadet/assessments/assessments_test.exs | 72 +++++++++---------- .../admin_settings_controller_test.exs | 59 --------------- .../controllers/settings_controller_test.exs | 28 -------- 3 files changed, 34 insertions(+), 125 deletions(-) delete mode 100644 test/cadet_web/admin_controllers/admin_settings_controller_test.exs delete mode 100644 test/cadet_web/controllers/settings_controller_test.exs diff --git a/test/cadet/assessments/assessments_test.exs b/test/cadet/assessments/assessments_test.exs index 9e86dba01..31bf6845e 100644 --- a/test/cadet/assessments/assessments_test.exs +++ b/test/cadet/assessments/assessments_test.exs @@ -144,12 +144,15 @@ defmodule Cadet.AssessmentsTest do end describe "contest voting" do - @tag :skip test "inserts votes into submission_votes table" do contest_question = insert(:programming_question) question = insert(:voting_question) # users = Enum.map(0..5, fn _x -> insert(:user, role: "student") end) - students = insert_list(5, :course_registration, %{role: :student}) + students = + insert_list(6, :course_registration, %{ + role: :student, + course: contest_question.assessment.course + }) Enum.map(students, fn student -> submission = @@ -166,7 +169,8 @@ defmodule Cadet.AssessmentsTest do ) end) - unattempted_student = insert(:course_registration, role: :student) + unattempted_student = + insert(:course_registration, %{role: :student, course: contest_question.assessment.course}) # unattempted submission will automatically be submitted after the assessment closes. unattempted_submission = @@ -191,7 +195,6 @@ defmodule Cadet.AssessmentsTest do assert length(Repo.all(SubmissionVotes, question_id: question.id)) == 6 * 5 + 6 end - @tag :skip test "create voting parameters with invalid contest number" do question = insert(:voting_question) @@ -200,12 +203,13 @@ defmodule Cadet.AssessmentsTest do assert status == :error end - @tag :skip test "deletes submission_votes when assessment is deleted" do contest_question = insert(:programming_question) - voting_assessment = insert(:assessment, type: "practical") + course = contest_question.assessment.course + config = contest_question.assessment.config + voting_assessment = insert(:assessment, %{course: course, config: config}) question = insert(:voting_question, assessment: voting_assessment) - students = insert_list(5, :course_registration, %{role: :student}) + students = insert_list(5, :course_registration, %{role: :student, course: course}) Enum.map(students, fn student -> submission = @@ -232,16 +236,14 @@ defmodule Cadet.AssessmentsTest do describe "contest voting leaderboard utility functions" do setup do - contest_assessment = insert(:assessment, type: "contest") - voting_assessment = insert(:assessment, type: "practical") + course = insert(:course) + config = insert(:assessment_config) + contest_assessment = insert(:assessment, %{course: course, config: config}) + voting_assessment = insert(:assessment, %{course: course, config: config}) voting_question = insert(:voting_question, assessment: voting_assessment) # generate 5 students - student_list = - Enum.map( - 1..5, - fn _index -> insert(:user) end - ) + student_list = insert_list(5, :course_registration, %{course: course, role: :student}) # generate contest submission for each student submission_list = @@ -281,7 +283,7 @@ defmodule Cadet.AssessmentsTest do insert( :submission_vote, rank: index + 1, - user: student, + voter: student, submission: submission, question: voting_question ) @@ -293,7 +295,6 @@ defmodule Cadet.AssessmentsTest do %{answers: ans_list, question_id: voting_question.id, student_list: student_list} end - @tag :skip test "computes correct relative_score with lexing/penalty and fetches highest x relative_score correctly", %{answers: _answers, question_id: question_id, student_list: _student_list} do Assessments.compute_relative_score(question_id) @@ -318,38 +319,43 @@ defmodule Cadet.AssessmentsTest do describe "contest leaderboard updating functions" do setup do - current_contest_assessment = insert(:assessment, type: "contest") + course = insert(:course) + config = insert(:assessment_config) + current_contest_assessment = insert(:assessment, %{course: course, config: config}) # contest_voting assessment that is still ongoing current_assessment = insert(:assessment, is_published: true, open_at: Timex.shift(Timex.now(), days: -1), close_at: Timex.shift(Timex.now(), days: +1), - type: "practical" + course: course, + config: config ) current_question = insert(:voting_question, assessment: current_assessment) - yesterday_contest_assessment = insert(:assessment, type: "contest") + yesterday_contest_assessment = insert(:assessment, %{course: course, config: config}) # contest_voting assessment closed yesterday yesterday_assessment = insert(:assessment, is_published: true, open_at: Timex.shift(Timex.now(), days: -5), close_at: Timex.shift(Timex.now(), hours: -4), - type: "practical" + course: course, + config: config ) yesterday_question = insert(:voting_question, assessment: yesterday_assessment) - past_contest_assessment = insert(:assessment, type: "contest") + past_contest_assessment = insert(:assessment, %{course: course, config: config}) # contest voting assessment closed >1 day ago past_assessment = insert(:assessment, is_published: true, open_at: Timex.shift(Timex.now(), days: -5), close_at: Timex.shift(Timex.now(), days: -4), - type: "practical" + course: course, + config: config ) past_question = @@ -358,11 +364,7 @@ defmodule Cadet.AssessmentsTest do ) # generate 5 students - student_list = - Enum.map( - 1..5, - fn _index -> insert(:user) end - ) + student_list = insert_list(5, :course_registration, %{course: course, role: :student}) # generate contest submission for each user current_submission_list = @@ -449,7 +451,7 @@ defmodule Cadet.AssessmentsTest do insert( :submission_vote, rank: index + 1, - user: student, + voter: student, submission: submission, question: current_question ) @@ -468,7 +470,7 @@ defmodule Cadet.AssessmentsTest do insert( :submission_vote, rank: index + 1, - user: student, + voter: student, submission: submission, question: yesterday_question ) @@ -487,7 +489,7 @@ defmodule Cadet.AssessmentsTest do insert( :submission_vote, rank: index + 1, - user: student, + voter: student, submission: submission, question: past_question ) @@ -503,7 +505,6 @@ defmodule Cadet.AssessmentsTest do } end - @tag :skip test "fetch_voting_questions_due_yesterday only fetching voting questions closed yesterday", %{ yesterday_question: yesterday_question, @@ -514,7 +515,6 @@ defmodule Cadet.AssessmentsTest do get_question_ids(Assessments.fetch_voting_questions_due_yesterday()) end - @tag :skip test "fetch_active_voting_questions only fetches active voting questions", %{ yesterday_question: _yesterday_question, @@ -525,9 +525,7 @@ defmodule Cadet.AssessmentsTest do get_question_ids(Assessments.fetch_active_voting_questions()) end - @tag :skip - test "update_final_contest_leaderboards correctly updates leaderboards - that voting closed yesterday", + test "update_final_contest_leaderboards correctly updates leaderboards that voting closed yesterday", %{ yesterday_question: yesterday_question, current_question: current_question, @@ -549,9 +547,7 @@ defmodule Cadet.AssessmentsTest do ) == [99.0, 89.0, 79.0, 69.0, 59.0] end - @tag :skip - test "update_rolling_contest_leaderboards correcly updates leaderboards - which voting is active", + test "update_rolling_contest_leaderboards correcly updates leaderboards which voting is active", %{ yesterday_question: yesterday_question, current_question: current_question, diff --git a/test/cadet_web/admin_controllers/admin_settings_controller_test.exs b/test/cadet_web/admin_controllers/admin_settings_controller_test.exs deleted file mode 100644 index 2f27288b5..000000000 --- a/test/cadet_web/admin_controllers/admin_settings_controller_test.exs +++ /dev/null @@ -1,59 +0,0 @@ -defmodule CadetWeb.AdminSettingsControllerTest do - use CadetWeb.ConnCase - - alias CadetWeb.AdminSettingsController - - test "swagger" do - AdminSettingsController.swagger_definitions() - AdminSettingsController.swagger_path_update(nil) - end - - describe "PUT /settings/sublanguage" do - @tag authenticate: :admin - test "succeeds", %{conn: conn} do - insert(:sublanguage, %{chapter: 4, variant: "gpu"}) - - conn = - put(conn, build_url(), %{ - "chapter" => Enum.random(1..4), - "variant" => "default" - }) - - assert response(conn, 200) == "OK" - end - - @tag authenticate: :staff - test "succeeds when no default sublanguage entry exists", %{conn: conn} do - conn = - put(conn, build_url(), %{ - "chapter" => Enum.random(1..4), - "variant" => "default" - }) - - assert response(conn, 200) == "OK" - end - - @tag authenticate: :student - test "rejects forbidden request for non-staff users", %{conn: conn} do - conn = put(conn, build_url(), %{"chapter" => 3, "variant" => "concurrent"}) - - assert response(conn, 403) == "Forbidden" - end - - @tag authenticate: :staff - test "rejects requests with invalid params", %{conn: conn} do - conn = put(conn, build_url(), %{"chapter" => 4, "variant" => "wasm"}) - - assert response(conn, 400) == "Invalid parameter(s)" - end - - @tag authenticate: :staff - test "rejects requests with missing params", %{conn: conn} do - conn = put(conn, build_url(), %{"variant" => "default"}) - - assert response(conn, 400) == "Missing parameter(s)" - end - end - - defp build_url, do: "/v2/admin/settings/sublanguage" -end diff --git a/test/cadet_web/controllers/settings_controller_test.exs b/test/cadet_web/controllers/settings_controller_test.exs deleted file mode 100644 index 6a3692a4c..000000000 --- a/test/cadet_web/controllers/settings_controller_test.exs +++ /dev/null @@ -1,28 +0,0 @@ -defmodule CadetWeb.SettingsControllerTest do - use CadetWeb.ConnCase - - alias CadetWeb.SettingsController - - test "swagger" do - SettingsController.swagger_definitions() - SettingsController.swagger_path_index(nil) - end - - describe "GET /settings/sublanguage" do - test "succeeds", %{conn: conn} do - insert(:sublanguage, %{chapter: 2, variant: "lazy"}) - - resp = conn |> get(build_url()) |> json_response(200) - - assert %{"sublanguage" => %{"chapter" => 2, "variant" => "lazy"}} = resp - end - - test "succeeds when no default sublanguage entry exists", %{conn: conn} do - resp = conn |> get(build_url()) |> json_response(200) - - assert %{"sublanguage" => %{"chapter" => 1, "variant" => "default"}} = resp - end - end - - defp build_url, do: "/v2/settings/sublanguage" -end From 55ae3fc75e7602621586e70e5614b14a19153f3f Mon Sep 17 00:00:00 2001 From: YaleChen299 Date: Sun, 4 Jul 2021 02:27:04 +0800 Subject: [PATCH 114/174] remove mentor from group --- lib/cadet/accounts/course_registrations.ex | 2 +- lib/cadet/courses/group.ex | 6 ++--- .../20210531155751_multitenant_upgrade.exs | 24 ++++--------------- priv/repo/seeds.exs | 4 +--- test/support/seeds.ex | 12 +--------- 5 files changed, 10 insertions(+), 38 deletions(-) diff --git a/lib/cadet/accounts/course_registrations.ex b/lib/cadet/accounts/course_registrations.ex index df2562865..e47c097c5 100644 --- a/lib/cadet/accounts/course_registrations.ex +++ b/lib/cadet/accounts/course_registrations.ex @@ -14,7 +14,7 @@ defmodule Cadet.Accounts.CourseRegistrations do # guide # only join with User if need name or user name - # only join with Group if need leader/mentor/students in group + # only join with Group if need leader/students in group # only join with Course if need course info/config # otherwise just use CourseRegistration diff --git a/lib/cadet/courses/group.ex b/lib/cadet/courses/group.ex index c082c1484..61df61066 100644 --- a/lib/cadet/courses/group.ex +++ b/lib/cadet/courses/group.ex @@ -12,20 +12,19 @@ defmodule Cadet.Courses.Group do schema "groups" do field(:name, :string) belongs_to(:leader, CourseRegistration) - belongs_to(:mentor, CourseRegistration) belongs_to(:course, Course) has_many(:students, CourseRegistration) end @required_fields ~w(name course_id)a - @optional_fields ~w(leader_id mentor_id)a + @optional_fields ~w(leader_id)a def changeset(group, attrs \\ %{}) do group |> cast(attrs, @required_fields ++ @optional_fields) |> validate_required(@required_fields) - |> add_belongs_to_id_from_model([:leader, :mentor, :course], attrs) + |> add_belongs_to_id_from_model([:leader, :course], attrs) |> validate_role # |> validate_course @@ -44,7 +43,6 @@ defmodule Cadet.Courses.Group do # defp validate_course(changeset) do # course_id = get_field(changeset, :course_id) # leader_id = get_field(changeset, :leader_id) - # mentor_id = get_field(changeset, :mentor_id) # if leader_id != nil && Repo.get(CourseRegistration, leader_id).course_id != course_id do # add_error(changeset, :leader, "does not belong to the same course ") diff --git a/priv/repo/migrations/20210531155751_multitenant_upgrade.exs b/priv/repo/migrations/20210531155751_multitenant_upgrade.exs index 8db6e2515..3eaf39ae7 100644 --- a/priv/repo/migrations/20210531155751_multitenant_upgrade.exs +++ b/priv/repo/migrations/20210531155751_multitenant_upgrade.exs @@ -63,16 +63,15 @@ defmodule Cadet.Repo.Migrations.MultitenantUpgrade do modify(:username, :string, null: false) end - # Prep for migration of leader_id and mentor_id from User entity to CourseRegistration entity. + # Prep for migration of leader_id from User entity to CourseRegistration entity. # Also make groups associated with a course. rename(table(:groups), :leader_id, to: :temp_leader_id) - rename(table(:groups), :mentor_id, to: :temp_mentor_id) drop(constraint(:groups, "groups_leader_id_fkey")) drop(constraint(:groups, "groups_mentor_id_fkey")) alter table(:groups) do + remove(:mentor_id) add(:leader_id, references(:course_registrations)) - add(:mentor_id, references(:course_registrations)) add(:course_id, references(:courses)) end @@ -171,8 +170,8 @@ defmodule Cadet.Repo.Migrations.MultitenantUpgrade do |> Repo.update() end) - # Handle groups (adding course_id, and updating leader_id and mentor_id to course registrations) - from(g in "groups", select: {g.id, g.temp_leader_id, g.temp_mentor_id}) + # Handle groups (adding course_id, and updating leader_id to course registrations) + from(g in "groups", select: {g.id, g.temp_leader_id}) |> Repo.all() |> Enum.each(fn group -> leader_id = @@ -193,22 +192,10 @@ defmodule Cadet.Repo.Migrations.MultitenantUpgrade do |> Map.fetch!(:id) end - mentor_id = - case elem(group, 2) do - nil -> - nil - - id -> - CourseRegistration - |> where(user_id: ^id) - |> Repo.one() - |> Map.fetch!(:id) - end - Group |> where(id: ^elem(group, 0)) |> Repo.one() - |> Group.changeset(%{leader_id: leader_id, mentor_id: mentor_id, course_id: course.id}) + |> Group.changeset(%{leader_id: leader_id, course_id: course.id}) |> Repo.update() end) @@ -345,7 +332,6 @@ defmodule Cadet.Repo.Migrations.MultitenantUpgrade do # Cleanup groups table, and make course_id and leader_id non-nullable alter table(:groups) do remove(:temp_leader_id) - remove(:temp_mentor_id) modify(:leader_id, references(:course_registrations), null: false, diff --git a/priv/repo/seeds.exs b/priv/repo/seeds.exs index d0f7249e0..7bb65997a 100644 --- a/priv/repo/seeds.exs +++ b/priv/repo/seeds.exs @@ -22,7 +22,6 @@ if Cadet.Env.env() == :dev do course2 = insert(:course, %{course_name: "Algorithm", course_short_name: "CS2040S"}) # Users avenger1 = insert(:user, %{name: "avenger", latest_viewed: course1}) - mentor1 = insert(:user, %{name: "mentor", latest_viewed: course1}) admin1 = insert(:user, %{name: "admin", latest_viewed: course1}) studenta1admin2 = insert(:user, %{name: "student a", latest_viewed: course1}) @@ -31,9 +30,8 @@ if Cadet.Env.env() == :dev do studentc1 = insert(:user, %{latest_viewed: course1}) # CourseRegistration and Group avenger1_cr = insert(:course_registration, %{user: avenger1, course: course1, role: :staff}) - mentor1_cr = insert(:course_registration, %{user: mentor1, course: course1, role: :staff}) admin1_cr = insert(:course_registration, %{user: admin1, course: course1, role: :admin}) - group = insert(:group, %{leader: avenger1_cr, mentor: mentor1_cr}) + group = insert(:group, %{leader: avenger1_cr}) student1a_cr = insert(:course_registration, %{ diff --git a/test/support/seeds.ex b/test/support/seeds.ex index aa7b1ee6b..7e3bcaf18 100644 --- a/test/support/seeds.ex +++ b/test/support/seeds.ex @@ -10,7 +10,6 @@ defmodule Cadet.Test.Seeds do %{ accounts: %{ avenger: avenger, - mentor: mentor, group: group, students: students, admin: admin @@ -38,18 +37,11 @@ defmodule Cadet.Test.Seeds do def assessments do if Cadet.Env.env() == :test do - # # User and Group - # avenger = insert(:user, %{name: "avenger", role: :staff}) - # mentor = insert(:user, %{name: "mentor", role: :staff}) - # group = insert(:group, %{leader: avenger, mentor: mentor}) - # students = insert_list(5, :student, %{group: group}) - # admin = insert(:user, %{name: "admin", role: :admin}) # Course course1 = insert(:course) course2 = insert(:course, %{course_name: "Algorithm", course_short_name: "CS2040S"}) # Users avenger1 = insert(:user, %{name: "avenger", latest_viewed: course1}) - mentor1 = insert(:user, %{name: "mentor", latest_viewed: course1}) admin1 = insert(:user, %{name: "admin", latest_viewed: course1}) studenta1admin2 = insert(:user, %{name: "student a", latest_viewed: course1}) @@ -58,9 +50,8 @@ defmodule Cadet.Test.Seeds do studentc1 = insert(:user, %{latest_viewed: course1}) # CourseRegistration and Group avenger1_cr = insert(:course_registration, %{user: avenger1, course: course1, role: :staff}) - mentor1_cr = insert(:course_registration, %{user: mentor1, course: course1, role: :staff}) admin1_cr = insert(:course_registration, %{user: admin1, course: course1, role: :admin}) - group = insert(:group, %{leader: avenger1_cr, mentor: mentor1_cr}) + group = insert(:group, %{leader: avenger1_cr}) student1a_cr = insert(:course_registration, %{ @@ -128,7 +119,6 @@ defmodule Cadet.Test.Seeds do }, course_regs: %{ avenger1_cr: avenger1_cr, - mentor1_cr: mentor1_cr, group: group, students: students, admin1_cr: admin1_cr From 14a4466f644765482d0e7a783c6e42c8d1ccf441 Mon Sep 17 00:00:00 2001 From: YaleChen299 Date: Sun, 4 Jul 2021 02:27:56 +0800 Subject: [PATCH 115/174] setting up admin grading controller test --- .../admin_grading_controller_test.exs | 312 +++--------------- 1 file changed, 42 insertions(+), 270 deletions(-) diff --git a/test/cadet_web/admin_controllers/admin_grading_controller_test.exs b/test/cadet_web/admin_controllers/admin_grading_controller_test.exs index ca45617d8..84e531df0 100644 --- a/test/cadet_web/admin_controllers/admin_grading_controller_test.exs +++ b/test/cadet_web/admin_controllers/admin_grading_controller_test.exs @@ -20,74 +20,84 @@ defmodule CadetWeb.AdminGradingControllerTest do describe "GET /, unauthenticated" do test "unauthorized", %{conn: conn} do - conn = get(conn, build_url()) + course = insert(:course) + conn = get(conn, build_url(course.id)) assert response(conn, 401) =~ "Unauthorised" end end describe "GET /:submissionid, unauthenticated" do test "unauthorized", %{conn: conn} do - conn = get(conn, build_url(1)) + course = insert(:course) + conn = get(conn, build_url(course.id, 1)) assert response(conn, 401) =~ "Unauthorised" end end describe "POST /:submissionid/:questionid, unauthenticated" do test "unauthorized", %{conn: conn} do - conn = post(conn, build_url(1, 3), %{}) + course = insert(:course) + conn = post(conn, build_url(course.id, 1, 3), %{}) assert response(conn, 401) =~ "Unauthorised" end end describe "GET /:submissionid/unsubmit, unauthenticated" do test "unauthorized", %{conn: conn} do - conn = post(conn, build_url_unsubmit(1)) + course = insert(:course) + conn = post(conn, build_url_unsubmit(course.id, 1)) assert response(conn, 401) =~ "Unauthorised" end end describe "GET /, student" do @tag authenticate: :student - test "unauthorized", %{conn: conn} do - conn = get(conn, build_url()) + test "Forbidden", %{conn: conn} do + course_id = conn.assigns.course_id + conn = get(conn, build_url(course_id)) assert response(conn, 403) =~ "Forbidden" end end describe "GET /?group=true, student" do @tag authenticate: :student - test "unauthorized", %{conn: conn} do - conn = get(conn, build_url(), %{"group" => "true"}) + test "Forbidden", %{conn: conn} do + course_id = conn.assigns.course_id + conn = get(conn, build_url(course_id), %{"group" => "true"}) assert response(conn, 403) =~ "Forbidden" end end describe "GET /:submissionid, student" do @tag authenticate: :student - test "unauthorized", %{conn: conn} do - conn = get(conn, build_url(1)) + test "Forbidden", %{conn: conn} do + course_id = conn.assigns.course_id + conn = get(conn, build_url(course_id, 1)) assert response(conn, 403) =~ "Forbidden" end end describe "POST /:submissionid/:questionid, student" do @tag authenticate: :student - test "unauthorized", %{conn: conn} do - conn = post(conn, build_url(1, 3), %{"grading" => %{}}) + test "Forbidden", %{conn: conn} do + course_id = conn.assigns.course_id + conn = post(conn, build_url(course_id, 1, 3), %{"grading" => %{}}) assert response(conn, 403) =~ "Forbidden" end @tag authenticate: :student test "missing parameter", %{conn: conn} do - conn = post(conn, build_url(1, 3), %{}) + course_id = conn.assigns.course_id + conn = post(conn, build_url(course_id, 1, 3), %{}) assert response(conn, 403) =~ "Forbidden" end end describe "GET /:submissionid/unsubmit, student" do @tag authenticate: :student - test "unauthorized", %{conn: conn} do - conn = post(conn, build_url_unsubmit(1)) + test "Forbidden", %{conn: conn} do + course_id = conn.assigns.course_id + conn = post(conn, build_url_unsubmit(course_id, 1)) assert response(conn, 403) =~ "Forbidden" end end @@ -134,48 +144,6 @@ defmodule CadetWeb.AdminGradingControllerTest do assert expected == Enum.sort_by(json_response(conn, 200), & &1["id"]) end - - @tag authenticate: :staff - test "pure mentor gets to see all students submissions", %{conn: conn} do - %{mentor: mentor, submissions: submissions, mission: mission} = seed_db(conn) - - conn = - conn - |> sign_in(mentor) - |> get(build_url()) - - expected = - Enum.map(submissions, fn submission -> - %{ - "xp" => 5000, - "xpAdjustment" => -2500, - "xpBonus" => 100, - "grade" => 1000, - "adjustment" => -500, - "id" => submission.id, - "student" => %{ - "name" => submission.student.name, - "id" => submission.student.id, - "groupName" => submission.student.group.name, - "groupLeaderId" => submission.student.group.leader_id - }, - "assessment" => %{ - "type" => "mission", - "maxGrade" => 1000, - "maxXp" => 5000, - "id" => mission.id, - "title" => mission.title, - "questionCount" => 5 - }, - "status" => Atom.to_string(submission.status), - "gradedCount" => 5, - "unsubmittedBy" => nil, - "unsubmittedAt" => nil - } - end) - - assert expected == Enum.sort_by(json_response(conn, 200), & &1["id"]) - end end describe "GET /?group=true, staff" do @@ -410,178 +378,6 @@ defmodule CadetWeb.AdminGradingControllerTest do assert expected == json_response(conn, 200) end - - @tag authenticate: :staff - test "pure mentor gets to view all submissions", %{conn: conn} do - %{mentor: mentor, grader: grader, submissions: submissions, answers: answers} = - seed_db(conn) - - submission = List.first(submissions) - - conn = - conn - |> sign_in(mentor) - |> get(build_url(submission.id)) - - expected = - answers - |> Enum.filter(&(&1.submission.id == submission.id)) - |> Enum.sort_by(& &1.question.display_order) - |> Enum.map( - &case &1.question.type do - :programming -> - %{ - "question" => %{ - "prepend" => &1.question.question.prepend, - "postpend" => &1.question.question.postpend, - "testcases" => - Enum.map( - &1.question.question.public, - fn testcase -> - for {k, v} <- testcase, - into: %{"type" => "public"}, - do: {Atom.to_string(k), v} - end - ) ++ - Enum.map( - &1.question.question.private, - fn testcase -> - for {k, v} <- testcase, - into: %{"type" => "private"}, - do: {Atom.to_string(k), v} - end - ), - "solutionTemplate" => &1.question.question.template, - "type" => "#{&1.question.type}", - "id" => &1.question.id, - "library" => %{ - "chapter" => &1.question.library.chapter, - "globals" => &1.question.library.globals, - "external" => %{ - "name" => "#{&1.question.library.external.name}", - "symbols" => &1.question.library.external.symbols - } - }, - "maxGrade" => &1.question.max_grade, - "maxXp" => &1.question.max_xp, - "content" => &1.question.question.content, - "answer" => &1.answer.code, - "autogradingStatus" => Atom.to_string(&1.autograding_status), - "autogradingResults" => &1.autograding_results - }, - "solution" => &1.question.question.solution, - "grade" => %{ - "grade" => &1.grade, - "adjustment" => &1.adjustment, - "xp" => &1.xp, - "xpAdjustment" => &1.xp_adjustment, - "grader" => %{ - "name" => grader.name, - "id" => grader.id - }, - "gradedAt" => format_datetime(&1.updated_at), - "comments" => &1.comments - }, - "student" => %{ - "name" => &1.submission.student.name, - "id" => &1.submission.student.id - } - } - - :mcq -> - %{ - "question" => %{ - "type" => "#{&1.question.type}", - "id" => &1.question.id, - "library" => %{ - "chapter" => &1.question.library.chapter, - "globals" => &1.question.library.globals, - "external" => %{ - "name" => "#{&1.question.library.external.name}", - "symbols" => &1.question.library.external.symbols - } - }, - "maxGrade" => &1.question.max_grade, - "maxXp" => &1.question.max_xp, - "content" => &1.question.question.content, - "answer" => &1.answer.choice_id, - "choices" => - for choice <- &1.question.question.choices do - %{ - "content" => choice.content, - "hint" => choice.hint, - "id" => choice.choice_id - } - end, - "autogradingStatus" => Atom.to_string(&1.autograding_status), - "autogradingResults" => &1.autograding_results - }, - "solution" => "", - "grade" => %{ - "grade" => &1.grade, - "adjustment" => &1.adjustment, - "xp" => &1.xp, - "xpAdjustment" => &1.xp_adjustment, - "grader" => %{ - "name" => grader.name, - "id" => grader.id - }, - "gradedAt" => format_datetime(&1.updated_at), - "comments" => &1.comments - }, - "student" => %{ - "name" => &1.submission.student.name, - "id" => &1.submission.student.id - } - } - - :voting -> - %{ - "question" => %{ - "prepend" => &1.question.question.prepend, - "solutionTemplate" => &1.question.question.template, - "type" => "#{&1.question.type}", - "id" => &1.question.id, - "library" => %{ - "chapter" => &1.question.library.chapter, - "globals" => &1.question.library.globals, - "external" => %{ - "name" => "#{&1.question.library.external.name}", - "symbols" => &1.question.library.external.symbols - } - }, - "maxGrade" => &1.question.max_grade, - "maxXp" => &1.question.max_xp, - "content" => &1.question.question.content, - "autogradingStatus" => Atom.to_string(&1.autograding_status), - "autogradingResults" => &1.autograding_results, - "answer" => nil, - "contestEntries" => [], - "contestLeaderboard" => [] - }, - "grade" => %{ - "grade" => &1.grade, - "adjustment" => &1.adjustment, - "xp" => &1.xp, - "xpAdjustment" => &1.xp_adjustment, - "grader" => %{ - "name" => grader.name, - "id" => grader.id - }, - "gradedAt" => format_datetime(&1.updated_at), - "comments" => &1.comments - }, - "student" => %{ - "name" => &1.submission.student.name, - "id" => &1.submission.student.id - }, - "solution" => "" - } - end - ) - - assert expected == json_response(conn, 200) - end end describe "POST /:submissionid/:questionid, staff" do @@ -640,34 +436,6 @@ defmodule CadetWeb.AdminGradingControllerTest do "xp_adjustment must make total be between 0 and question.max_xp" end - @tag authenticate: :staff - test "staff who isn't the grader of said answer can still grade submission and grader field is updated correctly", - %{conn: conn} do - %{mentor: mentor, answers: answers} = seed_db(conn) - - mentor_id = mentor.id - - answer = List.first(answers) - - conn = - conn - |> sign_in(mentor) - |> post(build_url(answer.submission.id, answer.question.id), %{ - "grading" => %{ - "adjustment" => -100, - "xpAdjustment" => -100 - } - }) - - assert response(conn, 200) == "OK" - - assert %{ - adjustment: -100, - xp_adjustment: -100, - grader_id: ^mentor_id - } = Repo.get(Answer, answer.id) - end - @tag authenticate: :staff test "missing parameter", %{conn: conn} do conn = post(conn, build_url(1, 3), %{}) @@ -1386,22 +1154,27 @@ defmodule CadetWeb.AdminGradingControllerTest do |> length() end - defp build_url, do: "/v2/admin/grading/" - defp build_url_summary, do: "/v2/admin/grading/summary" - defp build_url(submission_id), do: "#{build_url()}#{submission_id}" - defp build_url(submission_id, question_id), do: "#{build_url(submission_id)}/#{question_id}" - defp build_url_unsubmit(submission_id), do: "#{build_url(submission_id)}/unsubmit" - defp build_url_autograde(submission_id), do: "#{build_url(submission_id)}/autograde" + defp build_url(course_id), do: "/v2/courses/#{course_id}/admin/grading/" + defp build_url_summary(course_id), do: "/v2/courses/#{course_id}/admin/grading/summary" + defp build_url(course_id, submission_id), do: "#{build_url(course_id)}#{submission_id}" + + defp build_url(course_id, submission_id, question_id), + do: "#{build_url(course_id, submission_id)}/#{question_id}" + + defp build_url_unsubmit(course_id, submission_id), + do: "#{build_url(course_id, submission_id)}/unsubmit" + + defp build_url_autograde(course_id, submission_id), + do: "#{build_url(course_id, submission_id)}/autograde" - defp build_url_autograde(submission_id, question_id), - do: "#{build_url(submission_id, question_id)}/autograde" + defp build_url_autograde(course_id, submission_id, question_id), + do: "#{build_url(course_id, submission_id, question_id)}/autograde" defp seed_db(conn, override_grader \\ nil) do - grader = override_grader || conn.assigns[:current_user] - mentor = insert(:user, role: :staff) + course = insert(:course) + grader = override_grader || conn.assigns[:test_cr] - group = - insert(:group, %{leader_id: grader.id, leader: grader, mentor_id: mentor.id, mentor: mentor}) + group = insert(:group, %{leader_id: grader.id, leader: grader}) students = insert_list(5, :student, %{group: group}) mission = insert(:assessment, %{title: "mission", type: "mission", is_published: true}) @@ -1467,7 +1240,6 @@ defmodule CadetWeb.AdminGradingControllerTest do %{ grader: grader, - mentor: mentor, group: group, students: students, mission: mission, From 6d9d39446e95d9845df3b8762558e6cb3c2d9a5e Mon Sep 17 00:00:00 2001 From: Chow En Rong <53928333+chownces@users.noreply.github.com> Date: Sun, 4 Jul 2021 22:17:53 +0800 Subject: [PATCH 116/174] Updated admin PUT /users endpoint --- lib/cadet/accounts/course_registrations.ex | 8 +- lib/cadet/courses/courses.ex | 87 +++++++++++---- lib/cadet/courses/group.ex | 3 +- .../admin_user_controller.ex | 105 ++++++++++-------- lib/cadet_web/router.ex | 2 +- .../20210531155751_multitenant_upgrade.exs | 5 - .../accounts/course_registration_test.exs | 6 +- test/cadet/courses/courses_test.exs | 96 +++++++++++++--- test/cadet/courses/group_test.exs | 10 ++ .../admin_user_controller_test.exs | 29 +++-- 10 files changed, 240 insertions(+), 111 deletions(-) diff --git a/lib/cadet/accounts/course_registrations.ex b/lib/cadet/accounts/course_registrations.ex index e47c097c5..2472137c9 100644 --- a/lib/cadet/accounts/course_registrations.ex +++ b/lib/cadet/accounts/course_registrations.ex @@ -67,22 +67,22 @@ defmodule Cadet.Accounts.CourseRegistrations do |> Repo.all() end - def add_users_to_course(usernames_and_roles, course_id) do + def upsert_users_in_course(usernames_and_roles, course_id) do # Note: Usernames have already been namespaced in the controller usernames_and_roles |> Enum.reduce_while(nil, fn %{username: username, role: role}, _acc -> - add_users_to_course_helper(username, course_id, role) + upsert_users_in_course_helper(username, course_id, role) end) end - defp add_users_to_course_helper(username, course_id, role) do + defp upsert_users_in_course_helper(username, course_id, role) do case User |> where(username: ^username) |> Repo.one() do nil -> case Accounts.register(%{username: username}) do {:ok, _} -> - add_users_to_course_helper(username, course_id, role) + upsert_users_in_course_helper(username, course_id, role) {:error, changeset} -> {:halt, {:error, {:bad_request, full_error_messages(changeset)}}} diff --git a/lib/cadet/courses/courses.ex b/lib/cadet/courses/courses.ex index d2994fa54..1d174d6ed 100644 --- a/lib/cadet/courses/courses.ex +++ b/lib/cadet/courses/courses.ex @@ -175,43 +175,88 @@ defmodule Cadet.Courses do end end - @doc """ - Get a group based on the group name or create one if it doesn't exist - """ - @spec get_or_create_group(String.t()) :: {:ok, %Group{}} | {:error, Ecto.Changeset.t()} - def get_or_create_group(name) when is_binary(name) do - Group - |> where(name: ^name) - |> Repo.one() - |> case do - nil -> - %Group{} - |> Group.changeset(%{name: name}) - |> Repo.insert() + def upsert_groups_in_course(usernames_and_groups, course_id) do + usernames_and_groups + |> Enum.reduce_while(nil, fn %{username: username} = entry, _acc -> + with {:ok, groupname} <- Map.fetch(entry, :group) do + case upsert_groups_in_course_helper(username, course_id, groupname) do + {:ok, _} -> {:cont, :ok} + {:error, changeset} -> {:halt, {:error, {:bad_request, full_error_messages(changeset)}}} + end + else + # If no group is specified, continue reduction + :error -> {:cont, :ok} + end + end) + end - group -> - {:ok, group} + defp upsert_groups_in_course_helper(username, course_id, groupname) do + with {:get_group, {:ok, group}} <- {:get_group, get_or_create_group(groupname, course_id)}, + {:get_course_reg, %{role: role} = course_reg} <- + {:get_course_reg, + CourseRegistration + |> where( + user_id: ^(User |> where(username: ^username) |> Repo.one() |> Map.fetch!(:id)) + ) + |> where(course_id: ^course_id) + |> Repo.one()} do + # It is ok to assume that user course registions already exist, as they would have been created + # in the admin_user_controller before calling this function + case role do + # If student, update his course registration + :student -> + course_reg + |> CourseRegistration.changeset(%{group_id: group.id}) + |> Repo.update() + + # If admin or staff, set them as group leader + _ -> + group + |> Group.changeset(%{leader_id: course_reg.id}) + |> Repo.update() + end end end @doc """ - Updates a group based on the group name or create one if it doesn't exist + Get a group based on the group name and course id or create one if it doesn't exist """ - @spec insert_or_update_group(map()) :: {:ok, %Group{}} | {:error, Ecto.Changeset.t()} - def insert_or_update_group(params = %{name: name}) when is_binary(name) do + @spec get_or_create_group(String.t(), integer()) :: + {:ok, %Group{}} | {:error, Ecto.Changeset.t()} + def get_or_create_group(name, course_id) when is_binary(name) and is_ecto_id(course_id) do Group |> where(name: ^name) + |> where(course_id: ^course_id) |> Repo.one() |> case do nil -> - Group.changeset(%Group{}, params) + %Group{} + |> Group.changeset(%{name: name, course_id: course_id}) + |> Repo.insert() group -> - Group.changeset(group, params) + {:ok, group} end - |> Repo.insert_or_update() end + # @doc """ + # Updates a group based on the group name or create one if it doesn't exist + # """ + # @spec insert_or_update_group(map()) :: {:ok, %Group{}} | {:error, Ecto.Changeset.t()} + # def insert_or_update_group(params = %{name: name}) when is_binary(name) do + # Group + # |> where(name: ^name) + # |> Repo.one() + # |> case do + # nil -> + # Group.changeset(%Group{}, params) + + # group -> + # Group.changeset(group, params) + # end + # |> Repo.insert_or_update() + # end + # @doc """ # Reassign a student to a discussion group # This will un-assign student from the current discussion group diff --git a/lib/cadet/courses/group.ex b/lib/cadet/courses/group.ex index 61df61066..a6b3fef58 100644 --- a/lib/cadet/courses/group.ex +++ b/lib/cadet/courses/group.ex @@ -33,7 +33,8 @@ defmodule Cadet.Courses.Group do defp validate_role(changeset) do leader_id = get_field(changeset, :leader_id) - if leader_id != nil && Repo.get(CourseRegistration, leader_id).role != :staff do + if leader_id != nil && + !Enum.member?([:staff, :admin], Repo.get(CourseRegistration, leader_id).role) do add_error(changeset, :leader, "is not a staff") else changeset diff --git a/lib/cadet_web/admin_controllers/admin_user_controller.ex b/lib/cadet_web/admin_controllers/admin_user_controller.ex index a9dd83268..7b0e2ee62 100644 --- a/lib/cadet_web/admin_controllers/admin_user_controller.ex +++ b/lib/cadet_web/admin_controllers/admin_user_controller.ex @@ -5,7 +5,7 @@ defmodule CadetWeb.AdminUserController do import Ecto.Query alias Cadet.Repo - alias Cadet.Accounts + alias Cadet.{Accounts, Courses} alias Cadet.Accounts.{CourseRegistrations, CourseRegistration} alias Cadet.Auth.Provider @@ -19,61 +19,74 @@ defmodule CadetWeb.AdminUserController do end @add_users_role ~w(admin)a - def add_users(conn, %{ + def upsert_users_and_groups(conn, %{ "course_id" => course_id, - "users" => usernames_and_roles, + "users" => usernames_roles_groups, "provider" => provider }) do %{role: admin_role} = conn.assigns.course_reg - # Note: Usernames from frontend have not been namespaced yet - with {:validate_role, true} <- {:validate_role, admin_role in @add_users_role}, - {:validate_provider, true} <- - {:validate_provider, - Map.has_key?(Application.get_env(:cadet, :identity_providers, %{}), provider)}, - {:atomify_keys, usernames_and_roles} <- - {:atomify_keys, - Enum.map(usernames_and_roles, fn x -> - for({key, val} <- x, into: %{}, do: {String.to_atom(key), val}) - end)}, - {:validate_usernames, true} <- - {:validate_usernames, - Enum.reduce(usernames_and_roles, true, fn x, acc -> - acc and Map.has_key?(x, :username) and is_binary(x.username) and x.username != "" - end)}, - {:validate_roles, true} <- - {:validate_roles, - Enum.reduce(usernames_and_roles, true, fn x, acc -> - acc and Map.has_key?(x, :role) and - String.to_atom(x.role) in Cadet.Accounts.Role.__enums__() - end)}, - {:namespace, usernames_and_roles} <- - {:namespace, - Enum.map(usernames_and_roles, fn x -> - %{x | username: Provider.namespace(x.username, provider)} - end)} do - case Accounts.CourseRegistrations.add_users_to_course(usernames_and_roles, course_id) do - :ok -> + {:ok, conn} = + Repo.transaction(fn -> + # Note: Usernames from frontend have not been namespaced yet + with {:validate_role, true} <- {:validate_role, admin_role in @add_users_role}, + {:validate_provider, true} <- + {:validate_provider, + Map.has_key?(Application.get_env(:cadet, :identity_providers, %{}), provider)}, + {:atomify_keys, usernames_roles_groups} <- + {:atomify_keys, + Enum.map(usernames_roles_groups, fn x -> + for({key, val} <- x, into: %{}, do: {String.to_atom(key), val}) + end)}, + {:validate_usernames, true} <- + {:validate_usernames, + Enum.reduce(usernames_roles_groups, true, fn x, acc -> + acc and Map.has_key?(x, :username) and is_binary(x.username) and + x.username != "" + end)}, + {:validate_roles, true} <- + {:validate_roles, + Enum.reduce(usernames_roles_groups, true, fn x, acc -> + acc and Map.has_key?(x, :role) and + String.to_atom(x.role) in Cadet.Accounts.Role.__enums__() + end)}, + {:namespace, usernames_roles_groups} <- + {:namespace, + Enum.map(usernames_roles_groups, fn x -> + %{x | username: Provider.namespace(x.username, provider)} + end)}, + {:upsert_users, :ok} <- + {:upsert_users, + Accounts.CourseRegistrations.upsert_users_in_course( + usernames_roles_groups, + course_id + )}, + {:upsert_groups, :ok} <- + {:upsert_groups, + Courses.upsert_groups_in_course(usernames_roles_groups, course_id)} do text(conn, "OK") + else + {:validate_role, false} -> + conn |> put_status(:forbidden) |> text("User is not permitted to add users") - {:error, {status, message}} -> - conn - |> put_status(status) - |> text(message) - end - else - {:validate_role, false} -> - conn |> put_status(:forbidden) |> text("User is not permitted to add users") + {:validate_provider, false} -> + conn |> put_status(:bad_request) |> text("Invalid authentication provider") - {:validate_provider, false} -> - conn |> put_status(:bad_request) |> text("Invalid authentication provider") + {:validate_usernames, false} -> + conn |> put_status(:bad_request) |> text("Invalid username(s) provided") - {:validate_usernames, false} -> - conn |> put_status(:bad_request) |> text("Invalid username(s) provided") + {:validate_roles, false} -> + conn |> put_status(:bad_request) |> text("Invalid role(s) provided") - {:validate_roles, false} -> - conn |> put_status(:bad_request) |> text("Invalid role(s) provided") - end + {:upsert_users, {:error, {status, message}}} -> + conn |> put_status(status) |> text(message) + + {:upsert_groups, {:error, {status, message}}} -> + conn |> put_status(status) |> text(message) + end + end) + + conn end @update_role_roles ~w(admin)a diff --git a/lib/cadet_web/router.ex b/lib/cadet_web/router.ex index 9774d5b88..e83a402ae 100644 --- a/lib/cadet_web/router.ex +++ b/lib/cadet_web/router.ex @@ -129,7 +129,7 @@ defmodule CadetWeb.Router do ) get("/users", AdminUserController, :index) - put("/users", AdminUserController, :add_users) + put("/users", AdminUserController, :upsert_users_and_groups) put("/users/role", AdminUserController, :update_role) delete("/users", AdminUserController, :delete_user) post("/users/:userid/goals/:uuid/progress", AdminGoalsController, :update_progress) diff --git a/priv/repo/migrations/20210531155751_multitenant_upgrade.exs b/priv/repo/migrations/20210531155751_multitenant_upgrade.exs index 3eaf39ae7..fa91d9561 100644 --- a/priv/repo/migrations/20210531155751_multitenant_upgrade.exs +++ b/priv/repo/migrations/20210531155751_multitenant_upgrade.exs @@ -333,11 +333,6 @@ defmodule Cadet.Repo.Migrations.MultitenantUpgrade do alter table(:groups) do remove(:temp_leader_id) - modify(:leader_id, references(:course_registrations), - null: false, - from: references(:course_registrations) - ) - modify(:course_id, references(:courses), null: false, from: references(:courses)) end diff --git a/test/cadet/accounts/course_registration_test.exs b/test/cadet/accounts/course_registration_test.exs index 363fd5949..c592b7f4b 100644 --- a/test/cadet/accounts/course_registration_test.exs +++ b/test/cadet/accounts/course_registration_test.exs @@ -154,7 +154,7 @@ defmodule Cadet.Accounts.CourseRegistrationTest do end end - describe "add_users_to_course" do + describe "upsert_users_in_course" do # Note: roles are already validated in the controller test "successful", %{course2: course2} do user = insert(:user, %{username: "existing-user"}) @@ -169,7 +169,7 @@ defmodule Cadet.Accounts.CourseRegistrationTest do %{username: "admin1", role: "admin"} ] - assert :ok == CourseRegistrations.add_users_to_course(usernames_and_roles, course2.id) + assert :ok == CourseRegistrations.upsert_users_in_course(usernames_and_roles, course2.id) assert length(CourseRegistrations.get_users(course2.id)) == 5 end @@ -186,7 +186,7 @@ defmodule Cadet.Accounts.CourseRegistrationTest do %{username: "admin1", role: "admin"} ] - assert :ok == CourseRegistrations.add_users_to_course(usernames_and_roles, course2.id) + assert :ok == CourseRegistrations.upsert_users_in_course(usernames_and_roles, course2.id) assert length(CourseRegistrations.get_users(course2.id)) == 4 end end diff --git a/test/cadet/courses/courses_test.exs b/test/cadet/courses/courses_test.exs index 16dfff449..cd500ae7b 100644 --- a/test/cadet/courses/courses_test.exs +++ b/test/cadet/courses/courses_test.exs @@ -3,7 +3,7 @@ defmodule Cadet.CoursesTest do alias Cadet.{Courses, Repo} alias Cadet.Accounts.{CourseRegistration, User} - alias Cadet.Courses.{Course, Sourcecast, SourcecastUpload} + alias Cadet.Courses.{Course, Group, Sourcecast, SourcecastUpload} describe "create course config" do test "succeeds" do @@ -381,6 +381,64 @@ defmodule Cadet.CoursesTest do end end + describe "upsert_groups_in_course" do + setup do + course = insert(:course) + existing_group_leader = insert(:course_registration, %{course: course, role: :staff}) + + _existing_group = + insert(:group, %{name: "Group1", course: course, leader: existing_group_leader}) + + {:ok, course: course} + end + + test "succeeds", %{course: course} do + student1 = insert(:course_registration, %{course: course, group: nil, role: :student}) + student2 = insert(:course_registration, %{course: course, group: nil, role: :student}) + student3 = insert(:course_registration, %{course: course, group: nil, role: :student}) + staff1 = insert(:course_registration, %{course: course, group: nil, role: :staff}) + staff2 = insert(:course_registration, %{course: course, group: nil, role: :staff}) + admin1 = insert(:course_registration, %{course: course, group: nil, role: :admin}) + admin2 = insert(:course_registration, %{course: course, group: nil, role: :admin}) + + # Some entries do not have group specified + usernames_and_groups = [ + %{username: student1.user.username, group: "Group1"}, + %{username: student2.user.username, group: "Group2"}, + %{username: student3.user.username}, + %{username: staff1.user.username, group: "Group1"}, + %{username: staff2.user.username}, + %{username: admin1.user.username, group: "Group2"}, + %{username: admin2.user.username} + ] + + assert :ok == Courses.upsert_groups_in_course(usernames_and_groups, course.id) + + # Check that Group2 was created + assert length(Group |> where(course_id: ^course.id) |> Repo.all()) == 2 + + # Check that leaders were assigned/ updated correctly + group1 = Group |> where(course_id: ^course.id) |> where(name: "Group1") |> Repo.one() + group2 = Group |> where(course_id: ^course.id) |> where(name: "Group2") |> Repo.one() + assert group1 |> Map.fetch!(:leader_id) == staff1.id + assert group2 |> Map.fetch!(:leader_id) == admin1.id + + # Check that students were assigned to the correct groups + assert CourseRegistration |> where(id: ^student1.id) |> Repo.one() |> Map.fetch!(:group_id) == + group1.id + + assert CourseRegistration |> where(id: ^student2.id) |> Repo.one() |> Map.fetch!(:group_id) == + group2.id + + # Check that entries without group are 'ignored' + assert CourseRegistration |> where(id: ^student3.id) |> Repo.one() |> Map.fetch!(:group_id) == + nil + + assert length(Group |> where(leader_id: ^staff2.id) |> Repo.all()) == 0 + assert length(Group |> where(leader_id: ^admin2.id) |> Repo.all()) == 0 + end + end + describe "Sourcecast" do setup do on_exit(fn -> File.rm_rf!("uploads/test/sourcecasts") end) @@ -414,29 +472,31 @@ defmodule Cadet.CoursesTest do end end - # describe "get_or_create_group" do - # test "existing group" do - # group = insert(:group) + describe "get_or_create_group" do + test "existing group" do + course = insert(:course) + group = insert(:group, %{course: course}) - # {:ok, group_db} = Courses.get_or_create_group(group.name) + {:ok, group_db} = Courses.get_or_create_group(group.name, course.id) - # assert group_db.id == group.id - # assert group_db.leader_id == group.leader_id - # end + assert group_db.id == group.id + assert group_db.leader_id == group.leader_id + end - # test "non-existent group" do - # group_name = params_for(:group).name + test "non-existent group" do + course = insert(:course) + group_name = params_for(:group).name - # {:ok, _} = Courses.get_or_create_group(group_name) + {:ok, _} = Courses.get_or_create_group(group_name, course.id) - # group_db = - # Group - # |> where(name: ^group_name) - # |> Repo.one() + group_db = + Group + |> where(name: ^group_name) + |> Repo.one() - # refute is_nil(group_db) - # end - # end + refute is_nil(group_db) + end + end # describe "insert_or_update_group" do # test "existing group" do diff --git a/test/cadet/courses/group_test.exs b/test/cadet/courses/group_test.exs index f3a7c470e..945ac962f 100644 --- a/test/cadet/courses/group_test.exs +++ b/test/cadet/courses/group_test.exs @@ -8,5 +8,15 @@ defmodule Cadet.Courses.GroupTest do assert_changeset(%{name: "test", course_id: 1}, :valid) assert_changeset(%{name: "tst"}, :invalid) end + + test "validate role" do + student = insert(:course_registration, %{role: :student}) + staff = insert(:course_registration, %{role: :staff}) + admin = insert(:course_registration, %{role: :admin}) + + assert_changeset(%{name: "test", course_id: 1, leader_id: staff.id}, :valid) + assert_changeset(%{name: "test", course_id: 1, leader_id: admin.id}, :valid) + assert_changeset(%{name: "test", course_id: 1, leader_id: student.id}, :invalid) + end end end diff --git a/test/cadet_web/admin_controllers/admin_user_controller_test.exs b/test/cadet_web/admin_controllers/admin_user_controller_test.exs index 34b443843..24c4fb24a 100644 --- a/test/cadet_web/admin_controllers/admin_user_controller_test.exs +++ b/test/cadet_web/admin_controllers/admin_user_controller_test.exs @@ -6,7 +6,7 @@ defmodule CadetWeb.AdminUserControllerTest do alias CadetWeb.AdminUserController alias Cadet.Repo - alias Cadet.Courses.Course + alias Cadet.Courses.{Course, Group} alias Cadet.Accounts.CourseRegistration test "swagger" do @@ -84,22 +84,23 @@ defmodule CadetWeb.AdminUserControllerTest do describe "PUT /v2/courses/{course_id}/admin/users" do @tag authenticate: :admin - test "successfully namespaces and inserts users", %{conn: conn} do + test "successfully namespaces and inserts users, and assign groups", %{conn: conn} do course_id = conn.assigns[:course_id] course = Repo.get(Course, course_id) user = insert(:user, %{username: "test/existing-user"}) insert(:course_registration, %{course: course, user: user}) assert CourseRegistration |> where(course_id: ^course_id) |> Repo.all() |> Enum.count() == 2 + assert Group |> Repo.all() |> Enum.count() == 0 params = %{ users: [ - %{"username" => "existing-user", "role" => "student"}, + %{"username" => "existing-user", "role" => "student", "group" => "group1"}, %{"username" => "student1", "role" => "student"}, - %{"username" => "student2", "role" => "student"}, - %{"username" => "student3", "role" => "student"}, - %{"username" => "staff", "role" => "staff"}, - %{"username" => "admin", "role" => "admin"} + %{"username" => "student2", "role" => "student", "group" => "group2"}, + %{"username" => "student3", "role" => "student", "group" => "group2"}, + %{"username" => "staff", "role" => "staff", "group" => "group1"}, + %{"username" => "admin", "role" => "admin", "group" => "group2"} ], provider: "test" } @@ -108,7 +109,11 @@ defmodule CadetWeb.AdminUserControllerTest do assert response(resp, 200) == "OK" + # Users inserted assert CourseRegistration |> where(course_id: ^course_id) |> Repo.all() |> Enum.count() == 7 + + # Groups created + assert Group |> Repo.all() |> Enum.count() == 2 end @tag authenticate: :admin @@ -122,12 +127,12 @@ defmodule CadetWeb.AdminUserControllerTest do params = %{ users: [ - %{"username" => "existing-user", "role" => "student"}, + %{"username" => "existing-user", "role" => "student", "group" => "group1"}, %{"username" => "student1", "role" => "student"}, - %{"username" => "student2", "role" => "student"}, - %{"username" => "student2", "role" => "student"}, - %{"username" => "staff", "role" => "staff"}, - %{"username" => "admin", "role" => "admin"} + %{"username" => "student2", "role" => "student", "group" => "group2"}, + %{"username" => "student2", "role" => "student", "group" => "group2"}, + %{"username" => "staff", "role" => "staff", "group" => "group1"}, + %{"username" => "admin", "role" => "admin", "group" => "group2"} ], provider: "test" } From 4828abda0064fb74209e4089fe4c73f2cfb2f555 Mon Sep 17 00:00:00 2001 From: Chow En Rong <53928333+chownces@users.noreply.github.com> Date: Mon, 5 Jul 2021 00:00:07 +0800 Subject: [PATCH 117/174] Namespace existing usernames in migration file --- priv/repo/migrations/20210531155751_multitenant_upgrade.exs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/priv/repo/migrations/20210531155751_multitenant_upgrade.exs b/priv/repo/migrations/20210531155751_multitenant_upgrade.exs index fa91d9561..ae851ca6b 100644 --- a/priv/repo/migrations/20210531155751_multitenant_upgrade.exs +++ b/priv/repo/migrations/20210531155751_multitenant_upgrade.exs @@ -146,6 +146,10 @@ defmodule Cadet.Repo.Migrations.MultitenantUpgrade do }) |> Repo.insert() + # Namespace existing usernames + from(u in "users", update: [set: [username: fragment("? || ? ", "luminus/", u.username)]]) + |> Repo.update_all([]) + # Create course registrations for existing users from(u in "users", select: {u.id, u.role, u.group_id, u.game_states}) |> Repo.all() From 9b3cb80db5b755b4924df6476f3ab31bbad7ad72 Mon Sep 17 00:00:00 2001 From: Chow En Rong <53928333+chownces@users.noreply.github.com> Date: Mon, 5 Jul 2021 00:20:40 +0800 Subject: [PATCH 118/174] Updated migration file assessment type configs --- priv/repo/migrations/20210531155751_multitenant_upgrade.exs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/priv/repo/migrations/20210531155751_multitenant_upgrade.exs b/priv/repo/migrations/20210531155751_multitenant_upgrade.exs index ae851ca6b..8d4782546 100644 --- a/priv/repo/migrations/20210531155751_multitenant_upgrade.exs +++ b/priv/repo/migrations/20210531155751_multitenant_upgrade.exs @@ -210,8 +210,8 @@ defmodule Cadet.Repo.Migrations.MultitenantUpgrade do |> AssessmentConfig.changeset(%{ type: assessment_type, course_id: course.id, - is_graded: assessment_type in ["Missions", "Quests", "Contests"], - skippable: assessment_type == "Paths", + is_graded: assessment_type in ["Missions", "Quests", "Contests", "Others"], + skippable: assessment_type != "Paths", is_autograded: assessment_type != "Contests", early_submission_xp: 200, hours_before_early_xp_decay: 48 From 35d01563963ce76c6c61c4073a4453acb8131a63 Mon Sep 17 00:00:00 2001 From: Chow En Rong <53928333+chownces@users.noreply.github.com> Date: Mon, 5 Jul 2021 15:44:11 +0800 Subject: [PATCH 119/174] Updated add users logic --- lib/cadet/courses/courses.ex | 62 ++++++++++++++++--- test/cadet/courses/courses_test.exs | 94 ++++++++++++++++++++--------- 2 files changed, 117 insertions(+), 39 deletions(-) diff --git a/lib/cadet/courses/courses.ex b/lib/cadet/courses/courses.ex index 1d174d6ed..49c4a66b4 100644 --- a/lib/cadet/courses/courses.ex +++ b/lib/cadet/courses/courses.ex @@ -178,14 +178,18 @@ defmodule Cadet.Courses do def upsert_groups_in_course(usernames_and_groups, course_id) do usernames_and_groups |> Enum.reduce_while(nil, fn %{username: username} = entry, _acc -> - with {:ok, groupname} <- Map.fetch(entry, :group) do - case upsert_groups_in_course_helper(username, course_id, groupname) do - {:ok, _} -> {:cont, :ok} - {:error, changeset} -> {:halt, {:error, {:bad_request, full_error_messages(changeset)}}} - end - else - # If no group is specified, continue reduction - :error -> {:cont, :ok} + case Map.fetch(entry, :group) do + {:ok, groupname} -> + # Add users to group + upsert_groups_in_course_helper(username, course_id, groupname) + + :error -> + # Delete users from group + upsert_groups_in_course_helper(username, course_id) + end + |> case do + {:ok, _} -> {:cont, :ok} + {:error, changeset} -> {:halt, {:error, {:bad_request, full_error_messages(changeset)}}} end end) end @@ -209,8 +213,10 @@ defmodule Cadet.Courses do |> CourseRegistration.changeset(%{group_id: group.id}) |> Repo.update() - # If admin or staff, set them as group leader + # If admin or staff, remove their previous group assignment and set them as group leader _ -> + remove_staff_from_group(course_id, course_reg.id) + group |> Group.changeset(%{leader_id: course_reg.id}) |> Repo.update() @@ -218,6 +224,44 @@ defmodule Cadet.Courses do end end + defp upsert_groups_in_course_helper(username, course_id) do + with {:get_course_reg, %{role: role} = course_reg} <- + {:get_course_reg, + CourseRegistration + |> where( + user_id: ^(User |> where(username: ^username) |> Repo.one() |> Map.fetch!(:id)) + ) + |> where(course_id: ^course_id) + |> Repo.one()} do + case role do + :student -> + course_reg + |> CourseRegistration.changeset(%{group_id: nil}) + |> Repo.update() + + _ -> + remove_staff_from_group(course_id, course_reg.id) + {:ok, nil} + end + end + end + + defp remove_staff_from_group(course_id, leader_id) do + Group + |> where(course_id: ^course_id) + |> where(leader_id: ^leader_id) + |> Repo.one() + |> case do + nil -> + nil + + group -> + group + |> Group.changeset(%{leader_id: nil}) + |> Repo.update() + end + end + @doc """ Get a group based on the group name and course id or create one if it doesn't exist """ diff --git a/test/cadet/courses/courses_test.exs b/test/cadet/courses/courses_test.exs index cd500ae7b..f978f201e 100644 --- a/test/cadet/courses/courses_test.exs +++ b/test/cadet/courses/courses_test.exs @@ -386,56 +386,90 @@ defmodule Cadet.CoursesTest do course = insert(:course) existing_group_leader = insert(:course_registration, %{course: course, role: :staff}) - _existing_group = - insert(:group, %{name: "Group1", course: course, leader: existing_group_leader}) + existing_group = + insert(:group, %{name: "Existing Group", course: course, leader: existing_group_leader}) - {:ok, course: course} + existing_student = + insert(:course_registration, %{course: course, group: existing_group, role: :student}) + + {:ok, + course: course, + existing_group: existing_group, + existing_group_leader: existing_group_leader, + existing_student: existing_student} end - test "succeeds", %{course: course} do - student1 = insert(:course_registration, %{course: course, group: nil, role: :student}) - student2 = insert(:course_registration, %{course: course, group: nil, role: :student}) - student3 = insert(:course_registration, %{course: course, group: nil, role: :student}) - staff1 = insert(:course_registration, %{course: course, group: nil, role: :staff}) - staff2 = insert(:course_registration, %{course: course, group: nil, role: :staff}) - admin1 = insert(:course_registration, %{course: course, group: nil, role: :admin}) - admin2 = insert(:course_registration, %{course: course, group: nil, role: :admin}) + test "succeeds in upserting existing groups", %{ + course: course, + existing_group: existing_group, + existing_group_leader: existing_group_leader, + existing_student: existing_student + } do + student = insert(:course_registration, %{course: course, group: nil, role: :student}) + admin = insert(:course_registration, %{course: course, group: nil, role: :admin}) - # Some entries do not have group specified usernames_and_groups = [ - %{username: student1.user.username, group: "Group1"}, - %{username: student2.user.username, group: "Group2"}, - %{username: student3.user.username}, - %{username: staff1.user.username, group: "Group1"}, - %{username: staff2.user.username}, - %{username: admin1.user.username, group: "Group2"}, - %{username: admin2.user.username} + %{username: existing_student.user.username, group: "Group1"}, + %{username: admin.user.username, group: "Group2"}, + %{username: student.user.username, group: "Group2"}, + %{username: existing_group_leader.user.username, group: "Group1"} ] assert :ok == Courses.upsert_groups_in_course(usernames_and_groups, course.id) - # Check that Group2 was created - assert length(Group |> where(course_id: ^course.id) |> Repo.all()) == 2 + # Check that Group1 and Group2 were created + assert length(Group |> where(course_id: ^course.id) |> Repo.all()) == 3 # Check that leaders were assigned/ updated correctly + assert is_nil( + Group + |> where(id: ^existing_group.id) + |> Repo.one() + |> Map.fetch!(:leader_id) + ) + group1 = Group |> where(course_id: ^course.id) |> where(name: "Group1") |> Repo.one() group2 = Group |> where(course_id: ^course.id) |> where(name: "Group2") |> Repo.one() - assert group1 |> Map.fetch!(:leader_id) == staff1.id - assert group2 |> Map.fetch!(:leader_id) == admin1.id + assert group1 |> Map.fetch!(:leader_id) == existing_group_leader.id + assert group2 |> Map.fetch!(:leader_id) == admin.id # Check that students were assigned to the correct groups - assert CourseRegistration |> where(id: ^student1.id) |> Repo.one() |> Map.fetch!(:group_id) == + assert CourseRegistration + |> where(id: ^existing_student.id) + |> Repo.one() + |> Map.fetch!(:group_id) == group1.id - assert CourseRegistration |> where(id: ^student2.id) |> Repo.one() |> Map.fetch!(:group_id) == + assert CourseRegistration |> where(id: ^student.id) |> Repo.one() |> Map.fetch!(:group_id) == group2.id + end - # Check that entries without group are 'ignored' - assert CourseRegistration |> where(id: ^student3.id) |> Repo.one() |> Map.fetch!(:group_id) == - nil + test "succeeds (removes user from existing groups when group is not specified)", %{ + course: course, + existing_group: existing_group, + existing_group_leader: existing_group_leader, + existing_student: existing_student + } do + usernames_and_groups = [ + %{username: existing_student.user.username}, + %{username: existing_group_leader.user.username} + ] + + assert :ok == Courses.upsert_groups_in_course(usernames_and_groups, course.id) - assert length(Group |> where(leader_id: ^staff2.id) |> Repo.all()) == 0 - assert length(Group |> where(leader_id: ^admin2.id) |> Repo.all()) == 0 + assert is_nil( + Group + |> where(id: ^existing_group.id) + |> Repo.one() + |> Map.fetch!(:leader_id) + ) + + assert is_nil( + CourseRegistration + |> where(id: ^existing_student.id) + |> Repo.one() + |> Map.fetch!(:group_id) + ) end end From 2dba878320f17a93cab5a44965e569b1bd634444 Mon Sep 17 00:00:00 2001 From: YaleChen299 Date: Mon, 5 Jul 2021 16:53:53 +0800 Subject: [PATCH 120/174] updated admin grading controller with test(less grading summary) --- lib/cadet/assessments/assessments.ex | 175 ++++++++--- .../admin_grading_controller.ex | 31 +- .../admin_views/admin_grading_view.ex | 5 +- lib/cadet_web/helpers/view_helper.ex | 4 +- .../20210531155751_multitenant_upgrade.exs | 2 +- .../admin_grading_controller_test.exs | 289 +++++++++--------- 6 files changed, 284 insertions(+), 222 deletions(-) diff --git a/lib/cadet/assessments/assessments.ex b/lib/cadet/assessments/assessments.ex index cdc9c5605..c199ee1cd 100644 --- a/lib/cadet/assessments/assessments.ex +++ b/lib/cadet/assessments/assessments.ex @@ -707,11 +707,9 @@ defmodule Cadet.Assessments do end end - # :TODO update avenger_of? call - # def unsubmit_submission(submission_id, user = %User{id: user_id, role: role}) def unsubmit_submission( submission_id, - cr = %CourseRegistration{user_id: user_id, role: role} + cr = %CourseRegistration{id: course_reg_id, role: role} ) when is_ecto_id(submission_id) do submission = @@ -720,7 +718,7 @@ defmodule Cadet.Assessments do |> preload([_, a], assessment: a) |> Repo.get(submission_id) - bypass = role in @bypass_closed_roles and submission.student_id == user_id + bypass = role in @bypass_closed_roles and submission.student_id == course_reg_id with {:submission_found?, true} <- {:submission_found?, is_map(submission)}, {:is_open?, true} <- {:is_open?, bypass or is_open?(submission.assessment)}, @@ -737,7 +735,7 @@ defmodule Cadet.Assessments do |> Submission.changeset(%{ status: :attempted, xp_bonus: 0, - unsubmitted_by_id: user_id, + unsubmitted_by_id: course_reg_id, unsubmitted_at: Timex.now() }) |> Repo.update() @@ -772,7 +770,7 @@ defmodule Cadet.Assessments do Cadet.Accounts.Notifications.handle_unsubmit_notifications( submission.assessment.id, - Cadet.Accounts.get_user(submission.student_id) + Repo.get(CourseRegistration, submission.student_id) ) {:ok, nil} @@ -1083,16 +1081,16 @@ defmodule Cadet.Assessments do The return value is {:ok, submissions} if no errors, else it is {:error, {:unauthorized, "Forbidden."}} """ - @spec all_submissions_by_grader_for_index(%User{}) :: + @spec all_submissions_by_grader_for_index(%CourseRegistration{}) :: {:ok, String.t()} - def all_submissions_by_grader_for_index(grader = %User{}, group_only \\ false) do + def all_submissions_by_grader_for_index(grader = %CourseRegistration{}, group_only \\ false) do show_all = not group_only group_where = if show_all, do: "", else: - "where s.student_id in (select u.id from users u inner join groups g on u.group_id = g.id where g.leader_id = $1) or s.student_id = $1" + "where s.student_id in (select cr.id from course_registrations cr inner join groups g on cr.group_id = g.id where g.leader_id = $1) or s.student_id = $1" params = if show_all, do: [], else: [grader.id] @@ -1107,13 +1105,11 @@ defmodule Cadet.Assessments do s.id, s.status, s."unsubmittedAt", - s.grade, - s.adjustment, s.xp, s."xpAdjustment", s."xpBonus", s."gradedCount", - assts.jsn AS assessment, + assts.jsn as assessment, students.jsn as student, unsubmitters.jsn as "unsubmittedBy" from @@ -1124,8 +1120,6 @@ defmodule Cadet.Assessments do s.status, s.unsubmitted_at as "unsubmittedAt", s.unsubmitted_by_id, - sum(ans.grade) as grade, - sum(ans.adjustment) as adjustment, sum(ans.xp) as xp, sum(ans.xp_adjustment) as "xpAdjustment", s.xp_bonus as "xpBonus", @@ -1138,11 +1132,43 @@ defmodule Cadet.Assessments do inner join (select a.id, to_json(a) as jsn - from (select a.id, a.title, a.type, sum(q.max_grade) as "maxGrade", sum(q.max_xp) as "maxXp", count(q.id) as "questionCount" from assessments a left join questions q on a.id = q.assessment_id group by a.id) a) assts on assts.id = s.assessment_id + from + (select + a.id, + a.title, + max(ac.type) as "type", + sum(q.max_xp) as "maxXp", + count(q.id) as "questionCount" + from assessments a + left join + questions q on a.id = q.assessment_id + inner join + assessment_configs ac on ac.id = a.config_id + group by a.id) a) assts on assts.id = s.assessment_id inner join - (select u.id, to_json(u) as jsn from (select u.id, u.name, g.name as "groupName", g.leader_id as "groupLeaderId" from users u left join groups g on g.id = u.group_id) u) students on students.id = s.student_id + (select + cr.id, to_json(cr) as jsn + from + (select + cr.id, + u.name as "name", + g.name as "groupName", + g.leader_id as "groupLeaderId" + from course_registrations cr + left join + groups g on g.id = cr.group_id + inner join + users u on u.id = cr.user_id) cr) students on students.id = s.student_id left join - (select u.id, to_json(u) as jsn from (select u.id, u.name from users u) u) unsubmitters on s.unsubmitted_by_id = unsubmitters.id + (select + cr.id, to_json(cr) as jsn + from + (select + cr.id, + u.name + from course_registrations cr + inner join + users u on u.id = cr.user_id) cr) unsubmitters on s.unsubmitted_by_id = unsubmitters.id ) q """, params @@ -1160,13 +1186,16 @@ defmodule Cadet.Assessments do |> where(submission_id: ^id) |> join(:inner, [a], q in assoc(a, :question)) |> join(:inner, [_, q], ast in assoc(q, :assessment)) + |> join(:inner, [a, ..., ast], ac in assoc(ast, :config)) |> join(:left, [a, ...], g in assoc(a, :grader)) + |> join(:inner, [a, ..., g], gu in assoc(g, :user)) |> join(:inner, [a, ...], s in assoc(a, :submission)) |> join(:inner, [a, ..., s], st in assoc(s, :student)) - |> preload([_, q, ast, g, s, st], - question: {q, assessment: ast}, - grader: g, - submission: {s, student: st} + |> join(:inner, [a, ..., st], u in assoc(st, :user)) + |> preload([_, q, ast, ac, g, gu, s, st, u], + question: {q, assessment: {ast, config: ac}}, + grader: {g, user: gu}, + submission: {s, student: {st, user: u}} ) answers = @@ -1276,9 +1305,12 @@ defmodule Cadet.Assessments do {:error, {:unauthorized, "User is not permitted to grade."}} end - @spec force_regrade_submission(integer() | String.t(), %User{}) :: + @spec force_regrade_submission(integer() | String.t(), %CourseRegistration{}) :: {:ok, nil} | {:error, {:forbidden | :not_found, String.t()}} - def force_regrade_submission(submission_id, _requesting_user = %User{id: grader_id}) + def force_regrade_submission( + submission_id, + _requesting_user = %CourseRegistration{id: grader_id} + ) when is_ecto_id(submission_id) do with {:get, sub} when not is_nil(sub) <- {:get, Repo.get(Submission, submission_id)}, {:status, true} <- {:status, sub.student_id == grader_id or sub.status == :submitted} do @@ -1297,12 +1329,16 @@ defmodule Cadet.Assessments do {:error, {:forbidden, "User is not permitted to grade."}} end - @spec force_regrade_answer(integer() | String.t(), integer() | String.t(), %User{}) :: + @spec force_regrade_answer( + integer() | String.t(), + integer() | String.t(), + %CourseRegistration{} + ) :: {:ok, nil} | {:error, {:forbidden | :not_found, String.t()}} def force_regrade_answer( submission_id, question_id, - _requesting_user = %User{id: grader_id} + _requesting_user = %CourseRegistration{id: grader_id} ) when is_ecto_id(submission_id) and is_ecto_id(question_id) do answer = @@ -1353,50 +1389,92 @@ defmodule Cadet.Assessments do @type group_summary_entry :: %{ group_name: String.t(), leader_name: String.t(), - ungraded_missions: integer(), - submitted_missions: integer(), - ungraded_sidequests: number(), - submitted_sidequests: number() + ungraded: [map()], + submitted: [map()] + # ungraded_missions: integer(), + # submitted_missions: integer(), + # ungraded_sidequests: number(), + # submitted_sidequests: number() } - @spec get_group_grading_summary :: - {:ok, [group_summary_entry()]} - def get_group_grading_summary do + @spec get_group_grading_summary(integer()) :: + {:ok, [], [group_summary_entry()]} + def get_group_grading_summary(course_id) do subs = Answer |> join(:left, [ans], s in Submission, on: s.id == ans.submission_id) - |> join(:left, [ans, s], st in User, on: s.student_id == st.id) + |> join(:left, [ans, s], st in CourseRegistration, on: s.student_id == st.id) |> join(:left, [ans, s, st], a in Assessment, on: a.id == s.assessment_id) + |> join(:inner, [ans, s, st, a], ac in AssessmentConfig, on: ac.id == a.config_id) |> where( - [ans, s, st, a], + [ans, s, st, a, ac], not is_nil(st.group_id) and s.status == ^:submitted and - a.type in ^["mission", "sidequest"] + ac.is_graded and a.course_id == ^course_id ) - |> group_by([ans, s, st, a], s.id) - |> select([ans, s, st, a], %{ + |> group_by([ans, s, st, a, ac], s.id) + |> select([ans, s, st, a, ac], %{ group_id: max(st.group_id), - type: max(a.type), + config_id: max(ac.id), + config_type: max(ac.type), num_submitted: count(), num_ungraded: filter(count(), is_nil(ans.grader_id)) }) - rows = + raw_data = subs |> subquery() |> join(:left, [t], g in Group, on: t.group_id == g.id) - |> join(:left, [t, g], l in User, on: l.id == g.leader_id) - |> group_by([t, g, l], [t.group_id, g.name, l.name]) - |> select([t, g, l], %{ + |> join(:left, [t, g], l in CourseRegistration, on: l.id == g.leader_id) + |> join(:left, [t, g, l], lu in User, on: lu.id == l.user_id) + |> group_by([t, g, l, lu], [t.group_id, t.config_id, t.config_type, g.name, lu.name]) + |> select([t, g, l, lu], %{ group_name: g.name, - leader_name: l.name, - ungraded_missions: filter(count(), t.type == "mission" and t.num_ungraded > 0), - submitted_missions: filter(count(), t.type == "mission"), - ungraded_sidequests: filter(count(), t.type == "sidequest" and t.num_ungraded > 0), - submitted_sidequests: filter(count(), t.type == "sidequest") + leader_name: lu.name, + config_id: t.config_id, + config_type: t.config_type, + ungraded: filter(count(), t.num_ungraded > 0), + submitted: count() + # ungraded: cols |> Enum.map(fn graded_config -> Map.put(graded_config, :ungraded_count, filter(count(), t.config_id == graded_config.id and t.num_ungraded > 0))end), + # submitted: cols |> Enum.map(fn graded_config -> Map.put(graded_config, :submitted_count, filter(count(), t.config_id == graded_config.id))end) + # ungraded_missions: filter(count(), t.type == "mission" and t.num_ungraded > 0), + # submitted_missions: filter(count(), t.type == "mission"), + # ungraded_sidequests: filter(count(), t.type == "sidequest" and t.num_ungraded > 0), + # submitted_sidequests: filter(count(), t.type == "sidequest") + }) + |> Repo.all() + + graded_configs = + AssessmentConfig + |> where([ac], ac.course_id == ^course_id and ac.is_graded) + |> order_by(:order) + |> select([ac], %{ + id: :id, + type: :type }) |> Repo.all() - {:ok, rows} + data_by_groups = + raw_data + |> Enum.reduce(%{}, fn raw, acc -> + if Map.has_key?(acc, raw.group_name) do + acc + |> put_in([raw.group_name, "ungraded_" <> raw.config_type], raw.ungraded) + |> put_in([raw.group_name, "submitted_" <> raw.config_type], raw.submitted) + else + acc + |> put_in([raw.group_name], %{}) + |> put_in([raw.group_name, "group_name"], raw.group_name) + |> put_in([raw.group_name, "leader_name"], raw.leader_name) + |> put_in([raw.group_name, "ungraded_" <> raw.config_type], raw.ungraded) + |> put_in([raw.group_name, "submitted_" <> raw.config_type], raw.submitted) + end + end) + + # |> + + cols = graded_configs + rows = data_by_groups + {:ok, cols, rows} end defp create_empty_submission(cr = %CourseRegistration{}, assessment = %Assessment{}) do @@ -1444,7 +1522,6 @@ defmodule Cadet.Assessments do end end - # :TODO contest + check voting answer content def insert_or_update_voting_answer(submission_id, course_reg_id, question_id, answer_content) do set_rank_to_nil = SubmissionVotes diff --git a/lib/cadet_web/admin_controllers/admin_grading_controller.ex b/lib/cadet_web/admin_controllers/admin_grading_controller.ex index 53330148a..a65d13d33 100644 --- a/lib/cadet_web/admin_controllers/admin_grading_controller.ex +++ b/lib/cadet_web/admin_controllers/admin_grading_controller.ex @@ -5,11 +5,11 @@ defmodule CadetWeb.AdminGradingController do alias Cadet.Assessments def index(conn, %{"group" => group}) when group in ["true", "false"] do - user = conn.assigns[:current_user] + course_reg = conn.assigns[:course_reg] group = String.to_atom(group) - case Assessments.all_submissions_by_grader_for_index(user, group) do + case Assessments.all_submissions_by_grader_for_index(course_reg, group) do {:ok, submissions} -> conn |> put_status(:ok) @@ -43,19 +43,14 @@ defmodule CadetWeb.AdminGradingController do } ) when is_ecto_id(submission_id) and is_ecto_id(question_id) do - user = conn.assigns[:current_user] + course_reg = conn.assigns[:course_reg] - grading = - if raw_grading["xpAdjustment"] do - Map.put(raw_grading, "xp_adjustment", raw_grading["xpAdjustment"]) - else - raw_grading - end + grading = raw_grading |> snake_casify_string_keys() case Assessments.update_grading_info( %{submission_id: submission_id, question_id: question_id}, grading, - user + course_reg ) do {:ok, _} -> text(conn, "OK") @@ -74,9 +69,9 @@ defmodule CadetWeb.AdminGradingController do end def unsubmit(conn, %{"submissionid" => submission_id}) when is_ecto_id(submission_id) do - user = conn.assigns[:current_user] + course_reg = conn.assigns[:course_reg] - case Assessments.unsubmit_submission(submission_id, user) do + case Assessments.unsubmit_submission(submission_id, course_reg) do {:ok, nil} -> text(conn, "OK") @@ -94,9 +89,9 @@ defmodule CadetWeb.AdminGradingController do end def autograde_submission(conn, %{"submissionid" => submission_id}) do - user = conn.assigns[:current_user] + course_reg = conn.assigns[:course_reg] - case Assessments.force_regrade_submission(submission_id, user) do + case Assessments.force_regrade_submission(submission_id, course_reg) do {:ok, nil} -> send_resp(conn, :no_content, "") @@ -108,9 +103,9 @@ defmodule CadetWeb.AdminGradingController do end def autograde_answer(conn, %{"submissionid" => submission_id, "questionid" => question_id}) do - user = conn.assigns[:current_user] + course_reg = conn.assigns[:course_reg] - case Assessments.force_regrade_answer(submission_id, question_id, user) do + case Assessments.force_regrade_answer(submission_id, question_id, course_reg) do {:ok, nil} -> send_resp(conn, :no_content, "") @@ -121,8 +116,8 @@ defmodule CadetWeb.AdminGradingController do end end - def grading_summary(conn, _params) do - case Assessments.get_group_grading_summary() do + def grading_summary(conn, %{"course_id" => course_id}) do + case Assessments.get_group_grading_summary(course_id) do {:ok, summary} -> render(conn, "grading_summary.json", summary: summary) end diff --git a/lib/cadet_web/admin_views/admin_grading_view.ex b/lib/cadet_web/admin_views/admin_grading_view.ex index 934a50010..60a6cfb19 100644 --- a/lib/cadet_web/admin_views/admin_grading_view.ex +++ b/lib/cadet_web/admin_views/admin_grading_view.ex @@ -9,7 +9,8 @@ defmodule CadetWeb.AdminGradingView do def render("grading_info.json", %{answer: answer}) do transform_map_for_view(answer, %{ - student: &transform_map_for_view(&1.submission.student, [:name, :id]), + student: + &transform_map_for_view(&1.submission.student, %{name: fn st -> st.user.name end, id: :id}), question: &build_grading_question/1, solution: &(&1.question.question["solution"] || ""), grade: &build_grade/1 @@ -51,8 +52,6 @@ defmodule CadetWeb.AdminGradingView do transform_map_for_view(answer, %{ grader: grader_builder(grader), gradedAt: graded_at_builder(grader), - grade: :grade, - adjustment: :adjustment, xp: :xp, xpAdjustment: :xp_adjustment, comments: :comments diff --git a/lib/cadet_web/helpers/view_helper.ex b/lib/cadet_web/helpers/view_helper.ex index 033ca8fae..39310ea73 100644 --- a/lib/cadet_web/helpers/view_helper.ex +++ b/lib/cadet_web/helpers/view_helper.ex @@ -3,8 +3,8 @@ defmodule CadetWeb.ViewHelper do Helper functions shared throughout views """ - defp build_staff(user) do - transform_map_for_view(user, [:name, :id]) + defp build_staff(course_reg) do + transform_map_for_view(course_reg, %{name: fn st -> st.user.name end, id: :id}) end def unsubmitted_by_builder(nil), do: nil diff --git a/priv/repo/migrations/20210531155751_multitenant_upgrade.exs b/priv/repo/migrations/20210531155751_multitenant_upgrade.exs index fa91d9561..162e061b3 100644 --- a/priv/repo/migrations/20210531155751_multitenant_upgrade.exs +++ b/priv/repo/migrations/20210531155751_multitenant_upgrade.exs @@ -71,7 +71,7 @@ defmodule Cadet.Repo.Migrations.MultitenantUpgrade do alter table(:groups) do remove(:mentor_id) - add(:leader_id, references(:course_registrations)) + add(:leader_id, references(:course_registrations), null: true) add(:course_id, references(:courses)) end diff --git a/test/cadet_web/admin_controllers/admin_grading_controller_test.exs b/test/cadet_web/admin_controllers/admin_grading_controller_test.exs index 84e531df0..f9de65768 100644 --- a/test/cadet_web/admin_controllers/admin_grading_controller_test.exs +++ b/test/cadet_web/admin_controllers/admin_grading_controller_test.exs @@ -106,11 +106,12 @@ defmodule CadetWeb.AdminGradingControllerTest do @tag authenticate: :staff test "avenger gets to see all students submissions", %{conn: conn} do %{ + course: course, mission: mission, submissions: submissions } = seed_db(conn) - conn = get(conn, build_url()) + conn = get(conn, build_url(course.id)) expected = Enum.map(submissions, fn submission -> @@ -118,18 +119,15 @@ defmodule CadetWeb.AdminGradingControllerTest do "xp" => 5000, "xpAdjustment" => -2500, "xpBonus" => 100, - "grade" => 1000, - "adjustment" => -500, "id" => submission.id, "student" => %{ - "name" => submission.student.name, + "name" => submission.student.user.name, "id" => submission.student.id, "groupName" => submission.student.group.name, "groupLeaderId" => submission.student.group.leader_id }, "assessment" => %{ - "type" => "mission", - "maxGrade" => 1000, + "type" => mission.config.type, "maxXp" => 5000, "id" => mission.id, "title" => mission.title, @@ -151,10 +149,13 @@ defmodule CadetWeb.AdminGradingControllerTest do test "staff not leading a group to get empty", %{conn: conn} do seed_db(conn) + test_cr = conn.assigns.test_cr + new_staff = insert(:course_registration, %{course: test_cr.course, role: :staff}) + resp = conn - |> sign_in(insert(:user, role: :staff)) - |> get(build_url(), %{"group" => "true"}) + |> sign_in(new_staff.user) + |> get(build_url(test_cr.course_id), %{"group" => "true"}) |> json_response(200) assert resp == [] @@ -163,14 +164,16 @@ defmodule CadetWeb.AdminGradingControllerTest do @tag authenticate: :staff test "filtered by its own group", %{conn: conn} do %{ + course: course, mission: mission, submissions: submissions } = seed_db(conn) # just to insert more submissions - seed_db(conn, insert(:user, role: :staff)) + new_staff = insert(:course_registration, %{course: course, role: :staff}) + seed_db(conn, new_staff) - conn = get(conn, build_url(), %{"group" => "true"}) + conn = get(conn, build_url(course.id), %{"group" => "true"}) expected = Enum.map(submissions, fn submission -> @@ -178,18 +181,15 @@ defmodule CadetWeb.AdminGradingControllerTest do "xp" => 5000, "xpAdjustment" => -2500, "xpBonus" => 100, - "grade" => 1000, - "adjustment" => -500, "id" => submission.id, "student" => %{ - "name" => submission.student.name, + "name" => submission.student.user.name, "id" => submission.student.id, "groupName" => submission.student.group.name, "groupLeaderId" => submission.student.group.leader_id }, "assessment" => %{ - "type" => "mission", - "maxGrade" => 1000, + "type" => mission.config.type, "maxXp" => 5000, "id" => mission.id, "title" => mission.title, @@ -210,6 +210,7 @@ defmodule CadetWeb.AdminGradingControllerTest do @tag authenticate: :staff test "successful", %{conn: conn} do %{ + course: course, grader: grader, submissions: submissions, answers: answers @@ -217,7 +218,7 @@ defmodule CadetWeb.AdminGradingControllerTest do submission = List.first(submissions) - conn = get(conn, build_url(submission.id)) + conn = get(conn, build_url(course.id, submission.id)) expected = answers @@ -258,7 +259,6 @@ defmodule CadetWeb.AdminGradingControllerTest do "symbols" => &1.question.library.external.symbols } }, - "maxGrade" => &1.question.max_grade, "maxXp" => &1.question.max_xp, "content" => &1.question.question.content, "answer" => &1.answer.code, @@ -267,19 +267,17 @@ defmodule CadetWeb.AdminGradingControllerTest do }, "solution" => &1.question.question.solution, "grade" => %{ - "grade" => &1.grade, - "adjustment" => &1.adjustment, "xp" => &1.xp, "xpAdjustment" => &1.xp_adjustment, "grader" => %{ - "name" => grader.name, + "name" => grader.user.name, "id" => grader.id }, "gradedAt" => format_datetime(&1.updated_at), "comments" => &1.comments }, "student" => %{ - "name" => &1.submission.student.name, + "name" => &1.submission.student.user.name, "id" => &1.submission.student.id } } @@ -297,7 +295,6 @@ defmodule CadetWeb.AdminGradingControllerTest do "symbols" => &1.question.library.external.symbols } }, - "maxGrade" => &1.question.max_grade, "maxXp" => &1.question.max_xp, "content" => &1.question.question.content, "answer" => &1.answer.choice_id, @@ -314,19 +311,17 @@ defmodule CadetWeb.AdminGradingControllerTest do }, "solution" => "", "grade" => %{ - "grade" => &1.grade, - "adjustment" => &1.adjustment, "xp" => &1.xp, "xpAdjustment" => &1.xp_adjustment, "grader" => %{ - "name" => grader.name, + "name" => grader.user.name, "id" => grader.id }, "gradedAt" => format_datetime(&1.updated_at), "comments" => &1.comments }, "student" => %{ - "name" => &1.submission.student.name, + "name" => &1.submission.student.user.name, "id" => &1.submission.student.id } } @@ -346,7 +341,6 @@ defmodule CadetWeb.AdminGradingControllerTest do "symbols" => &1.question.library.external.symbols } }, - "maxGrade" => &1.question.max_grade, "maxXp" => &1.question.max_xp, "content" => &1.question.question.content, "autogradingStatus" => Atom.to_string(&1.autograding_status), @@ -356,19 +350,17 @@ defmodule CadetWeb.AdminGradingControllerTest do "contestLeaderboard" => [] }, "grade" => %{ - "grade" => &1.grade, - "adjustment" => &1.adjustment, "xp" => &1.xp, "xpAdjustment" => &1.xp_adjustment, "grader" => %{ - "name" => grader.name, + "name" => grader.user.name, "id" => grader.id }, "gradedAt" => format_datetime(&1.updated_at), "comments" => &1.comments }, "student" => %{ - "name" => &1.submission.student.name, + "name" => &1.submission.student.user.name, "id" => &1.submission.student.id }, "solution" => "" @@ -383,16 +375,15 @@ defmodule CadetWeb.AdminGradingControllerTest do describe "POST /:submissionid/:questionid, staff" do @tag authenticate: :staff test "successful", %{conn: conn} do - %{grader: grader, answers: answers} = seed_db(conn) + %{course: course, grader: grader, answers: answers} = seed_db(conn) grader_id = grader.id answer = List.first(answers) conn = - post(conn, build_url(answer.submission.id, answer.question.id), %{ + post(conn, build_url(course.id, answer.submission.id, answer.question.id), %{ "grading" => %{ - "adjustment" => -10, "xpAdjustment" => -10 } }) @@ -400,35 +391,19 @@ defmodule CadetWeb.AdminGradingControllerTest do assert response(conn, 200) == "OK" assert %{ - adjustment: -10, xp_adjustment: -10, grader_id: ^grader_id } = Repo.get(Answer, answer.id) end - @tag authenticate: :staff - test "invalid adjustment fails", %{conn: conn} do - %{answers: answers} = seed_db(conn) - - answer = List.first(answers) - - conn = - post(conn, build_url(answer.submission.id, answer.question.id), %{ - "grading" => %{"adjustment" => -9_999_999_999} - }) - - assert response(conn, 400) == - "adjustment must make total be between 0 and question.max_grade" - end - @tag authenticate: :staff test "invalid xp_adjustment fails", %{conn: conn} do - %{answers: answers} = seed_db(conn) + %{course: course, answers: answers} = seed_db(conn) answer = List.first(answers) conn = - post(conn, build_url(answer.submission.id, answer.question.id), %{ + post(conn, build_url(course.id, answer.submission.id, answer.question.id), %{ "grading" => %{"xpAdjustment" => -9_999_999_999} }) @@ -438,13 +413,14 @@ defmodule CadetWeb.AdminGradingControllerTest do @tag authenticate: :staff test "missing parameter", %{conn: conn} do - conn = post(conn, build_url(1, 3), %{}) + course_id = conn.assigns.course_id + conn = post(conn, build_url(course_id, 1, 3), %{}) assert response(conn, 400) =~ "Missing parameter" end @tag authenticate: :staff test "submission is not :submitted", %{conn: conn} do - %{grader: grader, mission: mission, questions: questions} = seed_db(conn) + %{course: course, grader: grader, mission: mission, questions: questions} = seed_db(conn) submission = insert(:submission, %{assessment: mission, status: :attempting}) @@ -453,8 +429,6 @@ defmodule CadetWeb.AdminGradingControllerTest do answer = insert(:answer, %{ grader_id: grader.id, - grade: 200, - adjustment: -100, xp: 1000, xp_adjustment: -500, question: question, @@ -468,9 +442,8 @@ defmodule CadetWeb.AdminGradingControllerTest do }) conn = - post(conn, build_url(answer.submission_id, answer.question_id), %{ + post(conn, build_url(course.id, answer.submission_id, answer.question_id), %{ "grading" => %{ - "adjustment" => -100, "xpAdjustment" => -100 } }) @@ -482,7 +455,7 @@ defmodule CadetWeb.AdminGradingControllerTest do describe "POST /:submissionid/unsubmit, staff" do @tag authenticate: :staff test "succeeds", %{conn: conn} do - %{grader: grader, students: students} = seed_db(conn) + %{course: course, config: config, grader: grader, students: students} = seed_db(conn) assessment = insert( @@ -490,7 +463,8 @@ defmodule CadetWeb.AdminGradingControllerTest do open_at: Timex.shift(Timex.now(), hours: -1), close_at: Timex.shift(Timex.now(), hours: 500), is_published: true, - type: "mission" + config: config, + course: course ) question = insert(:programming_question, assessment: assessment) @@ -509,8 +483,7 @@ defmodule CadetWeb.AdminGradingControllerTest do ) conn - |> sign_in(grader) - |> post(build_url_unsubmit(submission.id)) + |> post(build_url_unsubmit(course.id, submission.id)) |> response(200) submission_db = Repo.get(Submission, submission.id) @@ -525,8 +498,6 @@ defmodule CadetWeb.AdminGradingControllerTest do assert answer_db.grader_id == grader.id assert answer_db.xp == 0 assert answer_db.xp_adjustment == 0 - assert answer_db.grade == 0 - assert answer_db.adjustment == 0 assert answer_db.comments == answer.comments end @@ -534,7 +505,7 @@ defmodule CadetWeb.AdminGradingControllerTest do test "assessments which have not been submitted should not be allowed to unsubmit", %{ conn: conn } do - %{grader: grader, students: students} = seed_db(conn) + %{course: course, config: config, students: students} = seed_db(conn) assessment = insert( @@ -542,7 +513,8 @@ defmodule CadetWeb.AdminGradingControllerTest do open_at: Timex.shift(Timex.now(), hours: -1), close_at: Timex.shift(Timex.now(), hours: 500), is_published: true, - type: "mission" + config: config, + course: course ) question = insert(:programming_question, assessment: assessment) @@ -560,15 +532,14 @@ defmodule CadetWeb.AdminGradingControllerTest do conn = conn - |> sign_in(grader) - |> post(build_url_unsubmit(submission.id)) + |> post(build_url_unsubmit(course.id, submission.id)) assert response(conn, 400) =~ "Assessment has not been submitted" end @tag authenticate: :staff test "assessment that is not open anymore cannot be unsubmitted", %{conn: conn} do - %{grader: grader, students: students} = seed_db(conn) + %{course: course, config: config, students: students} = seed_db(conn) assessment = insert( @@ -576,7 +547,8 @@ defmodule CadetWeb.AdminGradingControllerTest do open_at: Timex.shift(Timex.now(), hours: 1), close_at: Timex.shift(Timex.now(), hours: 500), is_published: true, - type: "mission" + course: course, + config: config ) question = insert(:programming_question, assessment: assessment) @@ -594,8 +566,7 @@ defmodule CadetWeb.AdminGradingControllerTest do conn = conn - |> sign_in(grader) - |> post(build_url_unsubmit(submission.id)) + |> post(build_url_unsubmit(course.id, submission.id)) assert response(conn, 403) =~ "Assessment not open" end @@ -604,7 +575,7 @@ defmodule CadetWeb.AdminGradingControllerTest do test "avenger should not be allowed to unsubmit for students outside of their group", %{ conn: conn } do - %{students: students} = seed_db(conn) + %{course: course, config: config, students: students} = seed_db(conn) assessment = insert( @@ -612,10 +583,11 @@ defmodule CadetWeb.AdminGradingControllerTest do open_at: Timex.shift(Timex.now(), hours: -1), close_at: Timex.shift(Timex.now(), hours: 500), is_published: true, - type: "mission" + course: course, + config: config ) - other_grader = insert(:user, role: :staff) + other_grader = insert(:course_registration, %{role: :staff, course: course}) question = insert(:programming_question, assessment: assessment) student = List.first(students) @@ -631,8 +603,8 @@ defmodule CadetWeb.AdminGradingControllerTest do conn = conn - |> sign_in(other_grader) - |> post(build_url_unsubmit(submission.id)) + |> sign_in(other_grader.user) + |> post(build_url_unsubmit(course.id, submission.id)) assert response(conn, 403) =~ "Only Avenger of student or Admin is permitted to unsubmit" end @@ -641,16 +613,18 @@ defmodule CadetWeb.AdminGradingControllerTest do test "avenger should be allowed to unsubmit own submissions", %{ conn: conn } do + %{course: course, config: config, grader: grader} = seed_db(conn) + assessment = insert( :assessment, open_at: Timex.shift(Timex.now(), hours: -1), close_at: Timex.shift(Timex.now(), hours: 500), is_published: true, - type: "mission" + course: course, + config: config ) - grader = conn.assigns.current_user question = insert(:programming_question, assessment: assessment) submission = @@ -665,7 +639,7 @@ defmodule CadetWeb.AdminGradingControllerTest do conn = conn - |> post(build_url_unsubmit(submission.id)) + |> post(build_url_unsubmit(course.id, submission.id)) assert response(conn, 200) =~ "OK" end @@ -674,16 +648,18 @@ defmodule CadetWeb.AdminGradingControllerTest do test "avenger should be allowed to unsubmit own closed submissions", %{ conn: conn } do + %{course: course, config: config, grader: grader} = seed_db(conn) + assessment = insert( :assessment, open_at: Timex.shift(Timex.now(), hours: 1), close_at: Timex.shift(Timex.now(), hours: 500), is_published: true, - type: "mission" + course: course, + config: config ) - grader = conn.assigns.current_user question = insert(:programming_question, assessment: assessment) submission = @@ -698,7 +674,7 @@ defmodule CadetWeb.AdminGradingControllerTest do conn = conn - |> post(build_url_unsubmit(submission.id)) + |> post(build_url_unsubmit(course.id, submission.id)) assert response(conn, 200) =~ "OK" end @@ -707,9 +683,9 @@ defmodule CadetWeb.AdminGradingControllerTest do test "admin should be allowed to unsubmit", %{ conn: conn } do - %{students: students} = seed_db(conn) + %{course: course, config: config, students: students} = seed_db(conn) - admin = insert(:user, %{role: :admin}) + admin = insert(:course_registration, %{role: :admin, course: course}) assessment = insert( @@ -717,7 +693,8 @@ defmodule CadetWeb.AdminGradingControllerTest do open_at: Timex.shift(Timex.now(), hours: -1), close_at: Timex.shift(Timex.now(), hours: 500), is_published: true, - type: "mission" + course: course, + config: config ) question = insert(:programming_question, assessment: assessment) @@ -735,8 +712,8 @@ defmodule CadetWeb.AdminGradingControllerTest do ) conn - |> sign_in(admin) - |> post(build_url_unsubmit(submission.id)) + |> sign_in(admin.user) + |> post(build_url_unsubmit(course.id, submission.id)) submission_db = Repo.get(Submission, submission.id) answer_db = Repo.get(Answer, answer.id) @@ -750,8 +727,6 @@ defmodule CadetWeb.AdminGradingControllerTest do assert answer_db.grader_id == nil assert answer_db.xp == 0 assert answer_db.xp_adjustment == 0 - assert answer_db.grade == 0 - assert answer_db.adjustment == 0 end end @@ -759,16 +734,17 @@ defmodule CadetWeb.AdminGradingControllerTest do @tag authenticate: :staff test "can see all submissions", %{conn: conn} do %{ + course: course, mission: mission, submissions: submissions } = seed_db(conn) - admin = insert(:user, role: :admin) + admin = insert(:course_registration, course: course, role: :admin) conn = conn - |> sign_in(admin) - |> get(build_url()) + |> sign_in(admin.user) + |> get(build_url(course.id)) expected = Enum.map(submissions, fn submission -> @@ -776,18 +752,15 @@ defmodule CadetWeb.AdminGradingControllerTest do "xp" => 5000, "xpAdjustment" => -2500, "xpBonus" => 100, - "grade" => 1000, - "adjustment" => -500, "id" => submission.id, "student" => %{ - "name" => submission.student.name, + "name" => submission.student.user.name, "id" => submission.student.id, "groupName" => submission.student.group.name, "groupLeaderId" => submission.student.group.leader_id }, "assessment" => %{ - "type" => "mission", - "maxGrade" => 1000, + "type" => mission.config.type, "maxXp" => 5000, "id" => mission.id, "title" => mission.title, @@ -808,11 +781,12 @@ defmodule CadetWeb.AdminGradingControllerTest do @tag authenticate: :admin test "successful", %{conn: conn} do %{ + course: course, mission: mission, submissions: submissions } = seed_db(conn) - conn = get(conn, build_url(), %{"group" => "true"}) + conn = get(conn, build_url(course.id), %{"group" => "true"}) expected = Enum.map(submissions, fn submission -> @@ -820,18 +794,15 @@ defmodule CadetWeb.AdminGradingControllerTest do "xp" => 5000, "xpAdjustment" => -2500, "xpBonus" => 100, - "grade" => 1000, - "adjustment" => -500, "id" => submission.id, "student" => %{ - "name" => submission.student.name, + "name" => submission.student.user.name, "id" => submission.student.id, "groupName" => submission.student.group.name, "groupLeaderId" => submission.student.group.leader_id }, "assessment" => %{ - "type" => "mission", - "maxGrade" => 1000, + "type" => mission.config.type, "maxXp" => 5000, "id" => mission.id, "title" => mission.title, @@ -852,6 +823,7 @@ defmodule CadetWeb.AdminGradingControllerTest do @tag authenticate: :admin test "successful", %{conn: conn} do %{ + course: course, grader: grader, submissions: submissions, answers: answers @@ -859,7 +831,7 @@ defmodule CadetWeb.AdminGradingControllerTest do submission = List.first(submissions) - conn = get(conn, build_url(submission.id)) + conn = get(conn, build_url(course.id, submission.id)) expected = answers @@ -900,7 +872,6 @@ defmodule CadetWeb.AdminGradingControllerTest do "symbols" => &1.question.library.external.symbols } }, - "maxGrade" => &1.question.max_grade, "maxXp" => &1.question.max_xp, "content" => &1.question.question.content, "answer" => &1.answer.code, @@ -909,19 +880,17 @@ defmodule CadetWeb.AdminGradingControllerTest do }, "solution" => &1.question.question.solution, "grade" => %{ - "grade" => &1.grade, - "adjustment" => &1.adjustment, "xp" => &1.xp, "xpAdjustment" => &1.xp_adjustment, "grader" => %{ - "name" => grader.name, + "name" => grader.user.name, "id" => grader.id }, "gradedAt" => format_datetime(&1.updated_at), "comments" => &1.comments }, "student" => %{ - "name" => &1.submission.student.name, + "name" => &1.submission.student.user.name, "id" => &1.submission.student.id } } @@ -941,7 +910,6 @@ defmodule CadetWeb.AdminGradingControllerTest do }, "content" => &1.question.question.content, "answer" => &1.answer.choice_id, - "maxGrade" => &1.question.max_grade, "maxXp" => &1.question.max_xp, "choices" => for choice <- &1.question.question.choices do @@ -956,19 +924,17 @@ defmodule CadetWeb.AdminGradingControllerTest do }, "solution" => "", "grade" => %{ - "grade" => &1.grade, - "adjustment" => &1.adjustment, "xp" => &1.xp, "xpAdjustment" => &1.xp_adjustment, "grader" => %{ - "name" => grader.name, + "name" => grader.user.name, "id" => grader.id }, "gradedAt" => format_datetime(&1.updated_at), "comments" => &1.comments }, "student" => %{ - "name" => &1.submission.student.name, + "name" => &1.submission.student.user.name, "id" => &1.submission.student.id } } @@ -988,7 +954,6 @@ defmodule CadetWeb.AdminGradingControllerTest do "symbols" => &1.question.library.external.symbols } }, - "maxGrade" => &1.question.max_grade, "maxXp" => &1.question.max_xp, "content" => &1.question.question.content, "autogradingStatus" => Atom.to_string(&1.autograding_status), @@ -998,19 +963,17 @@ defmodule CadetWeb.AdminGradingControllerTest do "contestLeaderboard" => [] }, "grade" => %{ - "grade" => &1.grade, - "adjustment" => &1.adjustment, "xp" => &1.xp, "xpAdjustment" => &1.xp_adjustment, "grader" => %{ - "name" => grader.name, + "name" => grader.user.name, "id" => grader.id }, "gradedAt" => format_datetime(&1.updated_at), "comments" => &1.comments }, "student" => %{ - "name" => &1.submission.student.name, + "name" => &1.submission.student.user.name, "id" => &1.submission.student.id }, "solution" => "" @@ -1025,42 +988,45 @@ defmodule CadetWeb.AdminGradingControllerTest do describe "POST /:submissionid/:questionid, admin" do @tag authenticate: :admin test "succeeds", %{conn: conn} do - %{answers: answers} = seed_db(conn) + %{course: course, answers: answers} = seed_db(conn) answer = List.first(answers) conn = - post(conn, build_url(answer.submission.id, answer.question.id), %{ - "grading" => %{"adjustment" => -10} + post(conn, build_url(course.id, answer.submission.id, answer.question.id), %{ + "grading" => %{"xpAdjustment" => -10} }) assert response(conn, 200) == "OK" - assert %{adjustment: -10} = Repo.get(Answer, answer.id) + assert %{xp_adjustment: -10} = Repo.get(Answer, answer.id) end @tag authenticate: :admin test "missing parameter", %{conn: conn} do - conn = post(conn, build_url(1, 3), %{}) + course_id = conn.assigns.course_id + conn = post(conn, build_url(course_id, 1, 3), %{}) assert response(conn, 400) =~ "Missing parameter" end end describe "GET /summary" do @tag authenticate: :admin + @tag :skip test "admin can see summary", %{conn: conn} do %{ + course: course, submissions: submissions, group: group, grader: grader, answers: answers } = seed_db(conn) - conn = get(conn, build_url_summary()) + conn = get(conn, build_url_summary(course.id)) expected = [ %{ "groupName" => group.name, - "leaderName" => grader.name, + "leaderName" => grader.user.name, "submittedMissions" => count_submissions(submissions, answers, "mission"), "submittedSidequests" => count_submissions(submissions, answers, "sidequest"), "ungradedMissions" => count_submissions(submissions, answers, "mission", true), @@ -1072,6 +1038,7 @@ defmodule CadetWeb.AdminGradingControllerTest do end @tag authenticate: :student + @tag :skip test "student cannot see summary", %{conn: conn} do conn = get(conn, build_url_summary()) assert response(conn, 403) =~ "Forbidden" @@ -1081,44 +1048,51 @@ defmodule CadetWeb.AdminGradingControllerTest do describe "POST /grading/:submissionid/autograde" do setup %{conn: conn} do %{ + course: course, submissions: [submission, _] } = seed_db(conn) - %{submission: submission} + %{course: course, submission: submission} end @tag authenticate: :staff - test "staff can re-autograde submissions", %{conn: conn, submission: submission} do + test "staff can re-autograde submissions", %{ + conn: conn, + course: course, + submission: submission + } do with_mock Cadet.Autograder.GradingJob, force_grade_individual_submission: fn in_sub, _ -> assert submission.id == in_sub.id end do - assert conn |> post(build_url_autograde(submission.id)) |> response(204) + assert conn |> post(build_url_autograde(course.id, submission.id)) |> response(204) end end @tag authenticate: :student - test "student cannot re-autograde", %{conn: conn, submission: submission} do - assert conn |> post(build_url_autograde(submission.id)) |> response(403) + test "student cannot re-autograde", %{conn: conn, course: course, submission: submission} do + assert conn |> post(build_url_autograde(course.id, submission.id)) |> response(403) end @tag authenticate: :student - test "fails if not found", %{conn: conn} do - assert conn |> post(build_url_autograde(2_147_483_647)) |> response(403) + test "fails if not found", %{conn: conn, course: course} do + assert conn |> post(build_url_autograde(course.id, 2_147_483_647)) |> response(403) end end describe "POST /grading/:submissionid/:questionid/autograde" do setup %{conn: conn} do %{ + course: course, submissions: [submission | _], questions: [question | _] } = seed_db(conn) - %{submission: submission, question: question} + %{course: course, submission: submission, question: question} end @tag authenticate: :staff test "staff can re-autograde questions", %{ conn: conn, + course: course, submission: submission, question: question } do @@ -1127,18 +1101,27 @@ defmodule CadetWeb.AdminGradingControllerTest do assert question.id == in_q.id assert question.id == in_a.question_id end do - assert conn |> post(build_url_autograde(submission.id, question.id)) |> response(204) + assert conn + |> post(build_url_autograde(course.id, submission.id, question.id)) + |> response(204) end end @tag authenticate: :student - test "student cannot re-autograde", %{conn: conn, submission: submission, question: question} do - assert conn |> post(build_url_autograde(submission.id, question.id)) |> response(403) + test "student cannot re-autograde", %{ + conn: conn, + course: course, + submission: submission, + question: question + } do + assert conn + |> post(build_url_autograde(course.id, submission.id, question.id)) + |> response(403) end @tag authenticate: :student - test "fails if not found", %{conn: conn} do - assert conn |> post(build_url_autograde(2_147_483_647, 123_456)) |> response(403) + test "fails if not found", %{conn: conn, course: course} do + assert conn |> post(build_url_autograde(course.id, 2_147_483_647, 123_456)) |> response(403) end end @@ -1154,6 +1137,8 @@ defmodule CadetWeb.AdminGradingControllerTest do |> length() end + defp build_url_summary, do: "/v2/admin/grading/summary" + # old defp build_url(course_id), do: "/v2/courses/#{course_id}/admin/grading/" defp build_url_summary(course_id), do: "/v2/courses/#{course_id}/admin/grading/summary" defp build_url(course_id, submission_id), do: "#{build_url(course_id)}#{submission_id}" @@ -1171,20 +1156,28 @@ defmodule CadetWeb.AdminGradingControllerTest do do: "#{build_url(course_id, submission_id, question_id)}/autograde" defp seed_db(conn, override_grader \\ nil) do - course = insert(:course) grader = override_grader || conn.assigns[:test_cr] - group = insert(:group, %{leader_id: grader.id, leader: grader}) + course = grader.course + assessment_config = insert(:assessment_config, %{course: course}) + + group = insert(:group, %{course: course, leader_id: grader.id, leader: grader}) + + students = insert_list(5, :course_registration, %{course: course, group: group}) - students = insert_list(5, :student, %{group: group}) - mission = insert(:assessment, %{title: "mission", type: "mission", is_published: true}) + mission = + insert(:assessment, %{ + title: "mission", + course: course, + config: assessment_config, + is_published: true + }) questions = for index <- 0..2 do # insert with display order in reverse insert(:programming_question, %{ assessment: mission, - max_grade: 200, max_xp: 1000, display_order: 5 - index }) @@ -1192,7 +1185,6 @@ defmodule CadetWeb.AdminGradingControllerTest do [ insert(:mcq_question, %{ assessment: mission, - max_grade: 200, max_xp: 1000, display_order: 2 }) @@ -1200,7 +1192,6 @@ defmodule CadetWeb.AdminGradingControllerTest do [ insert(:voting_question, %{ assessment: mission, - max_grade: 200, max_xp: 1000, display_order: 1 }) @@ -1223,8 +1214,6 @@ defmodule CadetWeb.AdminGradingControllerTest do question <- questions do insert(:answer, %{ grader_id: grader.id, - grade: 200, - adjustment: -100, xp: 1000, xp_adjustment: -500, question: question, @@ -1239,6 +1228,8 @@ defmodule CadetWeb.AdminGradingControllerTest do end %{ + course: course, + config: assessment_config, grader: grader, group: group, students: students, From 1b91e64cc29850f9c52db714fcc010b50a1727a6 Mon Sep 17 00:00:00 2001 From: YaleChen299 Date: Mon, 5 Jul 2021 17:03:10 +0800 Subject: [PATCH 121/174] merging --- lib/cadet/courses/courses.ex | 912 ++++++------- .../20210531155751_multitenant_upgrade.exs | 780 ++++++------ test/cadet/courses/courses_test.exs | 1124 ++++++++--------- 3 files changed, 1408 insertions(+), 1408 deletions(-) diff --git a/lib/cadet/courses/courses.ex b/lib/cadet/courses/courses.ex index 49c4a66b4..43bd15cca 100644 --- a/lib/cadet/courses/courses.ex +++ b/lib/cadet/courses/courses.ex @@ -1,456 +1,456 @@ -defmodule Cadet.Courses do - @moduledoc """ - Courses context contains domain logic for Course administration - management such as course configuration, discussion groups and materials - """ - use Cadet, [:context, :display] - - import Ecto.Query - alias Ecto.Multi - - alias Cadet.Accounts.{CourseRegistration, User} - - alias Cadet.Courses.{ - AssessmentConfig, - Course, - Group, - Sourcecast, - SourcecastUpload - } - - @doc """ - Creates a new course configuration, course registration, and sets - the user's latest course id to the newly created course. - """ - def create_course_config(params, user) do - Multi.new() - |> Multi.insert(:course, Course.changeset(%Course{}, params)) - |> Multi.insert(:course_reg, fn %{course: course} -> - CourseRegistration.changeset(%CourseRegistration{}, %{ - course_id: course.id, - user_id: user.id, - role: :admin - }) - end) - |> Multi.update(:latest_viewed_id, fn %{course: course} -> - User - |> where(id: ^user.id) - |> Repo.one() - |> User.changeset(%{latest_viewed_id: course.id}) - end) - |> Repo.transaction() - end - - @doc """ - Returns the course configuration for the specified course. - """ - @spec get_course_config(integer) :: - {:ok, %Course{}} | {:error, {:bad_request, String.t()}} - def get_course_config(course_id) when is_ecto_id(course_id) do - case retrieve_course(course_id) do - nil -> - {:error, {:bad_request, "Invalid course id"}} - - course -> - assessment_configs = - AssessmentConfig - |> where(course_id: ^course_id) - |> Repo.all() - |> Enum.sort(&(&1.order < &2.order)) - |> Enum.map(& &1.type) - - {:ok, Map.put_new(course, :assessment_configs, assessment_configs)} - end - end - - @doc """ - Updates the general course configuration for the specified course - """ - @spec update_course_config(integer, %{}) :: - {:ok, %Course{}} | {:error, Ecto.Changeset.t()} | {:error, {:bad_request, String.t()}} - def update_course_config(course_id, params) when is_ecto_id(course_id) do - case retrieve_course(course_id) do - nil -> - {:error, {:bad_request, "Invalid course id"}} - - course -> - course - |> Course.changeset(params) - |> Repo.update() - end - end - - defp retrieve_course(course_id) when is_ecto_id(course_id) do - Course - |> where(id: ^course_id) - |> Repo.one() - end - - def get_assessment_configs(course_id) when is_ecto_id(course_id) do - AssessmentConfig - |> where([at], at.course_id == ^course_id) - |> order_by(:order) - |> Repo.all() - end - - def mass_upsert_and_reorder_assessment_configs(course_id, configs) do - if is_list(configs) do - configs_length = configs |> length() - - with true <- configs_length <= 8, - true <- configs_length >= 1 do - new_configs = - configs - |> Enum.map(fn elem -> - {:ok, config} = insert_or_update_assessment_config(course_id, elem) - Map.put(elem, :assessment_config_id, config.id) - end) - - reorder_assessment_configs(course_id, new_configs) - else - false -> {:error, {:bad_request, "Invalid parameter(s)"}} - end - else - {:error, {:bad_request, "Invalid parameter(s)"}} - end - end - - def insert_or_update_assessment_config( - course_id, - params = %{assessment_config_id: assessment_config_id} - ) do - AssessmentConfig - |> where(course_id: ^course_id) - |> where(id: ^assessment_config_id) - |> Repo.one() - |> case do - nil -> - AssessmentConfig.changeset(%AssessmentConfig{}, Map.put(params, :course_id, course_id)) - - at -> - AssessmentConfig.changeset(at, params) - end - |> Repo.insert_or_update() - end - - defp update_assessment_config( - course_id, - params = %{assessment_config_id: assessment_config_id} - ) do - AssessmentConfig - |> where(course_id: ^course_id) - |> where(id: ^assessment_config_id) - |> Repo.one() - |> case do - nil -> {:error, :no_such_entry} - at -> at |> AssessmentConfig.changeset(params) |> Repo.update() - end - end - - def reorder_assessment_configs(course_id, configs) do - Repo.transaction(fn -> - configs - |> Enum.each(fn elem -> - update_assessment_config(course_id, Map.put(elem, :order, nil)) - end) - - configs - |> Enum.with_index(1) - |> Enum.each(fn {elem, idx} -> - update_assessment_config(course_id, Map.put(elem, :order, idx)) - end) - end) - end - - @spec delete_assessment_config(integer(), map()) :: - {:ok, Ecto.Schema.t()} | {:error, Ecto.Changeset.t()} | {:error, :no_such_enrty} - def delete_assessment_config(course_id, params = %{assessment_config_id: assessment_config_id}) do - AssessmentConfig - |> where(course_id: ^course_id) - |> where(id: ^assessment_config_id) - |> Repo.one() - |> case do - nil -> {:error, :no_such_enrty} - at -> at |> AssessmentConfig.changeset(params) |> Repo.delete() - end - end - - def upsert_groups_in_course(usernames_and_groups, course_id) do - usernames_and_groups - |> Enum.reduce_while(nil, fn %{username: username} = entry, _acc -> - case Map.fetch(entry, :group) do - {:ok, groupname} -> - # Add users to group - upsert_groups_in_course_helper(username, course_id, groupname) - - :error -> - # Delete users from group - upsert_groups_in_course_helper(username, course_id) - end - |> case do - {:ok, _} -> {:cont, :ok} - {:error, changeset} -> {:halt, {:error, {:bad_request, full_error_messages(changeset)}}} - end - end) - end - - defp upsert_groups_in_course_helper(username, course_id, groupname) do - with {:get_group, {:ok, group}} <- {:get_group, get_or_create_group(groupname, course_id)}, - {:get_course_reg, %{role: role} = course_reg} <- - {:get_course_reg, - CourseRegistration - |> where( - user_id: ^(User |> where(username: ^username) |> Repo.one() |> Map.fetch!(:id)) - ) - |> where(course_id: ^course_id) - |> Repo.one()} do - # It is ok to assume that user course registions already exist, as they would have been created - # in the admin_user_controller before calling this function - case role do - # If student, update his course registration - :student -> - course_reg - |> CourseRegistration.changeset(%{group_id: group.id}) - |> Repo.update() - - # If admin or staff, remove their previous group assignment and set them as group leader - _ -> - remove_staff_from_group(course_id, course_reg.id) - - group - |> Group.changeset(%{leader_id: course_reg.id}) - |> Repo.update() - end - end - end - - defp upsert_groups_in_course_helper(username, course_id) do - with {:get_course_reg, %{role: role} = course_reg} <- - {:get_course_reg, - CourseRegistration - |> where( - user_id: ^(User |> where(username: ^username) |> Repo.one() |> Map.fetch!(:id)) - ) - |> where(course_id: ^course_id) - |> Repo.one()} do - case role do - :student -> - course_reg - |> CourseRegistration.changeset(%{group_id: nil}) - |> Repo.update() - - _ -> - remove_staff_from_group(course_id, course_reg.id) - {:ok, nil} - end - end - end - - defp remove_staff_from_group(course_id, leader_id) do - Group - |> where(course_id: ^course_id) - |> where(leader_id: ^leader_id) - |> Repo.one() - |> case do - nil -> - nil - - group -> - group - |> Group.changeset(%{leader_id: nil}) - |> Repo.update() - end - end - - @doc """ - Get a group based on the group name and course id or create one if it doesn't exist - """ - @spec get_or_create_group(String.t(), integer()) :: - {:ok, %Group{}} | {:error, Ecto.Changeset.t()} - def get_or_create_group(name, course_id) when is_binary(name) and is_ecto_id(course_id) do - Group - |> where(name: ^name) - |> where(course_id: ^course_id) - |> Repo.one() - |> case do - nil -> - %Group{} - |> Group.changeset(%{name: name, course_id: course_id}) - |> Repo.insert() - - group -> - {:ok, group} - end - end - - # @doc """ - # Updates a group based on the group name or create one if it doesn't exist - # """ - # @spec insert_or_update_group(map()) :: {:ok, %Group{}} | {:error, Ecto.Changeset.t()} - # def insert_or_update_group(params = %{name: name}) when is_binary(name) do - # Group - # |> where(name: ^name) - # |> Repo.one() - # |> case do - # nil -> - # Group.changeset(%Group{}, params) - - # group -> - # Group.changeset(group, params) - # end - # |> Repo.insert_or_update() - # end - - # @doc """ - # Reassign a student to a discussion group - # This will un-assign student from the current discussion group - # """ - # def assign_group(leader = %User{}, student = %User{}) do - # cond do - # leader.role == :student -> - # {:error, :invalid} - - # student.role != :student -> - # {:error, :invalid} - - # true -> - # Repo.transaction(fn -> - # {:ok, _} = unassign_group(student) - - # %Group{} - # |> Group.changeset(%{}) - # |> put_assoc(:leader, leader) - # |> put_assoc(:student, student) - # |> Repo.insert!() - # end) - # end - # end - - # @doc """ - # Remove existing student from discussion group, no-op if a student - # is unassigned - # """ - # def unassign_group(student = %User{}) do - # existing_group = Repo.get_by(Group, student_id: student.id) - - # if existing_group == nil do - # {:ok, nil} - # else - # Repo.delete(existing_group) - # end - # end - - # @doc """ - # Get list of students under staff discussion group - # """ - # def list_students_by_leader(staff = %CourseRegistration{}) do - # import Cadet.Course.Query, only: [group_members: 1] - - # staff - # |> group_members() - # |> Repo.all() - # |> Repo.preload([:student]) - # end - - @upload_file_roles ~w(admin staff)a - - @doc """ - Upload a sourcecast file. - - Note that there are no checks for whether the user belongs to the course, as this has been checked - inside a plug in the router. - """ - def upload_sourcecast_file( - _inserter = %CourseRegistration{user_id: user_id, course_id: course_id, role: role}, - attrs = %{} - ) do - if role in @upload_file_roles do - course_reg = - CourseRegistration - |> where(user_id: ^user_id) - |> where(course_id: ^course_id) - |> preload(:course) - |> preload(:user) - |> Repo.one() - - changeset = - %Sourcecast{} - |> Sourcecast.changeset(attrs) - |> put_assoc(:uploader, course_reg.user) - |> put_assoc(:course, course_reg.course) - - case Repo.insert(changeset) do - {:ok, sourcecast} -> - {:ok, sourcecast} - - {:error, changeset} -> - {:error, {:bad_request, full_error_messages(changeset)}} - end - else - {:error, {:forbidden, "User is not permitted to upload"}} - end - end - - @doc """ - Upload a public sourcecast file. - - Note that there are no checks for whether the user belongs to the course, as this has been checked - inside a plug in the router. - """ - def upload_sourcecast_file_public( - inserter, - _inserter_course_reg = %CourseRegistration{role: role}, - attrs = %{} - ) do - if role in @upload_file_roles do - changeset = - %Sourcecast{} - |> Sourcecast.changeset(attrs) - |> put_assoc(:uploader, inserter) - - case Repo.insert(changeset) do - {:ok, sourcecast} -> - {:ok, sourcecast} - - {:error, changeset} -> - {:error, {:bad_request, full_error_messages(changeset)}} - end - else - {:error, {:forbidden, "User is not permitted to upload"}} - end - end - - @doc """ - Delete a sourcecast file - - Note that there are no checks for whether the user belongs to the course, as this has been checked - inside a plug in the router. - """ - def delete_sourcecast_file(_deleter = %CourseRegistration{role: role}, sourcecast_id) do - if role in @upload_file_roles do - sourcecast = Repo.get(Sourcecast, sourcecast_id) - SourcecastUpload.delete({sourcecast.audio, sourcecast}) - Repo.delete(sourcecast) - else - {:error, {:forbidden, "User is not permitted to delete"}} - end - end - - @doc """ - Get sourcecast files - """ - def get_sourcecast_files(course_id) when is_ecto_id(course_id) do - Sourcecast - |> where(course_id: ^course_id) - |> Repo.all() - |> Repo.preload(:uploader) - end - - def get_sourcecast_files do - Sourcecast - # Public sourcecasts are those without course_id - |> where([s], is_nil(s.course_id)) - |> Repo.all() - |> Repo.preload(:uploader) - end -end +defmodule Cadet.Courses do + @moduledoc """ + Courses context contains domain logic for Course administration + management such as course configuration, discussion groups and materials + """ + use Cadet, [:context, :display] + + import Ecto.Query + alias Ecto.Multi + + alias Cadet.Accounts.{CourseRegistration, User} + + alias Cadet.Courses.{ + AssessmentConfig, + Course, + Group, + Sourcecast, + SourcecastUpload + } + + @doc """ + Creates a new course configuration, course registration, and sets + the user's latest course id to the newly created course. + """ + def create_course_config(params, user) do + Multi.new() + |> Multi.insert(:course, Course.changeset(%Course{}, params)) + |> Multi.insert(:course_reg, fn %{course: course} -> + CourseRegistration.changeset(%CourseRegistration{}, %{ + course_id: course.id, + user_id: user.id, + role: :admin + }) + end) + |> Multi.update(:latest_viewed_id, fn %{course: course} -> + User + |> where(id: ^user.id) + |> Repo.one() + |> User.changeset(%{latest_viewed_id: course.id}) + end) + |> Repo.transaction() + end + + @doc """ + Returns the course configuration for the specified course. + """ + @spec get_course_config(integer) :: + {:ok, %Course{}} | {:error, {:bad_request, String.t()}} + def get_course_config(course_id) when is_ecto_id(course_id) do + case retrieve_course(course_id) do + nil -> + {:error, {:bad_request, "Invalid course id"}} + + course -> + assessment_configs = + AssessmentConfig + |> where(course_id: ^course_id) + |> Repo.all() + |> Enum.sort(&(&1.order < &2.order)) + |> Enum.map(& &1.type) + + {:ok, Map.put_new(course, :assessment_configs, assessment_configs)} + end + end + + @doc """ + Updates the general course configuration for the specified course + """ + @spec update_course_config(integer, %{}) :: + {:ok, %Course{}} | {:error, Ecto.Changeset.t()} | {:error, {:bad_request, String.t()}} + def update_course_config(course_id, params) when is_ecto_id(course_id) do + case retrieve_course(course_id) do + nil -> + {:error, {:bad_request, "Invalid course id"}} + + course -> + course + |> Course.changeset(params) + |> Repo.update() + end + end + + defp retrieve_course(course_id) when is_ecto_id(course_id) do + Course + |> where(id: ^course_id) + |> Repo.one() + end + + def get_assessment_configs(course_id) when is_ecto_id(course_id) do + AssessmentConfig + |> where([at], at.course_id == ^course_id) + |> order_by(:order) + |> Repo.all() + end + + def mass_upsert_and_reorder_assessment_configs(course_id, configs) do + if is_list(configs) do + configs_length = configs |> length() + + with true <- configs_length <= 8, + true <- configs_length >= 1 do + new_configs = + configs + |> Enum.map(fn elem -> + {:ok, config} = insert_or_update_assessment_config(course_id, elem) + Map.put(elem, :assessment_config_id, config.id) + end) + + reorder_assessment_configs(course_id, new_configs) + else + false -> {:error, {:bad_request, "Invalid parameter(s)"}} + end + else + {:error, {:bad_request, "Invalid parameter(s)"}} + end + end + + def insert_or_update_assessment_config( + course_id, + params = %{assessment_config_id: assessment_config_id} + ) do + AssessmentConfig + |> where(course_id: ^course_id) + |> where(id: ^assessment_config_id) + |> Repo.one() + |> case do + nil -> + AssessmentConfig.changeset(%AssessmentConfig{}, Map.put(params, :course_id, course_id)) + + at -> + AssessmentConfig.changeset(at, params) + end + |> Repo.insert_or_update() + end + + defp update_assessment_config( + course_id, + params = %{assessment_config_id: assessment_config_id} + ) do + AssessmentConfig + |> where(course_id: ^course_id) + |> where(id: ^assessment_config_id) + |> Repo.one() + |> case do + nil -> {:error, :no_such_entry} + at -> at |> AssessmentConfig.changeset(params) |> Repo.update() + end + end + + def reorder_assessment_configs(course_id, configs) do + Repo.transaction(fn -> + configs + |> Enum.each(fn elem -> + update_assessment_config(course_id, Map.put(elem, :order, nil)) + end) + + configs + |> Enum.with_index(1) + |> Enum.each(fn {elem, idx} -> + update_assessment_config(course_id, Map.put(elem, :order, idx)) + end) + end) + end + + @spec delete_assessment_config(integer(), map()) :: + {:ok, Ecto.Schema.t()} | {:error, Ecto.Changeset.t()} | {:error, :no_such_enrty} + def delete_assessment_config(course_id, params = %{assessment_config_id: assessment_config_id}) do + AssessmentConfig + |> where(course_id: ^course_id) + |> where(id: ^assessment_config_id) + |> Repo.one() + |> case do + nil -> {:error, :no_such_enrty} + at -> at |> AssessmentConfig.changeset(params) |> Repo.delete() + end + end + + def upsert_groups_in_course(usernames_and_groups, course_id) do + usernames_and_groups + |> Enum.reduce_while(nil, fn %{username: username} = entry, _acc -> + case Map.fetch(entry, :group) do + {:ok, groupname} -> + # Add users to group + upsert_groups_in_course_helper(username, course_id, groupname) + + :error -> + # Delete users from group + upsert_groups_in_course_helper(username, course_id) + end + |> case do + {:ok, _} -> {:cont, :ok} + {:error, changeset} -> {:halt, {:error, {:bad_request, full_error_messages(changeset)}}} + end + end) + end + + defp upsert_groups_in_course_helper(username, course_id, groupname) do + with {:get_group, {:ok, group}} <- {:get_group, get_or_create_group(groupname, course_id)}, + {:get_course_reg, %{role: role} = course_reg} <- + {:get_course_reg, + CourseRegistration + |> where( + user_id: ^(User |> where(username: ^username) |> Repo.one() |> Map.fetch!(:id)) + ) + |> where(course_id: ^course_id) + |> Repo.one()} do + # It is ok to assume that user course registions already exist, as they would have been created + # in the admin_user_controller before calling this function + case role do + # If student, update his course registration + :student -> + course_reg + |> CourseRegistration.changeset(%{group_id: group.id}) + |> Repo.update() + + # If admin or staff, remove their previous group assignment and set them as group leader + _ -> + remove_staff_from_group(course_id, course_reg.id) + + group + |> Group.changeset(%{leader_id: course_reg.id}) + |> Repo.update() + end + end + end + + defp upsert_groups_in_course_helper(username, course_id) do + with {:get_course_reg, %{role: role} = course_reg} <- + {:get_course_reg, + CourseRegistration + |> where( + user_id: ^(User |> where(username: ^username) |> Repo.one() |> Map.fetch!(:id)) + ) + |> where(course_id: ^course_id) + |> Repo.one()} do + case role do + :student -> + course_reg + |> CourseRegistration.changeset(%{group_id: nil}) + |> Repo.update() + + _ -> + remove_staff_from_group(course_id, course_reg.id) + {:ok, nil} + end + end + end + + defp remove_staff_from_group(course_id, leader_id) do + Group + |> where(course_id: ^course_id) + |> where(leader_id: ^leader_id) + |> Repo.one() + |> case do + nil -> + nil + + group -> + group + |> Group.changeset(%{leader_id: nil}) + |> Repo.update() + end + end + + @doc """ + Get a group based on the group name and course id or create one if it doesn't exist + """ + @spec get_or_create_group(String.t(), integer()) :: + {:ok, %Group{}} | {:error, Ecto.Changeset.t()} + def get_or_create_group(name, course_id) when is_binary(name) and is_ecto_id(course_id) do + Group + |> where(name: ^name) + |> where(course_id: ^course_id) + |> Repo.one() + |> case do + nil -> + %Group{} + |> Group.changeset(%{name: name, course_id: course_id}) + |> Repo.insert() + + group -> + {:ok, group} + end + end + + # @doc """ + # Updates a group based on the group name or create one if it doesn't exist + # """ + # @spec insert_or_update_group(map()) :: {:ok, %Group{}} | {:error, Ecto.Changeset.t()} + # def insert_or_update_group(params = %{name: name}) when is_binary(name) do + # Group + # |> where(name: ^name) + # |> Repo.one() + # |> case do + # nil -> + # Group.changeset(%Group{}, params) + + # group -> + # Group.changeset(group, params) + # end + # |> Repo.insert_or_update() + # end + + # @doc """ + # Reassign a student to a discussion group + # This will un-assign student from the current discussion group + # """ + # def assign_group(leader = %User{}, student = %User{}) do + # cond do + # leader.role == :student -> + # {:error, :invalid} + + # student.role != :student -> + # {:error, :invalid} + + # true -> + # Repo.transaction(fn -> + # {:ok, _} = unassign_group(student) + + # %Group{} + # |> Group.changeset(%{}) + # |> put_assoc(:leader, leader) + # |> put_assoc(:student, student) + # |> Repo.insert!() + # end) + # end + # end + + # @doc """ + # Remove existing student from discussion group, no-op if a student + # is unassigned + # """ + # def unassign_group(student = %User{}) do + # existing_group = Repo.get_by(Group, student_id: student.id) + + # if existing_group == nil do + # {:ok, nil} + # else + # Repo.delete(existing_group) + # end + # end + + # @doc """ + # Get list of students under staff discussion group + # """ + # def list_students_by_leader(staff = %CourseRegistration{}) do + # import Cadet.Course.Query, only: [group_members: 1] + + # staff + # |> group_members() + # |> Repo.all() + # |> Repo.preload([:student]) + # end + + @upload_file_roles ~w(admin staff)a + + @doc """ + Upload a sourcecast file. + + Note that there are no checks for whether the user belongs to the course, as this has been checked + inside a plug in the router. + """ + def upload_sourcecast_file( + _inserter = %CourseRegistration{user_id: user_id, course_id: course_id, role: role}, + attrs = %{} + ) do + if role in @upload_file_roles do + course_reg = + CourseRegistration + |> where(user_id: ^user_id) + |> where(course_id: ^course_id) + |> preload(:course) + |> preload(:user) + |> Repo.one() + + changeset = + %Sourcecast{} + |> Sourcecast.changeset(attrs) + |> put_assoc(:uploader, course_reg.user) + |> put_assoc(:course, course_reg.course) + + case Repo.insert(changeset) do + {:ok, sourcecast} -> + {:ok, sourcecast} + + {:error, changeset} -> + {:error, {:bad_request, full_error_messages(changeset)}} + end + else + {:error, {:forbidden, "User is not permitted to upload"}} + end + end + + @doc """ + Upload a public sourcecast file. + + Note that there are no checks for whether the user belongs to the course, as this has been checked + inside a plug in the router. + """ + def upload_sourcecast_file_public( + inserter, + _inserter_course_reg = %CourseRegistration{role: role}, + attrs = %{} + ) do + if role in @upload_file_roles do + changeset = + %Sourcecast{} + |> Sourcecast.changeset(attrs) + |> put_assoc(:uploader, inserter) + + case Repo.insert(changeset) do + {:ok, sourcecast} -> + {:ok, sourcecast} + + {:error, changeset} -> + {:error, {:bad_request, full_error_messages(changeset)}} + end + else + {:error, {:forbidden, "User is not permitted to upload"}} + end + end + + @doc """ + Delete a sourcecast file + + Note that there are no checks for whether the user belongs to the course, as this has been checked + inside a plug in the router. + """ + def delete_sourcecast_file(_deleter = %CourseRegistration{role: role}, sourcecast_id) do + if role in @upload_file_roles do + sourcecast = Repo.get(Sourcecast, sourcecast_id) + SourcecastUpload.delete({sourcecast.audio, sourcecast}) + Repo.delete(sourcecast) + else + {:error, {:forbidden, "User is not permitted to delete"}} + end + end + + @doc """ + Get sourcecast files + """ + def get_sourcecast_files(course_id) when is_ecto_id(course_id) do + Sourcecast + |> where(course_id: ^course_id) + |> Repo.all() + |> Repo.preload(:uploader) + end + + def get_sourcecast_files do + Sourcecast + # Public sourcecasts are those without course_id + |> where([s], is_nil(s.course_id)) + |> Repo.all() + |> Repo.preload(:uploader) + end +end diff --git a/priv/repo/migrations/20210531155751_multitenant_upgrade.exs b/priv/repo/migrations/20210531155751_multitenant_upgrade.exs index 7c5716f07..4907d1459 100644 --- a/priv/repo/migrations/20210531155751_multitenant_upgrade.exs +++ b/priv/repo/migrations/20210531155751_multitenant_upgrade.exs @@ -1,390 +1,390 @@ -defmodule Cadet.Repo.Migrations.MultitenantUpgrade do - use Ecto.Migration - import Ecto.Query, only: [from: 2, where: 2] - - alias Cadet.Accounts.{CourseRegistration, Notification, Role, User} - alias Cadet.Assessments.{Assessment, Submission, SubmissionVotes} - alias Cadet.Courses.{AssessmentConfig, Course, Group, Sourcecast} - alias Cadet.Repo - alias Cadet.Stories.Story - - def change do - # Tracks course configurations - create table(:courses) do - add(:course_name, :string, null: false) - add(:course_short_name, :string) - add(:viewable, :boolean, null: false, default: true) - add(:enable_game, :boolean, null: false, default: true) - add(:enable_achievements, :boolean, null: false, default: true) - add(:enable_sourcecast, :boolean, null: false, default: true) - add(:source_chapter, :integer, null: false) - add(:source_variant, :string, null: false) - add(:module_help_text, :string) - timestamps() - end - - # Tracks assessment configurations per assessment type in a course - create table(:assessment_configs) do - add(:order, :integer, null: true) - add(:type, :string, null: false) - add(:course_id, references(:courses), null: false) - add(:is_graded, :boolean, null: false, default: true) - add(:is_autograded, :boolean, null: false, default: true) - add(:skippable, :boolean, null: false, default: true) - add(:early_submission_xp, :integer, null: false) - add(:hours_before_early_xp_decay, :integer, null: false) - timestamps() - end - - # Tracks course registrations (many-to-many r/s between users and courses) - create table(:course_registrations) do - add(:role, :role, null: false) - add(:game_states, :map, default: %{}) - add(:group_id, references(:groups)) - add(:user_id, references(:users), null: false) - add(:course_id, references(:courses), null: false) - timestamps() - end - - # Enforce that users cannot be enrolled twice in a course - create( - unique_index(:course_registrations, [:user_id, :course_id], - name: :course_registrations_user_id_course_id_index - ) - ) - - # latest_viewed_id to track which course to load after the user logs in. - # name and username modifications to allow for names to be nullable as accounts can - # now be precreated by any course instructor by specifying the username used in the - # respective auth provider. - alter table(:users) do - add(:latest_viewed_id, references(:courses), null: true) - modify(:name, :string, null: true) - modify(:username, :string, null: false) - end - - # Prep for migration of leader_id from User entity to CourseRegistration entity. - # Also make groups associated with a course. - rename(table(:groups), :leader_id, to: :temp_leader_id) - drop(constraint(:groups, "groups_leader_id_fkey")) - drop(constraint(:groups, "groups_mentor_id_fkey")) - - alter table(:groups) do - remove(:mentor_id) - add(:leader_id, references(:course_registrations), null: true) - add(:course_id, references(:courses)) - end - - # Make assessments related to an assessment config and a course - alter table(:assessments) do - add(:config_id, references(:assessment_configs)) - add(:course_id, references(:courses)) - end - - # Prep for migration of student_id and unsubmitted_by_id from User entity to CourseRegistration entity. - rename(table(:submissions), :student_id, to: :temp_student_id) - rename(table(:submissions), :unsubmitted_by_id, to: :temp_unsubmitted_by_id) - drop(constraint(:submissions, "submissions_student_id_fkey")) - drop(constraint(:submissions, "submissions_unsubmitted_by_id_fkey")) - - alter table(:submissions) do - add(:student_id, references(:course_registrations)) - add(:unsubmitted_by_id, references(:course_registrations)) - end - - alter table(:submission_votes) do - add(:voter_id, references(:course_registrations)) - end - - # Remove grade metric from backend - alter table(:answers) do - remove(:grade) - remove(:adjustment) - remove(:grader_id) - add(:grader_id, references(:course_registrations), null: true) - end - - alter table(:questions) do - remove(:max_grade) - end - - # Update notifications - alter table(:notifications) do - add(:course_reg_id, references(:course_registrations)) - end - - # Sourcecasts to be associated with a course - alter table(:sourcecasts) do - add(:course_id, references(:courses)) - end - - # Stories to be associated with a course - alter table(:stories) do - add(:course_id, references(:courses)) - end - - # Sublanguage is now being tracked under course configuration, and can be different depending on course - drop_if_exists(table(:sublanguages)) - - # Manual data entry and manipulation to migrate data from Source Academy Knight --> Rook. - # Note that in Knight, there was only 1 course running at a time, so it is okay to assume - # that all existing data belongs to that course. - execute( - fn -> - # Create the new course for migration - {:ok, course} = - %Course{} - |> Course.changeset(%{ - course_name: "CS1101S Programming Methodology (AY21/22 Sem 1)", - course_short_name: "CS1101S", - viewable: true, - enable_game: true, - enable_achievments: true, - enable_sourcecast: true, - source_chapter: 1, - source_variant: "default" - }) - |> Repo.insert() - - # Namespace existing usernames - from(u in "users", update: [set: [username: fragment("? || ? ", "luminus/", u.username)]]) - |> Repo.update_all([]) - - # Create course registrations for existing users - from(u in "users", select: {u.id, u.role, u.group_id, u.game_states}) - |> Repo.all() - |> Enum.each(fn user -> - %CourseRegistration{} - |> CourseRegistration.changeset(%{ - user_id: elem(user, 0), - role: elem(user, 1), - group_id: elem(user, 2), - game_states: elem(user, 3), - course_id: course.id - }) - |> Repo.insert() - end) - - # Add latest_viewed_id to existing users - User - |> Repo.all() - |> Enum.each(fn user -> - user - |> User.changeset(%{latest_viewed_id: course.id}) - |> Repo.update() - end) - - # Handle groups (adding course_id, and updating leader_id to course registrations) - from(g in "groups", select: {g.id, g.temp_leader_id}) - |> Repo.all() - |> Enum.each(fn group -> - leader_id = - case elem(group, 1) do - # leader_id is now going to be non-nullable. if it was previously nil, we will just - # assign a staff to be the leader_id during migration - nil -> - CourseRegistration - |> where(role: :staff) - |> Repo.one() - - Map.fetch!(:id) - - id -> - CourseRegistration - |> where(user_id: ^id) - |> Repo.one() - |> Map.fetch!(:id) - end - - Group - |> where(id: ^elem(group, 0)) - |> Repo.one() - |> Group.changeset(%{leader_id: leader_id, course_id: course.id}) - |> Repo.update() - end) - - # Create Assessment Configurations based on Source Academy Knight - ["Missions", "Quests", "Paths", "Contests", "Others"] - |> Enum.each(fn assessment_type -> - %AssessmentConfig{} - |> AssessmentConfig.changeset(%{ - type: assessment_type, - course_id: course.id, - is_graded: assessment_type in ["Missions", "Quests", "Contests", "Others"], - skippable: assessment_type != "Paths", - is_autograded: assessment_type != "Contests", - early_submission_xp: 200, - hours_before_early_xp_decay: 48 - }) - |> Repo.insert() - end) - - # Link existing assessments to an assessment config and course - from(a in "assessments", select: {a.id, a.type}) - |> Repo.all() - |> Enum.each(fn assessment -> - assessment_type = - case elem(assessment, 1) do - "mission" -> "Missions" - "sidequest" -> "Quests" - "path" -> "Paths" - "contest" -> "Contests" - "practical" -> "Others" - end - - assessment_config = - AssessmentConfig - |> where(type: ^assessment_type) - |> Repo.one() - - Assessment - |> where(id: ^elem(assessment, 0)) - |> Repo.one() - |> Assessment.changeset(%{config_id: assessment_config.id, course_id: course.id}) - |> Repo.update() - end) - - # Updating student_id and unsubmitted_by_id from User to CourseRegistration - from(s in "submissions", select: {s.id, s.temp_student_id, s.temp_unsubmitted_by_id}) - |> Repo.all() - |> Enum.each(fn submission -> - student_id = - CourseRegistration - |> where(user_id: ^elem(submission, 1)) - |> Repo.one() - |> Map.fetch!(:id) - - unsubmitted_by_id = - case elem(submission, 2) do - nil -> - nil - - id -> - CourseRegistration - |> where(user_id: ^id) - |> Repo.one() - |> Map.fetch!(:id) - end - - Submission - |> where(id: ^elem(submission, 0)) - |> Repo.one() - |> Submission.changeset(%{student_id: student_id, unsubmitted_by_id: unsubmitted_by_id}) - |> Repo.update() - end) - - from(s in "submission_votes", select: {s.id, s.user_id}) - |> Repo.all() - |> Enum.each(fn vote -> - voter_id = - CourseRegistration - |> where(user_id: ^elem(vote, 1)) - |> Repo.one() - |> Map.fetch!(:id) - - SubmissionVotes - |> where(id: ^elem(vote, 0)) - |> Repo.one() - |> SubmissionVotes.changeset(%{voter_id: voter_id}) - |> Repo.update() - end) - - from(n in "notifications", select: {n.id, n.user_id}) - |> Repo.all() - |> Enum.each(fn notification -> - course_reg_id = - CourseRegistration - |> where(user_id: ^elem(notification, 1)) - |> Repo.one() - |> Map.fetch!(:id) - - Notification - |> where(id: ^elem(notification, 0)) - |> Repo.one() - |> Notification.changeset(%{course_reg_id: course_reg_id}) - |> Repo.update() - end) - - # Add course id to all Sourcecasts - Sourcecast - |> Repo.all() - |> Enum.each(fn x -> - x - |> Sourcecast.changeset(%{course_id: course.id}) - |> Repo.update() - end) - - # Add course id to all Stories - Story - |> Repo.all() - |> Enum.each(fn x -> - x - |> Story.changeset(%{course_id: course.id}) - |> Repo.update() - end) - end, - fn -> nil end - ) - - # Cleanup users table after data migration - alter table(:users) do - remove(:role) - remove(:group_id) - remove(:game_states) - end - - # Cleanup groups table, and make course_id and leader_id non-nullable - alter table(:groups) do - remove(:temp_leader_id) - - modify(:course_id, references(:courses), null: false, from: references(:courses)) - end - - create(unique_index(:groups, [:name, :course_id])) - - # Cleanup assessments table, and make config_id and course_id non-nullable - alter table(:assessments) do - remove(:type) - modify(:config_id, references(:assessment_configs), null: false, from: references(:courses)) - modify(:course_id, references(:courses), null: false, from: references(:courses)) - end - - alter table(:submissions) do - remove(:temp_student_id) - remove(:temp_unsubmitted_by_id) - - modify(:student_id, references(:course_registrations), - null: false, - from: references(:course_registrations) - ) - end - - create(index(:submissions, :student_id)) - create(unique_index(:submissions, [:assessment_id, :student_id])) - - alter table(:submission_votes) do - remove(:user_id) - - modify(:voter_id, references(:course_registrations), - null: false, - from: references(:course_registrations) - ) - end - - create(unique_index(:submission_votes, [:voter_id, :question_id, :rank], name: :unique_score)) - - alter table(:notifications) do - remove(:user_id) - - modify(:course_reg_id, references(:course_registrations), - null: false, - from: references(:course_registrations) - ) - end - - # Set course_id to be non-nullable - alter table(:stories) do - modify(:course_id, references(:courses), null: false, from: references(:courses)) - end - end -end +defmodule Cadet.Repo.Migrations.MultitenantUpgrade do + use Ecto.Migration + import Ecto.Query, only: [from: 2, where: 2] + + alias Cadet.Accounts.{CourseRegistration, Notification, Role, User} + alias Cadet.Assessments.{Assessment, Submission, SubmissionVotes} + alias Cadet.Courses.{AssessmentConfig, Course, Group, Sourcecast} + alias Cadet.Repo + alias Cadet.Stories.Story + + def change do + # Tracks course configurations + create table(:courses) do + add(:course_name, :string, null: false) + add(:course_short_name, :string) + add(:viewable, :boolean, null: false, default: true) + add(:enable_game, :boolean, null: false, default: true) + add(:enable_achievements, :boolean, null: false, default: true) + add(:enable_sourcecast, :boolean, null: false, default: true) + add(:source_chapter, :integer, null: false) + add(:source_variant, :string, null: false) + add(:module_help_text, :string) + timestamps() + end + + # Tracks assessment configurations per assessment type in a course + create table(:assessment_configs) do + add(:order, :integer, null: true) + add(:type, :string, null: false) + add(:course_id, references(:courses), null: false) + add(:is_graded, :boolean, null: false, default: true) + add(:is_autograded, :boolean, null: false, default: true) + add(:skippable, :boolean, null: false, default: true) + add(:early_submission_xp, :integer, null: false) + add(:hours_before_early_xp_decay, :integer, null: false) + timestamps() + end + + # Tracks course registrations (many-to-many r/s between users and courses) + create table(:course_registrations) do + add(:role, :role, null: false) + add(:game_states, :map, default: %{}) + add(:group_id, references(:groups)) + add(:user_id, references(:users), null: false) + add(:course_id, references(:courses), null: false) + timestamps() + end + + # Enforce that users cannot be enrolled twice in a course + create( + unique_index(:course_registrations, [:user_id, :course_id], + name: :course_registrations_user_id_course_id_index + ) + ) + + # latest_viewed_id to track which course to load after the user logs in. + # name and username modifications to allow for names to be nullable as accounts can + # now be precreated by any course instructor by specifying the username used in the + # respective auth provider. + alter table(:users) do + add(:latest_viewed_id, references(:courses), null: true) + modify(:name, :string, null: true) + modify(:username, :string, null: false) + end + + # Prep for migration of leader_id from User entity to CourseRegistration entity. + # Also make groups associated with a course. + rename(table(:groups), :leader_id, to: :temp_leader_id) + drop(constraint(:groups, "groups_leader_id_fkey")) + drop(constraint(:groups, "groups_mentor_id_fkey")) + + alter table(:groups) do + remove(:mentor_id) + add(:leader_id, references(:course_registrations), null: true) + add(:course_id, references(:courses)) + end + + # Make assessments related to an assessment config and a course + alter table(:assessments) do + add(:config_id, references(:assessment_configs)) + add(:course_id, references(:courses)) + end + + # Prep for migration of student_id and unsubmitted_by_id from User entity to CourseRegistration entity. + rename(table(:submissions), :student_id, to: :temp_student_id) + rename(table(:submissions), :unsubmitted_by_id, to: :temp_unsubmitted_by_id) + drop(constraint(:submissions, "submissions_student_id_fkey")) + drop(constraint(:submissions, "submissions_unsubmitted_by_id_fkey")) + + alter table(:submissions) do + add(:student_id, references(:course_registrations)) + add(:unsubmitted_by_id, references(:course_registrations)) + end + + alter table(:submission_votes) do + add(:voter_id, references(:course_registrations)) + end + + # Remove grade metric from backend + alter table(:answers) do + remove(:grade) + remove(:adjustment) + remove(:grader_id) + add(:grader_id, references(:course_registrations), null: true) + end + + alter table(:questions) do + remove(:max_grade) + end + + # Update notifications + alter table(:notifications) do + add(:course_reg_id, references(:course_registrations)) + end + + # Sourcecasts to be associated with a course + alter table(:sourcecasts) do + add(:course_id, references(:courses)) + end + + # Stories to be associated with a course + alter table(:stories) do + add(:course_id, references(:courses)) + end + + # Sublanguage is now being tracked under course configuration, and can be different depending on course + drop_if_exists(table(:sublanguages)) + + # Manual data entry and manipulation to migrate data from Source Academy Knight --> Rook. + # Note that in Knight, there was only 1 course running at a time, so it is okay to assume + # that all existing data belongs to that course. + execute( + fn -> + # Create the new course for migration + {:ok, course} = + %Course{} + |> Course.changeset(%{ + course_name: "CS1101S Programming Methodology (AY21/22 Sem 1)", + course_short_name: "CS1101S", + viewable: true, + enable_game: true, + enable_achievments: true, + enable_sourcecast: true, + source_chapter: 1, + source_variant: "default" + }) + |> Repo.insert() + + # Namespace existing usernames + from(u in "users", update: [set: [username: fragment("? || ? ", "luminus/", u.username)]]) + |> Repo.update_all([]) + + # Create course registrations for existing users + from(u in "users", select: {u.id, u.role, u.group_id, u.game_states}) + |> Repo.all() + |> Enum.each(fn user -> + %CourseRegistration{} + |> CourseRegistration.changeset(%{ + user_id: elem(user, 0), + role: elem(user, 1), + group_id: elem(user, 2), + game_states: elem(user, 3), + course_id: course.id + }) + |> Repo.insert() + end) + + # Add latest_viewed_id to existing users + User + |> Repo.all() + |> Enum.each(fn user -> + user + |> User.changeset(%{latest_viewed_id: course.id}) + |> Repo.update() + end) + + # Handle groups (adding course_id, and updating leader_id to course registrations) + from(g in "groups", select: {g.id, g.temp_leader_id}) + |> Repo.all() + |> Enum.each(fn group -> + leader_id = + case elem(group, 1) do + # leader_id is now going to be non-nullable. if it was previously nil, we will just + # assign a staff to be the leader_id during migration + nil -> + CourseRegistration + |> where(role: :staff) + |> Repo.one() + + Map.fetch!(:id) + + id -> + CourseRegistration + |> where(user_id: ^id) + |> Repo.one() + |> Map.fetch!(:id) + end + + Group + |> where(id: ^elem(group, 0)) + |> Repo.one() + |> Group.changeset(%{leader_id: leader_id, course_id: course.id}) + |> Repo.update() + end) + + # Create Assessment Configurations based on Source Academy Knight + ["Missions", "Quests", "Paths", "Contests", "Others"] + |> Enum.each(fn assessment_type -> + %AssessmentConfig{} + |> AssessmentConfig.changeset(%{ + type: assessment_type, + course_id: course.id, + is_graded: assessment_type in ["Missions", "Quests", "Contests", "Others"], + skippable: assessment_type != "Paths", + is_autograded: assessment_type != "Contests", + early_submission_xp: 200, + hours_before_early_xp_decay: 48 + }) + |> Repo.insert() + end) + + # Link existing assessments to an assessment config and course + from(a in "assessments", select: {a.id, a.type}) + |> Repo.all() + |> Enum.each(fn assessment -> + assessment_type = + case elem(assessment, 1) do + "mission" -> "Missions" + "sidequest" -> "Quests" + "path" -> "Paths" + "contest" -> "Contests" + "practical" -> "Others" + end + + assessment_config = + AssessmentConfig + |> where(type: ^assessment_type) + |> Repo.one() + + Assessment + |> where(id: ^elem(assessment, 0)) + |> Repo.one() + |> Assessment.changeset(%{config_id: assessment_config.id, course_id: course.id}) + |> Repo.update() + end) + + # Updating student_id and unsubmitted_by_id from User to CourseRegistration + from(s in "submissions", select: {s.id, s.temp_student_id, s.temp_unsubmitted_by_id}) + |> Repo.all() + |> Enum.each(fn submission -> + student_id = + CourseRegistration + |> where(user_id: ^elem(submission, 1)) + |> Repo.one() + |> Map.fetch!(:id) + + unsubmitted_by_id = + case elem(submission, 2) do + nil -> + nil + + id -> + CourseRegistration + |> where(user_id: ^id) + |> Repo.one() + |> Map.fetch!(:id) + end + + Submission + |> where(id: ^elem(submission, 0)) + |> Repo.one() + |> Submission.changeset(%{student_id: student_id, unsubmitted_by_id: unsubmitted_by_id}) + |> Repo.update() + end) + + from(s in "submission_votes", select: {s.id, s.user_id}) + |> Repo.all() + |> Enum.each(fn vote -> + voter_id = + CourseRegistration + |> where(user_id: ^elem(vote, 1)) + |> Repo.one() + |> Map.fetch!(:id) + + SubmissionVotes + |> where(id: ^elem(vote, 0)) + |> Repo.one() + |> SubmissionVotes.changeset(%{voter_id: voter_id}) + |> Repo.update() + end) + + from(n in "notifications", select: {n.id, n.user_id}) + |> Repo.all() + |> Enum.each(fn notification -> + course_reg_id = + CourseRegistration + |> where(user_id: ^elem(notification, 1)) + |> Repo.one() + |> Map.fetch!(:id) + + Notification + |> where(id: ^elem(notification, 0)) + |> Repo.one() + |> Notification.changeset(%{course_reg_id: course_reg_id}) + |> Repo.update() + end) + + # Add course id to all Sourcecasts + Sourcecast + |> Repo.all() + |> Enum.each(fn x -> + x + |> Sourcecast.changeset(%{course_id: course.id}) + |> Repo.update() + end) + + # Add course id to all Stories + Story + |> Repo.all() + |> Enum.each(fn x -> + x + |> Story.changeset(%{course_id: course.id}) + |> Repo.update() + end) + end, + fn -> nil end + ) + + # Cleanup users table after data migration + alter table(:users) do + remove(:role) + remove(:group_id) + remove(:game_states) + end + + # Cleanup groups table, and make course_id and leader_id non-nullable + alter table(:groups) do + remove(:temp_leader_id) + + modify(:course_id, references(:courses), null: false, from: references(:courses)) + end + + create(unique_index(:groups, [:name, :course_id])) + + # Cleanup assessments table, and make config_id and course_id non-nullable + alter table(:assessments) do + remove(:type) + modify(:config_id, references(:assessment_configs), null: false, from: references(:courses)) + modify(:course_id, references(:courses), null: false, from: references(:courses)) + end + + alter table(:submissions) do + remove(:temp_student_id) + remove(:temp_unsubmitted_by_id) + + modify(:student_id, references(:course_registrations), + null: false, + from: references(:course_registrations) + ) + end + + create(index(:submissions, :student_id)) + create(unique_index(:submissions, [:assessment_id, :student_id])) + + alter table(:submission_votes) do + remove(:user_id) + + modify(:voter_id, references(:course_registrations), + null: false, + from: references(:course_registrations) + ) + end + + create(unique_index(:submission_votes, [:voter_id, :question_id, :rank], name: :unique_score)) + + alter table(:notifications) do + remove(:user_id) + + modify(:course_reg_id, references(:course_registrations), + null: false, + from: references(:course_registrations) + ) + end + + # Set course_id to be non-nullable + alter table(:stories) do + modify(:course_id, references(:courses), null: false, from: references(:courses)) + end + end +end diff --git a/test/cadet/courses/courses_test.exs b/test/cadet/courses/courses_test.exs index f978f201e..a00fd4056 100644 --- a/test/cadet/courses/courses_test.exs +++ b/test/cadet/courses/courses_test.exs @@ -1,562 +1,562 @@ -defmodule Cadet.CoursesTest do - use Cadet.DataCase - - alias Cadet.{Courses, Repo} - alias Cadet.Accounts.{CourseRegistration, User} - alias Cadet.Courses.{Course, Group, Sourcecast, SourcecastUpload} - - describe "create course config" do - test "succeeds" do - user = insert(:user) - - # Course precreated in User factory - old_courses = Course |> Repo.all() |> length() - - params = %{ - course_name: "CS1101S Programming Methodology (AY20/21 Sem 1)", - course_short_name: "CS1101S", - viewable: true, - enable_game: true, - enable_achievements: true, - enable_sourcecast: true, - source_chapter: 1, - source_variant: "default", - module_help_text: "Help Text" - } - - Courses.create_course_config(params, user) - - # New course created - new_courses = Course |> Repo.all() |> length() - assert new_courses - old_courses == 1 - - # New admin course registration for user - course_regs = CourseRegistration |> where(user_id: ^user.id) |> Repo.all() - assert length(course_regs) == 1 - assert Enum.at(course_regs, 0).role == :admin - - # User's latest_viewed course is updated - assert User |> where(id: ^user.id) |> Repo.one() |> Map.fetch!(:latest_viewed_id) == - Enum.at(course_regs, 0).course_id - end - end - - describe "get course config" do - test "succeeds" do - course = insert(:course) - insert(:assessment_config, %{order: 1, type: "Missions", course: course}) - insert(:assessment_config, %{order: 2, type: "Quests", course: course}) - - {:ok, course} = Courses.get_course_config(course.id) - assert course.course_name == "Programming Methodology" - assert course.course_short_name == "CS1101S" - assert course.viewable == true - assert course.enable_game == true - assert course.enable_achievements == true - assert course.enable_sourcecast == true - assert course.source_chapter == 1 - assert course.source_variant == "default" - assert course.module_help_text == "Help Text" - assert course.assessment_configs == ["Missions", "Quests"] - end - - test "returns with error for invalid course id" do - course = insert(:course) - - assert {:error, {:bad_request, "Invalid course id"}} = - Courses.get_course_config(course.id + 1) - end - end - - describe "update course config" do - test "succeeds (without sublanguage update)" do - course = insert(:course) - - {:ok, updated_course} = - Courses.update_course_config(course.id, %{ - course_name: "Data Structures and Algorithms", - course_short_name: "CS2040S", - viewable: false, - enable_game: false, - enable_achievements: false, - enable_sourcecast: false, - module_help_text: "" - }) - - assert updated_course.course_name == "Data Structures and Algorithms" - assert updated_course.course_short_name == "CS2040S" - assert updated_course.viewable == false - assert updated_course.enable_game == false - assert updated_course.enable_achievements == false - assert updated_course.enable_sourcecast == false - assert updated_course.source_chapter == 1 - assert updated_course.source_variant == "default" - assert updated_course.module_help_text == nil - end - - test "succeeds (with sublanguage update)" do - course = insert(:course) - new_chapter = Enum.random(1..4) - - {:ok, updated_course} = - Courses.update_course_config(course.id, %{ - course_name: "Data Structures and Algorithms", - course_short_name: "CS2040S", - viewable: false, - enable_game: false, - enable_achievements: false, - enable_sourcecast: false, - source_chapter: new_chapter, - source_variant: "default", - module_help_text: "help" - }) - - assert updated_course.course_name == "Data Structures and Algorithms" - assert updated_course.course_short_name == "CS2040S" - assert updated_course.viewable == false - assert updated_course.enable_game == false - assert updated_course.enable_achievements == false - assert updated_course.enable_sourcecast == false - assert updated_course.source_chapter == new_chapter - assert updated_course.source_variant == "default" - assert updated_course.module_help_text == "help" - end - - test "returns with error for invalid course id" do - course = insert(:course) - new_chapter = Enum.random(1..4) - - assert {:error, {:bad_request, "Invalid course id"}} = - Courses.update_course_config(course.id + 1, %{ - source_chapter: new_chapter, - source_variant: "default" - }) - end - - test "returns with error for failed updates" do - course = insert(:course) - - assert {:error, changeset} = - Courses.update_course_config(course.id, %{ - source_chapter: 0, - source_variant: "default" - }) - - assert %{source_chapter: ["is invalid"]} = errors_on(changeset) - - assert {:error, changeset} = - Courses.update_course_config(course.id, %{source_chapter: 2, source_variant: "gpu"}) - - assert %{source_variant: ["is invalid"]} = errors_on(changeset) - end - end - - describe "get assessment configs" do - test "succeeds" do - course = insert(:course) - - for i <- 1..5 do - insert(:assessment_config, %{order: 6 - i, type: "Mission#{i}", course: course}) - end - - assessment_configs = Courses.get_assessment_configs(course.id) - - assert length(assessment_configs) <= 5 - - assessment_configs - |> Enum.with_index(1) - |> Enum.each(fn {at, idx} -> - assert at.order == idx - assert at.type == "Mission#{6 - idx}" - end) - end - end - - describe "mass_upsert_and_reorder_assessment_configs" do - setup do - course = insert(:course) - config1 = insert(:assessment_config, %{order: 1, type: "Missions", course: course}) - config2 = insert(:assessment_config, %{order: 2, type: "Quests", course: course}) - config3 = insert(:assessment_config, %{order: 3, type: "Paths", course: course}) - config4 = insert(:assessment_config, %{order: 4, type: "Contests", course: course}) - expected = ["Paths", "Quests", "Missions", "Others", "Contests"] - - {:ok, - %{ - course: course, - expected: expected, - config1: config1, - config2: config2, - config3: config3, - config4: config4 - }} - end - - test "succeeds", %{ - course: course, - expected: expected, - config1: config1, - config2: config2, - config3: config3, - config4: config4 - } do - {:ok, _} = - Courses.mass_upsert_and_reorder_assessment_configs(course.id, [ - %{assessment_config_id: config1.id, type: "Paths"}, - %{assessment_config_id: config2.id, type: "Quests"}, - %{assessment_config_id: config3.id, type: "Missions"}, - %{assessment_config_id: config4.id, type: "Others"}, - %{assessment_config_id: -1, type: "Contests"} - ]) - - assessment_configs = Courses.get_assessment_configs(course.id) - - assert Enum.map(assessment_configs, & &1.type) == expected - end - - test "succeeds to capitalise", %{ - course: course, - expected: expected, - config1: config1, - config2: config2, - config3: config3, - config4: config4 - } do - {:ok, _} = - Courses.mass_upsert_and_reorder_assessment_configs(course.id, [ - %{assessment_config_id: config1.id, type: "Paths"}, - %{assessment_config_id: config2.id, type: "Quests"}, - %{assessment_config_id: config3.id, type: "Missions"}, - %{assessment_config_id: config4.id, type: "Others"}, - %{assessment_config_id: -1, type: "Contests"} - ]) - - assessment_configs = Courses.get_assessment_configs(course.id) - - assert Enum.map(assessment_configs, & &1.type) == expected - end - - # test "succeed to delete", %{course: course} do - # :ok = - # Courses.mass_upsert_and_reorder_assessment_configs(course.id, [ - # %{order: 1, type: "Paths"}, - # %{order: 2, type: "quests"}, - # %{order: 3, type: "missions"} - # ]) - - # assessment_configs = Courses.get_assessment_configs(course.id) - - # assert Enum.map(assessment_configs, & &1.type) == ["Paths", "Quests", "Missions"] - # end - - test "returns with error for empty list parameter", %{course: course} do - assert {:error, {:bad_request, "Invalid parameter(s)"}} = - Courses.mass_upsert_and_reorder_assessment_configs(course.id, []) - end - - test "returns with error for list parameter of greater than length 8", %{ - course: course, - config1: config1, - config2: config2, - config3: config3, - config4: config4 - } do - params = [ - %{assessment_config_id: config1.id, type: "Paths"}, - %{assessment_config_id: config2.id, type: "Quests"}, - %{assessment_config_id: config3.id, type: "Missions"}, - %{assessment_config_id: config4.id, type: "Others"}, - %{assessment_config_id: -1, type: "Contests"}, - %{assessment_config_id: -1, type: "Contests"}, - %{assessment_config_id: -1, type: "Contests"}, - %{assessment_config_id: -1, type: "Contests"}, - %{assessment_config_id: -1, type: "Contests"} - ] - - assert {:error, {:bad_request, "Invalid parameter(s)"}} = - Courses.mass_upsert_and_reorder_assessment_configs(course.id, params) - end - - test "returns with error for non-list parameter", %{course: course} do - params = %{course_id: course.id, order: 1, type: "Paths"} - - assert {:error, {:bad_request, "Invalid parameter(s)"}} = - Courses.mass_upsert_and_reorder_assessment_configs(course.id, params) - end - end - - describe "insert_or_update_assessment_config" do - test "succeeds with insert configs" do - course = insert(:course) - old_configs = Courses.get_assessment_configs(course.id) - - params = %{ - assessment_config_id: -1, - order: 1, - type: "Mission", - early_submission_xp: 100, - hours_before_early_xp_decay: 24 - } - - {:ok, updated_config} = Courses.insert_or_update_assessment_config(course.id, params) - - new_configs = Courses.get_assessment_configs(course.id) - assert old_configs == [] - assert length(new_configs) == 1 - assert updated_config.early_submission_xp == 100 - assert updated_config.hours_before_early_xp_decay == 24 - end - - test "succeeds with update" do - course = insert(:course) - config = insert(:assessment_config, %{course: course}) - - params = %{ - assessment_config_id: config.id, - type: "Mission", - early_submission_xp: 100, - hours_before_early_xp_decay: 24 - } - - {:ok, updated_config} = Courses.insert_or_update_assessment_config(course.id, params) - - assert updated_config.type == "Mission" - assert updated_config.early_submission_xp == 100 - assert updated_config.hours_before_early_xp_decay == 24 - end - end - - describe "reorder_assessment_config" do - test "succeeds" do - course = insert(:course) - config1 = insert(:assessment_config, %{order: 1, type: "Missions", course: course}) - config3 = insert(:assessment_config, %{order: 2, type: "Paths", course: course}) - config2 = insert(:assessment_config, %{order: 3, type: "Quests", course: course}) - config4 = insert(:assessment_config, %{order: 4, type: "Others", course: course}) - old_configs = Courses.get_assessment_configs(course.id) - - params = [ - %{assessment_config_id: config1.id, type: "Paths"}, - %{assessment_config_id: config2.id, type: "Quests"}, - %{assessment_config_id: config3.id, type: "Missions"}, - %{assessment_config_id: config4.id, type: "Others"} - ] - - expected = ["Paths", "Quests", "Missions", "Others"] - - {:ok, _} = Courses.reorder_assessment_configs(course.id, params) - - new_configs = Courses.get_assessment_configs(course.id) - assert length(old_configs) == length(new_configs) - assert Enum.map(new_configs, & &1.type) == expected - end - end - - describe "delete_assessment_config" do - test "succeeds" do - course = insert(:course) - config = insert(:assessment_config, %{order: 1, course: course}) - old_configs = Courses.get_assessment_configs(course.id) - - params = %{ - assessment_config_id: config.id - } - - {:ok, _} = Courses.delete_assessment_config(course.id, params) - - new_configs = Courses.get_assessment_configs(course.id) - assert length(old_configs) == 1 - assert new_configs == [] - end - - test "error" do - course = insert(:course) - insert(:assessment_config, %{order: 1, course: course}) - - params = %{ - assessment_config_id: -1 - } - - assert {:error, :no_such_enrty} == Courses.delete_assessment_config(course.id, params) - end - end - - describe "upsert_groups_in_course" do - setup do - course = insert(:course) - existing_group_leader = insert(:course_registration, %{course: course, role: :staff}) - - existing_group = - insert(:group, %{name: "Existing Group", course: course, leader: existing_group_leader}) - - existing_student = - insert(:course_registration, %{course: course, group: existing_group, role: :student}) - - {:ok, - course: course, - existing_group: existing_group, - existing_group_leader: existing_group_leader, - existing_student: existing_student} - end - - test "succeeds in upserting existing groups", %{ - course: course, - existing_group: existing_group, - existing_group_leader: existing_group_leader, - existing_student: existing_student - } do - student = insert(:course_registration, %{course: course, group: nil, role: :student}) - admin = insert(:course_registration, %{course: course, group: nil, role: :admin}) - - usernames_and_groups = [ - %{username: existing_student.user.username, group: "Group1"}, - %{username: admin.user.username, group: "Group2"}, - %{username: student.user.username, group: "Group2"}, - %{username: existing_group_leader.user.username, group: "Group1"} - ] - - assert :ok == Courses.upsert_groups_in_course(usernames_and_groups, course.id) - - # Check that Group1 and Group2 were created - assert length(Group |> where(course_id: ^course.id) |> Repo.all()) == 3 - - # Check that leaders were assigned/ updated correctly - assert is_nil( - Group - |> where(id: ^existing_group.id) - |> Repo.one() - |> Map.fetch!(:leader_id) - ) - - group1 = Group |> where(course_id: ^course.id) |> where(name: "Group1") |> Repo.one() - group2 = Group |> where(course_id: ^course.id) |> where(name: "Group2") |> Repo.one() - assert group1 |> Map.fetch!(:leader_id) == existing_group_leader.id - assert group2 |> Map.fetch!(:leader_id) == admin.id - - # Check that students were assigned to the correct groups - assert CourseRegistration - |> where(id: ^existing_student.id) - |> Repo.one() - |> Map.fetch!(:group_id) == - group1.id - - assert CourseRegistration |> where(id: ^student.id) |> Repo.one() |> Map.fetch!(:group_id) == - group2.id - end - - test "succeeds (removes user from existing groups when group is not specified)", %{ - course: course, - existing_group: existing_group, - existing_group_leader: existing_group_leader, - existing_student: existing_student - } do - usernames_and_groups = [ - %{username: existing_student.user.username}, - %{username: existing_group_leader.user.username} - ] - - assert :ok == Courses.upsert_groups_in_course(usernames_and_groups, course.id) - - assert is_nil( - Group - |> where(id: ^existing_group.id) - |> Repo.one() - |> Map.fetch!(:leader_id) - ) - - assert is_nil( - CourseRegistration - |> where(id: ^existing_student.id) - |> Repo.one() - |> Map.fetch!(:group_id) - ) - end - end - - describe "Sourcecast" do - setup do - on_exit(fn -> File.rm_rf!("uploads/test/sourcecasts") end) - end - - test "upload file to folder then delete it" do - inserter_course_registration = insert(:course_registration, %{role: :staff}) - - upload = %Plug.Upload{ - content_type: "audio/wav", - filename: "upload.wav", - path: "test/fixtures/upload.wav" - } - - result = - Courses.upload_sourcecast_file(inserter_course_registration, %{ - title: "Test Upload", - audio: upload, - playbackData: - "{\"init\":{\"editorValue\":\"// Type your program in here!\"},\"inputs\":[]}" - }) - - assert {:ok, sourcecast} = result - path = SourcecastUpload.url({sourcecast.audio, sourcecast}) - assert path =~ "/uploads/test/sourcecasts/upload.wav" - - deleter_course_registration = insert(:course_registration, %{role: :staff}) - assert {:ok, _} = Courses.delete_sourcecast_file(deleter_course_registration, sourcecast.id) - assert Repo.get(Sourcecast, sourcecast.id) == nil - refute File.exists?("uploads/test/sourcecasts/upload.wav") - end - end - - describe "get_or_create_group" do - test "existing group" do - course = insert(:course) - group = insert(:group, %{course: course}) - - {:ok, group_db} = Courses.get_or_create_group(group.name, course.id) - - assert group_db.id == group.id - assert group_db.leader_id == group.leader_id - end - - test "non-existent group" do - course = insert(:course) - group_name = params_for(:group).name - - {:ok, _} = Courses.get_or_create_group(group_name, course.id) - - group_db = - Group - |> where(name: ^group_name) - |> Repo.one() - - refute is_nil(group_db) - end - end - - # describe "insert_or_update_group" do - # test "existing group" do - # group = insert(:group) - # group_params = params_with_assocs(:group, name: group.name) - # Courses.insert_or_update_group(group_params) - - # updated_group = - # Group - # |> where(name: ^group.name) - # |> Repo.one() - - # assert updated_group.id == group.id - # assert updated_group.leader_id == group_params.leader_id - # end - - # test "non-existent group" do - # group_params = params_with_assocs(:group) - # Courses.insert_or_update_group(group_params) - - # updated_group = - # Group - # |> where(name: ^group_params.name) - # |> Repo.one() - - # assert updated_group.leader_id == group_params.leader_id - # end - # end -end +defmodule Cadet.CoursesTest do + use Cadet.DataCase + + alias Cadet.{Courses, Repo} + alias Cadet.Accounts.{CourseRegistration, User} + alias Cadet.Courses.{Course, Group, Sourcecast, SourcecastUpload} + + describe "create course config" do + test "succeeds" do + user = insert(:user) + + # Course precreated in User factory + old_courses = Course |> Repo.all() |> length() + + params = %{ + course_name: "CS1101S Programming Methodology (AY20/21 Sem 1)", + course_short_name: "CS1101S", + viewable: true, + enable_game: true, + enable_achievements: true, + enable_sourcecast: true, + source_chapter: 1, + source_variant: "default", + module_help_text: "Help Text" + } + + Courses.create_course_config(params, user) + + # New course created + new_courses = Course |> Repo.all() |> length() + assert new_courses - old_courses == 1 + + # New admin course registration for user + course_regs = CourseRegistration |> where(user_id: ^user.id) |> Repo.all() + assert length(course_regs) == 1 + assert Enum.at(course_regs, 0).role == :admin + + # User's latest_viewed course is updated + assert User |> where(id: ^user.id) |> Repo.one() |> Map.fetch!(:latest_viewed_id) == + Enum.at(course_regs, 0).course_id + end + end + + describe "get course config" do + test "succeeds" do + course = insert(:course) + insert(:assessment_config, %{order: 1, type: "Missions", course: course}) + insert(:assessment_config, %{order: 2, type: "Quests", course: course}) + + {:ok, course} = Courses.get_course_config(course.id) + assert course.course_name == "Programming Methodology" + assert course.course_short_name == "CS1101S" + assert course.viewable == true + assert course.enable_game == true + assert course.enable_achievements == true + assert course.enable_sourcecast == true + assert course.source_chapter == 1 + assert course.source_variant == "default" + assert course.module_help_text == "Help Text" + assert course.assessment_configs == ["Missions", "Quests"] + end + + test "returns with error for invalid course id" do + course = insert(:course) + + assert {:error, {:bad_request, "Invalid course id"}} = + Courses.get_course_config(course.id + 1) + end + end + + describe "update course config" do + test "succeeds (without sublanguage update)" do + course = insert(:course) + + {:ok, updated_course} = + Courses.update_course_config(course.id, %{ + course_name: "Data Structures and Algorithms", + course_short_name: "CS2040S", + viewable: false, + enable_game: false, + enable_achievements: false, + enable_sourcecast: false, + module_help_text: "" + }) + + assert updated_course.course_name == "Data Structures and Algorithms" + assert updated_course.course_short_name == "CS2040S" + assert updated_course.viewable == false + assert updated_course.enable_game == false + assert updated_course.enable_achievements == false + assert updated_course.enable_sourcecast == false + assert updated_course.source_chapter == 1 + assert updated_course.source_variant == "default" + assert updated_course.module_help_text == nil + end + + test "succeeds (with sublanguage update)" do + course = insert(:course) + new_chapter = Enum.random(1..4) + + {:ok, updated_course} = + Courses.update_course_config(course.id, %{ + course_name: "Data Structures and Algorithms", + course_short_name: "CS2040S", + viewable: false, + enable_game: false, + enable_achievements: false, + enable_sourcecast: false, + source_chapter: new_chapter, + source_variant: "default", + module_help_text: "help" + }) + + assert updated_course.course_name == "Data Structures and Algorithms" + assert updated_course.course_short_name == "CS2040S" + assert updated_course.viewable == false + assert updated_course.enable_game == false + assert updated_course.enable_achievements == false + assert updated_course.enable_sourcecast == false + assert updated_course.source_chapter == new_chapter + assert updated_course.source_variant == "default" + assert updated_course.module_help_text == "help" + end + + test "returns with error for invalid course id" do + course = insert(:course) + new_chapter = Enum.random(1..4) + + assert {:error, {:bad_request, "Invalid course id"}} = + Courses.update_course_config(course.id + 1, %{ + source_chapter: new_chapter, + source_variant: "default" + }) + end + + test "returns with error for failed updates" do + course = insert(:course) + + assert {:error, changeset} = + Courses.update_course_config(course.id, %{ + source_chapter: 0, + source_variant: "default" + }) + + assert %{source_chapter: ["is invalid"]} = errors_on(changeset) + + assert {:error, changeset} = + Courses.update_course_config(course.id, %{source_chapter: 2, source_variant: "gpu"}) + + assert %{source_variant: ["is invalid"]} = errors_on(changeset) + end + end + + describe "get assessment configs" do + test "succeeds" do + course = insert(:course) + + for i <- 1..5 do + insert(:assessment_config, %{order: 6 - i, type: "Mission#{i}", course: course}) + end + + assessment_configs = Courses.get_assessment_configs(course.id) + + assert length(assessment_configs) <= 5 + + assessment_configs + |> Enum.with_index(1) + |> Enum.each(fn {at, idx} -> + assert at.order == idx + assert at.type == "Mission#{6 - idx}" + end) + end + end + + describe "mass_upsert_and_reorder_assessment_configs" do + setup do + course = insert(:course) + config1 = insert(:assessment_config, %{order: 1, type: "Missions", course: course}) + config2 = insert(:assessment_config, %{order: 2, type: "Quests", course: course}) + config3 = insert(:assessment_config, %{order: 3, type: "Paths", course: course}) + config4 = insert(:assessment_config, %{order: 4, type: "Contests", course: course}) + expected = ["Paths", "Quests", "Missions", "Others", "Contests"] + + {:ok, + %{ + course: course, + expected: expected, + config1: config1, + config2: config2, + config3: config3, + config4: config4 + }} + end + + test "succeeds", %{ + course: course, + expected: expected, + config1: config1, + config2: config2, + config3: config3, + config4: config4 + } do + {:ok, _} = + Courses.mass_upsert_and_reorder_assessment_configs(course.id, [ + %{assessment_config_id: config1.id, type: "Paths"}, + %{assessment_config_id: config2.id, type: "Quests"}, + %{assessment_config_id: config3.id, type: "Missions"}, + %{assessment_config_id: config4.id, type: "Others"}, + %{assessment_config_id: -1, type: "Contests"} + ]) + + assessment_configs = Courses.get_assessment_configs(course.id) + + assert Enum.map(assessment_configs, & &1.type) == expected + end + + test "succeeds to capitalise", %{ + course: course, + expected: expected, + config1: config1, + config2: config2, + config3: config3, + config4: config4 + } do + {:ok, _} = + Courses.mass_upsert_and_reorder_assessment_configs(course.id, [ + %{assessment_config_id: config1.id, type: "Paths"}, + %{assessment_config_id: config2.id, type: "Quests"}, + %{assessment_config_id: config3.id, type: "Missions"}, + %{assessment_config_id: config4.id, type: "Others"}, + %{assessment_config_id: -1, type: "Contests"} + ]) + + assessment_configs = Courses.get_assessment_configs(course.id) + + assert Enum.map(assessment_configs, & &1.type) == expected + end + + # test "succeed to delete", %{course: course} do + # :ok = + # Courses.mass_upsert_and_reorder_assessment_configs(course.id, [ + # %{order: 1, type: "Paths"}, + # %{order: 2, type: "quests"}, + # %{order: 3, type: "missions"} + # ]) + + # assessment_configs = Courses.get_assessment_configs(course.id) + + # assert Enum.map(assessment_configs, & &1.type) == ["Paths", "Quests", "Missions"] + # end + + test "returns with error for empty list parameter", %{course: course} do + assert {:error, {:bad_request, "Invalid parameter(s)"}} = + Courses.mass_upsert_and_reorder_assessment_configs(course.id, []) + end + + test "returns with error for list parameter of greater than length 8", %{ + course: course, + config1: config1, + config2: config2, + config3: config3, + config4: config4 + } do + params = [ + %{assessment_config_id: config1.id, type: "Paths"}, + %{assessment_config_id: config2.id, type: "Quests"}, + %{assessment_config_id: config3.id, type: "Missions"}, + %{assessment_config_id: config4.id, type: "Others"}, + %{assessment_config_id: -1, type: "Contests"}, + %{assessment_config_id: -1, type: "Contests"}, + %{assessment_config_id: -1, type: "Contests"}, + %{assessment_config_id: -1, type: "Contests"}, + %{assessment_config_id: -1, type: "Contests"} + ] + + assert {:error, {:bad_request, "Invalid parameter(s)"}} = + Courses.mass_upsert_and_reorder_assessment_configs(course.id, params) + end + + test "returns with error for non-list parameter", %{course: course} do + params = %{course_id: course.id, order: 1, type: "Paths"} + + assert {:error, {:bad_request, "Invalid parameter(s)"}} = + Courses.mass_upsert_and_reorder_assessment_configs(course.id, params) + end + end + + describe "insert_or_update_assessment_config" do + test "succeeds with insert configs" do + course = insert(:course) + old_configs = Courses.get_assessment_configs(course.id) + + params = %{ + assessment_config_id: -1, + order: 1, + type: "Mission", + early_submission_xp: 100, + hours_before_early_xp_decay: 24 + } + + {:ok, updated_config} = Courses.insert_or_update_assessment_config(course.id, params) + + new_configs = Courses.get_assessment_configs(course.id) + assert old_configs == [] + assert length(new_configs) == 1 + assert updated_config.early_submission_xp == 100 + assert updated_config.hours_before_early_xp_decay == 24 + end + + test "succeeds with update" do + course = insert(:course) + config = insert(:assessment_config, %{course: course}) + + params = %{ + assessment_config_id: config.id, + type: "Mission", + early_submission_xp: 100, + hours_before_early_xp_decay: 24 + } + + {:ok, updated_config} = Courses.insert_or_update_assessment_config(course.id, params) + + assert updated_config.type == "Mission" + assert updated_config.early_submission_xp == 100 + assert updated_config.hours_before_early_xp_decay == 24 + end + end + + describe "reorder_assessment_config" do + test "succeeds" do + course = insert(:course) + config1 = insert(:assessment_config, %{order: 1, type: "Missions", course: course}) + config3 = insert(:assessment_config, %{order: 2, type: "Paths", course: course}) + config2 = insert(:assessment_config, %{order: 3, type: "Quests", course: course}) + config4 = insert(:assessment_config, %{order: 4, type: "Others", course: course}) + old_configs = Courses.get_assessment_configs(course.id) + + params = [ + %{assessment_config_id: config1.id, type: "Paths"}, + %{assessment_config_id: config2.id, type: "Quests"}, + %{assessment_config_id: config3.id, type: "Missions"}, + %{assessment_config_id: config4.id, type: "Others"} + ] + + expected = ["Paths", "Quests", "Missions", "Others"] + + {:ok, _} = Courses.reorder_assessment_configs(course.id, params) + + new_configs = Courses.get_assessment_configs(course.id) + assert length(old_configs) == length(new_configs) + assert Enum.map(new_configs, & &1.type) == expected + end + end + + describe "delete_assessment_config" do + test "succeeds" do + course = insert(:course) + config = insert(:assessment_config, %{order: 1, course: course}) + old_configs = Courses.get_assessment_configs(course.id) + + params = %{ + assessment_config_id: config.id + } + + {:ok, _} = Courses.delete_assessment_config(course.id, params) + + new_configs = Courses.get_assessment_configs(course.id) + assert length(old_configs) == 1 + assert new_configs == [] + end + + test "error" do + course = insert(:course) + insert(:assessment_config, %{order: 1, course: course}) + + params = %{ + assessment_config_id: -1 + } + + assert {:error, :no_such_enrty} == Courses.delete_assessment_config(course.id, params) + end + end + + describe "upsert_groups_in_course" do + setup do + course = insert(:course) + existing_group_leader = insert(:course_registration, %{course: course, role: :staff}) + + existing_group = + insert(:group, %{name: "Existing Group", course: course, leader: existing_group_leader}) + + existing_student = + insert(:course_registration, %{course: course, group: existing_group, role: :student}) + + {:ok, + course: course, + existing_group: existing_group, + existing_group_leader: existing_group_leader, + existing_student: existing_student} + end + + test "succeeds in upserting existing groups", %{ + course: course, + existing_group: existing_group, + existing_group_leader: existing_group_leader, + existing_student: existing_student + } do + student = insert(:course_registration, %{course: course, group: nil, role: :student}) + admin = insert(:course_registration, %{course: course, group: nil, role: :admin}) + + usernames_and_groups = [ + %{username: existing_student.user.username, group: "Group1"}, + %{username: admin.user.username, group: "Group2"}, + %{username: student.user.username, group: "Group2"}, + %{username: existing_group_leader.user.username, group: "Group1"} + ] + + assert :ok == Courses.upsert_groups_in_course(usernames_and_groups, course.id) + + # Check that Group1 and Group2 were created + assert length(Group |> where(course_id: ^course.id) |> Repo.all()) == 3 + + # Check that leaders were assigned/ updated correctly + assert is_nil( + Group + |> where(id: ^existing_group.id) + |> Repo.one() + |> Map.fetch!(:leader_id) + ) + + group1 = Group |> where(course_id: ^course.id) |> where(name: "Group1") |> Repo.one() + group2 = Group |> where(course_id: ^course.id) |> where(name: "Group2") |> Repo.one() + assert group1 |> Map.fetch!(:leader_id) == existing_group_leader.id + assert group2 |> Map.fetch!(:leader_id) == admin.id + + # Check that students were assigned to the correct groups + assert CourseRegistration + |> where(id: ^existing_student.id) + |> Repo.one() + |> Map.fetch!(:group_id) == + group1.id + + assert CourseRegistration |> where(id: ^student.id) |> Repo.one() |> Map.fetch!(:group_id) == + group2.id + end + + test "succeeds (removes user from existing groups when group is not specified)", %{ + course: course, + existing_group: existing_group, + existing_group_leader: existing_group_leader, + existing_student: existing_student + } do + usernames_and_groups = [ + %{username: existing_student.user.username}, + %{username: existing_group_leader.user.username} + ] + + assert :ok == Courses.upsert_groups_in_course(usernames_and_groups, course.id) + + assert is_nil( + Group + |> where(id: ^existing_group.id) + |> Repo.one() + |> Map.fetch!(:leader_id) + ) + + assert is_nil( + CourseRegistration + |> where(id: ^existing_student.id) + |> Repo.one() + |> Map.fetch!(:group_id) + ) + end + end + + describe "Sourcecast" do + setup do + on_exit(fn -> File.rm_rf!("uploads/test/sourcecasts") end) + end + + test "upload file to folder then delete it" do + inserter_course_registration = insert(:course_registration, %{role: :staff}) + + upload = %Plug.Upload{ + content_type: "audio/wav", + filename: "upload.wav", + path: "test/fixtures/upload.wav" + } + + result = + Courses.upload_sourcecast_file(inserter_course_registration, %{ + title: "Test Upload", + audio: upload, + playbackData: + "{\"init\":{\"editorValue\":\"// Type your program in here!\"},\"inputs\":[]}" + }) + + assert {:ok, sourcecast} = result + path = SourcecastUpload.url({sourcecast.audio, sourcecast}) + assert path =~ "/uploads/test/sourcecasts/upload.wav" + + deleter_course_registration = insert(:course_registration, %{role: :staff}) + assert {:ok, _} = Courses.delete_sourcecast_file(deleter_course_registration, sourcecast.id) + assert Repo.get(Sourcecast, sourcecast.id) == nil + refute File.exists?("uploads/test/sourcecasts/upload.wav") + end + end + + describe "get_or_create_group" do + test "existing group" do + course = insert(:course) + group = insert(:group, %{course: course}) + + {:ok, group_db} = Courses.get_or_create_group(group.name, course.id) + + assert group_db.id == group.id + assert group_db.leader_id == group.leader_id + end + + test "non-existent group" do + course = insert(:course) + group_name = params_for(:group).name + + {:ok, _} = Courses.get_or_create_group(group_name, course.id) + + group_db = + Group + |> where(name: ^group_name) + |> Repo.one() + + refute is_nil(group_db) + end + end + + # describe "insert_or_update_group" do + # test "existing group" do + # group = insert(:group) + # group_params = params_with_assocs(:group, name: group.name) + # Courses.insert_or_update_group(group_params) + + # updated_group = + # Group + # |> where(name: ^group.name) + # |> Repo.one() + + # assert updated_group.id == group.id + # assert updated_group.leader_id == group_params.leader_id + # end + + # test "non-existent group" do + # group_params = params_with_assocs(:group) + # Courses.insert_or_update_group(group_params) + + # updated_group = + # Group + # |> where(name: ^group_params.name) + # |> Repo.one() + + # assert updated_group.leader_id == group_params.leader_id + # end + # end +end From 15cdaedb9a1edb762d8dd69e37b3baf5f31d985b Mon Sep 17 00:00:00 2001 From: Chow En Rong <53928333+chownces@users.noreply.github.com> Date: Mon, 5 Jul 2021 20:24:27 +0800 Subject: [PATCH 122/174] Updated devices --- lib/cadet_web/router.ex | 12 ++++++------ test/cadet/devices/devices_test.exs | 4 ++-- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/lib/cadet_web/router.ex b/lib/cadet_web/router.ex index e83a402ae..1a6d5ca5f 100644 --- a/lib/cadet_web/router.ex +++ b/lib/cadet_web/router.ex @@ -58,6 +58,12 @@ defmodule CadetWeb.Router do put("/user/latest_viewed", UserController, :update_latest_viewed) post("/config/create", CoursesController, :create) + + get("/devices", DevicesController, :index) + post("/devices", DevicesController, :register) + post("/devices/:id", DevicesController, :edit) + delete("/devices/:id", DevicesController, :deregister) + get("/devices/:id/ws_endpoint", DevicesController, :get_ws_endpoint) end # Authenticated Pages with course @@ -87,12 +93,6 @@ defmodule CadetWeb.Router do put("/user/game_states", UserController, :update_game_states) get("/config", CoursesController, :index) - - get("/devices", DevicesController, :index) - post("/devices", DevicesController, :register) - post("/devices/:id", DevicesController, :edit) - delete("/devices/:id", DevicesController, :deregister) - get("/devices/:id/ws_endpoint", DevicesController, :get_ws_endpoint) end # Authenticated Pages diff --git a/test/cadet/devices/devices_test.exs b/test/cadet/devices/devices_test.exs index 5764d7042..577f2a961 100644 --- a/test/cadet/devices/devices_test.exs +++ b/test/cadet/devices/devices_test.exs @@ -9,7 +9,7 @@ defmodule Cadet.DevicesTest do @registration_compare_fields ~w(id title device_id user_id)a setup do - user = insert(:user, %{role: :student}) + user = insert(:user) device = insert(:device, client_key: nil, client_cert: nil) {:ok, registration} = @@ -65,7 +65,7 @@ defmodule Cadet.DevicesTest do end test "add existing device to new user", %{device: device} do - user = insert(:user, %{role: :student}) + user = insert(:user) title = Faker.Person.En.first_name() assert {:ok, %DeviceRegistration{} = registration} = From af49aff0d9cfcedd39c3c9f9066d9ee61150ad50 Mon Sep 17 00:00:00 2001 From: YaleChen299 Date: Mon, 5 Jul 2021 20:41:43 +0800 Subject: [PATCH 123/174] refactor assessment config booleans to question --- lib/cadet/assessments/assessments.ex | 4 +- lib/cadet/assessments/question.ex | 6 ++- lib/cadet/courses/assessment_config.ex | 11 ++--- lib/cadet/jobs/xml_parser.ex | 13 ++++++ .../admin_views/admin_courses_view.ex | 5 +- .../admin_views/admin_grading_view.ex | 4 +- lib/cadet_web/views/assessments_helpers.ex | 46 +++++++------------ lib/cadet_web/views/assessments_view.ex | 4 +- lib/cadet_web/views/user_view.ex | 5 +- .../20210531155751_multitenant_upgrade.exs | 26 ++++++++--- test/cadet/updater/xml_parser_test.exs | 5 +- .../admin_courses_controller_test.exs | 18 ++++---- .../assessments_controller_test.exs | 7 ++- .../controllers/user_controller_test.exs | 15 +++--- test/support/seeds.ex | 19 +++++--- 15 files changed, 101 insertions(+), 87 deletions(-) diff --git a/lib/cadet/assessments/assessments.ex b/lib/cadet/assessments/assessments.ex index c199ee1cd..ad2eef76b 100644 --- a/lib/cadet/assessments/assessments.ex +++ b/lib/cadet/assessments/assessments.ex @@ -1409,7 +1409,7 @@ defmodule Cadet.Assessments do |> where( [ans, s, st, a, ac], not is_nil(st.group_id) and s.status == ^:submitted and - ac.is_graded and a.course_id == ^course_id + ac.show_grading_summary and a.course_id == ^course_id ) |> group_by([ans, s, st, a, ac], s.id) |> select([ans, s, st, a, ac], %{ @@ -1445,7 +1445,7 @@ defmodule Cadet.Assessments do graded_configs = AssessmentConfig - |> where([ac], ac.course_id == ^course_id and ac.is_graded) + |> where([ac], ac.course_id == ^course_id and ac.show_grading_summary) |> order_by(:order) |> select([ac], %{ id: :id, diff --git a/lib/cadet/assessments/question.ex b/lib/cadet/assessments/question.ex index 3420b9473..695abcb78 100644 --- a/lib/cadet/assessments/question.ex +++ b/lib/cadet/assessments/question.ex @@ -13,6 +13,9 @@ defmodule Cadet.Assessments.Question do field(:question, :map) field(:type, QuestionType) field(:max_xp, :integer) + field(:show_solution, :boolean, default: false) + field(:build_hidden_testcases, :boolean, default: false) + field(:blocking, :boolean, default: false) field(:answer, :map, virtual: true) embeds_one(:library, Library, on_replace: :update) embeds_one(:grading_library, Library, on_replace: :update) @@ -21,7 +24,8 @@ defmodule Cadet.Assessments.Question do end @required_fields ~w(question type assessment_id)a - @optional_fields ~w(display_order max_xp)a + @optional_fields ~w(display_order max_xp show_solution + build_hidden_testcases blocking)a @required_embeds ~w(library)a def changeset(question, params) do diff --git a/lib/cadet/courses/assessment_config.ex b/lib/cadet/courses/assessment_config.ex index 524073d34..00c1d55a9 100644 --- a/lib/cadet/courses/assessment_config.ex +++ b/lib/cadet/courses/assessment_config.ex @@ -10,12 +10,9 @@ defmodule Cadet.Courses.AssessmentConfig do schema "assessment_configs" do field(:order, :integer) field(:type, :string) - field(:is_graded, :boolean, default: true) - # a graded type will not build solutions and will build private testcases as hidden - field(:skippable, :boolean, default: true) - # only for frontend to determine if a student can go to next question without attempting - field(:is_autograded, :boolean, default: true) - # assessment will be autograded a day after due day + field(:show_grading_summary, :boolean, default: true) + field(:is_manually_graded, :boolean, default: true) + # used by fronend to determine display styles field(:early_submission_xp, :integer, default: 0) field(:hours_before_early_xp_decay, :integer, default: 0) @@ -26,7 +23,7 @@ defmodule Cadet.Courses.AssessmentConfig do @required_fields ~w(course_id)a @optional_fields ~w(order type early_submission_xp - hours_before_early_xp_decay is_graded skippable is_autograded)a + hours_before_early_xp_decay show_grading_summary is_manually_graded)a def changeset(assessment_config, params) do assessment_config diff --git a/lib/cadet/jobs/xml_parser.ex b/lib/cadet/jobs/xml_parser.ex index 76026af96..4ea0b956c 100644 --- a/lib/cadet/jobs/xml_parser.ex +++ b/lib/cadet/jobs/xml_parser.ex @@ -145,11 +145,15 @@ defmodule Cadet.Updater.XMLParser do type: ~x"./@type"o |> transform_by(&process_charlist/1), # max_grade: ~x"./@maxgrade"oi, max_xp: ~x"./@maxxp"oi, + show_solution: ~x"./@showsolution"os, + build_hidden_testcases: ~x"./@buildhiddentestcases"os, + blocking: ~x"./@blocking"os, entity: ~x"." ) |> Enum.map(fn param -> with {:no_missing_attr?, true} <- {:no_missing_attr?, not is_nil(param[:type]) and not is_nil(param[:max_xp])}, + question when is_map(question) <- process_question_booleans(param), question when is_map(question) <- process_question_by_question_type(param), question when is_map(question) <- process_question_library(question, default_library, default_grading_library), @@ -179,6 +183,15 @@ defmodule Cadet.Updater.XMLParser do Logger.error("Changeset: #{inspect(changeset, pretty: true)}") end + @spec process_question_booleans(map()) :: map() + defp process_question_booleans(question) do + flags = [:show_solution, :build_hidden_testcases, :blocking] + flags + |> Enum.reduce(question, fn flag, acc -> + put_in(acc[flag], acc[flag] == "true") + end) + end + @spec process_question_by_question_type(map()) :: map() | {:error, String.t()} defp process_question_by_question_type(question) do question[:entity] diff --git a/lib/cadet_web/admin_views/admin_courses_view.ex b/lib/cadet_web/admin_views/admin_courses_view.ex index b22cefee1..2de1c970c 100644 --- a/lib/cadet_web/admin_views/admin_courses_view.ex +++ b/lib/cadet_web/admin_views/admin_courses_view.ex @@ -9,9 +9,8 @@ defmodule CadetWeb.AdminCoursesView do transform_map_for_view(config, %{ assessmentConfigId: :id, type: :type, - skippable: :skippable, - isGraded: :is_graded, - isAutograded: :is_autograded, + displayInDashboard: :show_grading_summary, + isManuallyGraded: :is_manually_graded, earlySubmissionXp: :early_submission_xp, hoursBeforeEarlyXpDecay: :hours_before_early_xp_decay }) diff --git a/lib/cadet_web/admin_views/admin_grading_view.ex b/lib/cadet_web/admin_views/admin_grading_view.ex index 60a6cfb19..299df6273 100644 --- a/lib/cadet_web/admin_views/admin_grading_view.ex +++ b/lib/cadet_web/admin_views/admin_grading_view.ex @@ -35,8 +35,8 @@ defmodule CadetWeb.AdminGradingView do defp build_grading_question(answer) do results = build_autograding_results(answer.autograding_results) - %{question: answer.question, assessment_config: answer.question.assessment.config} - |> build_question_by_assessment_config(true) + %{question: answer.question} + |> build_question_by_question_config(true) |> Map.put(:answer, answer.answer["code"] || answer.answer["choice_id"]) |> Map.put(:autogradingStatus, answer.autograding_status) |> Map.put(:autogradingResults, results) diff --git a/lib/cadet_web/views/assessments_helpers.ex b/lib/cadet_web/views/assessments_helpers.ex index b8e0d030f..5d04666aa 100644 --- a/lib/cadet_web/views/assessments_helpers.ex +++ b/lib/cadet_web/views/assessments_helpers.ex @@ -16,36 +16,26 @@ defmodule CadetWeb.AssessmentsHelpers do transform_map_for_view(external_library, [:name, :symbols]) end - def build_question_by_assessment_config( - %{ - question: question, - assessment_config: assessment_config - }, + def build_question_by_question_config( + %{question: question}, all_testcases? \\ false ) do Map.merge( build_generic_question_fields(%{question: question}), build_question_content_by_config( - %{ - question: question, - assessment_config: assessment_config - }, + %{question: question}, all_testcases? ) ) end - def build_question_with_answer_and_solution_if_ungraded(%{ - question: question, - assessment: assessment - }) do + def build_question_with_answer_and_solution_if_ungraded(%{question: question}) do components = [ - build_question_by_assessment_config(%{ - question: question, - assessment_config: assessment.config + build_question_by_question_config(%{ + question: question }), build_answer_fields_by_question_type(%{question: question}), - build_solution_if_ungraded_by_config(%{question: question, assessment: assessment}) + build_solution_if_ungraded_by_config(%{question: question}) ] components @@ -63,10 +53,9 @@ defmodule CadetWeb.AssessmentsHelpers do end defp build_solution_if_ungraded_by_config(%{ - question: %{question: question, type: question_type}, - assessment: %{config: assessment_config} + question: %{question: question, type: question_type, show_solution: show_solution} }) do - if !assessment_config.is_graded do + if show_solution do solution_getter = case question_type do :programming -> &Map.get(&1, "solution") @@ -173,7 +162,7 @@ defmodule CadetWeb.AssessmentsHelpers do }) end - defp build_testcases(%{assessment_config: assessment_config}, all_testcases?) do + defp build_testcases(%{build_hidden: build_hidden}, all_testcases?) do cond do all_testcases? -> &Enum.concat( @@ -182,7 +171,7 @@ defmodule CadetWeb.AssessmentsHelpers do ) # build hidden testcases if ungraded - !assessment_config.is_graded -> + build_hidden -> &Enum.concat( Enum.map(&1["public"], fn testcase -> build_testcase(testcase, "public") end), Enum.map(&1["private"], fn testcase -> build_testcase(testcase, "hidden") end) @@ -193,10 +182,10 @@ defmodule CadetWeb.AssessmentsHelpers do end end - defp build_postpend(%{assessment_config: assessment_config}, all_testcases?) do - case {all_testcases?, assessment_config.is_graded} do + defp build_postpend(%{build_hidden: build_hidden}, all_testcases?) do + case {all_testcases?, build_hidden} do {true, _} -> & &1["postpend"] - {_, false} -> & &1["postpend"] + {_, true} -> & &1["postpend"] # Create a 1-arity function to return an empty postpend for non-paths _ -> fn _question -> "" end end @@ -204,8 +193,7 @@ defmodule CadetWeb.AssessmentsHelpers do defp build_question_content_by_config( %{ - question: %{question: question, type: question_type}, - assessment_config: assessment_config + question: %{question: question, type: question_type, build_hidden_testcases: build_hidden_testcases} }, all_testcases? ) do @@ -215,8 +203,8 @@ defmodule CadetWeb.AssessmentsHelpers do content: "content", prepend: "prepend", solutionTemplate: "template", - postpend: build_postpend(%{assessment_config: assessment_config}, all_testcases?), - testcases: build_testcases(%{assessment_config: assessment_config}, all_testcases?) + postpend: build_postpend(%{build_hidden: build_hidden_testcases}, all_testcases?), + testcases: build_testcases(%{build_hidden: build_hidden_testcases}, all_testcases?) }) :mcq -> diff --git a/lib/cadet_web/views/assessments_view.ex b/lib/cadet_web/views/assessments_view.ex index d475537e8..a2050f157 100644 --- a/lib/cadet_web/views/assessments_view.ex +++ b/lib/cadet_web/views/assessments_view.ex @@ -17,6 +17,7 @@ defmodule CadetWeb.AssessmentsView do openAt: &format_datetime(&1.open_at), closeAt: &format_datetime(&1.close_at), type: & &1.config.type, + isManuallyGraded: & &1.config.is_manually_graded, story: :story, number: :number, reading: :reading, @@ -47,8 +48,7 @@ defmodule CadetWeb.AssessmentsView do questions: &Enum.map(&1.questions, fn question -> build_question_with_answer_and_solution_if_ungraded(%{ - question: question, - assessment: assessment + question: question }) end) } diff --git a/lib/cadet_web/views/user_view.ex b/lib/cadet_web/views/user_view.ex index d62a7a1d4..f8b79cce7 100644 --- a/lib/cadet_web/views/user_view.ex +++ b/lib/cadet_web/views/user_view.ex @@ -117,9 +117,8 @@ defmodule CadetWeb.UserView do transform_map_for_view(config, %{ assessmentConfigId: :id, type: :type, - skippable: :skippable, - isGraded: :is_graded, - isAutograded: :is_autograded, + displayInDashboard: :show_grading_summary, + isManuallyGraded: :is_manually_graded, earlySubmissionXp: :early_submission_xp, hoursBeforeEarlyXpDecay: :hours_before_early_xp_decay }) diff --git a/priv/repo/migrations/20210531155751_multitenant_upgrade.exs b/priv/repo/migrations/20210531155751_multitenant_upgrade.exs index 4907d1459..e55b826c0 100644 --- a/priv/repo/migrations/20210531155751_multitenant_upgrade.exs +++ b/priv/repo/migrations/20210531155751_multitenant_upgrade.exs @@ -28,9 +28,8 @@ defmodule Cadet.Repo.Migrations.MultitenantUpgrade do add(:order, :integer, null: true) add(:type, :string, null: false) add(:course_id, references(:courses), null: false) - add(:is_graded, :boolean, null: false, default: true) - add(:is_autograded, :boolean, null: false, default: true) - add(:skippable, :boolean, null: false, default: true) + add(:show_grading_summary, :boolean, null: false, default: true) + add(:is_manually_graded, :boolean, null: false, default: true) add(:early_submission_xp, :integer, null: false) add(:hours_before_early_xp_decay, :integer, null: false) timestamps() @@ -106,6 +105,9 @@ defmodule Cadet.Repo.Migrations.MultitenantUpgrade do alter table(:questions) do remove(:max_grade) + add(:show_solution, :boolean, null: false, default: false) + add(:build_hidden_testcases, :boolean, null: false, default: false) + add(:blocking, :boolean, null: false, default: false) end # Update notifications @@ -203,6 +205,19 @@ defmodule Cadet.Repo.Migrations.MultitenantUpgrade do |> Repo.update() end) + # update existing questions with new question config + from(q in "questions", join: a in "assessments", on: a.id == q.assessment_id, where: a.type == "path", select: {q.id}) + |> Repo.all() + |> Enum.each(fn question -> + Question + |> Repo.get(question.id) + |> Question.changeset(%{ + show_solution: false, + build_hidden_testcases: false, + blocking: false}) + |> Repo.update() + end) + # Create Assessment Configurations based on Source Academy Knight ["Missions", "Quests", "Paths", "Contests", "Others"] |> Enum.each(fn assessment_type -> @@ -210,9 +225,8 @@ defmodule Cadet.Repo.Migrations.MultitenantUpgrade do |> AssessmentConfig.changeset(%{ type: assessment_type, course_id: course.id, - is_graded: assessment_type in ["Missions", "Quests", "Contests", "Others"], - skippable: assessment_type != "Paths", - is_autograded: assessment_type != "Contests", + show_grading_summary: assessment_type in ["Missions", "Quests"], + is_manually_graded: assessment_type != "Paths", early_submission_xp: 200, hours_before_early_xp_decay: 48 }) diff --git a/test/cadet/updater/xml_parser_test.exs b/test/cadet/updater/xml_parser_test.exs index 6198baee1..ef5a6a824 100644 --- a/test/cadet/updater/xml_parser_test.exs +++ b/test/cadet/updater/xml_parser_test.exs @@ -17,15 +17,12 @@ defmodule Cadet.Updater.XMLParserTest do insert(:assessment_config, %{ course: course, order: 3, - skippable: false, - is_graded: false, type: "path" }), - insert(:assessment_config, %{course: course, order: 4, is_autograded: false}), + insert(:assessment_config, %{course: course, order: 4}), insert(:assessment_config, %{ course: course, order: 5, - is_graded: false, type: "practical" }) ] diff --git a/test/cadet_web/admin_controllers/admin_courses_controller_test.exs b/test/cadet_web/admin_controllers/admin_courses_controller_test.exs index 778207035..0128334ff 100644 --- a/test/cadet_web/admin_controllers/admin_courses_controller_test.exs +++ b/test/cadet_web/admin_controllers/admin_courses_controller_test.exs @@ -151,7 +151,8 @@ defmodule CadetWeb.AdminCoursesControllerTest do config2 = insert(:assessment_config, %{ - is_graded: false, + show_grading_summary: false, + is_manually_graded: false, order: 2, type: "Mission2", course: course @@ -166,27 +167,24 @@ defmodule CadetWeb.AdminCoursesControllerTest do %{ "earlySubmissionXp" => 200, "hoursBeforeEarlyXpDecay" => 48, - "isAutograded" => true, - "isGraded" => true, - "skippable" => true, + "displayInDashboard" => true, + "isManuallyGraded" => true, "type" => "Mission1", "assessmentConfigId" => config1.id }, %{ "earlySubmissionXp" => 200, "hoursBeforeEarlyXpDecay" => 48, - "isAutograded" => true, - "isGraded" => false, - "skippable" => true, + "displayInDashboard" => false, + "isManuallyGraded" => false, "type" => "Mission2", "assessmentConfigId" => config2.id }, %{ "earlySubmissionXp" => 200, "hoursBeforeEarlyXpDecay" => 48, - "isAutograded" => true, - "isGraded" => true, - "skippable" => true, + "displayInDashboard" => true, + "isManuallyGraded" => true, "type" => "Mission3", "assessmentConfigId" => config3.id } diff --git a/test/cadet_web/controllers/assessments_controller_test.exs b/test/cadet_web/controllers/assessments_controller_test.exs index 95bec94b2..c7f31a87a 100644 --- a/test/cadet_web/controllers/assessments_controller_test.exs +++ b/test/cadet_web/controllers/assessments_controller_test.exs @@ -71,6 +71,7 @@ defmodule CadetWeb.AssessmentsControllerTest do "openAt" => format_datetime(&1.open_at), "closeAt" => format_datetime(&1.close_at), "type" => &1.config.type, + "isManuallyGraded" => &1.config.is_manually_graded, "coverImage" => &1.cover_picture, "maxXp" => 4800, "status" => get_assessment_status(course_reg, &1), @@ -153,6 +154,7 @@ defmodule CadetWeb.AssessmentsControllerTest do "openAt" => format_datetime(&1.open_at), "closeAt" => format_datetime(&1.close_at), "type" => &1.config.type, + "isManuallyGraded" => &1.config.is_manually_graded, "coverImage" => &1.cover_picture, "maxXp" => 4800, "status" => get_assessment_status(student, &1), @@ -262,6 +264,7 @@ defmodule CadetWeb.AssessmentsControllerTest do "openAt" => format_datetime(&1.open_at), "closeAt" => format_datetime(&1.close_at), "type" => &1.config.type, + "isManuallyGraded" => &1.config.is_manually_graded, "coverImage" => &1.cover_picture, "maxXp" => 4800, "status" => get_assessment_status(course_reg, &1), @@ -344,7 +347,7 @@ defmodule CadetWeb.AssessmentsControllerTest do "solutionTemplate" => &1.question.template, "prepend" => &1.question.prepend, "postpend" => - if not assessment.config.is_graded do + if &1.build_hidden_testcases do &1.question.postpend else "" @@ -358,7 +361,7 @@ defmodule CadetWeb.AssessmentsControllerTest do do: {Atom.to_string(k), v} end ) ++ - if not assessment.config.is_graded do + if &1.build_hidden_testcases do Enum.map( &1.question.private, fn testcase -> diff --git a/test/cadet_web/controllers/user_controller_test.exs b/test/cadet_web/controllers/user_controller_test.exs index 93e133afa..169299a94 100644 --- a/test/cadet_web/controllers/user_controller_test.exs +++ b/test/cadet_web/controllers/user_controller_test.exs @@ -101,28 +101,25 @@ defmodule CadetWeb.UserControllerTest do }, "assessmentConfigurations" => [ %{ - "skippable" => true, "type" => "test type 1", - "isAutograded" => true, - "isGraded" => true, + "displayInDashboard" => true, + "isManuallyGraded" => true, "assessmentConfigId" => config1.id, "earlySubmissionXp" => 200, "hoursBeforeEarlyXpDecay" => 48 }, %{ - "skippable" => true, "type" => "test type 2", - "isAutograded" => true, - "isGraded" => true, + "displayInDashboard" => true, + "isManuallyGraded" => true, "assessmentConfigId" => config2.id, "earlySubmissionXp" => 200, "hoursBeforeEarlyXpDecay" => 48 }, %{ - "skippable" => true, "type" => "test type 3", - "isAutograded" => true, - "isGraded" => true, + "displayInDashboard" => true, + "isManuallyGraded" => true, "assessmentConfigId" => config3.id, "earlySubmissionXp" => 200, "hoursBeforeEarlyXpDecay" => 48 diff --git a/test/support/seeds.ex b/test/support/seeds.ex index 7e3bcaf18..6631061be 100644 --- a/test/support/seeds.ex +++ b/test/support/seeds.ex @@ -88,15 +88,14 @@ defmodule Cadet.Test.Seeds do insert(:assessment_config, %{ course: course1, order: 3, - skippable: false, - is_graded: false, + show_grading_summary: false, + is_manually_graded: false, type: "path" }), - insert(:assessment_config, %{course: course1, order: 4, is_autograded: false}), + insert(:assessment_config, %{course: course1, order: 4}), insert(:assessment_config, %{ course: course1, order: 5, - is_graded: false, type: "practical" }) ] @@ -143,7 +142,9 @@ defmodule Cadet.Test.Seeds do insert(:programming_question, %{ display_order: id, assessment: assessment, - max_xp: 1000 + max_xp: 1000, + build_hidden_testcases: assessment.config.type == "path", + show_solution: assessment.config.type == "path" }) end) @@ -152,7 +153,9 @@ defmodule Cadet.Test.Seeds do insert(:mcq_question, %{ display_order: id, assessment: assessment, - max_xp: 500 + max_xp: 500, + build_hidden_testcases: assessment.config.type == "path", + show_solution: assessment.config.type == "path" }) end) @@ -161,7 +164,9 @@ defmodule Cadet.Test.Seeds do insert(:voting_question, %{ display_order: id, assessment: assessment, - max_xp: 100 + max_xp: 100, + build_hidden_testcases: assessment.config.type == "path", + show_solution: assessment.config.type == "path" }) end) From 351ebf5c3afc319b7cf73a212e09a9d48d203c9c Mon Sep 17 00:00:00 2001 From: YaleChen299 Date: Mon, 5 Jul 2021 20:42:33 +0800 Subject: [PATCH 124/174] code formatting --- lib/cadet/courses/courses.ex | 912 ++++++------- lib/cadet/jobs/xml_parser.ex | 1 + lib/cadet_web/views/assessments_helpers.ex | 6 +- .../20210531155751_multitenant_upgrade.exs | 814 ++++++------ test/cadet/courses/courses_test.exs | 1124 ++++++++--------- 5 files changed, 1434 insertions(+), 1423 deletions(-) diff --git a/lib/cadet/courses/courses.ex b/lib/cadet/courses/courses.ex index 43bd15cca..49c4a66b4 100644 --- a/lib/cadet/courses/courses.ex +++ b/lib/cadet/courses/courses.ex @@ -1,456 +1,456 @@ -defmodule Cadet.Courses do - @moduledoc """ - Courses context contains domain logic for Course administration - management such as course configuration, discussion groups and materials - """ - use Cadet, [:context, :display] - - import Ecto.Query - alias Ecto.Multi - - alias Cadet.Accounts.{CourseRegistration, User} - - alias Cadet.Courses.{ - AssessmentConfig, - Course, - Group, - Sourcecast, - SourcecastUpload - } - - @doc """ - Creates a new course configuration, course registration, and sets - the user's latest course id to the newly created course. - """ - def create_course_config(params, user) do - Multi.new() - |> Multi.insert(:course, Course.changeset(%Course{}, params)) - |> Multi.insert(:course_reg, fn %{course: course} -> - CourseRegistration.changeset(%CourseRegistration{}, %{ - course_id: course.id, - user_id: user.id, - role: :admin - }) - end) - |> Multi.update(:latest_viewed_id, fn %{course: course} -> - User - |> where(id: ^user.id) - |> Repo.one() - |> User.changeset(%{latest_viewed_id: course.id}) - end) - |> Repo.transaction() - end - - @doc """ - Returns the course configuration for the specified course. - """ - @spec get_course_config(integer) :: - {:ok, %Course{}} | {:error, {:bad_request, String.t()}} - def get_course_config(course_id) when is_ecto_id(course_id) do - case retrieve_course(course_id) do - nil -> - {:error, {:bad_request, "Invalid course id"}} - - course -> - assessment_configs = - AssessmentConfig - |> where(course_id: ^course_id) - |> Repo.all() - |> Enum.sort(&(&1.order < &2.order)) - |> Enum.map(& &1.type) - - {:ok, Map.put_new(course, :assessment_configs, assessment_configs)} - end - end - - @doc """ - Updates the general course configuration for the specified course - """ - @spec update_course_config(integer, %{}) :: - {:ok, %Course{}} | {:error, Ecto.Changeset.t()} | {:error, {:bad_request, String.t()}} - def update_course_config(course_id, params) when is_ecto_id(course_id) do - case retrieve_course(course_id) do - nil -> - {:error, {:bad_request, "Invalid course id"}} - - course -> - course - |> Course.changeset(params) - |> Repo.update() - end - end - - defp retrieve_course(course_id) when is_ecto_id(course_id) do - Course - |> where(id: ^course_id) - |> Repo.one() - end - - def get_assessment_configs(course_id) when is_ecto_id(course_id) do - AssessmentConfig - |> where([at], at.course_id == ^course_id) - |> order_by(:order) - |> Repo.all() - end - - def mass_upsert_and_reorder_assessment_configs(course_id, configs) do - if is_list(configs) do - configs_length = configs |> length() - - with true <- configs_length <= 8, - true <- configs_length >= 1 do - new_configs = - configs - |> Enum.map(fn elem -> - {:ok, config} = insert_or_update_assessment_config(course_id, elem) - Map.put(elem, :assessment_config_id, config.id) - end) - - reorder_assessment_configs(course_id, new_configs) - else - false -> {:error, {:bad_request, "Invalid parameter(s)"}} - end - else - {:error, {:bad_request, "Invalid parameter(s)"}} - end - end - - def insert_or_update_assessment_config( - course_id, - params = %{assessment_config_id: assessment_config_id} - ) do - AssessmentConfig - |> where(course_id: ^course_id) - |> where(id: ^assessment_config_id) - |> Repo.one() - |> case do - nil -> - AssessmentConfig.changeset(%AssessmentConfig{}, Map.put(params, :course_id, course_id)) - - at -> - AssessmentConfig.changeset(at, params) - end - |> Repo.insert_or_update() - end - - defp update_assessment_config( - course_id, - params = %{assessment_config_id: assessment_config_id} - ) do - AssessmentConfig - |> where(course_id: ^course_id) - |> where(id: ^assessment_config_id) - |> Repo.one() - |> case do - nil -> {:error, :no_such_entry} - at -> at |> AssessmentConfig.changeset(params) |> Repo.update() - end - end - - def reorder_assessment_configs(course_id, configs) do - Repo.transaction(fn -> - configs - |> Enum.each(fn elem -> - update_assessment_config(course_id, Map.put(elem, :order, nil)) - end) - - configs - |> Enum.with_index(1) - |> Enum.each(fn {elem, idx} -> - update_assessment_config(course_id, Map.put(elem, :order, idx)) - end) - end) - end - - @spec delete_assessment_config(integer(), map()) :: - {:ok, Ecto.Schema.t()} | {:error, Ecto.Changeset.t()} | {:error, :no_such_enrty} - def delete_assessment_config(course_id, params = %{assessment_config_id: assessment_config_id}) do - AssessmentConfig - |> where(course_id: ^course_id) - |> where(id: ^assessment_config_id) - |> Repo.one() - |> case do - nil -> {:error, :no_such_enrty} - at -> at |> AssessmentConfig.changeset(params) |> Repo.delete() - end - end - - def upsert_groups_in_course(usernames_and_groups, course_id) do - usernames_and_groups - |> Enum.reduce_while(nil, fn %{username: username} = entry, _acc -> - case Map.fetch(entry, :group) do - {:ok, groupname} -> - # Add users to group - upsert_groups_in_course_helper(username, course_id, groupname) - - :error -> - # Delete users from group - upsert_groups_in_course_helper(username, course_id) - end - |> case do - {:ok, _} -> {:cont, :ok} - {:error, changeset} -> {:halt, {:error, {:bad_request, full_error_messages(changeset)}}} - end - end) - end - - defp upsert_groups_in_course_helper(username, course_id, groupname) do - with {:get_group, {:ok, group}} <- {:get_group, get_or_create_group(groupname, course_id)}, - {:get_course_reg, %{role: role} = course_reg} <- - {:get_course_reg, - CourseRegistration - |> where( - user_id: ^(User |> where(username: ^username) |> Repo.one() |> Map.fetch!(:id)) - ) - |> where(course_id: ^course_id) - |> Repo.one()} do - # It is ok to assume that user course registions already exist, as they would have been created - # in the admin_user_controller before calling this function - case role do - # If student, update his course registration - :student -> - course_reg - |> CourseRegistration.changeset(%{group_id: group.id}) - |> Repo.update() - - # If admin or staff, remove their previous group assignment and set them as group leader - _ -> - remove_staff_from_group(course_id, course_reg.id) - - group - |> Group.changeset(%{leader_id: course_reg.id}) - |> Repo.update() - end - end - end - - defp upsert_groups_in_course_helper(username, course_id) do - with {:get_course_reg, %{role: role} = course_reg} <- - {:get_course_reg, - CourseRegistration - |> where( - user_id: ^(User |> where(username: ^username) |> Repo.one() |> Map.fetch!(:id)) - ) - |> where(course_id: ^course_id) - |> Repo.one()} do - case role do - :student -> - course_reg - |> CourseRegistration.changeset(%{group_id: nil}) - |> Repo.update() - - _ -> - remove_staff_from_group(course_id, course_reg.id) - {:ok, nil} - end - end - end - - defp remove_staff_from_group(course_id, leader_id) do - Group - |> where(course_id: ^course_id) - |> where(leader_id: ^leader_id) - |> Repo.one() - |> case do - nil -> - nil - - group -> - group - |> Group.changeset(%{leader_id: nil}) - |> Repo.update() - end - end - - @doc """ - Get a group based on the group name and course id or create one if it doesn't exist - """ - @spec get_or_create_group(String.t(), integer()) :: - {:ok, %Group{}} | {:error, Ecto.Changeset.t()} - def get_or_create_group(name, course_id) when is_binary(name) and is_ecto_id(course_id) do - Group - |> where(name: ^name) - |> where(course_id: ^course_id) - |> Repo.one() - |> case do - nil -> - %Group{} - |> Group.changeset(%{name: name, course_id: course_id}) - |> Repo.insert() - - group -> - {:ok, group} - end - end - - # @doc """ - # Updates a group based on the group name or create one if it doesn't exist - # """ - # @spec insert_or_update_group(map()) :: {:ok, %Group{}} | {:error, Ecto.Changeset.t()} - # def insert_or_update_group(params = %{name: name}) when is_binary(name) do - # Group - # |> where(name: ^name) - # |> Repo.one() - # |> case do - # nil -> - # Group.changeset(%Group{}, params) - - # group -> - # Group.changeset(group, params) - # end - # |> Repo.insert_or_update() - # end - - # @doc """ - # Reassign a student to a discussion group - # This will un-assign student from the current discussion group - # """ - # def assign_group(leader = %User{}, student = %User{}) do - # cond do - # leader.role == :student -> - # {:error, :invalid} - - # student.role != :student -> - # {:error, :invalid} - - # true -> - # Repo.transaction(fn -> - # {:ok, _} = unassign_group(student) - - # %Group{} - # |> Group.changeset(%{}) - # |> put_assoc(:leader, leader) - # |> put_assoc(:student, student) - # |> Repo.insert!() - # end) - # end - # end - - # @doc """ - # Remove existing student from discussion group, no-op if a student - # is unassigned - # """ - # def unassign_group(student = %User{}) do - # existing_group = Repo.get_by(Group, student_id: student.id) - - # if existing_group == nil do - # {:ok, nil} - # else - # Repo.delete(existing_group) - # end - # end - - # @doc """ - # Get list of students under staff discussion group - # """ - # def list_students_by_leader(staff = %CourseRegistration{}) do - # import Cadet.Course.Query, only: [group_members: 1] - - # staff - # |> group_members() - # |> Repo.all() - # |> Repo.preload([:student]) - # end - - @upload_file_roles ~w(admin staff)a - - @doc """ - Upload a sourcecast file. - - Note that there are no checks for whether the user belongs to the course, as this has been checked - inside a plug in the router. - """ - def upload_sourcecast_file( - _inserter = %CourseRegistration{user_id: user_id, course_id: course_id, role: role}, - attrs = %{} - ) do - if role in @upload_file_roles do - course_reg = - CourseRegistration - |> where(user_id: ^user_id) - |> where(course_id: ^course_id) - |> preload(:course) - |> preload(:user) - |> Repo.one() - - changeset = - %Sourcecast{} - |> Sourcecast.changeset(attrs) - |> put_assoc(:uploader, course_reg.user) - |> put_assoc(:course, course_reg.course) - - case Repo.insert(changeset) do - {:ok, sourcecast} -> - {:ok, sourcecast} - - {:error, changeset} -> - {:error, {:bad_request, full_error_messages(changeset)}} - end - else - {:error, {:forbidden, "User is not permitted to upload"}} - end - end - - @doc """ - Upload a public sourcecast file. - - Note that there are no checks for whether the user belongs to the course, as this has been checked - inside a plug in the router. - """ - def upload_sourcecast_file_public( - inserter, - _inserter_course_reg = %CourseRegistration{role: role}, - attrs = %{} - ) do - if role in @upload_file_roles do - changeset = - %Sourcecast{} - |> Sourcecast.changeset(attrs) - |> put_assoc(:uploader, inserter) - - case Repo.insert(changeset) do - {:ok, sourcecast} -> - {:ok, sourcecast} - - {:error, changeset} -> - {:error, {:bad_request, full_error_messages(changeset)}} - end - else - {:error, {:forbidden, "User is not permitted to upload"}} - end - end - - @doc """ - Delete a sourcecast file - - Note that there are no checks for whether the user belongs to the course, as this has been checked - inside a plug in the router. - """ - def delete_sourcecast_file(_deleter = %CourseRegistration{role: role}, sourcecast_id) do - if role in @upload_file_roles do - sourcecast = Repo.get(Sourcecast, sourcecast_id) - SourcecastUpload.delete({sourcecast.audio, sourcecast}) - Repo.delete(sourcecast) - else - {:error, {:forbidden, "User is not permitted to delete"}} - end - end - - @doc """ - Get sourcecast files - """ - def get_sourcecast_files(course_id) when is_ecto_id(course_id) do - Sourcecast - |> where(course_id: ^course_id) - |> Repo.all() - |> Repo.preload(:uploader) - end - - def get_sourcecast_files do - Sourcecast - # Public sourcecasts are those without course_id - |> where([s], is_nil(s.course_id)) - |> Repo.all() - |> Repo.preload(:uploader) - end -end +defmodule Cadet.Courses do + @moduledoc """ + Courses context contains domain logic for Course administration + management such as course configuration, discussion groups and materials + """ + use Cadet, [:context, :display] + + import Ecto.Query + alias Ecto.Multi + + alias Cadet.Accounts.{CourseRegistration, User} + + alias Cadet.Courses.{ + AssessmentConfig, + Course, + Group, + Sourcecast, + SourcecastUpload + } + + @doc """ + Creates a new course configuration, course registration, and sets + the user's latest course id to the newly created course. + """ + def create_course_config(params, user) do + Multi.new() + |> Multi.insert(:course, Course.changeset(%Course{}, params)) + |> Multi.insert(:course_reg, fn %{course: course} -> + CourseRegistration.changeset(%CourseRegistration{}, %{ + course_id: course.id, + user_id: user.id, + role: :admin + }) + end) + |> Multi.update(:latest_viewed_id, fn %{course: course} -> + User + |> where(id: ^user.id) + |> Repo.one() + |> User.changeset(%{latest_viewed_id: course.id}) + end) + |> Repo.transaction() + end + + @doc """ + Returns the course configuration for the specified course. + """ + @spec get_course_config(integer) :: + {:ok, %Course{}} | {:error, {:bad_request, String.t()}} + def get_course_config(course_id) when is_ecto_id(course_id) do + case retrieve_course(course_id) do + nil -> + {:error, {:bad_request, "Invalid course id"}} + + course -> + assessment_configs = + AssessmentConfig + |> where(course_id: ^course_id) + |> Repo.all() + |> Enum.sort(&(&1.order < &2.order)) + |> Enum.map(& &1.type) + + {:ok, Map.put_new(course, :assessment_configs, assessment_configs)} + end + end + + @doc """ + Updates the general course configuration for the specified course + """ + @spec update_course_config(integer, %{}) :: + {:ok, %Course{}} | {:error, Ecto.Changeset.t()} | {:error, {:bad_request, String.t()}} + def update_course_config(course_id, params) when is_ecto_id(course_id) do + case retrieve_course(course_id) do + nil -> + {:error, {:bad_request, "Invalid course id"}} + + course -> + course + |> Course.changeset(params) + |> Repo.update() + end + end + + defp retrieve_course(course_id) when is_ecto_id(course_id) do + Course + |> where(id: ^course_id) + |> Repo.one() + end + + def get_assessment_configs(course_id) when is_ecto_id(course_id) do + AssessmentConfig + |> where([at], at.course_id == ^course_id) + |> order_by(:order) + |> Repo.all() + end + + def mass_upsert_and_reorder_assessment_configs(course_id, configs) do + if is_list(configs) do + configs_length = configs |> length() + + with true <- configs_length <= 8, + true <- configs_length >= 1 do + new_configs = + configs + |> Enum.map(fn elem -> + {:ok, config} = insert_or_update_assessment_config(course_id, elem) + Map.put(elem, :assessment_config_id, config.id) + end) + + reorder_assessment_configs(course_id, new_configs) + else + false -> {:error, {:bad_request, "Invalid parameter(s)"}} + end + else + {:error, {:bad_request, "Invalid parameter(s)"}} + end + end + + def insert_or_update_assessment_config( + course_id, + params = %{assessment_config_id: assessment_config_id} + ) do + AssessmentConfig + |> where(course_id: ^course_id) + |> where(id: ^assessment_config_id) + |> Repo.one() + |> case do + nil -> + AssessmentConfig.changeset(%AssessmentConfig{}, Map.put(params, :course_id, course_id)) + + at -> + AssessmentConfig.changeset(at, params) + end + |> Repo.insert_or_update() + end + + defp update_assessment_config( + course_id, + params = %{assessment_config_id: assessment_config_id} + ) do + AssessmentConfig + |> where(course_id: ^course_id) + |> where(id: ^assessment_config_id) + |> Repo.one() + |> case do + nil -> {:error, :no_such_entry} + at -> at |> AssessmentConfig.changeset(params) |> Repo.update() + end + end + + def reorder_assessment_configs(course_id, configs) do + Repo.transaction(fn -> + configs + |> Enum.each(fn elem -> + update_assessment_config(course_id, Map.put(elem, :order, nil)) + end) + + configs + |> Enum.with_index(1) + |> Enum.each(fn {elem, idx} -> + update_assessment_config(course_id, Map.put(elem, :order, idx)) + end) + end) + end + + @spec delete_assessment_config(integer(), map()) :: + {:ok, Ecto.Schema.t()} | {:error, Ecto.Changeset.t()} | {:error, :no_such_enrty} + def delete_assessment_config(course_id, params = %{assessment_config_id: assessment_config_id}) do + AssessmentConfig + |> where(course_id: ^course_id) + |> where(id: ^assessment_config_id) + |> Repo.one() + |> case do + nil -> {:error, :no_such_enrty} + at -> at |> AssessmentConfig.changeset(params) |> Repo.delete() + end + end + + def upsert_groups_in_course(usernames_and_groups, course_id) do + usernames_and_groups + |> Enum.reduce_while(nil, fn %{username: username} = entry, _acc -> + case Map.fetch(entry, :group) do + {:ok, groupname} -> + # Add users to group + upsert_groups_in_course_helper(username, course_id, groupname) + + :error -> + # Delete users from group + upsert_groups_in_course_helper(username, course_id) + end + |> case do + {:ok, _} -> {:cont, :ok} + {:error, changeset} -> {:halt, {:error, {:bad_request, full_error_messages(changeset)}}} + end + end) + end + + defp upsert_groups_in_course_helper(username, course_id, groupname) do + with {:get_group, {:ok, group}} <- {:get_group, get_or_create_group(groupname, course_id)}, + {:get_course_reg, %{role: role} = course_reg} <- + {:get_course_reg, + CourseRegistration + |> where( + user_id: ^(User |> where(username: ^username) |> Repo.one() |> Map.fetch!(:id)) + ) + |> where(course_id: ^course_id) + |> Repo.one()} do + # It is ok to assume that user course registions already exist, as they would have been created + # in the admin_user_controller before calling this function + case role do + # If student, update his course registration + :student -> + course_reg + |> CourseRegistration.changeset(%{group_id: group.id}) + |> Repo.update() + + # If admin or staff, remove their previous group assignment and set them as group leader + _ -> + remove_staff_from_group(course_id, course_reg.id) + + group + |> Group.changeset(%{leader_id: course_reg.id}) + |> Repo.update() + end + end + end + + defp upsert_groups_in_course_helper(username, course_id) do + with {:get_course_reg, %{role: role} = course_reg} <- + {:get_course_reg, + CourseRegistration + |> where( + user_id: ^(User |> where(username: ^username) |> Repo.one() |> Map.fetch!(:id)) + ) + |> where(course_id: ^course_id) + |> Repo.one()} do + case role do + :student -> + course_reg + |> CourseRegistration.changeset(%{group_id: nil}) + |> Repo.update() + + _ -> + remove_staff_from_group(course_id, course_reg.id) + {:ok, nil} + end + end + end + + defp remove_staff_from_group(course_id, leader_id) do + Group + |> where(course_id: ^course_id) + |> where(leader_id: ^leader_id) + |> Repo.one() + |> case do + nil -> + nil + + group -> + group + |> Group.changeset(%{leader_id: nil}) + |> Repo.update() + end + end + + @doc """ + Get a group based on the group name and course id or create one if it doesn't exist + """ + @spec get_or_create_group(String.t(), integer()) :: + {:ok, %Group{}} | {:error, Ecto.Changeset.t()} + def get_or_create_group(name, course_id) when is_binary(name) and is_ecto_id(course_id) do + Group + |> where(name: ^name) + |> where(course_id: ^course_id) + |> Repo.one() + |> case do + nil -> + %Group{} + |> Group.changeset(%{name: name, course_id: course_id}) + |> Repo.insert() + + group -> + {:ok, group} + end + end + + # @doc """ + # Updates a group based on the group name or create one if it doesn't exist + # """ + # @spec insert_or_update_group(map()) :: {:ok, %Group{}} | {:error, Ecto.Changeset.t()} + # def insert_or_update_group(params = %{name: name}) when is_binary(name) do + # Group + # |> where(name: ^name) + # |> Repo.one() + # |> case do + # nil -> + # Group.changeset(%Group{}, params) + + # group -> + # Group.changeset(group, params) + # end + # |> Repo.insert_or_update() + # end + + # @doc """ + # Reassign a student to a discussion group + # This will un-assign student from the current discussion group + # """ + # def assign_group(leader = %User{}, student = %User{}) do + # cond do + # leader.role == :student -> + # {:error, :invalid} + + # student.role != :student -> + # {:error, :invalid} + + # true -> + # Repo.transaction(fn -> + # {:ok, _} = unassign_group(student) + + # %Group{} + # |> Group.changeset(%{}) + # |> put_assoc(:leader, leader) + # |> put_assoc(:student, student) + # |> Repo.insert!() + # end) + # end + # end + + # @doc """ + # Remove existing student from discussion group, no-op if a student + # is unassigned + # """ + # def unassign_group(student = %User{}) do + # existing_group = Repo.get_by(Group, student_id: student.id) + + # if existing_group == nil do + # {:ok, nil} + # else + # Repo.delete(existing_group) + # end + # end + + # @doc """ + # Get list of students under staff discussion group + # """ + # def list_students_by_leader(staff = %CourseRegistration{}) do + # import Cadet.Course.Query, only: [group_members: 1] + + # staff + # |> group_members() + # |> Repo.all() + # |> Repo.preload([:student]) + # end + + @upload_file_roles ~w(admin staff)a + + @doc """ + Upload a sourcecast file. + + Note that there are no checks for whether the user belongs to the course, as this has been checked + inside a plug in the router. + """ + def upload_sourcecast_file( + _inserter = %CourseRegistration{user_id: user_id, course_id: course_id, role: role}, + attrs = %{} + ) do + if role in @upload_file_roles do + course_reg = + CourseRegistration + |> where(user_id: ^user_id) + |> where(course_id: ^course_id) + |> preload(:course) + |> preload(:user) + |> Repo.one() + + changeset = + %Sourcecast{} + |> Sourcecast.changeset(attrs) + |> put_assoc(:uploader, course_reg.user) + |> put_assoc(:course, course_reg.course) + + case Repo.insert(changeset) do + {:ok, sourcecast} -> + {:ok, sourcecast} + + {:error, changeset} -> + {:error, {:bad_request, full_error_messages(changeset)}} + end + else + {:error, {:forbidden, "User is not permitted to upload"}} + end + end + + @doc """ + Upload a public sourcecast file. + + Note that there are no checks for whether the user belongs to the course, as this has been checked + inside a plug in the router. + """ + def upload_sourcecast_file_public( + inserter, + _inserter_course_reg = %CourseRegistration{role: role}, + attrs = %{} + ) do + if role in @upload_file_roles do + changeset = + %Sourcecast{} + |> Sourcecast.changeset(attrs) + |> put_assoc(:uploader, inserter) + + case Repo.insert(changeset) do + {:ok, sourcecast} -> + {:ok, sourcecast} + + {:error, changeset} -> + {:error, {:bad_request, full_error_messages(changeset)}} + end + else + {:error, {:forbidden, "User is not permitted to upload"}} + end + end + + @doc """ + Delete a sourcecast file + + Note that there are no checks for whether the user belongs to the course, as this has been checked + inside a plug in the router. + """ + def delete_sourcecast_file(_deleter = %CourseRegistration{role: role}, sourcecast_id) do + if role in @upload_file_roles do + sourcecast = Repo.get(Sourcecast, sourcecast_id) + SourcecastUpload.delete({sourcecast.audio, sourcecast}) + Repo.delete(sourcecast) + else + {:error, {:forbidden, "User is not permitted to delete"}} + end + end + + @doc """ + Get sourcecast files + """ + def get_sourcecast_files(course_id) when is_ecto_id(course_id) do + Sourcecast + |> where(course_id: ^course_id) + |> Repo.all() + |> Repo.preload(:uploader) + end + + def get_sourcecast_files do + Sourcecast + # Public sourcecasts are those without course_id + |> where([s], is_nil(s.course_id)) + |> Repo.all() + |> Repo.preload(:uploader) + end +end diff --git a/lib/cadet/jobs/xml_parser.ex b/lib/cadet/jobs/xml_parser.ex index 4ea0b956c..07b471479 100644 --- a/lib/cadet/jobs/xml_parser.ex +++ b/lib/cadet/jobs/xml_parser.ex @@ -186,6 +186,7 @@ defmodule Cadet.Updater.XMLParser do @spec process_question_booleans(map()) :: map() defp process_question_booleans(question) do flags = [:show_solution, :build_hidden_testcases, :blocking] + flags |> Enum.reduce(question, fn flag, acc -> put_in(acc[flag], acc[flag] == "true") diff --git a/lib/cadet_web/views/assessments_helpers.ex b/lib/cadet_web/views/assessments_helpers.ex index 5d04666aa..1979ca572 100644 --- a/lib/cadet_web/views/assessments_helpers.ex +++ b/lib/cadet_web/views/assessments_helpers.ex @@ -193,7 +193,11 @@ defmodule CadetWeb.AssessmentsHelpers do defp build_question_content_by_config( %{ - question: %{question: question, type: question_type, build_hidden_testcases: build_hidden_testcases} + question: %{ + question: question, + type: question_type, + build_hidden_testcases: build_hidden_testcases + } }, all_testcases? ) do diff --git a/priv/repo/migrations/20210531155751_multitenant_upgrade.exs b/priv/repo/migrations/20210531155751_multitenant_upgrade.exs index e55b826c0..933488f0a 100644 --- a/priv/repo/migrations/20210531155751_multitenant_upgrade.exs +++ b/priv/repo/migrations/20210531155751_multitenant_upgrade.exs @@ -1,404 +1,410 @@ -defmodule Cadet.Repo.Migrations.MultitenantUpgrade do - use Ecto.Migration - import Ecto.Query, only: [from: 2, where: 2] - - alias Cadet.Accounts.{CourseRegistration, Notification, Role, User} - alias Cadet.Assessments.{Assessment, Submission, SubmissionVotes} - alias Cadet.Courses.{AssessmentConfig, Course, Group, Sourcecast} - alias Cadet.Repo - alias Cadet.Stories.Story - - def change do - # Tracks course configurations - create table(:courses) do - add(:course_name, :string, null: false) - add(:course_short_name, :string) - add(:viewable, :boolean, null: false, default: true) - add(:enable_game, :boolean, null: false, default: true) - add(:enable_achievements, :boolean, null: false, default: true) - add(:enable_sourcecast, :boolean, null: false, default: true) - add(:source_chapter, :integer, null: false) - add(:source_variant, :string, null: false) - add(:module_help_text, :string) - timestamps() - end - - # Tracks assessment configurations per assessment type in a course - create table(:assessment_configs) do - add(:order, :integer, null: true) - add(:type, :string, null: false) - add(:course_id, references(:courses), null: false) - add(:show_grading_summary, :boolean, null: false, default: true) - add(:is_manually_graded, :boolean, null: false, default: true) - add(:early_submission_xp, :integer, null: false) - add(:hours_before_early_xp_decay, :integer, null: false) - timestamps() - end - - # Tracks course registrations (many-to-many r/s between users and courses) - create table(:course_registrations) do - add(:role, :role, null: false) - add(:game_states, :map, default: %{}) - add(:group_id, references(:groups)) - add(:user_id, references(:users), null: false) - add(:course_id, references(:courses), null: false) - timestamps() - end - - # Enforce that users cannot be enrolled twice in a course - create( - unique_index(:course_registrations, [:user_id, :course_id], - name: :course_registrations_user_id_course_id_index - ) - ) - - # latest_viewed_id to track which course to load after the user logs in. - # name and username modifications to allow for names to be nullable as accounts can - # now be precreated by any course instructor by specifying the username used in the - # respective auth provider. - alter table(:users) do - add(:latest_viewed_id, references(:courses), null: true) - modify(:name, :string, null: true) - modify(:username, :string, null: false) - end - - # Prep for migration of leader_id from User entity to CourseRegistration entity. - # Also make groups associated with a course. - rename(table(:groups), :leader_id, to: :temp_leader_id) - drop(constraint(:groups, "groups_leader_id_fkey")) - drop(constraint(:groups, "groups_mentor_id_fkey")) - - alter table(:groups) do - remove(:mentor_id) - add(:leader_id, references(:course_registrations), null: true) - add(:course_id, references(:courses)) - end - - # Make assessments related to an assessment config and a course - alter table(:assessments) do - add(:config_id, references(:assessment_configs)) - add(:course_id, references(:courses)) - end - - # Prep for migration of student_id and unsubmitted_by_id from User entity to CourseRegistration entity. - rename(table(:submissions), :student_id, to: :temp_student_id) - rename(table(:submissions), :unsubmitted_by_id, to: :temp_unsubmitted_by_id) - drop(constraint(:submissions, "submissions_student_id_fkey")) - drop(constraint(:submissions, "submissions_unsubmitted_by_id_fkey")) - - alter table(:submissions) do - add(:student_id, references(:course_registrations)) - add(:unsubmitted_by_id, references(:course_registrations)) - end - - alter table(:submission_votes) do - add(:voter_id, references(:course_registrations)) - end - - # Remove grade metric from backend - alter table(:answers) do - remove(:grade) - remove(:adjustment) - remove(:grader_id) - add(:grader_id, references(:course_registrations), null: true) - end - - alter table(:questions) do - remove(:max_grade) - add(:show_solution, :boolean, null: false, default: false) - add(:build_hidden_testcases, :boolean, null: false, default: false) - add(:blocking, :boolean, null: false, default: false) - end - - # Update notifications - alter table(:notifications) do - add(:course_reg_id, references(:course_registrations)) - end - - # Sourcecasts to be associated with a course - alter table(:sourcecasts) do - add(:course_id, references(:courses)) - end - - # Stories to be associated with a course - alter table(:stories) do - add(:course_id, references(:courses)) - end - - # Sublanguage is now being tracked under course configuration, and can be different depending on course - drop_if_exists(table(:sublanguages)) - - # Manual data entry and manipulation to migrate data from Source Academy Knight --> Rook. - # Note that in Knight, there was only 1 course running at a time, so it is okay to assume - # that all existing data belongs to that course. - execute( - fn -> - # Create the new course for migration - {:ok, course} = - %Course{} - |> Course.changeset(%{ - course_name: "CS1101S Programming Methodology (AY21/22 Sem 1)", - course_short_name: "CS1101S", - viewable: true, - enable_game: true, - enable_achievments: true, - enable_sourcecast: true, - source_chapter: 1, - source_variant: "default" - }) - |> Repo.insert() - - # Namespace existing usernames - from(u in "users", update: [set: [username: fragment("? || ? ", "luminus/", u.username)]]) - |> Repo.update_all([]) - - # Create course registrations for existing users - from(u in "users", select: {u.id, u.role, u.group_id, u.game_states}) - |> Repo.all() - |> Enum.each(fn user -> - %CourseRegistration{} - |> CourseRegistration.changeset(%{ - user_id: elem(user, 0), - role: elem(user, 1), - group_id: elem(user, 2), - game_states: elem(user, 3), - course_id: course.id - }) - |> Repo.insert() - end) - - # Add latest_viewed_id to existing users - User - |> Repo.all() - |> Enum.each(fn user -> - user - |> User.changeset(%{latest_viewed_id: course.id}) - |> Repo.update() - end) - - # Handle groups (adding course_id, and updating leader_id to course registrations) - from(g in "groups", select: {g.id, g.temp_leader_id}) - |> Repo.all() - |> Enum.each(fn group -> - leader_id = - case elem(group, 1) do - # leader_id is now going to be non-nullable. if it was previously nil, we will just - # assign a staff to be the leader_id during migration - nil -> - CourseRegistration - |> where(role: :staff) - |> Repo.one() - - Map.fetch!(:id) - - id -> - CourseRegistration - |> where(user_id: ^id) - |> Repo.one() - |> Map.fetch!(:id) - end - - Group - |> where(id: ^elem(group, 0)) - |> Repo.one() - |> Group.changeset(%{leader_id: leader_id, course_id: course.id}) - |> Repo.update() - end) - - # update existing questions with new question config - from(q in "questions", join: a in "assessments", on: a.id == q.assessment_id, where: a.type == "path", select: {q.id}) - |> Repo.all() - |> Enum.each(fn question -> - Question - |> Repo.get(question.id) - |> Question.changeset(%{ - show_solution: false, - build_hidden_testcases: false, - blocking: false}) - |> Repo.update() - end) - - # Create Assessment Configurations based on Source Academy Knight - ["Missions", "Quests", "Paths", "Contests", "Others"] - |> Enum.each(fn assessment_type -> - %AssessmentConfig{} - |> AssessmentConfig.changeset(%{ - type: assessment_type, - course_id: course.id, - show_grading_summary: assessment_type in ["Missions", "Quests"], - is_manually_graded: assessment_type != "Paths", - early_submission_xp: 200, - hours_before_early_xp_decay: 48 - }) - |> Repo.insert() - end) - - # Link existing assessments to an assessment config and course - from(a in "assessments", select: {a.id, a.type}) - |> Repo.all() - |> Enum.each(fn assessment -> - assessment_type = - case elem(assessment, 1) do - "mission" -> "Missions" - "sidequest" -> "Quests" - "path" -> "Paths" - "contest" -> "Contests" - "practical" -> "Others" - end - - assessment_config = - AssessmentConfig - |> where(type: ^assessment_type) - |> Repo.one() - - Assessment - |> where(id: ^elem(assessment, 0)) - |> Repo.one() - |> Assessment.changeset(%{config_id: assessment_config.id, course_id: course.id}) - |> Repo.update() - end) - - # Updating student_id and unsubmitted_by_id from User to CourseRegistration - from(s in "submissions", select: {s.id, s.temp_student_id, s.temp_unsubmitted_by_id}) - |> Repo.all() - |> Enum.each(fn submission -> - student_id = - CourseRegistration - |> where(user_id: ^elem(submission, 1)) - |> Repo.one() - |> Map.fetch!(:id) - - unsubmitted_by_id = - case elem(submission, 2) do - nil -> - nil - - id -> - CourseRegistration - |> where(user_id: ^id) - |> Repo.one() - |> Map.fetch!(:id) - end - - Submission - |> where(id: ^elem(submission, 0)) - |> Repo.one() - |> Submission.changeset(%{student_id: student_id, unsubmitted_by_id: unsubmitted_by_id}) - |> Repo.update() - end) - - from(s in "submission_votes", select: {s.id, s.user_id}) - |> Repo.all() - |> Enum.each(fn vote -> - voter_id = - CourseRegistration - |> where(user_id: ^elem(vote, 1)) - |> Repo.one() - |> Map.fetch!(:id) - - SubmissionVotes - |> where(id: ^elem(vote, 0)) - |> Repo.one() - |> SubmissionVotes.changeset(%{voter_id: voter_id}) - |> Repo.update() - end) - - from(n in "notifications", select: {n.id, n.user_id}) - |> Repo.all() - |> Enum.each(fn notification -> - course_reg_id = - CourseRegistration - |> where(user_id: ^elem(notification, 1)) - |> Repo.one() - |> Map.fetch!(:id) - - Notification - |> where(id: ^elem(notification, 0)) - |> Repo.one() - |> Notification.changeset(%{course_reg_id: course_reg_id}) - |> Repo.update() - end) - - # Add course id to all Sourcecasts - Sourcecast - |> Repo.all() - |> Enum.each(fn x -> - x - |> Sourcecast.changeset(%{course_id: course.id}) - |> Repo.update() - end) - - # Add course id to all Stories - Story - |> Repo.all() - |> Enum.each(fn x -> - x - |> Story.changeset(%{course_id: course.id}) - |> Repo.update() - end) - end, - fn -> nil end - ) - - # Cleanup users table after data migration - alter table(:users) do - remove(:role) - remove(:group_id) - remove(:game_states) - end - - # Cleanup groups table, and make course_id and leader_id non-nullable - alter table(:groups) do - remove(:temp_leader_id) - - modify(:course_id, references(:courses), null: false, from: references(:courses)) - end - - create(unique_index(:groups, [:name, :course_id])) - - # Cleanup assessments table, and make config_id and course_id non-nullable - alter table(:assessments) do - remove(:type) - modify(:config_id, references(:assessment_configs), null: false, from: references(:courses)) - modify(:course_id, references(:courses), null: false, from: references(:courses)) - end - - alter table(:submissions) do - remove(:temp_student_id) - remove(:temp_unsubmitted_by_id) - - modify(:student_id, references(:course_registrations), - null: false, - from: references(:course_registrations) - ) - end - - create(index(:submissions, :student_id)) - create(unique_index(:submissions, [:assessment_id, :student_id])) - - alter table(:submission_votes) do - remove(:user_id) - - modify(:voter_id, references(:course_registrations), - null: false, - from: references(:course_registrations) - ) - end - - create(unique_index(:submission_votes, [:voter_id, :question_id, :rank], name: :unique_score)) - - alter table(:notifications) do - remove(:user_id) - - modify(:course_reg_id, references(:course_registrations), - null: false, - from: references(:course_registrations) - ) - end - - # Set course_id to be non-nullable - alter table(:stories) do - modify(:course_id, references(:courses), null: false, from: references(:courses)) - end - end -end +defmodule Cadet.Repo.Migrations.MultitenantUpgrade do + use Ecto.Migration + import Ecto.Query, only: [from: 2, where: 2] + + alias Cadet.Accounts.{CourseRegistration, Notification, Role, User} + alias Cadet.Assessments.{Assessment, Submission, SubmissionVotes} + alias Cadet.Courses.{AssessmentConfig, Course, Group, Sourcecast} + alias Cadet.Repo + alias Cadet.Stories.Story + + def change do + # Tracks course configurations + create table(:courses) do + add(:course_name, :string, null: false) + add(:course_short_name, :string) + add(:viewable, :boolean, null: false, default: true) + add(:enable_game, :boolean, null: false, default: true) + add(:enable_achievements, :boolean, null: false, default: true) + add(:enable_sourcecast, :boolean, null: false, default: true) + add(:source_chapter, :integer, null: false) + add(:source_variant, :string, null: false) + add(:module_help_text, :string) + timestamps() + end + + # Tracks assessment configurations per assessment type in a course + create table(:assessment_configs) do + add(:order, :integer, null: true) + add(:type, :string, null: false) + add(:course_id, references(:courses), null: false) + add(:show_grading_summary, :boolean, null: false, default: true) + add(:is_manually_graded, :boolean, null: false, default: true) + add(:early_submission_xp, :integer, null: false) + add(:hours_before_early_xp_decay, :integer, null: false) + timestamps() + end + + # Tracks course registrations (many-to-many r/s between users and courses) + create table(:course_registrations) do + add(:role, :role, null: false) + add(:game_states, :map, default: %{}) + add(:group_id, references(:groups)) + add(:user_id, references(:users), null: false) + add(:course_id, references(:courses), null: false) + timestamps() + end + + # Enforce that users cannot be enrolled twice in a course + create( + unique_index(:course_registrations, [:user_id, :course_id], + name: :course_registrations_user_id_course_id_index + ) + ) + + # latest_viewed_id to track which course to load after the user logs in. + # name and username modifications to allow for names to be nullable as accounts can + # now be precreated by any course instructor by specifying the username used in the + # respective auth provider. + alter table(:users) do + add(:latest_viewed_id, references(:courses), null: true) + modify(:name, :string, null: true) + modify(:username, :string, null: false) + end + + # Prep for migration of leader_id from User entity to CourseRegistration entity. + # Also make groups associated with a course. + rename(table(:groups), :leader_id, to: :temp_leader_id) + drop(constraint(:groups, "groups_leader_id_fkey")) + drop(constraint(:groups, "groups_mentor_id_fkey")) + + alter table(:groups) do + remove(:mentor_id) + add(:leader_id, references(:course_registrations), null: true) + add(:course_id, references(:courses)) + end + + # Make assessments related to an assessment config and a course + alter table(:assessments) do + add(:config_id, references(:assessment_configs)) + add(:course_id, references(:courses)) + end + + # Prep for migration of student_id and unsubmitted_by_id from User entity to CourseRegistration entity. + rename(table(:submissions), :student_id, to: :temp_student_id) + rename(table(:submissions), :unsubmitted_by_id, to: :temp_unsubmitted_by_id) + drop(constraint(:submissions, "submissions_student_id_fkey")) + drop(constraint(:submissions, "submissions_unsubmitted_by_id_fkey")) + + alter table(:submissions) do + add(:student_id, references(:course_registrations)) + add(:unsubmitted_by_id, references(:course_registrations)) + end + + alter table(:submission_votes) do + add(:voter_id, references(:course_registrations)) + end + + # Remove grade metric from backend + alter table(:answers) do + remove(:grade) + remove(:adjustment) + remove(:grader_id) + add(:grader_id, references(:course_registrations), null: true) + end + + alter table(:questions) do + remove(:max_grade) + add(:show_solution, :boolean, null: false, default: false) + add(:build_hidden_testcases, :boolean, null: false, default: false) + add(:blocking, :boolean, null: false, default: false) + end + + # Update notifications + alter table(:notifications) do + add(:course_reg_id, references(:course_registrations)) + end + + # Sourcecasts to be associated with a course + alter table(:sourcecasts) do + add(:course_id, references(:courses)) + end + + # Stories to be associated with a course + alter table(:stories) do + add(:course_id, references(:courses)) + end + + # Sublanguage is now being tracked under course configuration, and can be different depending on course + drop_if_exists(table(:sublanguages)) + + # Manual data entry and manipulation to migrate data from Source Academy Knight --> Rook. + # Note that in Knight, there was only 1 course running at a time, so it is okay to assume + # that all existing data belongs to that course. + execute( + fn -> + # Create the new course for migration + {:ok, course} = + %Course{} + |> Course.changeset(%{ + course_name: "CS1101S Programming Methodology (AY21/22 Sem 1)", + course_short_name: "CS1101S", + viewable: true, + enable_game: true, + enable_achievments: true, + enable_sourcecast: true, + source_chapter: 1, + source_variant: "default" + }) + |> Repo.insert() + + # Namespace existing usernames + from(u in "users", update: [set: [username: fragment("? || ? ", "luminus/", u.username)]]) + |> Repo.update_all([]) + + # Create course registrations for existing users + from(u in "users", select: {u.id, u.role, u.group_id, u.game_states}) + |> Repo.all() + |> Enum.each(fn user -> + %CourseRegistration{} + |> CourseRegistration.changeset(%{ + user_id: elem(user, 0), + role: elem(user, 1), + group_id: elem(user, 2), + game_states: elem(user, 3), + course_id: course.id + }) + |> Repo.insert() + end) + + # Add latest_viewed_id to existing users + User + |> Repo.all() + |> Enum.each(fn user -> + user + |> User.changeset(%{latest_viewed_id: course.id}) + |> Repo.update() + end) + + # Handle groups (adding course_id, and updating leader_id to course registrations) + from(g in "groups", select: {g.id, g.temp_leader_id}) + |> Repo.all() + |> Enum.each(fn group -> + leader_id = + case elem(group, 1) do + # leader_id is now going to be non-nullable. if it was previously nil, we will just + # assign a staff to be the leader_id during migration + nil -> + CourseRegistration + |> where(role: :staff) + |> Repo.one() + + Map.fetch!(:id) + + id -> + CourseRegistration + |> where(user_id: ^id) + |> Repo.one() + |> Map.fetch!(:id) + end + + Group + |> where(id: ^elem(group, 0)) + |> Repo.one() + |> Group.changeset(%{leader_id: leader_id, course_id: course.id}) + |> Repo.update() + end) + + # update existing questions with new question config + from(q in "questions", + join: a in "assessments", + on: a.id == q.assessment_id, + where: a.type == "path", + select: {q.id} + ) + |> Repo.all() + |> Enum.each(fn question -> + Question + |> Repo.get(question.id) + |> Question.changeset(%{ + show_solution: false, + build_hidden_testcases: false, + blocking: false + }) + |> Repo.update() + end) + + # Create Assessment Configurations based on Source Academy Knight + ["Missions", "Quests", "Paths", "Contests", "Others"] + |> Enum.each(fn assessment_type -> + %AssessmentConfig{} + |> AssessmentConfig.changeset(%{ + type: assessment_type, + course_id: course.id, + show_grading_summary: assessment_type in ["Missions", "Quests"], + is_manually_graded: assessment_type != "Paths", + early_submission_xp: 200, + hours_before_early_xp_decay: 48 + }) + |> Repo.insert() + end) + + # Link existing assessments to an assessment config and course + from(a in "assessments", select: {a.id, a.type}) + |> Repo.all() + |> Enum.each(fn assessment -> + assessment_type = + case elem(assessment, 1) do + "mission" -> "Missions" + "sidequest" -> "Quests" + "path" -> "Paths" + "contest" -> "Contests" + "practical" -> "Others" + end + + assessment_config = + AssessmentConfig + |> where(type: ^assessment_type) + |> Repo.one() + + Assessment + |> where(id: ^elem(assessment, 0)) + |> Repo.one() + |> Assessment.changeset(%{config_id: assessment_config.id, course_id: course.id}) + |> Repo.update() + end) + + # Updating student_id and unsubmitted_by_id from User to CourseRegistration + from(s in "submissions", select: {s.id, s.temp_student_id, s.temp_unsubmitted_by_id}) + |> Repo.all() + |> Enum.each(fn submission -> + student_id = + CourseRegistration + |> where(user_id: ^elem(submission, 1)) + |> Repo.one() + |> Map.fetch!(:id) + + unsubmitted_by_id = + case elem(submission, 2) do + nil -> + nil + + id -> + CourseRegistration + |> where(user_id: ^id) + |> Repo.one() + |> Map.fetch!(:id) + end + + Submission + |> where(id: ^elem(submission, 0)) + |> Repo.one() + |> Submission.changeset(%{student_id: student_id, unsubmitted_by_id: unsubmitted_by_id}) + |> Repo.update() + end) + + from(s in "submission_votes", select: {s.id, s.user_id}) + |> Repo.all() + |> Enum.each(fn vote -> + voter_id = + CourseRegistration + |> where(user_id: ^elem(vote, 1)) + |> Repo.one() + |> Map.fetch!(:id) + + SubmissionVotes + |> where(id: ^elem(vote, 0)) + |> Repo.one() + |> SubmissionVotes.changeset(%{voter_id: voter_id}) + |> Repo.update() + end) + + from(n in "notifications", select: {n.id, n.user_id}) + |> Repo.all() + |> Enum.each(fn notification -> + course_reg_id = + CourseRegistration + |> where(user_id: ^elem(notification, 1)) + |> Repo.one() + |> Map.fetch!(:id) + + Notification + |> where(id: ^elem(notification, 0)) + |> Repo.one() + |> Notification.changeset(%{course_reg_id: course_reg_id}) + |> Repo.update() + end) + + # Add course id to all Sourcecasts + Sourcecast + |> Repo.all() + |> Enum.each(fn x -> + x + |> Sourcecast.changeset(%{course_id: course.id}) + |> Repo.update() + end) + + # Add course id to all Stories + Story + |> Repo.all() + |> Enum.each(fn x -> + x + |> Story.changeset(%{course_id: course.id}) + |> Repo.update() + end) + end, + fn -> nil end + ) + + # Cleanup users table after data migration + alter table(:users) do + remove(:role) + remove(:group_id) + remove(:game_states) + end + + # Cleanup groups table, and make course_id and leader_id non-nullable + alter table(:groups) do + remove(:temp_leader_id) + + modify(:course_id, references(:courses), null: false, from: references(:courses)) + end + + create(unique_index(:groups, [:name, :course_id])) + + # Cleanup assessments table, and make config_id and course_id non-nullable + alter table(:assessments) do + remove(:type) + modify(:config_id, references(:assessment_configs), null: false, from: references(:courses)) + modify(:course_id, references(:courses), null: false, from: references(:courses)) + end + + alter table(:submissions) do + remove(:temp_student_id) + remove(:temp_unsubmitted_by_id) + + modify(:student_id, references(:course_registrations), + null: false, + from: references(:course_registrations) + ) + end + + create(index(:submissions, :student_id)) + create(unique_index(:submissions, [:assessment_id, :student_id])) + + alter table(:submission_votes) do + remove(:user_id) + + modify(:voter_id, references(:course_registrations), + null: false, + from: references(:course_registrations) + ) + end + + create(unique_index(:submission_votes, [:voter_id, :question_id, :rank], name: :unique_score)) + + alter table(:notifications) do + remove(:user_id) + + modify(:course_reg_id, references(:course_registrations), + null: false, + from: references(:course_registrations) + ) + end + + # Set course_id to be non-nullable + alter table(:stories) do + modify(:course_id, references(:courses), null: false, from: references(:courses)) + end + end +end diff --git a/test/cadet/courses/courses_test.exs b/test/cadet/courses/courses_test.exs index a00fd4056..f978f201e 100644 --- a/test/cadet/courses/courses_test.exs +++ b/test/cadet/courses/courses_test.exs @@ -1,562 +1,562 @@ -defmodule Cadet.CoursesTest do - use Cadet.DataCase - - alias Cadet.{Courses, Repo} - alias Cadet.Accounts.{CourseRegistration, User} - alias Cadet.Courses.{Course, Group, Sourcecast, SourcecastUpload} - - describe "create course config" do - test "succeeds" do - user = insert(:user) - - # Course precreated in User factory - old_courses = Course |> Repo.all() |> length() - - params = %{ - course_name: "CS1101S Programming Methodology (AY20/21 Sem 1)", - course_short_name: "CS1101S", - viewable: true, - enable_game: true, - enable_achievements: true, - enable_sourcecast: true, - source_chapter: 1, - source_variant: "default", - module_help_text: "Help Text" - } - - Courses.create_course_config(params, user) - - # New course created - new_courses = Course |> Repo.all() |> length() - assert new_courses - old_courses == 1 - - # New admin course registration for user - course_regs = CourseRegistration |> where(user_id: ^user.id) |> Repo.all() - assert length(course_regs) == 1 - assert Enum.at(course_regs, 0).role == :admin - - # User's latest_viewed course is updated - assert User |> where(id: ^user.id) |> Repo.one() |> Map.fetch!(:latest_viewed_id) == - Enum.at(course_regs, 0).course_id - end - end - - describe "get course config" do - test "succeeds" do - course = insert(:course) - insert(:assessment_config, %{order: 1, type: "Missions", course: course}) - insert(:assessment_config, %{order: 2, type: "Quests", course: course}) - - {:ok, course} = Courses.get_course_config(course.id) - assert course.course_name == "Programming Methodology" - assert course.course_short_name == "CS1101S" - assert course.viewable == true - assert course.enable_game == true - assert course.enable_achievements == true - assert course.enable_sourcecast == true - assert course.source_chapter == 1 - assert course.source_variant == "default" - assert course.module_help_text == "Help Text" - assert course.assessment_configs == ["Missions", "Quests"] - end - - test "returns with error for invalid course id" do - course = insert(:course) - - assert {:error, {:bad_request, "Invalid course id"}} = - Courses.get_course_config(course.id + 1) - end - end - - describe "update course config" do - test "succeeds (without sublanguage update)" do - course = insert(:course) - - {:ok, updated_course} = - Courses.update_course_config(course.id, %{ - course_name: "Data Structures and Algorithms", - course_short_name: "CS2040S", - viewable: false, - enable_game: false, - enable_achievements: false, - enable_sourcecast: false, - module_help_text: "" - }) - - assert updated_course.course_name == "Data Structures and Algorithms" - assert updated_course.course_short_name == "CS2040S" - assert updated_course.viewable == false - assert updated_course.enable_game == false - assert updated_course.enable_achievements == false - assert updated_course.enable_sourcecast == false - assert updated_course.source_chapter == 1 - assert updated_course.source_variant == "default" - assert updated_course.module_help_text == nil - end - - test "succeeds (with sublanguage update)" do - course = insert(:course) - new_chapter = Enum.random(1..4) - - {:ok, updated_course} = - Courses.update_course_config(course.id, %{ - course_name: "Data Structures and Algorithms", - course_short_name: "CS2040S", - viewable: false, - enable_game: false, - enable_achievements: false, - enable_sourcecast: false, - source_chapter: new_chapter, - source_variant: "default", - module_help_text: "help" - }) - - assert updated_course.course_name == "Data Structures and Algorithms" - assert updated_course.course_short_name == "CS2040S" - assert updated_course.viewable == false - assert updated_course.enable_game == false - assert updated_course.enable_achievements == false - assert updated_course.enable_sourcecast == false - assert updated_course.source_chapter == new_chapter - assert updated_course.source_variant == "default" - assert updated_course.module_help_text == "help" - end - - test "returns with error for invalid course id" do - course = insert(:course) - new_chapter = Enum.random(1..4) - - assert {:error, {:bad_request, "Invalid course id"}} = - Courses.update_course_config(course.id + 1, %{ - source_chapter: new_chapter, - source_variant: "default" - }) - end - - test "returns with error for failed updates" do - course = insert(:course) - - assert {:error, changeset} = - Courses.update_course_config(course.id, %{ - source_chapter: 0, - source_variant: "default" - }) - - assert %{source_chapter: ["is invalid"]} = errors_on(changeset) - - assert {:error, changeset} = - Courses.update_course_config(course.id, %{source_chapter: 2, source_variant: "gpu"}) - - assert %{source_variant: ["is invalid"]} = errors_on(changeset) - end - end - - describe "get assessment configs" do - test "succeeds" do - course = insert(:course) - - for i <- 1..5 do - insert(:assessment_config, %{order: 6 - i, type: "Mission#{i}", course: course}) - end - - assessment_configs = Courses.get_assessment_configs(course.id) - - assert length(assessment_configs) <= 5 - - assessment_configs - |> Enum.with_index(1) - |> Enum.each(fn {at, idx} -> - assert at.order == idx - assert at.type == "Mission#{6 - idx}" - end) - end - end - - describe "mass_upsert_and_reorder_assessment_configs" do - setup do - course = insert(:course) - config1 = insert(:assessment_config, %{order: 1, type: "Missions", course: course}) - config2 = insert(:assessment_config, %{order: 2, type: "Quests", course: course}) - config3 = insert(:assessment_config, %{order: 3, type: "Paths", course: course}) - config4 = insert(:assessment_config, %{order: 4, type: "Contests", course: course}) - expected = ["Paths", "Quests", "Missions", "Others", "Contests"] - - {:ok, - %{ - course: course, - expected: expected, - config1: config1, - config2: config2, - config3: config3, - config4: config4 - }} - end - - test "succeeds", %{ - course: course, - expected: expected, - config1: config1, - config2: config2, - config3: config3, - config4: config4 - } do - {:ok, _} = - Courses.mass_upsert_and_reorder_assessment_configs(course.id, [ - %{assessment_config_id: config1.id, type: "Paths"}, - %{assessment_config_id: config2.id, type: "Quests"}, - %{assessment_config_id: config3.id, type: "Missions"}, - %{assessment_config_id: config4.id, type: "Others"}, - %{assessment_config_id: -1, type: "Contests"} - ]) - - assessment_configs = Courses.get_assessment_configs(course.id) - - assert Enum.map(assessment_configs, & &1.type) == expected - end - - test "succeeds to capitalise", %{ - course: course, - expected: expected, - config1: config1, - config2: config2, - config3: config3, - config4: config4 - } do - {:ok, _} = - Courses.mass_upsert_and_reorder_assessment_configs(course.id, [ - %{assessment_config_id: config1.id, type: "Paths"}, - %{assessment_config_id: config2.id, type: "Quests"}, - %{assessment_config_id: config3.id, type: "Missions"}, - %{assessment_config_id: config4.id, type: "Others"}, - %{assessment_config_id: -1, type: "Contests"} - ]) - - assessment_configs = Courses.get_assessment_configs(course.id) - - assert Enum.map(assessment_configs, & &1.type) == expected - end - - # test "succeed to delete", %{course: course} do - # :ok = - # Courses.mass_upsert_and_reorder_assessment_configs(course.id, [ - # %{order: 1, type: "Paths"}, - # %{order: 2, type: "quests"}, - # %{order: 3, type: "missions"} - # ]) - - # assessment_configs = Courses.get_assessment_configs(course.id) - - # assert Enum.map(assessment_configs, & &1.type) == ["Paths", "Quests", "Missions"] - # end - - test "returns with error for empty list parameter", %{course: course} do - assert {:error, {:bad_request, "Invalid parameter(s)"}} = - Courses.mass_upsert_and_reorder_assessment_configs(course.id, []) - end - - test "returns with error for list parameter of greater than length 8", %{ - course: course, - config1: config1, - config2: config2, - config3: config3, - config4: config4 - } do - params = [ - %{assessment_config_id: config1.id, type: "Paths"}, - %{assessment_config_id: config2.id, type: "Quests"}, - %{assessment_config_id: config3.id, type: "Missions"}, - %{assessment_config_id: config4.id, type: "Others"}, - %{assessment_config_id: -1, type: "Contests"}, - %{assessment_config_id: -1, type: "Contests"}, - %{assessment_config_id: -1, type: "Contests"}, - %{assessment_config_id: -1, type: "Contests"}, - %{assessment_config_id: -1, type: "Contests"} - ] - - assert {:error, {:bad_request, "Invalid parameter(s)"}} = - Courses.mass_upsert_and_reorder_assessment_configs(course.id, params) - end - - test "returns with error for non-list parameter", %{course: course} do - params = %{course_id: course.id, order: 1, type: "Paths"} - - assert {:error, {:bad_request, "Invalid parameter(s)"}} = - Courses.mass_upsert_and_reorder_assessment_configs(course.id, params) - end - end - - describe "insert_or_update_assessment_config" do - test "succeeds with insert configs" do - course = insert(:course) - old_configs = Courses.get_assessment_configs(course.id) - - params = %{ - assessment_config_id: -1, - order: 1, - type: "Mission", - early_submission_xp: 100, - hours_before_early_xp_decay: 24 - } - - {:ok, updated_config} = Courses.insert_or_update_assessment_config(course.id, params) - - new_configs = Courses.get_assessment_configs(course.id) - assert old_configs == [] - assert length(new_configs) == 1 - assert updated_config.early_submission_xp == 100 - assert updated_config.hours_before_early_xp_decay == 24 - end - - test "succeeds with update" do - course = insert(:course) - config = insert(:assessment_config, %{course: course}) - - params = %{ - assessment_config_id: config.id, - type: "Mission", - early_submission_xp: 100, - hours_before_early_xp_decay: 24 - } - - {:ok, updated_config} = Courses.insert_or_update_assessment_config(course.id, params) - - assert updated_config.type == "Mission" - assert updated_config.early_submission_xp == 100 - assert updated_config.hours_before_early_xp_decay == 24 - end - end - - describe "reorder_assessment_config" do - test "succeeds" do - course = insert(:course) - config1 = insert(:assessment_config, %{order: 1, type: "Missions", course: course}) - config3 = insert(:assessment_config, %{order: 2, type: "Paths", course: course}) - config2 = insert(:assessment_config, %{order: 3, type: "Quests", course: course}) - config4 = insert(:assessment_config, %{order: 4, type: "Others", course: course}) - old_configs = Courses.get_assessment_configs(course.id) - - params = [ - %{assessment_config_id: config1.id, type: "Paths"}, - %{assessment_config_id: config2.id, type: "Quests"}, - %{assessment_config_id: config3.id, type: "Missions"}, - %{assessment_config_id: config4.id, type: "Others"} - ] - - expected = ["Paths", "Quests", "Missions", "Others"] - - {:ok, _} = Courses.reorder_assessment_configs(course.id, params) - - new_configs = Courses.get_assessment_configs(course.id) - assert length(old_configs) == length(new_configs) - assert Enum.map(new_configs, & &1.type) == expected - end - end - - describe "delete_assessment_config" do - test "succeeds" do - course = insert(:course) - config = insert(:assessment_config, %{order: 1, course: course}) - old_configs = Courses.get_assessment_configs(course.id) - - params = %{ - assessment_config_id: config.id - } - - {:ok, _} = Courses.delete_assessment_config(course.id, params) - - new_configs = Courses.get_assessment_configs(course.id) - assert length(old_configs) == 1 - assert new_configs == [] - end - - test "error" do - course = insert(:course) - insert(:assessment_config, %{order: 1, course: course}) - - params = %{ - assessment_config_id: -1 - } - - assert {:error, :no_such_enrty} == Courses.delete_assessment_config(course.id, params) - end - end - - describe "upsert_groups_in_course" do - setup do - course = insert(:course) - existing_group_leader = insert(:course_registration, %{course: course, role: :staff}) - - existing_group = - insert(:group, %{name: "Existing Group", course: course, leader: existing_group_leader}) - - existing_student = - insert(:course_registration, %{course: course, group: existing_group, role: :student}) - - {:ok, - course: course, - existing_group: existing_group, - existing_group_leader: existing_group_leader, - existing_student: existing_student} - end - - test "succeeds in upserting existing groups", %{ - course: course, - existing_group: existing_group, - existing_group_leader: existing_group_leader, - existing_student: existing_student - } do - student = insert(:course_registration, %{course: course, group: nil, role: :student}) - admin = insert(:course_registration, %{course: course, group: nil, role: :admin}) - - usernames_and_groups = [ - %{username: existing_student.user.username, group: "Group1"}, - %{username: admin.user.username, group: "Group2"}, - %{username: student.user.username, group: "Group2"}, - %{username: existing_group_leader.user.username, group: "Group1"} - ] - - assert :ok == Courses.upsert_groups_in_course(usernames_and_groups, course.id) - - # Check that Group1 and Group2 were created - assert length(Group |> where(course_id: ^course.id) |> Repo.all()) == 3 - - # Check that leaders were assigned/ updated correctly - assert is_nil( - Group - |> where(id: ^existing_group.id) - |> Repo.one() - |> Map.fetch!(:leader_id) - ) - - group1 = Group |> where(course_id: ^course.id) |> where(name: "Group1") |> Repo.one() - group2 = Group |> where(course_id: ^course.id) |> where(name: "Group2") |> Repo.one() - assert group1 |> Map.fetch!(:leader_id) == existing_group_leader.id - assert group2 |> Map.fetch!(:leader_id) == admin.id - - # Check that students were assigned to the correct groups - assert CourseRegistration - |> where(id: ^existing_student.id) - |> Repo.one() - |> Map.fetch!(:group_id) == - group1.id - - assert CourseRegistration |> where(id: ^student.id) |> Repo.one() |> Map.fetch!(:group_id) == - group2.id - end - - test "succeeds (removes user from existing groups when group is not specified)", %{ - course: course, - existing_group: existing_group, - existing_group_leader: existing_group_leader, - existing_student: existing_student - } do - usernames_and_groups = [ - %{username: existing_student.user.username}, - %{username: existing_group_leader.user.username} - ] - - assert :ok == Courses.upsert_groups_in_course(usernames_and_groups, course.id) - - assert is_nil( - Group - |> where(id: ^existing_group.id) - |> Repo.one() - |> Map.fetch!(:leader_id) - ) - - assert is_nil( - CourseRegistration - |> where(id: ^existing_student.id) - |> Repo.one() - |> Map.fetch!(:group_id) - ) - end - end - - describe "Sourcecast" do - setup do - on_exit(fn -> File.rm_rf!("uploads/test/sourcecasts") end) - end - - test "upload file to folder then delete it" do - inserter_course_registration = insert(:course_registration, %{role: :staff}) - - upload = %Plug.Upload{ - content_type: "audio/wav", - filename: "upload.wav", - path: "test/fixtures/upload.wav" - } - - result = - Courses.upload_sourcecast_file(inserter_course_registration, %{ - title: "Test Upload", - audio: upload, - playbackData: - "{\"init\":{\"editorValue\":\"// Type your program in here!\"},\"inputs\":[]}" - }) - - assert {:ok, sourcecast} = result - path = SourcecastUpload.url({sourcecast.audio, sourcecast}) - assert path =~ "/uploads/test/sourcecasts/upload.wav" - - deleter_course_registration = insert(:course_registration, %{role: :staff}) - assert {:ok, _} = Courses.delete_sourcecast_file(deleter_course_registration, sourcecast.id) - assert Repo.get(Sourcecast, sourcecast.id) == nil - refute File.exists?("uploads/test/sourcecasts/upload.wav") - end - end - - describe "get_or_create_group" do - test "existing group" do - course = insert(:course) - group = insert(:group, %{course: course}) - - {:ok, group_db} = Courses.get_or_create_group(group.name, course.id) - - assert group_db.id == group.id - assert group_db.leader_id == group.leader_id - end - - test "non-existent group" do - course = insert(:course) - group_name = params_for(:group).name - - {:ok, _} = Courses.get_or_create_group(group_name, course.id) - - group_db = - Group - |> where(name: ^group_name) - |> Repo.one() - - refute is_nil(group_db) - end - end - - # describe "insert_or_update_group" do - # test "existing group" do - # group = insert(:group) - # group_params = params_with_assocs(:group, name: group.name) - # Courses.insert_or_update_group(group_params) - - # updated_group = - # Group - # |> where(name: ^group.name) - # |> Repo.one() - - # assert updated_group.id == group.id - # assert updated_group.leader_id == group_params.leader_id - # end - - # test "non-existent group" do - # group_params = params_with_assocs(:group) - # Courses.insert_or_update_group(group_params) - - # updated_group = - # Group - # |> where(name: ^group_params.name) - # |> Repo.one() - - # assert updated_group.leader_id == group_params.leader_id - # end - # end -end +defmodule Cadet.CoursesTest do + use Cadet.DataCase + + alias Cadet.{Courses, Repo} + alias Cadet.Accounts.{CourseRegistration, User} + alias Cadet.Courses.{Course, Group, Sourcecast, SourcecastUpload} + + describe "create course config" do + test "succeeds" do + user = insert(:user) + + # Course precreated in User factory + old_courses = Course |> Repo.all() |> length() + + params = %{ + course_name: "CS1101S Programming Methodology (AY20/21 Sem 1)", + course_short_name: "CS1101S", + viewable: true, + enable_game: true, + enable_achievements: true, + enable_sourcecast: true, + source_chapter: 1, + source_variant: "default", + module_help_text: "Help Text" + } + + Courses.create_course_config(params, user) + + # New course created + new_courses = Course |> Repo.all() |> length() + assert new_courses - old_courses == 1 + + # New admin course registration for user + course_regs = CourseRegistration |> where(user_id: ^user.id) |> Repo.all() + assert length(course_regs) == 1 + assert Enum.at(course_regs, 0).role == :admin + + # User's latest_viewed course is updated + assert User |> where(id: ^user.id) |> Repo.one() |> Map.fetch!(:latest_viewed_id) == + Enum.at(course_regs, 0).course_id + end + end + + describe "get course config" do + test "succeeds" do + course = insert(:course) + insert(:assessment_config, %{order: 1, type: "Missions", course: course}) + insert(:assessment_config, %{order: 2, type: "Quests", course: course}) + + {:ok, course} = Courses.get_course_config(course.id) + assert course.course_name == "Programming Methodology" + assert course.course_short_name == "CS1101S" + assert course.viewable == true + assert course.enable_game == true + assert course.enable_achievements == true + assert course.enable_sourcecast == true + assert course.source_chapter == 1 + assert course.source_variant == "default" + assert course.module_help_text == "Help Text" + assert course.assessment_configs == ["Missions", "Quests"] + end + + test "returns with error for invalid course id" do + course = insert(:course) + + assert {:error, {:bad_request, "Invalid course id"}} = + Courses.get_course_config(course.id + 1) + end + end + + describe "update course config" do + test "succeeds (without sublanguage update)" do + course = insert(:course) + + {:ok, updated_course} = + Courses.update_course_config(course.id, %{ + course_name: "Data Structures and Algorithms", + course_short_name: "CS2040S", + viewable: false, + enable_game: false, + enable_achievements: false, + enable_sourcecast: false, + module_help_text: "" + }) + + assert updated_course.course_name == "Data Structures and Algorithms" + assert updated_course.course_short_name == "CS2040S" + assert updated_course.viewable == false + assert updated_course.enable_game == false + assert updated_course.enable_achievements == false + assert updated_course.enable_sourcecast == false + assert updated_course.source_chapter == 1 + assert updated_course.source_variant == "default" + assert updated_course.module_help_text == nil + end + + test "succeeds (with sublanguage update)" do + course = insert(:course) + new_chapter = Enum.random(1..4) + + {:ok, updated_course} = + Courses.update_course_config(course.id, %{ + course_name: "Data Structures and Algorithms", + course_short_name: "CS2040S", + viewable: false, + enable_game: false, + enable_achievements: false, + enable_sourcecast: false, + source_chapter: new_chapter, + source_variant: "default", + module_help_text: "help" + }) + + assert updated_course.course_name == "Data Structures and Algorithms" + assert updated_course.course_short_name == "CS2040S" + assert updated_course.viewable == false + assert updated_course.enable_game == false + assert updated_course.enable_achievements == false + assert updated_course.enable_sourcecast == false + assert updated_course.source_chapter == new_chapter + assert updated_course.source_variant == "default" + assert updated_course.module_help_text == "help" + end + + test "returns with error for invalid course id" do + course = insert(:course) + new_chapter = Enum.random(1..4) + + assert {:error, {:bad_request, "Invalid course id"}} = + Courses.update_course_config(course.id + 1, %{ + source_chapter: new_chapter, + source_variant: "default" + }) + end + + test "returns with error for failed updates" do + course = insert(:course) + + assert {:error, changeset} = + Courses.update_course_config(course.id, %{ + source_chapter: 0, + source_variant: "default" + }) + + assert %{source_chapter: ["is invalid"]} = errors_on(changeset) + + assert {:error, changeset} = + Courses.update_course_config(course.id, %{source_chapter: 2, source_variant: "gpu"}) + + assert %{source_variant: ["is invalid"]} = errors_on(changeset) + end + end + + describe "get assessment configs" do + test "succeeds" do + course = insert(:course) + + for i <- 1..5 do + insert(:assessment_config, %{order: 6 - i, type: "Mission#{i}", course: course}) + end + + assessment_configs = Courses.get_assessment_configs(course.id) + + assert length(assessment_configs) <= 5 + + assessment_configs + |> Enum.with_index(1) + |> Enum.each(fn {at, idx} -> + assert at.order == idx + assert at.type == "Mission#{6 - idx}" + end) + end + end + + describe "mass_upsert_and_reorder_assessment_configs" do + setup do + course = insert(:course) + config1 = insert(:assessment_config, %{order: 1, type: "Missions", course: course}) + config2 = insert(:assessment_config, %{order: 2, type: "Quests", course: course}) + config3 = insert(:assessment_config, %{order: 3, type: "Paths", course: course}) + config4 = insert(:assessment_config, %{order: 4, type: "Contests", course: course}) + expected = ["Paths", "Quests", "Missions", "Others", "Contests"] + + {:ok, + %{ + course: course, + expected: expected, + config1: config1, + config2: config2, + config3: config3, + config4: config4 + }} + end + + test "succeeds", %{ + course: course, + expected: expected, + config1: config1, + config2: config2, + config3: config3, + config4: config4 + } do + {:ok, _} = + Courses.mass_upsert_and_reorder_assessment_configs(course.id, [ + %{assessment_config_id: config1.id, type: "Paths"}, + %{assessment_config_id: config2.id, type: "Quests"}, + %{assessment_config_id: config3.id, type: "Missions"}, + %{assessment_config_id: config4.id, type: "Others"}, + %{assessment_config_id: -1, type: "Contests"} + ]) + + assessment_configs = Courses.get_assessment_configs(course.id) + + assert Enum.map(assessment_configs, & &1.type) == expected + end + + test "succeeds to capitalise", %{ + course: course, + expected: expected, + config1: config1, + config2: config2, + config3: config3, + config4: config4 + } do + {:ok, _} = + Courses.mass_upsert_and_reorder_assessment_configs(course.id, [ + %{assessment_config_id: config1.id, type: "Paths"}, + %{assessment_config_id: config2.id, type: "Quests"}, + %{assessment_config_id: config3.id, type: "Missions"}, + %{assessment_config_id: config4.id, type: "Others"}, + %{assessment_config_id: -1, type: "Contests"} + ]) + + assessment_configs = Courses.get_assessment_configs(course.id) + + assert Enum.map(assessment_configs, & &1.type) == expected + end + + # test "succeed to delete", %{course: course} do + # :ok = + # Courses.mass_upsert_and_reorder_assessment_configs(course.id, [ + # %{order: 1, type: "Paths"}, + # %{order: 2, type: "quests"}, + # %{order: 3, type: "missions"} + # ]) + + # assessment_configs = Courses.get_assessment_configs(course.id) + + # assert Enum.map(assessment_configs, & &1.type) == ["Paths", "Quests", "Missions"] + # end + + test "returns with error for empty list parameter", %{course: course} do + assert {:error, {:bad_request, "Invalid parameter(s)"}} = + Courses.mass_upsert_and_reorder_assessment_configs(course.id, []) + end + + test "returns with error for list parameter of greater than length 8", %{ + course: course, + config1: config1, + config2: config2, + config3: config3, + config4: config4 + } do + params = [ + %{assessment_config_id: config1.id, type: "Paths"}, + %{assessment_config_id: config2.id, type: "Quests"}, + %{assessment_config_id: config3.id, type: "Missions"}, + %{assessment_config_id: config4.id, type: "Others"}, + %{assessment_config_id: -1, type: "Contests"}, + %{assessment_config_id: -1, type: "Contests"}, + %{assessment_config_id: -1, type: "Contests"}, + %{assessment_config_id: -1, type: "Contests"}, + %{assessment_config_id: -1, type: "Contests"} + ] + + assert {:error, {:bad_request, "Invalid parameter(s)"}} = + Courses.mass_upsert_and_reorder_assessment_configs(course.id, params) + end + + test "returns with error for non-list parameter", %{course: course} do + params = %{course_id: course.id, order: 1, type: "Paths"} + + assert {:error, {:bad_request, "Invalid parameter(s)"}} = + Courses.mass_upsert_and_reorder_assessment_configs(course.id, params) + end + end + + describe "insert_or_update_assessment_config" do + test "succeeds with insert configs" do + course = insert(:course) + old_configs = Courses.get_assessment_configs(course.id) + + params = %{ + assessment_config_id: -1, + order: 1, + type: "Mission", + early_submission_xp: 100, + hours_before_early_xp_decay: 24 + } + + {:ok, updated_config} = Courses.insert_or_update_assessment_config(course.id, params) + + new_configs = Courses.get_assessment_configs(course.id) + assert old_configs == [] + assert length(new_configs) == 1 + assert updated_config.early_submission_xp == 100 + assert updated_config.hours_before_early_xp_decay == 24 + end + + test "succeeds with update" do + course = insert(:course) + config = insert(:assessment_config, %{course: course}) + + params = %{ + assessment_config_id: config.id, + type: "Mission", + early_submission_xp: 100, + hours_before_early_xp_decay: 24 + } + + {:ok, updated_config} = Courses.insert_or_update_assessment_config(course.id, params) + + assert updated_config.type == "Mission" + assert updated_config.early_submission_xp == 100 + assert updated_config.hours_before_early_xp_decay == 24 + end + end + + describe "reorder_assessment_config" do + test "succeeds" do + course = insert(:course) + config1 = insert(:assessment_config, %{order: 1, type: "Missions", course: course}) + config3 = insert(:assessment_config, %{order: 2, type: "Paths", course: course}) + config2 = insert(:assessment_config, %{order: 3, type: "Quests", course: course}) + config4 = insert(:assessment_config, %{order: 4, type: "Others", course: course}) + old_configs = Courses.get_assessment_configs(course.id) + + params = [ + %{assessment_config_id: config1.id, type: "Paths"}, + %{assessment_config_id: config2.id, type: "Quests"}, + %{assessment_config_id: config3.id, type: "Missions"}, + %{assessment_config_id: config4.id, type: "Others"} + ] + + expected = ["Paths", "Quests", "Missions", "Others"] + + {:ok, _} = Courses.reorder_assessment_configs(course.id, params) + + new_configs = Courses.get_assessment_configs(course.id) + assert length(old_configs) == length(new_configs) + assert Enum.map(new_configs, & &1.type) == expected + end + end + + describe "delete_assessment_config" do + test "succeeds" do + course = insert(:course) + config = insert(:assessment_config, %{order: 1, course: course}) + old_configs = Courses.get_assessment_configs(course.id) + + params = %{ + assessment_config_id: config.id + } + + {:ok, _} = Courses.delete_assessment_config(course.id, params) + + new_configs = Courses.get_assessment_configs(course.id) + assert length(old_configs) == 1 + assert new_configs == [] + end + + test "error" do + course = insert(:course) + insert(:assessment_config, %{order: 1, course: course}) + + params = %{ + assessment_config_id: -1 + } + + assert {:error, :no_such_enrty} == Courses.delete_assessment_config(course.id, params) + end + end + + describe "upsert_groups_in_course" do + setup do + course = insert(:course) + existing_group_leader = insert(:course_registration, %{course: course, role: :staff}) + + existing_group = + insert(:group, %{name: "Existing Group", course: course, leader: existing_group_leader}) + + existing_student = + insert(:course_registration, %{course: course, group: existing_group, role: :student}) + + {:ok, + course: course, + existing_group: existing_group, + existing_group_leader: existing_group_leader, + existing_student: existing_student} + end + + test "succeeds in upserting existing groups", %{ + course: course, + existing_group: existing_group, + existing_group_leader: existing_group_leader, + existing_student: existing_student + } do + student = insert(:course_registration, %{course: course, group: nil, role: :student}) + admin = insert(:course_registration, %{course: course, group: nil, role: :admin}) + + usernames_and_groups = [ + %{username: existing_student.user.username, group: "Group1"}, + %{username: admin.user.username, group: "Group2"}, + %{username: student.user.username, group: "Group2"}, + %{username: existing_group_leader.user.username, group: "Group1"} + ] + + assert :ok == Courses.upsert_groups_in_course(usernames_and_groups, course.id) + + # Check that Group1 and Group2 were created + assert length(Group |> where(course_id: ^course.id) |> Repo.all()) == 3 + + # Check that leaders were assigned/ updated correctly + assert is_nil( + Group + |> where(id: ^existing_group.id) + |> Repo.one() + |> Map.fetch!(:leader_id) + ) + + group1 = Group |> where(course_id: ^course.id) |> where(name: "Group1") |> Repo.one() + group2 = Group |> where(course_id: ^course.id) |> where(name: "Group2") |> Repo.one() + assert group1 |> Map.fetch!(:leader_id) == existing_group_leader.id + assert group2 |> Map.fetch!(:leader_id) == admin.id + + # Check that students were assigned to the correct groups + assert CourseRegistration + |> where(id: ^existing_student.id) + |> Repo.one() + |> Map.fetch!(:group_id) == + group1.id + + assert CourseRegistration |> where(id: ^student.id) |> Repo.one() |> Map.fetch!(:group_id) == + group2.id + end + + test "succeeds (removes user from existing groups when group is not specified)", %{ + course: course, + existing_group: existing_group, + existing_group_leader: existing_group_leader, + existing_student: existing_student + } do + usernames_and_groups = [ + %{username: existing_student.user.username}, + %{username: existing_group_leader.user.username} + ] + + assert :ok == Courses.upsert_groups_in_course(usernames_and_groups, course.id) + + assert is_nil( + Group + |> where(id: ^existing_group.id) + |> Repo.one() + |> Map.fetch!(:leader_id) + ) + + assert is_nil( + CourseRegistration + |> where(id: ^existing_student.id) + |> Repo.one() + |> Map.fetch!(:group_id) + ) + end + end + + describe "Sourcecast" do + setup do + on_exit(fn -> File.rm_rf!("uploads/test/sourcecasts") end) + end + + test "upload file to folder then delete it" do + inserter_course_registration = insert(:course_registration, %{role: :staff}) + + upload = %Plug.Upload{ + content_type: "audio/wav", + filename: "upload.wav", + path: "test/fixtures/upload.wav" + } + + result = + Courses.upload_sourcecast_file(inserter_course_registration, %{ + title: "Test Upload", + audio: upload, + playbackData: + "{\"init\":{\"editorValue\":\"// Type your program in here!\"},\"inputs\":[]}" + }) + + assert {:ok, sourcecast} = result + path = SourcecastUpload.url({sourcecast.audio, sourcecast}) + assert path =~ "/uploads/test/sourcecasts/upload.wav" + + deleter_course_registration = insert(:course_registration, %{role: :staff}) + assert {:ok, _} = Courses.delete_sourcecast_file(deleter_course_registration, sourcecast.id) + assert Repo.get(Sourcecast, sourcecast.id) == nil + refute File.exists?("uploads/test/sourcecasts/upload.wav") + end + end + + describe "get_or_create_group" do + test "existing group" do + course = insert(:course) + group = insert(:group, %{course: course}) + + {:ok, group_db} = Courses.get_or_create_group(group.name, course.id) + + assert group_db.id == group.id + assert group_db.leader_id == group.leader_id + end + + test "non-existent group" do + course = insert(:course) + group_name = params_for(:group).name + + {:ok, _} = Courses.get_or_create_group(group_name, course.id) + + group_db = + Group + |> where(name: ^group_name) + |> Repo.one() + + refute is_nil(group_db) + end + end + + # describe "insert_or_update_group" do + # test "existing group" do + # group = insert(:group) + # group_params = params_with_assocs(:group, name: group.name) + # Courses.insert_or_update_group(group_params) + + # updated_group = + # Group + # |> where(name: ^group.name) + # |> Repo.one() + + # assert updated_group.id == group.id + # assert updated_group.leader_id == group_params.leader_id + # end + + # test "non-existent group" do + # group_params = params_with_assocs(:group) + # Courses.insert_or_update_group(group_params) + + # updated_group = + # Group + # |> where(name: ^group_params.name) + # |> Repo.one() + + # assert updated_group.leader_id == group_params.leader_id + # end + # end +end From 3ff07515da750bb22b4f6f68134d30f80c5c2883 Mon Sep 17 00:00:00 2001 From: Chow En Rong <53928333+chownces@users.noreply.github.com> Date: Mon, 5 Jul 2021 22:34:31 +0800 Subject: [PATCH 125/174] Fix migration file --- .../20210531155751_multitenant_upgrade.exs | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/priv/repo/migrations/20210531155751_multitenant_upgrade.exs b/priv/repo/migrations/20210531155751_multitenant_upgrade.exs index 933488f0a..2661955bb 100644 --- a/priv/repo/migrations/20210531155751_multitenant_upgrade.exs +++ b/priv/repo/migrations/20210531155751_multitenant_upgrade.exs @@ -3,7 +3,7 @@ defmodule Cadet.Repo.Migrations.MultitenantUpgrade do import Ecto.Query, only: [from: 2, where: 2] alias Cadet.Accounts.{CourseRegistration, Notification, Role, User} - alias Cadet.Assessments.{Assessment, Submission, SubmissionVotes} + alias Cadet.Assessments.{Assessment, Question, Submission, SubmissionVotes} alias Cadet.Courses.{AssessmentConfig, Course, Group, Sourcecast} alias Cadet.Repo alias Cadet.Stories.Story @@ -205,21 +205,22 @@ defmodule Cadet.Repo.Migrations.MultitenantUpgrade do |> Repo.update() end) - # update existing questions with new question config + # Update existing Path questions with new question config + # The questions from other assessment types are not updated as these fields default to false from(q in "questions", join: a in "assessments", on: a.id == q.assessment_id, where: a.type == "path", - select: {q.id} + select: q.id ) |> Repo.all() - |> Enum.each(fn question -> + |> Enum.each(fn question_id -> Question - |> Repo.get(question.id) + |> Repo.get(question_id) |> Question.changeset(%{ - show_solution: false, - build_hidden_testcases: false, - blocking: false + show_solution: true, + build_hidden_testcases: true, + blocking: true }) |> Repo.update() end) From 0def54192be98114f63ad4114794067262718879 Mon Sep 17 00:00:00 2001 From: Chow En Rong <53928333+chownces@users.noreply.github.com> Date: Mon, 5 Jul 2021 23:13:50 +0800 Subject: [PATCH 126/174] Fix errors when testing with frontend --- lib/cadet/jobs/autograder/grading_job.ex | 4 ---- lib/cadet_web/views/assessments_helpers.ex | 3 ++- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/lib/cadet/jobs/autograder/grading_job.ex b/lib/cadet/jobs/autograder/grading_job.ex index 62936b74a..63b2ac542 100644 --- a/lib/cadet/jobs/autograder/grading_job.ex +++ b/lib/cadet/jobs/autograder/grading_job.ex @@ -144,14 +144,12 @@ defmodule Cadet.Autograder.GradingJob do |> where([_, sv], is_nil(sv.rank)) |> Repo.exists?() - grade = if is_nil_entries, do: 0, else: question.max_grade xp = if is_nil_entries, do: 0, else: question.max_xp answer |> Answer.autograding_changeset(%{ adjustment: 0, xp_adjustment: 0, - grade: grade, xp: xp, autograding_status: :success }) @@ -165,14 +163,12 @@ defmodule Cadet.Autograder.GradingJob do |> Map.get("choice_id") correct? = answer.answer["choice_id"] == correct_choice - grade = if correct?, do: question.max_grade, else: 0 xp = if correct?, do: question.max_xp, else: 0 answer |> Answer.autograding_changeset(%{ adjustment: 0, xp_adjustment: 0, - grade: grade, xp: xp, autograding_status: :success }) diff --git a/lib/cadet_web/views/assessments_helpers.ex b/lib/cadet_web/views/assessments_helpers.ex index 1979ca572..7c473790a 100644 --- a/lib/cadet_web/views/assessments_helpers.ex +++ b/lib/cadet_web/views/assessments_helpers.ex @@ -48,7 +48,8 @@ defmodule CadetWeb.AssessmentsHelpers do id: :id, type: :type, library: &build_library(%{library: &1.library}), - maxXp: :max_xp + maxXp: :max_xp, + blocking: :blocking }) end From 75388d51dbf9b194e2076e607e5256348d2bf8c7 Mon Sep 17 00:00:00 2001 From: YaleChen299 Date: Mon, 5 Jul 2021 23:34:22 +0800 Subject: [PATCH 127/174] added isManuallyGraded field to admin grading endpoint --- lib/cadet/assessments/assessments.ex | 1 + .../admin_controllers/admin_grading_controller_test.exs | 4 ++++ 2 files changed, 5 insertions(+) diff --git a/lib/cadet/assessments/assessments.ex b/lib/cadet/assessments/assessments.ex index ad2eef76b..b32055be7 100644 --- a/lib/cadet/assessments/assessments.ex +++ b/lib/cadet/assessments/assessments.ex @@ -1136,6 +1136,7 @@ defmodule Cadet.Assessments do (select a.id, a.title, + bool_or(ac.is_manually_graded) as "isManuallyGraded", max(ac.type) as "type", sum(q.max_xp) as "maxXp", count(q.id) as "questionCount" diff --git a/test/cadet_web/admin_controllers/admin_grading_controller_test.exs b/test/cadet_web/admin_controllers/admin_grading_controller_test.exs index f9de65768..c42dbbd16 100644 --- a/test/cadet_web/admin_controllers/admin_grading_controller_test.exs +++ b/test/cadet_web/admin_controllers/admin_grading_controller_test.exs @@ -128,6 +128,7 @@ defmodule CadetWeb.AdminGradingControllerTest do }, "assessment" => %{ "type" => mission.config.type, + "isManuallyGraded" => mission.config.is_manually_graded, "maxXp" => 5000, "id" => mission.id, "title" => mission.title, @@ -190,6 +191,7 @@ defmodule CadetWeb.AdminGradingControllerTest do }, "assessment" => %{ "type" => mission.config.type, + "isManuallyGraded" => mission.config.is_manually_graded, "maxXp" => 5000, "id" => mission.id, "title" => mission.title, @@ -761,6 +763,7 @@ defmodule CadetWeb.AdminGradingControllerTest do }, "assessment" => %{ "type" => mission.config.type, + "isManuallyGraded" => mission.config.is_manually_graded, "maxXp" => 5000, "id" => mission.id, "title" => mission.title, @@ -803,6 +806,7 @@ defmodule CadetWeb.AdminGradingControllerTest do }, "assessment" => %{ "type" => mission.config.type, + "isManuallyGraded" => mission.config.is_manually_graded, "maxXp" => 5000, "id" => mission.id, "title" => mission.title, From 3f651fd85f94ee6074df39f88dd1db540a10d2a7 Mon Sep 17 00:00:00 2001 From: YaleChen299 Date: Mon, 5 Jul 2021 23:47:24 +0800 Subject: [PATCH 128/174] fix testcases --- .../admin_controllers/admin_grading_controller_test.exs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/test/cadet_web/admin_controllers/admin_grading_controller_test.exs b/test/cadet_web/admin_controllers/admin_grading_controller_test.exs index c42dbbd16..70dca34b5 100644 --- a/test/cadet_web/admin_controllers/admin_grading_controller_test.exs +++ b/test/cadet_web/admin_controllers/admin_grading_controller_test.exs @@ -252,6 +252,7 @@ defmodule CadetWeb.AdminGradingControllerTest do ), "solutionTemplate" => &1.question.question.template, "type" => "#{&1.question.type}", + "blocking" => &1.question.blocking, "id" => &1.question.id, "library" => %{ "chapter" => &1.question.library.chapter, @@ -288,6 +289,7 @@ defmodule CadetWeb.AdminGradingControllerTest do %{ "question" => %{ "type" => "#{&1.question.type}", + "blocking" => &1.question.blocking, "id" => &1.question.id, "library" => %{ "chapter" => &1.question.library.chapter, @@ -334,6 +336,7 @@ defmodule CadetWeb.AdminGradingControllerTest do "prepend" => &1.question.question.prepend, "solutionTemplate" => &1.question.question.template, "type" => "#{&1.question.type}", + "blocking" => &1.question.blocking, "id" => &1.question.id, "library" => %{ "chapter" => &1.question.library.chapter, @@ -867,6 +870,7 @@ defmodule CadetWeb.AdminGradingControllerTest do ), "solutionTemplate" => &1.question.question.template, "type" => "#{&1.question.type}", + "blocking" => &1.question.blocking, "id" => &1.question.id, "library" => %{ "chapter" => &1.question.library.chapter, @@ -903,6 +907,7 @@ defmodule CadetWeb.AdminGradingControllerTest do %{ "question" => %{ "type" => "#{&1.question.type}", + "blocking" => &1.question.blocking, "id" => &1.question.id, "library" => %{ "chapter" => &1.question.library.chapter, @@ -949,6 +954,7 @@ defmodule CadetWeb.AdminGradingControllerTest do "prepend" => &1.question.question.prepend, "solutionTemplate" => &1.question.question.template, "type" => "#{&1.question.type}", + "blocking" => &1.question.blocking, "id" => &1.question.id, "library" => %{ "chapter" => &1.question.library.chapter, From fb2f35d1908082095cb2d75cde2a2bcf53c33f09 Mon Sep 17 00:00:00 2001 From: YaleChen299 Date: Tue, 6 Jul 2021 00:16:47 +0800 Subject: [PATCH 129/174] delete assessment config deletes relations + tested --- lib/cadet/courses/courses.ex | 29 +++++++++++++++++++++-------- test/cadet/courses/courses_test.exs | 4 ++++ 2 files changed, 25 insertions(+), 8 deletions(-) diff --git a/lib/cadet/courses/courses.ex b/lib/cadet/courses/courses.ex index 49c4a66b4..3b0847411 100644 --- a/lib/cadet/courses/courses.ex +++ b/lib/cadet/courses/courses.ex @@ -18,6 +18,9 @@ defmodule Cadet.Courses do SourcecastUpload } + alias Cadet.Assessments + alias Cadet.Assessments.Assessment + @doc """ Creates a new course configuration, course registration, and sets the user's latest course id to the newly created course. @@ -164,14 +167,24 @@ defmodule Cadet.Courses do @spec delete_assessment_config(integer(), map()) :: {:ok, Ecto.Schema.t()} | {:error, Ecto.Changeset.t()} | {:error, :no_such_enrty} - def delete_assessment_config(course_id, params = %{assessment_config_id: assessment_config_id}) do - AssessmentConfig - |> where(course_id: ^course_id) - |> where(id: ^assessment_config_id) - |> Repo.one() - |> case do - nil -> {:error, :no_such_enrty} - at -> at |> AssessmentConfig.changeset(params) |> Repo.delete() + def delete_assessment_config(course_id, %{assessment_config_id: assessment_config_id}) do + config = + AssessmentConfig + |> where(course_id: ^course_id) + |> where(id: ^assessment_config_id) + |> Repo.one() + + case config do + nil -> + {:error, :no_such_enrty} + + config -> + Assessment + |> where(config_id: ^config.id) + |> Repo.all() + |> Enum.each(fn assessment -> Assessments.delete_assessment(assessment.id) end) + + Repo.delete(config) end end diff --git a/test/cadet/courses/courses_test.exs b/test/cadet/courses/courses_test.exs index f978f201e..b2118e2d3 100644 --- a/test/cadet/courses/courses_test.exs +++ b/test/cadet/courses/courses_test.exs @@ -4,6 +4,7 @@ defmodule Cadet.CoursesTest do alias Cadet.{Courses, Repo} alias Cadet.Accounts.{CourseRegistration, User} alias Cadet.Courses.{Course, Group, Sourcecast, SourcecastUpload} + alias Cadet.Assessments.Assessment describe "create course config" do test "succeeds" do @@ -356,7 +357,9 @@ defmodule Cadet.CoursesTest do test "succeeds" do course = insert(:course) config = insert(:assessment_config, %{order: 1, course: course}) + assessment = insert(:assessment, %{course: course, config: config}) old_configs = Courses.get_assessment_configs(course.id) + refute Assessment |> Repo.get(assessment.id) |> is_nil() params = %{ assessment_config_id: config.id @@ -367,6 +370,7 @@ defmodule Cadet.CoursesTest do new_configs = Courses.get_assessment_configs(course.id) assert length(old_configs) == 1 assert new_configs == [] + assert Assessment |> Repo.get(assessment.id) |> is_nil() end test "error" do From e4299fcd2ed383af86a04b96e755d134a19585fb Mon Sep 17 00:00:00 2001 From: YaleChen299 Date: Tue, 6 Jul 2021 02:49:18 +0800 Subject: [PATCH 130/174] fix assessment grading view --- lib/cadet/assessments/assessments.ex | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/lib/cadet/assessments/assessments.ex b/lib/cadet/assessments/assessments.ex index b32055be7..89434e5a6 100644 --- a/lib/cadet/assessments/assessments.ex +++ b/lib/cadet/assessments/assessments.ex @@ -235,12 +235,14 @@ defmodule Cadet.Assessments do |> where(assessment_id: ^id) |> join(:left, [q], a in subquery(answer_query), on: q.id == a.question_id) |> join(:left, [_, a], g in assoc(a, :grader)) - |> select([q, a, g], {q, a, g}) + |> join(:left, [_, _, g], u in assoc(g, :user)) + |> select([q, a, g, u], {q, a, g, u}) |> order_by(:display_order) |> Repo.all() |> Enum.map(fn - {q, nil, _} -> %{q | answer: %Answer{grader: nil}} - {q, a, g} -> %{q | answer: %Answer{a | grader: g}} + {q, nil, _, _} -> %{q | answer: %Answer{grader: nil}} + {q, a, nil, _} -> %{q | answer: %Answer{a | grader: nil}} + {q, a, g, u} -> %{q | answer: %Answer{a | grader: %CourseRegistration{g | user: u}}} end) |> load_contest_voting_entries(course_reg.id) @@ -1189,7 +1191,7 @@ defmodule Cadet.Assessments do |> join(:inner, [_, q], ast in assoc(q, :assessment)) |> join(:inner, [a, ..., ast], ac in assoc(ast, :config)) |> join(:left, [a, ...], g in assoc(a, :grader)) - |> join(:inner, [a, ..., g], gu in assoc(g, :user)) + |> join(:left, [a, ..., g], gu in assoc(g, :user)) |> join(:inner, [a, ...], s in assoc(a, :submission)) |> join(:inner, [a, ..., s], st in assoc(s, :student)) |> join(:inner, [a, ..., st], u in assoc(st, :user)) From db6f7f883576a122c6a7982bda165e43dd2006b6 Mon Sep 17 00:00:00 2001 From: YaleChen299 Date: Tue, 6 Jul 2021 03:25:21 +0800 Subject: [PATCH 131/174] fix format and credo issues --- lib/cadet/accounts/course_registrations.ex | 3 +-- lib/cadet/assessments/assessments.ex | 2 +- lib/cadet/courses/courses.ex | 4 +++- lib/cadet_web/views/assessments_view.ex | 9 ++++++--- .../admin_courses_controller_test.exs | 3 +-- test/support/conn_case.ex | 12 +++++++----- 6 files changed, 19 insertions(+), 14 deletions(-) diff --git a/lib/cadet/accounts/course_registrations.ex b/lib/cadet/accounts/course_registrations.ex index 2472137c9..ca509a7fa 100644 --- a/lib/cadet/accounts/course_registrations.ex +++ b/lib/cadet/accounts/course_registrations.ex @@ -6,8 +6,7 @@ defmodule Cadet.Accounts.CourseRegistrations do import Ecto.Query - alias Cadet.Repo - alias Cadet.Accounts + alias Cadet.{Repo, Accounts} alias Cadet.Accounts.{User, CourseRegistration} alias Cadet.Assessments.{Answer, Submission} alias Cadet.Courses.AssessmentConfig diff --git a/lib/cadet/assessments/assessments.ex b/lib/cadet/assessments/assessments.ex index 89434e5a6..9c018adcc 100644 --- a/lib/cadet/assessments/assessments.ex +++ b/lib/cadet/assessments/assessments.ex @@ -691,7 +691,7 @@ defmodule Cadet.Assessments do def finalise_submission(submission = %Submission{}) do with {:status, :attempted} <- {:status, submission.status}, {:ok, updated_submission} <- update_submission_status_and_xp_bonus(submission) do - # TODO: Couple with update_submission_status_and_xp_bonus to ensure notification is sent + # Couple with update_submission_status_and_xp_bonus to ensure notification is sent Notifications.write_notification_when_student_submits(submission) # Begin autograding job GradingJob.force_grade_individual_submission(updated_submission) diff --git a/lib/cadet/courses/courses.ex b/lib/cadet/courses/courses.ex index 3b0847411..997803b50 100644 --- a/lib/cadet/courses/courses.ex +++ b/lib/cadet/courses/courses.ex @@ -191,7 +191,9 @@ defmodule Cadet.Courses do def upsert_groups_in_course(usernames_and_groups, course_id) do usernames_and_groups |> Enum.reduce_while(nil, fn %{username: username} = entry, _acc -> - case Map.fetch(entry, :group) do + entry + |> Map.fetch(:group) + |> case do {:ok, groupname} -> # Add users to group upsert_groups_in_course_helper(username, course_id, groupname) diff --git a/lib/cadet_web/views/assessments_view.ex b/lib/cadet_web/views/assessments_view.ex index a2050f157..20aad951f 100644 --- a/lib/cadet_web/views/assessments_view.ex +++ b/lib/cadet_web/views/assessments_view.ex @@ -47,9 +47,12 @@ defmodule CadetWeb.AssessmentsView do missionPDF: &Cadet.Assessments.Upload.url({&1.mission_pdf, &1}), questions: &Enum.map(&1.questions, fn question -> - build_question_with_answer_and_solution_if_ungraded(%{ - question: question - }) + map = + build_question_with_answer_and_solution_if_ungraded(%{ + question: question + }) + + map end) } ) diff --git a/test/cadet_web/admin_controllers/admin_courses_controller_test.exs b/test/cadet_web/admin_controllers/admin_courses_controller_test.exs index 0128334ff..6a16128f4 100644 --- a/test/cadet_web/admin_controllers/admin_courses_controller_test.exs +++ b/test/cadet_web/admin_controllers/admin_courses_controller_test.exs @@ -3,8 +3,7 @@ defmodule CadetWeb.AdminCoursesControllerTest do import Cadet.SharedHelper - alias Cadet.Repo - alias Cadet.Courses + alias Cadet.{Repo, Courses} alias Cadet.Courses.Course alias CadetWeb.AdminCoursesController diff --git a/test/support/conn_case.ex b/test/support/conn_case.ex index dda40a8f1..4abea05db 100644 --- a/test/support/conn_case.ex +++ b/test/support/conn_case.ex @@ -17,6 +17,8 @@ defmodule CadetWeb.ConnCase do import Plug.Conn + alias Cadet.Factory + using do quote do # Import conveniences for testing with connections @@ -50,13 +52,13 @@ defmodule CadetWeb.ConnCase do conn = Phoenix.ConnTest.build_conn() if tags[:authenticate] do - course = Cadet.Factory.insert(:course) - user = Cadet.Factory.insert(:user, %{latest_viewed: course}) + course = Factory.insert(:course) + user = Factory.insert(:user, %{latest_viewed: course}) course_registration = cond do is_atom(tags[:authenticate]) -> - Cadet.Factory.insert(:course_registration, %{ + Factory.insert(:course_registration, %{ user: user, course: course, role: tags[:authenticate] @@ -64,7 +66,7 @@ defmodule CadetWeb.ConnCase do # :TODO: This is_map case has not been handled. To recheck in the future. is_map(tags[:authenticate]) -> - Cadet.Factory.insert(:course_registration, tags[:authenticate]) + Factory.insert(:course_registration, tags[:authenticate]) true -> nil @@ -83,7 +85,7 @@ defmodule CadetWeb.ConnCase do {:ok, conn: conn} else if tags[:sign_in] do - user = Cadet.Factory.insert(:user, tags[:sign_in]) + user = Factory.insert(:user, tags[:sign_in]) conn = sign_in(conn, user) {:ok, conn: conn} else From 45db4ef6805f7d678602c2a9d55cb0f555933269 Mon Sep 17 00:00:00 2001 From: Chow En Rong <53928333+chownces@users.noreply.github.com> Date: Tue, 6 Jul 2021 12:38:08 +0800 Subject: [PATCH 132/174] Update migration file --- priv/repo/migrations/20210531155751_multitenant_upgrade.exs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/priv/repo/migrations/20210531155751_multitenant_upgrade.exs b/priv/repo/migrations/20210531155751_multitenant_upgrade.exs index 2661955bb..146f9f120 100644 --- a/priv/repo/migrations/20210531155751_multitenant_upgrade.exs +++ b/priv/repo/migrations/20210531155751_multitenant_upgrade.exs @@ -227,9 +227,11 @@ defmodule Cadet.Repo.Migrations.MultitenantUpgrade do # Create Assessment Configurations based on Source Academy Knight ["Missions", "Quests", "Paths", "Contests", "Others"] - |> Enum.each(fn assessment_type -> + |> Enum.with_index(1) + |> Enum.each(fn {assessment_type, idx} -> %AssessmentConfig{} |> AssessmentConfig.changeset(%{ + order: idx, type: assessment_type, course_id: course.id, show_grading_summary: assessment_type in ["Missions", "Quests"], From 6ae5609bdb268287135f00f6ce8fad76dad55aaa Mon Sep 17 00:00:00 2001 From: YaleChen299 Date: Tue, 6 Jul 2021 13:37:32 +0800 Subject: [PATCH 133/174] added unique constaints for assessment number and course_id --- lib/cadet/assessments/assessment.ex | 1 + lib/cadet/assessments/assessments.ex | 6 +++++- priv/repo/migrations/20210531155751_multitenant_upgrade.exs | 3 +++ 3 files changed, 9 insertions(+), 1 deletion(-) diff --git a/lib/cadet/assessments/assessment.ex b/lib/cadet/assessments/assessment.ex index da1b2e5c0..547e3e6e3 100644 --- a/lib/cadet/assessments/assessment.ex +++ b/lib/cadet/assessments/assessment.ex @@ -56,6 +56,7 @@ defmodule Cadet.Assessments.Assessment do |> add_belongs_to_id_from_model([:config, :course], params) |> foreign_key_constraint(:config_id) |> foreign_key_constraint(:course_id) + |> unique_constraint([:number, :course_id]) |> validate_config_course |> validate_open_close_date end diff --git a/lib/cadet/assessments/assessments.ex b/lib/cadet/assessments/assessments.ex index 9c018adcc..bd1f852ea 100644 --- a/lib/cadet/assessments/assessments.ex +++ b/lib/cadet/assessments/assessments.ex @@ -419,9 +419,13 @@ defmodule Cadet.Assessments do end @spec insert_or_update_assessment_changeset(map(), boolean()) :: Ecto.Changeset.t() - defp insert_or_update_assessment_changeset(params = %{number: number}, force_update) do + defp insert_or_update_assessment_changeset( + params = %{number: number, course_id: course_id}, + force_update + ) do Assessment |> where(number: ^number) + |> where(course_id: ^course_id) |> Repo.one() |> case do nil -> diff --git a/priv/repo/migrations/20210531155751_multitenant_upgrade.exs b/priv/repo/migrations/20210531155751_multitenant_upgrade.exs index 2661955bb..cce649d1c 100644 --- a/priv/repo/migrations/20210531155751_multitenant_upgrade.exs +++ b/priv/repo/migrations/20210531155751_multitenant_upgrade.exs @@ -80,6 +80,9 @@ defmodule Cadet.Repo.Migrations.MultitenantUpgrade do add(:course_id, references(:courses)) end + drop(unique_index(:assessments, [:number])) + create(unique_index(:assessments, [:number, :course_id])) + # Prep for migration of student_id and unsubmitted_by_id from User entity to CourseRegistration entity. rename(table(:submissions), :student_id, to: :temp_student_id) rename(table(:submissions), :unsubmitted_by_id, to: :temp_unsubmitted_by_id) From 250dcbd18de76517504188609464719bba3774f9 Mon Sep 17 00:00:00 2001 From: Chow En Rong <53928333+chownces@users.noreply.github.com> Date: Fri, 9 Jul 2021 17:33:17 +0800 Subject: [PATCH 134/174] rename user view crId to courseRegId --- .../admin_user_controller.ex | 8 +++--- lib/cadet_web/admin_views/admin_user_view.ex | 2 +- lib/cadet_web/views/user_view.ex | 2 +- .../admin_user_controller_test.exs | 28 +++++++++---------- .../controllers/user_controller_test.exs | 4 +-- 5 files changed, 22 insertions(+), 22 deletions(-) diff --git a/lib/cadet_web/admin_controllers/admin_user_controller.ex b/lib/cadet_web/admin_controllers/admin_user_controller.ex index 7b0e2ee62..263beec4a 100644 --- a/lib/cadet_web/admin_controllers/admin_user_controller.ex +++ b/lib/cadet_web/admin_controllers/admin_user_controller.ex @@ -90,7 +90,7 @@ defmodule CadetWeb.AdminUserController do end @update_role_roles ~w(admin)a - def update_role(conn, %{"role" => role, "crId" => coursereg_id}) do + def update_role(conn, %{"role" => role, "courseRegId" => coursereg_id}) do %{id: admin_course_reg_id, role: admin_role, course_id: admin_course_id} = conn.assigns.course_reg @@ -125,7 +125,7 @@ defmodule CadetWeb.AdminUserController do end @delete_user_roles ~w(admin)a - def delete_user(conn, %{"crId" => coursereg_id}) do + def delete_user(conn, %{"courseRegId" => coursereg_id}) do %{id: admin_course_reg_id, role: admin_role, course_id: admin_course_id} = conn.assigns.course_reg @@ -208,7 +208,7 @@ defmodule CadetWeb.AdminUserController do course_id(:path, :integer, "Course ID", required: true) role(:body, :role, "The new role", required: true) - crId(:body, :integer, "The course registration of the user whose role is to be updated", + courseRegId(:body, :integer, "The course registration of the user whose role is to be updated", required: true ) end @@ -232,7 +232,7 @@ defmodule CadetWeb.AdminUserController do parameters do course_id(:path, :integer, "Course ID", required: true) - crId(:body, :integer, "The course registration of the user whose role is to be updated", + courseRegId(:body, :integer, "The course registration of the user whose role is to be updated", required: true ) end diff --git a/lib/cadet_web/admin_views/admin_user_view.ex b/lib/cadet_web/admin_views/admin_user_view.ex index 70b9d8a51..61c48ca30 100644 --- a/lib/cadet_web/admin_views/admin_user_view.ex +++ b/lib/cadet_web/admin_views/admin_user_view.ex @@ -7,7 +7,7 @@ defmodule CadetWeb.AdminUserView do def render("cr.json", %{cr: cr}) do %{ - crId: cr.id, + courseRegId: cr.id, course_id: cr.course_id, name: cr.user.name, username: cr.user.username, diff --git a/lib/cadet_web/views/user_view.ex b/lib/cadet_web/views/user_view.ex index f8b79cce7..3b4f14a2d 100644 --- a/lib/cadet_web/views/user_view.ex +++ b/lib/cadet_web/views/user_view.ex @@ -67,7 +67,7 @@ defmodule CadetWeb.UserView do _ -> %{ - crId: latest.id, + courseRegId: latest.id, courseId: latest.course_id, role: latest.role, group: diff --git a/test/cadet_web/admin_controllers/admin_user_controller_test.exs b/test/cadet_web/admin_controllers/admin_user_controller_test.exs index 24c4fb24a..cbba73182 100644 --- a/test/cadet_web/admin_controllers/admin_user_controller_test.exs +++ b/test/cadet_web/admin_controllers/admin_user_controller_test.exs @@ -296,7 +296,7 @@ defmodule CadetWeb.AdminUserControllerTest do params = %{ "role" => "staff", - "crId" => user_course_reg.id + "courseRegId" => user_course_reg.id } resp = put(conn, build_url_users_role(course_id), params) @@ -316,7 +316,7 @@ defmodule CadetWeb.AdminUserControllerTest do params = %{ "role" => "student", - "crId" => user_course_reg.id + "courseRegId" => user_course_reg.id } resp = put(conn, build_url_users_role(course_id), params) @@ -336,7 +336,7 @@ defmodule CadetWeb.AdminUserControllerTest do params = %{ "role" => "staff", - "crId" => user_course_reg.id + "courseRegId" => user_course_reg.id } resp = put(conn, build_url_users_role(course_id), params) @@ -352,7 +352,7 @@ defmodule CadetWeb.AdminUserControllerTest do params = %{ "role" => "staff", - "crId" => 10_000 + "courseRegId" => 10_000 } conn = put(conn, build_url_users_role(course_id), params) @@ -367,7 +367,7 @@ defmodule CadetWeb.AdminUserControllerTest do params = %{ "role" => "staff", - "crId" => user_course_reg.id + "courseRegId" => user_course_reg.id } conn = put(conn, build_url_users_role(course_id), params) @@ -385,7 +385,7 @@ defmodule CadetWeb.AdminUserControllerTest do params = %{ "role" => "staff", - "crId" => user_course_reg.id + "courseRegId" => user_course_reg.id } conn = put(conn, build_url_users_role(course_id), params) @@ -403,7 +403,7 @@ defmodule CadetWeb.AdminUserControllerTest do params = %{ "role" => "avenger", - "crId" => user_course_reg.id + "courseRegId" => user_course_reg.id } conn = put(conn, build_url_users_role(course_id), params) @@ -424,7 +424,7 @@ defmodule CadetWeb.AdminUserControllerTest do user_course_reg = insert(:course_registration, %{role: :student, course: course}) params = %{ - "crId" => user_course_reg.id + "courseRegId" => user_course_reg.id } resp = delete(conn, build_url_users(course_id), params) @@ -442,7 +442,7 @@ defmodule CadetWeb.AdminUserControllerTest do user_course_reg = insert(:course_registration, %{role: :staff, course: course}) params = %{ - "crId" => user_course_reg.id + "courseRegId" => user_course_reg.id } resp = delete(conn, build_url_users(course_id), params) @@ -460,7 +460,7 @@ defmodule CadetWeb.AdminUserControllerTest do user_course_reg = insert(:course_registration, %{role: :student, course: course}) params = %{ - "crId" => user_course_reg.id + "courseRegId" => user_course_reg.id } conn = delete(conn, build_url_users(course_id), params) @@ -483,7 +483,7 @@ defmodule CadetWeb.AdminUserControllerTest do |> Repo.one() params = %{ - "crId" => own_course_reg.id + "courseRegId" => own_course_reg.id } conn = delete(conn, build_url_users(course_id), params) @@ -499,7 +499,7 @@ defmodule CadetWeb.AdminUserControllerTest do course_id = conn.assigns[:course_id] params = %{ - "crId" => 1 + "courseRegId" => 1 } conn = delete(conn, build_url_users(course_id), params) @@ -516,7 +516,7 @@ defmodule CadetWeb.AdminUserControllerTest do user_course_reg = insert(:course_registration, %{role: :admin, course: course}) params = %{ - "crId" => user_course_reg.id + "courseRegId" => user_course_reg.id } conn = delete(conn, build_url_users(course_id), params) @@ -532,7 +532,7 @@ defmodule CadetWeb.AdminUserControllerTest do user_course_reg = insert(:course_registration, %{role: :student}) params = %{ - "crId" => user_course_reg.id + "courseRegId" => user_course_reg.id } conn = delete(conn, build_url_users(course_id), params) diff --git a/test/cadet_web/controllers/user_controller_test.exs b/test/cadet_web/controllers/user_controller_test.exs index 169299a94..c71bc29f3 100644 --- a/test/cadet_web/controllers/user_controller_test.exs +++ b/test/cadet_web/controllers/user_controller_test.exs @@ -79,7 +79,7 @@ defmodule CadetWeb.UserControllerTest do ] }, "courseRegistration" => %{ - "crId" => cr.id, + "courseRegId" => cr.id, "courseId" => course.id, "role" => "#{cr.role}", "group" => nil, @@ -378,7 +378,7 @@ defmodule CadetWeb.UserControllerTest do expected = %{ "courseRegistration" => %{ - "crId" => cr.id, + "courseRegId" => cr.id, "courseId" => course.id, "role" => "#{cr.role}", "group" => nil, From af51543b621044ea911873bd9361c92d2002ee12 Mon Sep 17 00:00:00 2001 From: Chow En Rong <53928333+chownces@users.noreply.github.com> Date: Fri, 9 Jul 2021 17:35:19 +0800 Subject: [PATCH 135/174] updated jobs and autograder with test --- lib/cadet/jobs/autograder/grading_job.ex | 4 +- lib/cadet/jobs/autograder/lambda_worker.ex | 13 ++- .../jobs/autograder/result_store_worker.ex | 15 +-- lib/cadet/jobs/autograder/utilities.ex | 14 ++- .../admin_user_controller.ex | 10 +- .../jobs/autograder/grading_job_test.exs | 102 +++++++++++------- .../jobs/autograder/lambda_worker_test.exs | 14 ++- .../autograder/result_store_worker_test.exs | 29 +++-- test/cadet/jobs/autograder/utilities_test.exs | 44 ++++---- .../custom_cassettes/autograder/errors#1.json | 2 +- .../autograder/success#1.json | 2 +- 11 files changed, 132 insertions(+), 117 deletions(-) diff --git a/lib/cadet/jobs/autograder/grading_job.ex b/lib/cadet/jobs/autograder/grading_job.ex index 63b2ac542..2349ad7d1 100644 --- a/lib/cadet/jobs/autograder/grading_job.ex +++ b/lib/cadet/jobs/autograder/grading_job.ex @@ -139,7 +139,7 @@ defmodule Cadet.Autograder.GradingJob do Submission |> where(id: ^submission_id) |> join(:inner, [s], sv in SubmissionVotes, - on: sv.user_id == s.student_id and sv.question_id == ^question.id + on: sv.voter_id == s.student_id and sv.question_id == ^question.id ) |> where([_, sv], is_nil(sv.rank)) |> Repo.exists?() @@ -148,7 +148,6 @@ defmodule Cadet.Autograder.GradingJob do answer |> Answer.autograding_changeset(%{ - adjustment: 0, xp_adjustment: 0, xp: xp, autograding_status: :success @@ -167,7 +166,6 @@ defmodule Cadet.Autograder.GradingJob do answer |> Answer.autograding_changeset(%{ - adjustment: 0, xp_adjustment: 0, xp: xp, autograding_status: :success diff --git a/lib/cadet/jobs/autograder/lambda_worker.ex b/lib/cadet/jobs/autograder/lambda_worker.ex index a6c8563f2..e2b9c20e2 100644 --- a/lib/cadet/jobs/autograder/lambda_worker.ex +++ b/lib/cadet/jobs/autograder/lambda_worker.ex @@ -56,7 +56,8 @@ defmodule Cadet.Autograder.LambdaWorker do %{ answer_id: answer.id, result: %{ - grade: 0, + score: 0, + max_score: 1, status: :failed, result: [ %{ @@ -105,7 +106,8 @@ defmodule Cadet.Autograder.LambdaWorker do # %{"errorMessage" => "${message}"} if Map.has_key?(response, "errorMessage") do %{ - grade: 0, + score: 0, + max_score: 1, status: :failed, result: [ %{ @@ -117,7 +119,12 @@ defmodule Cadet.Autograder.LambdaWorker do ] } else - %{grade: response["totalScore"], result: response["results"], status: :success} + %{ + score: response["totalScore"], + max_score: response["maxScore"], + result: response["results"], + status: :success + } end end end diff --git a/lib/cadet/jobs/autograder/result_store_worker.ex b/lib/cadet/jobs/autograder/result_store_worker.ex index 6fa601234..0851de8de 100644 --- a/lib/cadet/jobs/autograder/result_store_worker.ex +++ b/lib/cadet/jobs/autograder/result_store_worker.ex @@ -56,7 +56,7 @@ defmodule Cadet.Autograder.ResultStoreWorker do defp update_answer(answer = %Answer{}, result = %{status: status}, overwrite) do xp = cond do - answer.question.max_grade == 0 and length(result.result) > 0 -> + result.max_score == 0 and length(result.result) > 0 -> testcase_results = result.result num_passed = @@ -64,23 +64,14 @@ defmodule Cadet.Autograder.ResultStoreWorker do Integer.floor_div(answer.question.max_xp * num_passed, length(testcase_results)) - answer.question.max_grade == 0 -> + result.max_score == 0 -> 0 true -> - Integer.floor_div(answer.question.max_xp * result.grade, answer.question.max_grade) - end - - new_adjustment = - if not overwrite and answer.grader_id do - answer.adjustment - result.grade - else - 0 + Integer.floor_div(answer.question.max_xp * result.score, result.max_score) end changes = %{ - adjustment: new_adjustment, - grade: result.grade, xp: xp, autograding_status: status, autograding_results: result.result diff --git a/lib/cadet/jobs/autograder/utilities.ex b/lib/cadet/jobs/autograder/utilities.ex index b491c58c3..23f3ed80f 100644 --- a/lib/cadet/jobs/autograder/utilities.ex +++ b/lib/cadet/jobs/autograder/utilities.ex @@ -8,7 +8,7 @@ defmodule Cadet.Autograder.Utilities do import Ecto.Query - alias Cadet.Accounts.User + alias Cadet.Accounts.CourseRegistration alias Cadet.Assessments.{Answer, Assessment, Question, Submission} def dispatch_programming_answer(answer = %Answer{}, question = %Question{}, overwrite \\ false) do @@ -26,15 +26,15 @@ defmodule Cadet.Autograder.Utilities do end def fetch_submissions(assessment_id) when is_ecto_id(assessment_id) do - User + CourseRegistration |> where(role: "student") |> join( :left, - [u], + [cr], s in Submission, - on: u.id == s.student_id and s.assessment_id == ^assessment_id + on: cr.id == s.student_id and s.assessment_id == ^assessment_id ) - |> select([u, s], %{student_id: u.id, submission: s}) + |> select([cr, s], %{student_id: cr.id, submission: s}) |> Repo.all() end @@ -42,10 +42,8 @@ defmodule Cadet.Autograder.Utilities do Assessment |> where(is_published: true) |> where([a], a.close_at < ^Timex.now() and a.close_at >= ^Timex.shift(Timex.now(), days: -1)) - |> join(:inner, [a], c in assoc(a, :config)) - |> where([a, c], not c.is_contest) |> join(:inner, [a, c], q in assoc(a, :questions)) - |> preload([_, _, q], questions: q) + |> preload([_, q], questions: q) |> Repo.all() |> Enum.map(&sort_assessment_questions(&1)) end diff --git a/lib/cadet_web/admin_controllers/admin_user_controller.ex b/lib/cadet_web/admin_controllers/admin_user_controller.ex index 263beec4a..29b1b751e 100644 --- a/lib/cadet_web/admin_controllers/admin_user_controller.ex +++ b/lib/cadet_web/admin_controllers/admin_user_controller.ex @@ -208,7 +208,10 @@ defmodule CadetWeb.AdminUserController do course_id(:path, :integer, "Course ID", required: true) role(:body, :role, "The new role", required: true) - courseRegId(:body, :integer, "The course registration of the user whose role is to be updated", + courseRegId( + :body, + :integer, + "The course registration of the user whose role is to be updated", required: true ) end @@ -232,7 +235,10 @@ defmodule CadetWeb.AdminUserController do parameters do course_id(:path, :integer, "Course ID", required: true) - courseRegId(:body, :integer, "The course registration of the user whose role is to be updated", + courseRegId( + :body, + :integer, + "The course registration of the user whose role is to be updated", required: true ) end diff --git a/test/cadet/jobs/autograder/grading_job_test.exs b/test/cadet/jobs/autograder/grading_job_test.exs index 1e5ce488f..27ca0b66d 100644 --- a/test/cadet/jobs/autograder/grading_job_test.exs +++ b/test/cadet/jobs/autograder/grading_job_test.exs @@ -23,12 +23,16 @@ defmodule Cadet.Autograder.GradingJobTest do describe "#force_grade_individual_submission, all programming questions" do setup do + course = insert(:course) + assessment_config = insert(:assessment_config, %{course: course}) + assessments = insert_list(3, :assessment, %{ is_published: true, open_at: Timex.shift(Timex.now(), days: -5), close_at: Timex.shift(Timex.now(), hours: -4), - type: "mission" + config: assessment_config, + course: course }) questions = @@ -36,13 +40,13 @@ defmodule Cadet.Autograder.GradingJobTest do insert_list(3, :programming_question, %{assessment: assessment}) end - %{assessments: Enum.zip(assessments, questions)} + %{course: course, assessments: Enum.zip(assessments, questions)} end test "all assessments attempted, all questions graded, assocs preloaded, should enqueue all jobs", - %{assessments: assessments} do + %{course: course, assessments: assessments} do with_mock Que, add: fn _, _ -> nil end do - student = insert(:user, %{role: :student}) + student = insert(:course_registration, %{course: course, role: :student}) [{assessment, questions} | _] = assessments @@ -73,9 +77,9 @@ defmodule Cadet.Autograder.GradingJobTest do test "all assessments attempted, all questions graded, no assocs preloaded, " <> "should enqueue all jobs", - %{assessments: assessments} do + %{course: course, assessments: assessments} do with_mock Que, add: fn _, _ -> nil end do - student = insert(:user, %{role: :student}) + student = insert(:course_registration, %{course: course, role: :student}) [{assessment, questions} | _] = assessments @@ -107,12 +111,16 @@ defmodule Cadet.Autograder.GradingJobTest do describe "#grade_all_due_yesterday, all programming questions" do setup do + course = insert(:course) + assessment_config = insert(:assessment_config, %{course: course}) + assessments = insert_list(3, :assessment, %{ is_published: true, open_at: Timex.shift(Timex.now(), days: -5), close_at: Timex.shift(Timex.now(), hours: -4), - type: "mission" + config: assessment_config, + course: course }) questions = @@ -120,14 +128,15 @@ defmodule Cadet.Autograder.GradingJobTest do insert_list(3, :programming_question, %{assessment: assessment}) end - %{assessments: Enum.zip(assessments, questions)} + %{course: course, assessments: Enum.zip(assessments, questions)} end test "all assessments attempted, all questions answered, should enqueue all jobs", %{ + course: course, assessments: assessments } do with_mock Que, add: fn _, _ -> nil end do - student = insert(:user, %{role: :student}) + student = insert(:course_registration, %{course: course, role: :student}) submissions_answers = Enum.map(assessments, fn {assessment, questions} -> @@ -165,9 +174,10 @@ defmodule Cadet.Autograder.GradingJobTest do end test "all assessments attempted, all questions graded, should not do anything", %{ + course: course, assessments: assessments } do - student = insert(:user, %{role: :student}) + student = insert(:course_registration, %{course: course, role: :student}) Enum.map(assessments, fn {assessment, questions} -> submission = @@ -190,11 +200,14 @@ defmodule Cadet.Autograder.GradingJobTest do test "all assessments attempted, should update all submission statuses and create notifications", %{ + course: course, assessments: assessments } do with_mock Que, add: fn _, _ -> nil end do - group = insert(:group) - student = insert(:student, %{group_id: group.id}) + group = insert(:group, %{course: course}) + + student = + insert(:course_registration, %{course: course, role: :student, group_id: group.id}) submissions = Enum.map(assessments, fn {assessment, _} -> @@ -216,11 +229,14 @@ defmodule Cadet.Autograder.GradingJobTest do test "all assessments submitted, should not create notifications", %{ + course: course, assessments: assessments } do with_mock Que, add: fn _, _ -> nil end do - group = insert(:group) - student = insert(:student, %{group_id: group.id}) + group = insert(:group, %{course: course}) + + student = + insert(:course_registration, %{course: course, role: :student, group_id: group.id}) submissions = Enum.map(assessments, fn {assessment, _} -> @@ -239,10 +255,11 @@ defmodule Cadet.Autograder.GradingJobTest do end test "all assessments unattempted, should create submissions", %{ + course: course, assessments: assessments } do with_mock Que, add: fn _, _ -> nil end do - student = insert(:user, %{role: :student}) + student = insert(:course_registration, %{course: course, role: :student}) GradingJob.grade_all_due_yesterday() @@ -261,9 +278,10 @@ defmodule Cadet.Autograder.GradingJobTest do test "all assessments attempting, no questions answered, " <> "should insert empty answers, should not enqueue any", %{ + course: course, assessments: assessments } do - student = insert(:user, %{role: :student}) + student = insert(:course_registration, %{course: course, role: :student}) for {assessment, _} <- assessments do insert(:submission, %{student: student, assessment: assessment, status: :attempting}) @@ -281,7 +299,6 @@ defmodule Cadet.Autograder.GradingJobTest do assert Enum.count(answers) == 9 for answer <- answers do - assert answer.grade == 0 assert answer.autograding_status == :success assert answer.answer == %{"code" => "// Question was left blank by the student."} end @@ -293,10 +310,11 @@ defmodule Cadet.Autograder.GradingJobTest do test "all assessments attempting, first question unanswered, " <> "should insert empty answer, should dispatch submitted answers", %{ + course: course, assessments: assessments } do with_mock Que, add: fn _, _ -> nil end do - student = insert(:user, %{role: :student}) + student = insert(:course_registration, %{course: course, role: :student}) # Do not answer first question in each assessment submissions_answers = @@ -343,7 +361,6 @@ defmodule Cadet.Autograder.GradingJobTest do |> Enum.filter(&(&1.question_id in unanswered_question_ids)) for answer <- inserted_empty_answers do - assert answer.grade == 0 assert answer.xp == 0 assert answer.autograding_status == :success assert answer.answer == %{"code" => "// Question was left blank by the student."} @@ -353,10 +370,11 @@ defmodule Cadet.Autograder.GradingJobTest do test "all assessments attempted, all questions answered, instance raced, should not do anything", %{ + course: course, assessments: assessments } do with_mock Cadet.Jobs.Log, log_execution: fn _name, _period -> false end do - student = insert(:user, %{role: :student}) + student = insert(:course_registration, %{course: course, role: :student}) submissions_answers = Enum.map(assessments, fn {assessment, questions} -> @@ -395,28 +413,33 @@ defmodule Cadet.Autograder.GradingJobTest do describe "#grade_all_due_yesterday, all mcq questions" do setup do + course = insert(:course) + assessment_config = insert(:assessment_config, %{course: course}) + assessments = insert_list(3, :assessment, %{ is_published: true, open_at: Timex.shift(Timex.now(), days: -5), close_at: Timex.shift(Timex.now(), hours: -4), - type: "mission" + config: assessment_config, + course: course }) questions = for assessment <- assessments do - insert_list(3, :mcq_question, %{max_grade: 20, assessment: assessment}) + insert_list(3, :mcq_question, %{max_xp: 200, assessment: assessment}) end - %{assessments: Enum.zip(assessments, questions)} + %{course: course, assessments: Enum.zip(assessments, questions)} end test "all assessments attempted, all questions unanswered, " <> "should insert empty answers, should not enqueue any", %{ + course: course, assessments: assessments } do - student = insert(:user, %{role: :student}) + student = insert(:course_registration, %{course: course, role: :student}) for {assessment, _} <- assessments do insert(:submission, %{student: student, assessment: assessment, status: :attempting}) @@ -434,7 +457,6 @@ defmodule Cadet.Autograder.GradingJobTest do assert Enum.count(answers) == 9 for answer <- answers do - assert answer.grade == 0 assert answer.xp == 0 assert answer.autograding_status == :success assert answer.answer == %{"choice_id" => 0} @@ -446,9 +468,10 @@ defmodule Cadet.Autograder.GradingJobTest do test "all assessments attempted, all questions answered, " <> "should grade all questions, should not enqueue any", %{ + course: course, assessments: assessments } do - student = insert(:user, %{role: :student}) + student = insert(:course_registration, %{course: course, role: :student}) submissions_answers = Enum.map(assessments, fn {assessment, questions} -> @@ -481,10 +504,8 @@ defmodule Cadet.Autograder.GradingJobTest do # seeded questions have correct choice as 0 if answer_db.answer["choice_id"] == 0 do - assert answer_db.grade == question.max_grade assert answer_db.xp == question.max_xp else - assert answer_db.grade == 0 assert answer_db.xp == 0 end @@ -497,28 +518,33 @@ defmodule Cadet.Autograder.GradingJobTest do describe "#grade_all_due_yesterday, all voting questions" do setup do + course = insert(:course) + assessment_config = insert(:assessment_config, %{course: course}) + assessments = insert_list(3, :assessment, %{ is_published: true, open_at: Timex.shift(Timex.now(), days: -5), close_at: Timex.shift(Timex.now(), hours: -4), - type: "mission" + config: assessment_config, + course: course }) questions = for assessment <- assessments do - insert_list(3, :voting_question, %{max_grade: 20, assessment: assessment}) + insert_list(3, :voting_question, %{max_xp: 20, assessment: assessment}) end - %{assessments: Enum.zip(assessments, questions)} + %{course: course, assessments: Enum.zip(assessments, questions)} end test "all assessments attempted, all questions unanswered, " <> "should insert empty answers, should not enqueue any", %{ + course: course, assessments: assessments } do - student = insert(:user, %{role: :student}) + student = insert(:course_registration, %{course: course, role: :student}) for {assessment, _} <- assessments do insert(:submission, %{student: student, assessment: assessment, status: :attempting}) @@ -536,7 +562,6 @@ defmodule Cadet.Autograder.GradingJobTest do assert Enum.count(answers) == 9 for answer <- answers do - assert answer.grade == 0 assert answer.xp == 0 assert answer.autograding_status == :success assert answer.answer == %{"completed" => false} @@ -548,9 +573,10 @@ defmodule Cadet.Autograder.GradingJobTest do test "all assessments attempted, all questions aswered, " <> "should grade all questions, should not enqueue any", %{ + course: course, assessments: assessments } do - student = insert(:user, %{role: :student}) + student = insert(:course_registration, %{course: course, role: :student}) submissions_answers = for {assessment, questions} <- assessments do @@ -560,8 +586,8 @@ defmodule Cadet.Autograder.GradingJobTest do answers = for question <- questions do case Enum.random(0..1) do - 0 -> insert(:submission_vote, %{user: student, question: question, rank: 1}) - 1 -> insert(:submission_vote, %{user: student, question: question}) + 0 -> insert(:submission_vote, %{voter: student, question: question, rank: 1}) + 1 -> insert(:submission_vote, %{voter: student, question: question}) end insert(:answer, %{ @@ -586,7 +612,7 @@ defmodule Cadet.Autograder.GradingJobTest do for {question, answer} <- Enum.zip(questions, answers) do is_nil_entries = SubmissionVotes - |> where(user_id: ^student.id) + |> where(voter_id: ^student.id) |> where(question_id: ^question.id) |> where([sv], is_nil(sv.rank)) |> Repo.exists?() @@ -594,10 +620,8 @@ defmodule Cadet.Autograder.GradingJobTest do answer_db = Repo.get(Answer, answer.id) if is_nil_entries do - assert answer_db.grade == 0 assert answer_db.xp == 0 else - assert answer_db.grade == question.max_grade assert answer_db.xp == question.max_xp end diff --git a/test/cadet/jobs/autograder/lambda_worker_test.exs b/test/cadet/jobs/autograder/lambda_worker_test.exs index 03535b164..faa73cd22 100644 --- a/test/cadet/jobs/autograder/lambda_worker_test.exs +++ b/test/cadet/jobs/autograder/lambda_worker_test.exs @@ -32,7 +32,7 @@ defmodule Cadet.Autograder.LambdaWorkerTest do submission = insert(:submission, %{ - student: insert(:user, %{role: :student}), + student: insert(:course_registration, %{role: :student}), assessment: question.assessment }) @@ -63,7 +63,8 @@ defmodule Cadet.Autograder.LambdaWorkerTest do %{"resultType" => "pass", "score" => 1}, %{"resultType" => "pass", "score" => 1} ], - grade: 2, + score: 2, + max_score: 2, status: :success } }) @@ -112,7 +113,8 @@ defmodule Cadet.Autograder.LambdaWorkerTest do ] } ], - grade: 0, + score: 0, + max_score: 2, status: :success } }) @@ -133,7 +135,8 @@ defmodule Cadet.Autograder.LambdaWorkerTest do Que.add(ResultStoreWorker, %{ answer_id: answer.id, result: %{ - grade: 0, + score: 0, + max_score: 1, status: :failed, result: [ %{ @@ -202,7 +205,8 @@ defmodule Cadet.Autograder.LambdaWorkerTest do %{ answer_id: answer.id, result: %{ - grade: 0, + score: 0, + max_score: 1, status: :failed, result: [ %{ diff --git a/test/cadet/jobs/autograder/result_store_worker_test.exs b/test/cadet/jobs/autograder/result_store_worker_test.exs index 7a20d6493..a907b809d 100644 --- a/test/cadet/jobs/autograder/result_store_worker_test.exs +++ b/test/cadet/jobs/autograder/result_store_worker_test.exs @@ -8,7 +8,7 @@ defmodule Cadet.Autograder.ResultStoreWorkerTest do setup do answer = insert(:answer, %{question: insert(:question), submission: insert(:submission)}) - success_no_errors = %{status: :success, grade: 10, result: []} + success_no_errors = %{status: :success, score: 10, max_score: 10, result: []} success_with_errors = %{ result: [ @@ -37,7 +37,8 @@ defmodule Cadet.Autograder.ResultStoreWorkerTest do ] } ], - grade: 0, + score: 0, + max_score: 10, status: :success } @@ -47,7 +48,8 @@ defmodule Cadet.Autograder.ResultStoreWorkerTest do "systemError" => "Autograder runtime error. Please contact a system administrator" } ], - grade: 0, + score: 0, + max_score: 10, status: :failed } @@ -83,16 +85,13 @@ defmodule Cadet.Autograder.ResultStoreWorkerTest do end) end) - assert answer.grade == result.grade - assert answer.adjustment == 0 - - if answer.question.max_grade == 0 do + if result.max_score == 0 do assert answer.xp == 0 else assert answer.xp == Integer.floor_div( - answer.question.max_xp * answer.grade, - answer.question.max_grade + answer.question.max_xp * result.score, + result.max_score ) end @@ -102,13 +101,12 @@ defmodule Cadet.Autograder.ResultStoreWorkerTest do end test "after manual grading", %{results: results} do - grader = insert(:user, %{role: :staff}) + grader = insert(:course_registration, %{role: :staff}) answer = insert(:answer, %{ question: insert(:question), submission: insert(:submission), - adjustment: 5, grader_id: grader.id }) @@ -128,16 +126,13 @@ defmodule Cadet.Autograder.ResultStoreWorkerTest do end) end) - assert answer.grade == result.grade - assert answer.adjustment == 5 - result.grade - - if answer.question.max_grade == 0 do + if result.max_score == 0 do assert answer.xp == 0 else assert answer.xp == Integer.floor_div( - answer.question.max_xp * answer.grade, - answer.question.max_grade + answer.question.max_xp * result.score, + result.max_score ) end diff --git a/test/cadet/jobs/autograder/utilities_test.exs b/test/cadet/jobs/autograder/utilities_test.exs index 6619eb87b..dfc234a36 100644 --- a/test/cadet/jobs/autograder/utilities_test.exs +++ b/test/cadet/jobs/autograder/utilities_test.exs @@ -1,17 +1,20 @@ defmodule Cadet.Autograder.UtilitiesTest do use Cadet.DataCase - alias Cadet.Assessments.Assessment alias Cadet.Autograder.Utilities describe "fetch_assessments_due_yesterday" do test "it only returns yesterday's assessments" do + course = insert(:course) + config = insert(:assessment_config, %{course: course}) + yesterday = insert_list(2, :assessment, %{ is_published: true, open_at: Timex.shift(Timex.now(), days: -5), close_at: Timex.shift(Timex.now(), hours: -4), - type: "mission" + course: course, + config: config }) past = @@ -19,7 +22,8 @@ defmodule Cadet.Autograder.UtilitiesTest do is_published: true, open_at: Timex.shift(Timex.now(), days: -5), close_at: Timex.shift(Timex.now(), days: -4), - type: "mission" + course: course, + config: config }) future = @@ -27,7 +31,8 @@ defmodule Cadet.Autograder.UtilitiesTest do is_published: true, open_at: Timex.shift(Timex.now(), days: -3), close_at: Timex.shift(Timex.now(), days: 4), - type: "mission" + course: course, + config: config }) for assessment <- yesterday ++ past ++ future do @@ -38,32 +43,17 @@ defmodule Cadet.Autograder.UtilitiesTest do get_assessments_ids(Utilities.fetch_assessments_due_yesterday()) end - test "it return only paths, missions, sidequests" do - assessments = - for type <- Assessment.assessment_types() do - insert(:assessment, %{ - is_published: true, - open_at: Timex.shift(Timex.now(), days: -5), - close_at: Timex.shift(Timex.now(), hours: -4), - type: type - }) - end - - for assessment <- assessments do - insert_list(2, :programming_question, %{assessment: assessment}) - end - - assert get_assessments_ids(Enum.filter(assessments, &(!&1.config.is_contest))) == - get_assessments_ids(Utilities.fetch_assessments_due_yesterday()) - end - test "it returns assessment questions in sorted order" do + course = insert(:course) + config = insert(:assessment_config, %{course: course}) + assessment = insert(:assessment, %{ is_published: true, open_at: Timex.shift(Timex.now(), days: -5), close_at: Timex.shift(Timex.now(), hours: -4), - type: "mission" + course: course, + config: config }) insert_list(5, :programming_question, %{assessment: assessment}) @@ -80,8 +70,10 @@ defmodule Cadet.Autograder.UtilitiesTest do describe "fetch_submissions" do setup do - assessment = insert(:assessment, %{is_published: true}) - students = insert_list(5, :user, %{role: :student}) + course = insert(:course) + config = insert(:assessment_config, %{course: course}) + assessment = insert(:assessment, %{is_published: true, course: course, config: config}) + students = insert_list(5, :course_registration, %{role: :student, course: course}) %{students: students, assessment: assessment} end diff --git a/test/fixtures/custom_cassettes/autograder/errors#1.json b/test/fixtures/custom_cassettes/autograder/errors#1.json index 639ac9d49..6539101e2 100644 --- a/test/fixtures/custom_cassettes/autograder/errors#1.json +++ b/test/fixtures/custom_cassettes/autograder/errors#1.json @@ -21,7 +21,7 @@ "response": { "binary": false, "body": - "{\r\n \"totalScore\": 0,\r\n \"results\": [\r\n {\r\n \"resultType\": \"error\",\r\n \"errors\": [\r\n {\r\n \"errorType\": \"syntax\",\r\n \"line\": 1,\r\n \"location\": \"student\",\r\n \"errorLine\": \"consst f = i => i === 0 ? 0 : i < 3 ? 1 : f(i-1) + f(i-2);\",\r\n \"errorExplanation\": \"SyntaxError: Unexpected token (2:7)\"\r\n }\r\n ]\r\n },\r\n {\r\n \"resultType\": \"error\",\r\n \"errors\": [\r\n {\r\n \"errorType\": \"syntax\",\r\n \"line\": 1,\r\n \"location\": \"student\",\r\n \"errorLine\": \"consst f = i => i === 0 ? 0 : i < 3 ? 1 : f(i-1) + f(i-2);\",\r\n \"errorExplanation\": \"SyntaxError: Unexpected token (2:7)\"\r\n }\r\n ]\r\n }\r\n ]\r\n}", + "{\r\n \"totalScore\": 0,\r\n \"maxScore\": 2,\r\n \"results\": [\r\n {\r\n \"resultType\": \"error\",\r\n \"errors\": [\r\n {\r\n \"errorType\": \"syntax\",\r\n \"line\": 1,\r\n \"location\": \"student\",\r\n \"errorLine\": \"consst f = i => i === 0 ? 0 : i < 3 ? 1 : f(i-1) + f(i-2);\",\r\n \"errorExplanation\": \"SyntaxError: Unexpected token (2:7)\"\r\n }\r\n ]\r\n },\r\n {\r\n \"resultType\": \"error\",\r\n \"errors\": [\r\n {\r\n \"errorType\": \"syntax\",\r\n \"line\": 1,\r\n \"location\": \"student\",\r\n \"errorLine\": \"consst f = i => i === 0 ? 0 : i < 3 ? 1 : f(i-1) + f(i-2);\",\r\n \"errorExplanation\": \"SyntaxError: Unexpected token (2:7)\"\r\n }\r\n ]\r\n }\r\n ]\r\n}", "headers": { "Date": "Thu, 09 Aug 2018 09:53:29 GMT", "Content-Type": "application/json", diff --git a/test/fixtures/custom_cassettes/autograder/success#1.json b/test/fixtures/custom_cassettes/autograder/success#1.json index 68f3f6845..e0257695d 100644 --- a/test/fixtures/custom_cassettes/autograder/success#1.json +++ b/test/fixtures/custom_cassettes/autograder/success#1.json @@ -22,7 +22,7 @@ "response": { "binary": false, "body": - "{\r\n \"totalScore\": 2,\r\n \"results\": [\r\n {\r\n \"resultType\": \"pass\",\r\n \"score\": 1\r\n },\r\n {\r\n \"resultType\": \"pass\",\r\n \"score\": 1\r\n }\r\n ]\r\n}", + "{\r\n \"totalScore\": 2,\r\n \"maxScore\": 2,\r\n \"results\": [\r\n {\r\n \"resultType\": \"pass\",\r\n \"score\": 1\r\n },\r\n {\r\n \"resultType\": \"pass\",\r\n \"score\": 1\r\n }\r\n ]\r\n}", "headers": { "Date": "Thu, 09 Aug 2018 09:50:53 GMT", "Content-Type": "application/json", From 6866be58bc9711b3ae2cbc5270f981953682ad23 Mon Sep 17 00:00:00 2001 From: Chow En Rong <53928333+chownces@users.noreply.github.com> Date: Fri, 9 Jul 2021 17:35:50 +0800 Subject: [PATCH 136/174] update assessments controller test --- .../admin_controllers/admin_goals_controller_test.exs | 2 +- test/cadet_web/controllers/assessments_controller_test.exs | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/test/cadet_web/admin_controllers/admin_goals_controller_test.exs b/test/cadet_web/admin_controllers/admin_goals_controller_test.exs index 1059952c8..7a13d6bb4 100644 --- a/test/cadet_web/admin_controllers/admin_goals_controller_test.exs +++ b/test/cadet_web/admin_controllers/admin_goals_controller_test.exs @@ -169,7 +169,7 @@ defmodule CadetWeb.AdminGoalsControllerTest do describe "POST /admin/users/:userid/goals/:uuid/progress" do setup do {:ok, g} = %Goal{uuid: UUID.generate()} |> Map.merge(goal_literal(5)) |> Repo.insert() - {:ok, u} = %User{name: "a", role: :student} |> Repo.insert() + u = insert(:course_registration, %{role: :student}) %{goal: g, user: u} end diff --git a/test/cadet_web/controllers/assessments_controller_test.exs b/test/cadet_web/controllers/assessments_controller_test.exs index c7f31a87a..276fc53ac 100644 --- a/test/cadet_web/controllers/assessments_controller_test.exs +++ b/test/cadet_web/controllers/assessments_controller_test.exs @@ -343,6 +343,7 @@ defmodule CadetWeb.AssessmentsControllerTest do &%{ "id" => &1.id, "type" => "#{&1.type}", + "blocking" => &1.blocking, "content" => &1.question.content, "solutionTemplate" => &1.question.template, "prepend" => &1.question.prepend, @@ -382,6 +383,7 @@ defmodule CadetWeb.AssessmentsControllerTest do &%{ "id" => &1.id, "type" => "#{&1.type}", + "blocking" => &1.blocking, "content" => &1.question.content, "choices" => Enum.map( @@ -403,6 +405,7 @@ defmodule CadetWeb.AssessmentsControllerTest do &%{ "id" => &1.id, "type" => "#{&1.type}", + "blocking" => &1.blocking, "content" => &1.question.content, "solutionTemplate" => &1.question.template, "prepend" => &1.question.prepend From ab2f58c4f1a62b455dd585f58e95cc43516935f9 Mon Sep 17 00:00:00 2001 From: Chow En Rong <53928333+chownces@users.noreply.github.com> Date: Fri, 9 Jul 2021 17:36:16 +0800 Subject: [PATCH 137/174] updated grading summary with test --- lib/cadet/assessments/assessments.ex | 42 ++++++----- .../admin_grading_controller.ex | 4 +- .../admin_views/admin_grading_view.ex | 15 +--- .../admin_grading_controller_test.exs | 69 +++++++++++++------ 4 files changed, 77 insertions(+), 53 deletions(-) diff --git a/lib/cadet/assessments/assessments.ex b/lib/cadet/assessments/assessments.ex index bd1f852ea..4ed186a52 100644 --- a/lib/cadet/assessments/assessments.ex +++ b/lib/cadet/assessments/assessments.ex @@ -1441,22 +1441,17 @@ defmodule Cadet.Assessments do config_type: t.config_type, ungraded: filter(count(), t.num_ungraded > 0), submitted: count() - # ungraded: cols |> Enum.map(fn graded_config -> Map.put(graded_config, :ungraded_count, filter(count(), t.config_id == graded_config.id and t.num_ungraded > 0))end), - # submitted: cols |> Enum.map(fn graded_config -> Map.put(graded_config, :submitted_count, filter(count(), t.config_id == graded_config.id))end) - # ungraded_missions: filter(count(), t.type == "mission" and t.num_ungraded > 0), - # submitted_missions: filter(count(), t.type == "mission"), - # ungraded_sidequests: filter(count(), t.type == "sidequest" and t.num_ungraded > 0), - # submitted_sidequests: filter(count(), t.type == "sidequest") }) |> Repo.all() - graded_configs = + showing_configs = AssessmentConfig |> where([ac], ac.course_id == ^course_id and ac.show_grading_summary) |> order_by(:order) + |> group_by([ac], ac.id) |> select([ac], %{ - id: :id, - type: :type + id: ac.id, + type: ac.type }) |> Repo.all() @@ -1465,22 +1460,33 @@ defmodule Cadet.Assessments do |> Enum.reduce(%{}, fn raw, acc -> if Map.has_key?(acc, raw.group_name) do acc - |> put_in([raw.group_name, "ungraded_" <> raw.config_type], raw.ungraded) - |> put_in([raw.group_name, "submitted_" <> raw.config_type], raw.submitted) + |> put_in([raw.group_name, "ungraded" <> raw.config_type], raw.ungraded) + |> put_in([raw.group_name, "submitted" <> raw.config_type], raw.submitted) else acc |> put_in([raw.group_name], %{}) - |> put_in([raw.group_name, "group_name"], raw.group_name) - |> put_in([raw.group_name, "leader_name"], raw.leader_name) - |> put_in([raw.group_name, "ungraded_" <> raw.config_type], raw.ungraded) - |> put_in([raw.group_name, "submitted_" <> raw.config_type], raw.submitted) + |> put_in([raw.group_name, "groupName"], raw.group_name) + |> put_in([raw.group_name, "leaderName"], raw.leader_name) + |> put_in([raw.group_name, "ungraded" <> raw.config_type], raw.ungraded) + |> put_in([raw.group_name, "submitted" <> raw.config_type], raw.submitted) end end) - # |> + headings = + showing_configs + |> Enum.reduce([], fn config, acc -> + acc ++ ["submitted" <> config.type, "ungraded" <> config.type] + end) + + default_row_data = + headings + |> Enum.reduce(%{}, fn heading, acc -> + put_in(acc, [heading], 0) + end) + + rows = data_by_groups |> Enum.map(fn {_k, row} -> Map.merge(default_row_data, row) end) + cols = headings ++ ["groupName", "leaderName"] - cols = graded_configs - rows = data_by_groups {:ok, cols, rows} end diff --git a/lib/cadet_web/admin_controllers/admin_grading_controller.ex b/lib/cadet_web/admin_controllers/admin_grading_controller.ex index a65d13d33..eb628107b 100644 --- a/lib/cadet_web/admin_controllers/admin_grading_controller.ex +++ b/lib/cadet_web/admin_controllers/admin_grading_controller.ex @@ -118,8 +118,8 @@ defmodule CadetWeb.AdminGradingController do def grading_summary(conn, %{"course_id" => course_id}) do case Assessments.get_group_grading_summary(course_id) do - {:ok, summary} -> - render(conn, "grading_summary.json", summary: summary) + {:ok, cols, summary} -> + render(conn, "grading_summary.json", cols: cols, summary: summary) end end diff --git a/lib/cadet_web/admin_views/admin_grading_view.ex b/lib/cadet_web/admin_views/admin_grading_view.ex index 299df6273..478c5d206 100644 --- a/lib/cadet_web/admin_views/admin_grading_view.ex +++ b/lib/cadet_web/admin_views/admin_grading_view.ex @@ -17,19 +17,8 @@ defmodule CadetWeb.AdminGradingView do }) end - def render("grading_summary.json", %{summary: summary}) do - render_many(summary, CadetWeb.AdminGradingView, "grading_summary_entry.json", as: :entry) - end - - def render("grading_summary_entry.json", %{entry: entry}) do - transform_map_for_view(entry, %{ - groupName: :group_name, - leaderName: :leader_name, - ungradedMissions: :ungraded_missions, - submittedMissions: :submitted_missions, - ungradedSidequests: :ungraded_sidequests, - submittedSidequests: :submitted_sidequests - }) + def render("grading_summary.json", %{cols: cols, summary: summary}) do + %{columns: cols, rows: summary} end defp build_grading_question(answer) do diff --git a/test/cadet_web/admin_controllers/admin_grading_controller_test.exs b/test/cadet_web/admin_controllers/admin_grading_controller_test.exs index 70dca34b5..7a1fe2416 100644 --- a/test/cadet_web/admin_controllers/admin_grading_controller_test.exs +++ b/test/cadet_web/admin_controllers/admin_grading_controller_test.exs @@ -1021,36 +1021,67 @@ defmodule CadetWeb.AdminGradingControllerTest do describe "GET /summary" do @tag authenticate: :admin - @tag :skip test "admin can see summary", %{conn: conn} do %{ course: course, + config: config1, submissions: submissions, group: group, grader: grader, answers: answers } = seed_db(conn) - conn = get(conn, build_url_summary(course.id)) - - expected = [ - %{ - "groupName" => group.name, - "leaderName" => grader.user.name, - "submittedMissions" => count_submissions(submissions, answers, "mission"), - "submittedSidequests" => count_submissions(submissions, answers, "sidequest"), - "ungradedMissions" => count_submissions(submissions, answers, "mission", true), - "ungradedSidequests" => count_submissions(submissions, answers, "sidequest", true) - } - ] + %{ + submissions: submissions2, + config: config2, + group: group2, + grader: grader2, + answers: answers2 + } = seed_db(conn, insert(:course_registration, %{course: course, role: :staff})) + + resp = conn |> get(build_url_summary(course.id)) |> json_response(200) + + expected = %{ + "columns" => [ + "submitted" <> config1.type, + "ungraded" <> config1.type, + "submitted" <> config2.type, + "ungraded" <> config2.type, + "groupName", + "leaderName" + ], + "rows" => [ + %{ + "groupName" => group.name, + "leaderName" => grader.user.name, + ("submitted" <> config1.type) => count_submissions(submissions, answers, config1.id), + ("submitted" <> config2.type) => count_submissions(submissions, answers, config2.id), + ("ungraded" <> config1.type) => + count_submissions(submissions, answers, config1.id, true), + ("ungraded" <> config2.type) => + count_submissions(submissions, answers, config2.id, true) + }, + %{ + "groupName" => group2.name, + "leaderName" => grader2.user.name, + ("submitted" <> config1.type) => + count_submissions(submissions2, answers2, config1.id), + ("submitted" <> config2.type) => + count_submissions(submissions2, answers2, config2.id), + ("ungraded" <> config1.type) => + count_submissions(submissions2, answers2, config1.id, true), + ("ungraded" <> config2.type) => + count_submissions(submissions2, answers2, config2.id, true) + } + ] + } - assert expected == Enum.sort_by(json_response(conn, 200), & &1["groupName"]) + assert expected == resp end @tag authenticate: :student - @tag :skip test "student cannot see summary", %{conn: conn} do - conn = get(conn, build_url_summary()) + conn = get(conn, build_url_summary(conn.assigns.course_id)) assert response(conn, 403) =~ "Forbidden" end end @@ -1135,10 +1166,10 @@ defmodule CadetWeb.AdminGradingControllerTest do end end - defp count_submissions(submissions, answers, type, only_ungraded \\ false) do + defp count_submissions(submissions, answers, config_id, only_ungraded \\ false) do submissions |> Enum.filter(fn s -> - s.status == :submitted and s.assessment.type == type and + s.status == :submitted and s.assessment.config_id == config_id and (not only_ungraded or answers |> Enum.filter(fn a -> a.submission == s and is_nil(a.grader_id) end) @@ -1147,8 +1178,6 @@ defmodule CadetWeb.AdminGradingControllerTest do |> length() end - defp build_url_summary, do: "/v2/admin/grading/summary" - # old defp build_url(course_id), do: "/v2/courses/#{course_id}/admin/grading/" defp build_url_summary(course_id), do: "/v2/courses/#{course_id}/admin/grading/summary" defp build_url(course_id, submission_id), do: "#{build_url(course_id)}#{submission_id}" From 9a48889728de5ceb33ec856c526da373d31cf639 Mon Sep 17 00:00:00 2001 From: Chow En Rong <53928333+chownces@users.noreply.github.com> Date: Fri, 9 Jul 2021 17:37:38 +0800 Subject: [PATCH 138/174] fix spec and rename field --- lib/cadet/assessments/assessments.ex | 13 +------------ lib/cadet_web/admin_views/admin_grading_view.ex | 2 +- .../admin_grading_controller_test.exs | 2 +- 3 files changed, 3 insertions(+), 14 deletions(-) diff --git a/lib/cadet/assessments/assessments.ex b/lib/cadet/assessments/assessments.ex index 4ed186a52..fa728835e 100644 --- a/lib/cadet/assessments/assessments.ex +++ b/lib/cadet/assessments/assessments.ex @@ -1393,19 +1393,8 @@ defmodule Cadet.Assessments do Timex.between?(Timex.now(), open_at, close_at, inclusive: :start) and is_published end - @type group_summary_entry :: %{ - group_name: String.t(), - leader_name: String.t(), - ungraded: [map()], - submitted: [map()] - # ungraded_missions: integer(), - # submitted_missions: integer(), - # ungraded_sidequests: number(), - # submitted_sidequests: number() - } - @spec get_group_grading_summary(integer()) :: - {:ok, [], [group_summary_entry()]} + {:ok, [String.t(), ...], []} def get_group_grading_summary(course_id) do subs = Answer diff --git a/lib/cadet_web/admin_views/admin_grading_view.ex b/lib/cadet_web/admin_views/admin_grading_view.ex index 478c5d206..ccb9f25f3 100644 --- a/lib/cadet_web/admin_views/admin_grading_view.ex +++ b/lib/cadet_web/admin_views/admin_grading_view.ex @@ -18,7 +18,7 @@ defmodule CadetWeb.AdminGradingView do end def render("grading_summary.json", %{cols: cols, summary: summary}) do - %{columns: cols, rows: summary} + %{cols: cols, rows: summary} end defp build_grading_question(answer) do diff --git a/test/cadet_web/admin_controllers/admin_grading_controller_test.exs b/test/cadet_web/admin_controllers/admin_grading_controller_test.exs index 7a1fe2416..72ddfd8ca 100644 --- a/test/cadet_web/admin_controllers/admin_grading_controller_test.exs +++ b/test/cadet_web/admin_controllers/admin_grading_controller_test.exs @@ -1042,7 +1042,7 @@ defmodule CadetWeb.AdminGradingControllerTest do resp = conn |> get(build_url_summary(course.id)) |> json_response(200) expected = %{ - "columns" => [ + "cols" => [ "submitted" <> config1.type, "ungraded" <> config1.type, "submitted" <> config2.type, From 23b3975b4e85a54e3e2ee10914a9469bc9a8cf3c Mon Sep 17 00:00:00 2001 From: Chow En Rong <53928333+chownces@users.noreply.github.com> Date: Fri, 9 Jul 2021 17:38:34 +0800 Subject: [PATCH 139/174] reorder grading summary --- lib/cadet/assessments/assessments.ex | 2 +- .../admin_controllers/admin_grading_controller_test.exs | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/cadet/assessments/assessments.ex b/lib/cadet/assessments/assessments.ex index fa728835e..31de1c879 100644 --- a/lib/cadet/assessments/assessments.ex +++ b/lib/cadet/assessments/assessments.ex @@ -1474,7 +1474,7 @@ defmodule Cadet.Assessments do end) rows = data_by_groups |> Enum.map(fn {_k, row} -> Map.merge(default_row_data, row) end) - cols = headings ++ ["groupName", "leaderName"] + cols = ["groupName", "leaderName"] ++ headings {:ok, cols, rows} end diff --git a/test/cadet_web/admin_controllers/admin_grading_controller_test.exs b/test/cadet_web/admin_controllers/admin_grading_controller_test.exs index 72ddfd8ca..44054b970 100644 --- a/test/cadet_web/admin_controllers/admin_grading_controller_test.exs +++ b/test/cadet_web/admin_controllers/admin_grading_controller_test.exs @@ -1043,12 +1043,12 @@ defmodule CadetWeb.AdminGradingControllerTest do expected = %{ "cols" => [ + "groupName", + "leaderName", "submitted" <> config1.type, "ungraded" <> config1.type, "submitted" <> config2.type, - "ungraded" <> config2.type, - "groupName", - "leaderName" + "ungraded" <> config2.type ], "rows" => [ %{ From a4afef6b66373e2c494972abe10dee6d2b67d9a6 Mon Sep 17 00:00:00 2001 From: YaleChen299 Date: Fri, 9 Jul 2021 23:23:10 +0800 Subject: [PATCH 140/174] fix grader_id in migration --- .../20210531155751_multitenant_upgrade.exs | 32 +++++++++++++++++-- 1 file changed, 30 insertions(+), 2 deletions(-) diff --git a/priv/repo/migrations/20210531155751_multitenant_upgrade.exs b/priv/repo/migrations/20210531155751_multitenant_upgrade.exs index 2a05a0620..23192c22a 100644 --- a/priv/repo/migrations/20210531155751_multitenant_upgrade.exs +++ b/priv/repo/migrations/20210531155751_multitenant_upgrade.exs @@ -3,7 +3,7 @@ defmodule Cadet.Repo.Migrations.MultitenantUpgrade do import Ecto.Query, only: [from: 2, where: 2] alias Cadet.Accounts.{CourseRegistration, Notification, Role, User} - alias Cadet.Assessments.{Assessment, Question, Submission, SubmissionVotes} + alias Cadet.Assessments.{Answer, Assessment, Question, Submission, SubmissionVotes} alias Cadet.Courses.{AssessmentConfig, Course, Group, Sourcecast} alias Cadet.Repo alias Cadet.Stories.Story @@ -98,11 +98,13 @@ defmodule Cadet.Repo.Migrations.MultitenantUpgrade do add(:voter_id, references(:course_registrations)) end + rename(table(:answers), :grader_id, to: :temp_grader_id) + drop(constraint(:answers, "answers_grader_id_fkey")) + # Remove grade metric from backend alter table(:answers) do remove(:grade) remove(:adjustment) - remove(:grader_id) add(:grader_id, references(:course_registrations), null: true) end @@ -299,6 +301,28 @@ defmodule Cadet.Repo.Migrations.MultitenantUpgrade do |> Repo.update() end) + from(a in "answers", select: {a.id, a.temp_grader_id}) + |> Repo.all() + |> Enum.each(fn answer -> + case elem(answer, 1) do + nil -> + nil + + user_id -> + grader_id = + CourseRegistration + |> where(user_id: ^user_id) + |> Repo.one() + |> Map.fetch!(:id) + + Answer + |> where(id: ^elem(answer, 0)) + |> Repo.one() + |> Answer.grading_changeset(%{grader_id: grader_id}) + |> Repo.update() + end + end) + from(s in "submission_votes", select: {s.id, s.user_id}) |> Repo.all() |> Enum.each(fn vote -> @@ -385,6 +409,10 @@ defmodule Cadet.Repo.Migrations.MultitenantUpgrade do ) end + alter table(:answers) do + remove(:temp_grader_id) + end + create(index(:submissions, :student_id)) create(unique_index(:submissions, [:assessment_id, :student_id])) From 0f22c01a1a91e928304d63ae9ec2e7543112b681 Mon Sep 17 00:00:00 2001 From: YaleChen299 Date: Sat, 10 Jul 2021 00:11:18 +0800 Subject: [PATCH 141/174] grading index filter by course --- lib/cadet/assessments/assessments.ex | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/lib/cadet/assessments/assessments.ex b/lib/cadet/assessments/assessments.ex index 31de1c879..1fe657188 100644 --- a/lib/cadet/assessments/assessments.ex +++ b/lib/cadet/assessments/assessments.ex @@ -1089,16 +1089,16 @@ defmodule Cadet.Assessments do """ @spec all_submissions_by_grader_for_index(%CourseRegistration{}) :: {:ok, String.t()} - def all_submissions_by_grader_for_index(grader = %CourseRegistration{}, group_only \\ false) do + def all_submissions_by_grader_for_index(grader = %CourseRegistration{course_id: course_id}, group_only \\ false) do show_all = not group_only group_where = if show_all, do: "", else: - "where s.student_id in (select cr.id from course_registrations cr inner join groups g on cr.group_id = g.id where g.leader_id = $1) or s.student_id = $1" + "where s.student_id in (select cr.id from course_registrations cr inner join groups g on cr.group_id = g.id where g.leader_id = $2) or s.student_id = $2" - params = if show_all, do: [], else: [grader.id] + params = if show_all, do: [course_id], else: [course_id, grader.id] # We bypass Ecto here and use a raw query to generate JSON directly from # PostgreSQL, because doing it in Elixir/Erlang is too inefficient. @@ -1151,6 +1151,7 @@ defmodule Cadet.Assessments do questions q on a.id = q.assessment_id inner join assessment_configs ac on ac.id = a.config_id + where a.course_id = $1 group by a.id) a) assts on assts.id = s.assessment_id inner join (select From 68beb23eba3b66fc81166c088dc08ccc184c18ae Mon Sep 17 00:00:00 2001 From: YaleChen299 Date: Sat, 10 Jul 2021 00:22:21 +0800 Subject: [PATCH 142/174] read all notifications in the previous course --- lib/cadet/assessments/assessments.ex | 5 ++++- priv/repo/migrations/20210531155751_multitenant_upgrade.exs | 2 +- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/lib/cadet/assessments/assessments.ex b/lib/cadet/assessments/assessments.ex index 1fe657188..469486193 100644 --- a/lib/cadet/assessments/assessments.ex +++ b/lib/cadet/assessments/assessments.ex @@ -1089,7 +1089,10 @@ defmodule Cadet.Assessments do """ @spec all_submissions_by_grader_for_index(%CourseRegistration{}) :: {:ok, String.t()} - def all_submissions_by_grader_for_index(grader = %CourseRegistration{course_id: course_id}, group_only \\ false) do + def all_submissions_by_grader_for_index( + grader = %CourseRegistration{course_id: course_id}, + group_only \\ false + ) do show_all = not group_only group_where = diff --git a/priv/repo/migrations/20210531155751_multitenant_upgrade.exs b/priv/repo/migrations/20210531155751_multitenant_upgrade.exs index 23192c22a..cc438efd4 100644 --- a/priv/repo/migrations/20210531155751_multitenant_upgrade.exs +++ b/priv/repo/migrations/20210531155751_multitenant_upgrade.exs @@ -351,7 +351,7 @@ defmodule Cadet.Repo.Migrations.MultitenantUpgrade do Notification |> where(id: ^elem(notification, 0)) |> Repo.one() - |> Notification.changeset(%{course_reg_id: course_reg_id}) + |> Notification.changeset(%{read: true, course_reg_id: course_reg_id}) |> Repo.update() end) From 1accd2981865f5ff912611160e6c012259b0c507 Mon Sep 17 00:00:00 2001 From: YaleChen299 Date: Sat, 10 Jul 2021 01:22:07 +0800 Subject: [PATCH 143/174] prepare for multitenant deploy --- lib/cadet/accounts/course_registrations.ex | 32 ---------------------- 1 file changed, 32 deletions(-) diff --git a/lib/cadet/accounts/course_registrations.ex b/lib/cadet/accounts/course_registrations.ex index ca509a7fa..f1e4833b5 100644 --- a/lib/cadet/accounts/course_registrations.ex +++ b/lib/cadet/accounts/course_registrations.ex @@ -162,18 +162,6 @@ defmodule Cadet.Accounts.CourseRegistrations do end def update_role(role, coursereg_id) do - # with {:get_cr, course_reg} when not is_nil(course_reg) <- - # {:get_cr, CourseRegistration |> where(id: ^coursereg_id) |> Repo.one()} do - # case course_reg - # |> CourseRegistration.changeset(%{role: role}) - # |> Repo.update() do - # {:ok, _} = result -> result - # {:error, changeset} -> {:error, {:bad_request, full_error_messages(changeset)}} - # end - # else - # {:get_cr, nil} -> {:error, {:bad_request, "User course registration does not exist"}} - # end - case CourseRegistration |> where(id: ^coursereg_id) |> Repo.one() do nil -> {:error, {:bad_request, "User course registration does not exist"}} @@ -190,26 +178,6 @@ defmodule Cadet.Accounts.CourseRegistrations do def delete_course_registration(coursereg_id) do # TODO: Handle deletions of achievement entries, etc. too - - # with {:get_cr, course_reg} when not is_nil(course_reg) <- - # {:get_cr, CourseRegistration |> where(id: ^coursereg_id) |> Repo.one()} do - # # Delete submissions and answers before deleting user - # Submission - # |> where(student_id: ^course_reg.id) - # |> Repo.all() - # |> Enum.each(fn x -> - # Answer - # |> where(submission_id: ^x.id) - # |> Repo.delete_all() - - # Repo.delete(x) - # end) - - # Repo.delete(course_reg) - # else - # {:get_cr, nil} -> {:error, {:bad_request, "User course registration does not exist"}} - # end - case CourseRegistration |> where(id: ^coursereg_id) |> Repo.one() do nil -> {:error, {:bad_request, "User course registration does not exist"}} From b2423221009473ab3bde7283fa4d22be5a6ff7eb Mon Sep 17 00:00:00 2001 From: YaleChen299 Date: Sat, 10 Jul 2021 01:24:12 +0800 Subject: [PATCH 144/174] prepare for multitenant deploy --- .github/workflows/cd.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/cd.yml b/.github/workflows/cd.yml index 543e6e4bb..38e53367b 100644 --- a/.github/workflows/cd.yml +++ b/.github/workflows/cd.yml @@ -4,6 +4,7 @@ on: branches: - stable - master + - multitenant-deploy paths: - 'config/**' - 'lib/**' From 221938212be26c6dfcd73b9e5f56fdb819b705b3 Mon Sep 17 00:00:00 2001 From: angelsl Date: Sat, 10 Jul 2021 01:51:13 +0800 Subject: [PATCH 145/174] Debug workflow --- .github/workflows/cd.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/cd.yml b/.github/workflows/cd.yml index 38e53367b..9d5fd58fa 100644 --- a/.github/workflows/cd.yml +++ b/.github/workflows/cd.yml @@ -55,6 +55,7 @@ jobs: mix deps.get - name: mix release run: | + ls -la _build/prod rm -f _build/prod/cadet-0.0.1.tar.gz mix release - name: Create release From e4da6d1d0d0964a0a2f492d1a0f52a644f4bfd78 Mon Sep 17 00:00:00 2001 From: angelsl Date: Sat, 10 Jul 2021 01:56:54 +0800 Subject: [PATCH 146/174] Specify --overwrite --- .github/workflows/cd.yml | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/.github/workflows/cd.yml b/.github/workflows/cd.yml index 9d5fd58fa..b72538cb4 100644 --- a/.github/workflows/cd.yml +++ b/.github/workflows/cd.yml @@ -54,10 +54,7 @@ jobs: run: | mix deps.get - name: mix release - run: | - ls -la _build/prod - rm -f _build/prod/cadet-0.0.1.tar.gz - mix release + run: mix release --overwrite - name: Create release uses: marvinpinto/action-automatic-releases@latest with: From d5b2b2fe7bcd6ca9c81a85be96d0aaee5d21421b Mon Sep 17 00:00:00 2001 From: YaleChen299 Date: Sat, 10 Jul 2021 02:16:25 +0800 Subject: [PATCH 147/174] update init.sh url --- deployment/init.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/deployment/init.sh b/deployment/init.sh index 045ee1669..6b1d502bb 100644 --- a/deployment/init.sh +++ b/deployment/init.sh @@ -8,9 +8,9 @@ set -euxo pipefail BASEDIR=/opt/cadet -PKGURL='https://github.com/source-academy/cadet/releases/download/latest-stable/cadet-0.0.1.tar.gz' +PKGURL='https://github.com/source-academy/cadet/releases/download/latest-multitenant-deploy/cadet-0.0.1.tar.gz' PKGPATH='/run/cadet-init/cadet-0.0.1.tar.gz' -SVCURL=${SVCURL:-'https://raw.githubusercontent.com/source-academy/cadet/stable/deployment/cadet.service'} +SVCURL=${SVCURL:-'https://raw.githubusercontent.com/source-academy/cadet/multitenant-deploy/deployment/cadet.service'} SVCPATH='/etc/systemd/system/cadet.service' if [ "$EUID" -ne 0 ]; then From 9a877832316dd0dc3249d3da856e7fac8421a642 Mon Sep 17 00:00:00 2001 From: YaleChen299 Date: Sat, 10 Jul 2021 22:43:03 +0800 Subject: [PATCH 148/174] add role in /user call course array --- lib/cadet_web/views/user_view.ex | 1 + .../admin_controllers/admin_grading_controller_test.exs | 3 ++- test/cadet_web/controllers/user_controller_test.exs | 8 +++++--- 3 files changed, 8 insertions(+), 4 deletions(-) diff --git a/lib/cadet_web/views/user_view.ex b/lib/cadet_web/views/user_view.ex index 3b4f14a2d..5308c2603 100644 --- a/lib/cadet_web/views/user_view.ex +++ b/lib/cadet_web/views/user_view.ex @@ -51,6 +51,7 @@ defmodule CadetWeb.UserView do courseId: cr.course_id, courseName: cr.course.course_name, courseShortName: cr.course.course_short_name, + role: cr.role, viewable: cr.course.viewable } end diff --git a/test/cadet_web/admin_controllers/admin_grading_controller_test.exs b/test/cadet_web/admin_controllers/admin_grading_controller_test.exs index 44054b970..cde9621e9 100644 --- a/test/cadet_web/admin_controllers/admin_grading_controller_test.exs +++ b/test/cadet_web/admin_controllers/admin_grading_controller_test.exs @@ -1076,7 +1076,8 @@ defmodule CadetWeb.AdminGradingControllerTest do ] } - assert expected == resp + assert expected["cols"] == resp["cols"] + assert expected["rows"] == Enum.sort_by(resp["rows"], & &1["groupName"]) end @tag authenticate: :student diff --git a/test/cadet_web/controllers/user_controller_test.exs b/test/cadet_web/controllers/user_controller_test.exs index c71bc29f3..a3c7dd8ea 100644 --- a/test/cadet_web/controllers/user_controller_test.exs +++ b/test/cadet_web/controllers/user_controller_test.exs @@ -22,7 +22,7 @@ defmodule CadetWeb.UserControllerTest do config3 = insert(:assessment_config, %{order: 3, type: "test type 3", course: course}) config1 = insert(:assessment_config, %{order: 1, type: "test type 1", course: course}) cr = Repo.get_by(CourseRegistration, course_id: course.id, user_id: user.id) - another_cr = insert(:course_registration, %{user: user}) + another_cr = insert(:course_registration, %{user: user, role: :admin}) assessment = insert(:assessment, %{is_published: true, course: course}) question = insert(:question, %{assessment: assessment}) @@ -68,13 +68,15 @@ defmodule CadetWeb.UserControllerTest do "courseId" => user.latest_viewed_id, "courseShortName" => "CS1101S", "courseName" => "Programming Methodology", - "viewable" => true + "viewable" => true, + "role" => "#{cr.role}" }, %{ "courseId" => another_cr.course_id, "courseShortName" => "CS1101S", "courseName" => "Programming Methodology", - "viewable" => true + "viewable" => true, + "role" => "#{another_cr.role}" } ] }, From 62da24f79313eba3e1516b81ddedd30ba46a6cb4 Mon Sep 17 00:00:00 2001 From: YaleChen299 Date: Sat, 10 Jul 2021 22:43:03 +0800 Subject: [PATCH 149/174] add role in /user call course array --- lib/cadet_web/views/user_view.ex | 1 + .../admin_controllers/admin_grading_controller_test.exs | 3 ++- test/cadet_web/controllers/user_controller_test.exs | 8 +++++--- 3 files changed, 8 insertions(+), 4 deletions(-) diff --git a/lib/cadet_web/views/user_view.ex b/lib/cadet_web/views/user_view.ex index 3b4f14a2d..5308c2603 100644 --- a/lib/cadet_web/views/user_view.ex +++ b/lib/cadet_web/views/user_view.ex @@ -51,6 +51,7 @@ defmodule CadetWeb.UserView do courseId: cr.course_id, courseName: cr.course.course_name, courseShortName: cr.course.course_short_name, + role: cr.role, viewable: cr.course.viewable } end diff --git a/test/cadet_web/admin_controllers/admin_grading_controller_test.exs b/test/cadet_web/admin_controllers/admin_grading_controller_test.exs index 44054b970..cde9621e9 100644 --- a/test/cadet_web/admin_controllers/admin_grading_controller_test.exs +++ b/test/cadet_web/admin_controllers/admin_grading_controller_test.exs @@ -1076,7 +1076,8 @@ defmodule CadetWeb.AdminGradingControllerTest do ] } - assert expected == resp + assert expected["cols"] == resp["cols"] + assert expected["rows"] == Enum.sort_by(resp["rows"], & &1["groupName"]) end @tag authenticate: :student diff --git a/test/cadet_web/controllers/user_controller_test.exs b/test/cadet_web/controllers/user_controller_test.exs index c71bc29f3..a3c7dd8ea 100644 --- a/test/cadet_web/controllers/user_controller_test.exs +++ b/test/cadet_web/controllers/user_controller_test.exs @@ -22,7 +22,7 @@ defmodule CadetWeb.UserControllerTest do config3 = insert(:assessment_config, %{order: 3, type: "test type 3", course: course}) config1 = insert(:assessment_config, %{order: 1, type: "test type 1", course: course}) cr = Repo.get_by(CourseRegistration, course_id: course.id, user_id: user.id) - another_cr = insert(:course_registration, %{user: user}) + another_cr = insert(:course_registration, %{user: user, role: :admin}) assessment = insert(:assessment, %{is_published: true, course: course}) question = insert(:question, %{assessment: assessment}) @@ -68,13 +68,15 @@ defmodule CadetWeb.UserControllerTest do "courseId" => user.latest_viewed_id, "courseShortName" => "CS1101S", "courseName" => "Programming Methodology", - "viewable" => true + "viewable" => true, + "role" => "#{cr.role}" }, %{ "courseId" => another_cr.course_id, "courseShortName" => "CS1101S", "courseName" => "Programming Methodology", - "viewable" => true + "viewable" => true, + "role" => "#{another_cr.role}" } ] }, From fbde0584c23e5824d19a7f4b8f9f8109fb99f9b5 Mon Sep 17 00:00:00 2001 From: YaleChen299 Date: Sun, 11 Jul 2021 17:04:15 +0800 Subject: [PATCH 150/174] clean up unused code --- lib/cadet/accounts/course_registrations.ex | 15 -- lib/cadet/helpers/model_helper.ex | 20 -- lib/mix/tasks/users/import.ex | 246 +++++++++--------- .../accounts/course_registration_test.exs | 56 ---- 4 files changed, 123 insertions(+), 214 deletions(-) diff --git a/lib/cadet/accounts/course_registrations.ex b/lib/cadet/accounts/course_registrations.ex index f1e4833b5..db0dbf2f9 100644 --- a/lib/cadet/accounts/course_registrations.ex +++ b/lib/cadet/accounts/course_registrations.ex @@ -137,21 +137,6 @@ defmodule Cadet.Accounts.CourseRegistrations do |> Repo.insert_or_update() end - # TODO: Remove eventually (duplicate of delete_course_registration) - # @spec delete_record(map()) :: - # {:ok, Ecto.Schema.t()} | {:error, Ecto.Changeset.t()} | {:error, :no_such_enrty} - # def delete_record(params = %{user_id: user_id, course_id: course_id}) - # when is_ecto_id(user_id) and is_ecto_id(course_id) do - # CourseRegistration - # |> where(user_id: ^user_id) - # |> where(course_id: ^course_id) - # |> Repo.one() - # |> case do - # nil -> {:error, :no_such_enrty} - # cr -> CourseRegistration.changeset(cr, params) |> Repo.delete() - # end - # end - def update_game_states(cr = %CourseRegistration{}, new_game_state = %{}) do case cr |> CourseRegistration.changeset(%{game_states: new_game_state}) diff --git a/lib/cadet/helpers/model_helper.ex b/lib/cadet/helpers/model_helper.ex index 9d982ef19..ce960cd3b 100644 --- a/lib/cadet/helpers/model_helper.ex +++ b/lib/cadet/helpers/model_helper.ex @@ -54,26 +54,6 @@ defmodule Cadet.ModelHelper do end) end - @doc """ - Given a changeset for a model that has some `belongs_to` associations, this function will attach only one id to the changeset if the models are provided in the parameters. - - example: - ``` - defmodule MyTest do - schema "my_test" do - belongs_to(:bossman, User) - end - - def changeset(my_test, params) do - # params = %{bossman: %User{}} - - my_test - |> cast(params, []) - |> add_belongs_to_id_from_model(:bossman, params) - end - end - ``` - """ def add_belongs_to_id_from_model(changeset, assoc, params) when is_atom(assoc) do assoc_id_field = String.to_atom("#{assoc}_id") diff --git a/lib/mix/tasks/users/import.ex b/lib/mix/tasks/users/import.ex index c05cdf4c9..eca2e0326 100644 --- a/lib/mix/tasks/users/import.ex +++ b/lib/mix/tasks/users/import.ex @@ -18,127 +18,127 @@ defmodule Mix.Tasks.Cadet.Users.Import do Note that group names must be unique. """ - @shortdoc "Import user and grouping information from csv files." - - use Mix.Task - - require Logger - - alias Cadet.{Accounts, Courses, Repo} - alias Cadet.Courses.Group - alias Cadet.Accounts.User - - def run(_args) do - # Required for Ecto to work properly, from Mix.Ecto - if function_exported?(Mix.Task, :run, 2), do: Mix.Task.run("app.start") - - students_csv_path = trimmed_gets("Path to students csv (leave blank to skip): ") - leaders_csv_path = trimmed_gets("Path to leaders csv (leave blank to skip): ") - mentors_csv_path = trimmed_gets("Path to mentors csv (leave blank to skip): ") - - Repo.transaction(fn -> - students_csv_path != "" && process_students_csv(students_csv_path) - leaders_csv_path != "" && process_leaders_csv(leaders_csv_path) - mentors_csv_path != "" && process_mentors_csv(mentors_csv_path) - end) - end - - defp process_students_csv(path) when is_binary(path) do - if File.exists?(path) do - csv_stream = path |> File.stream!() |> CSV.decode(strip_fields: true) - - for {:ok, [name, username, group_name]} <- csv_stream do - with {:ok, _group = %Group{}} <- Courses.get_or_create_group(group_name), - {:ok, %User{}} <- - Accounts.insert_or_update_user(%{ - username: username, - name: name - # role: :student, - # group: group - }) do - :ok - else - error -> - Logger.error( - "Unable to insert student (name: #{name}, username: #{username}, " <> - "group_name: #{group_name})" - ) - - Logger.error("error: #{inspect(error, pretty: true)}") - - Repo.rollback(error) - end - end - - Logger.info("Imported students csv at #{path}") - else - Logger.error("Cannot find students csv at #{path}") - end - end - - defp process_leaders_csv(path) when is_binary(path) do - if File.exists?(path) do - csv_stream = path |> File.stream!() |> CSV.decode(strip_fields: true) - - for {:ok, [name, username, group_name]} <- csv_stream do - with {:ok, leader = %User{}} <- - Accounts.insert_or_update_user(%{username: username, name: name}), - {:ok, %Group{}} <- - Courses.insert_or_update_group(%{name: group_name, leader: leader}) do - :ok - else - error -> - Logger.error( - "Unable to insert leader (name: #{name}, username: #{username}, " <> - "group_name: #{group_name})" - ) - - Logger.error("error: #{inspect(error, pretty: true)}") - - Repo.rollback(error) - end - end - - Logger.info("Imported leaders csv at #{path}") - else - Logger.error("Cannot find leaders csv at #{path}") - end - end - - # :TODO check mentor is staff before update group and add enroll course logit - defp process_mentors_csv(path) when is_binary(path) do - if File.exists?(path) do - csv_stream = path |> File.stream!() |> CSV.decode(strip_fields: true) - - for {:ok, [name, username, group_name]} <- csv_stream do - with {:ok, mentor = %User{}} <- - Accounts.insert_or_update_user(%{username: username, name: name}), - {:ok, %Group{}} <- - Courses.insert_or_update_group(%{name: group_name, mentor: mentor}) do - :ok - else - error -> - Logger.error( - "Unable to insert mentor (name: #{name}, username: #{username}, " <> - "group_name: #{group_name})" - ) - - Logger.error("error: #{inspect(error, pretty: true)}") - - Repo.rollback(error) - end - end - - Logger.info("Imported mentors csv at #{path}") - else - Logger.error("Cannot find mentors csv at #{path}") - end - end - - @spec trimmed_gets(String.t()) :: String.t() - defp trimmed_gets(prompt) when is_binary(prompt) do - prompt - |> IO.gets() - |> String.trim() - end + # @shortdoc "Import user and grouping information from csv files." + + # use Mix.Task + + # require Logger + + # alias Cadet.{Accounts, Courses, Repo} + # alias Cadet.Courses.Group + # alias Cadet.Accounts.User + + # def run(_args) do + # # Required for Ecto to work properly, from Mix.Ecto + # if function_exported?(Mix.Task, :run, 2), do: Mix.Task.run("app.start") + + # students_csv_path = trimmed_gets("Path to students csv (leave blank to skip): ") + # leaders_csv_path = trimmed_gets("Path to leaders csv (leave blank to skip): ") + # mentors_csv_path = trimmed_gets("Path to mentors csv (leave blank to skip): ") + + # Repo.transaction(fn -> + # students_csv_path != "" && process_students_csv(students_csv_path) + # leaders_csv_path != "" && process_leaders_csv(leaders_csv_path) + # mentors_csv_path != "" && process_mentors_csv(mentors_csv_path) + # end) + # end + + # defp process_students_csv(path) when is_binary(path) do + # if File.exists?(path) do + # csv_stream = path |> File.stream!() |> CSV.decode(strip_fields: true) + + # for {:ok, [name, username, group_name]} <- csv_stream do + # with {:ok, _group = %Group{}} <- Courses.get_or_create_group(group_name), + # {:ok, %User{}} <- + # Accounts.insert_or_update_user(%{ + # username: username, + # name: name + # # role: :student, + # # group: group + # }) do + # :ok + # else + # error -> + # Logger.error( + # "Unable to insert student (name: #{name}, username: #{username}, " <> + # "group_name: #{group_name})" + # ) + + # Logger.error("error: #{inspect(error, pretty: true)}") + + # Repo.rollback(error) + # end + # end + + # Logger.info("Imported students csv at #{path}") + # else + # Logger.error("Cannot find students csv at #{path}") + # end + # end + + # defp process_leaders_csv(path) when is_binary(path) do + # if File.exists?(path) do + # csv_stream = path |> File.stream!() |> CSV.decode(strip_fields: true) + + # for {:ok, [name, username, group_name]} <- csv_stream do + # with {:ok, leader = %User{}} <- + # Accounts.insert_or_update_user(%{username: username, name: name}), + # {:ok, %Group{}} <- + # Courses.insert_or_update_group(%{name: group_name, leader: leader}) do + # :ok + # else + # error -> + # Logger.error( + # "Unable to insert leader (name: #{name}, username: #{username}, " <> + # "group_name: #{group_name})" + # ) + + # Logger.error("error: #{inspect(error, pretty: true)}") + + # Repo.rollback(error) + # end + # end + + # Logger.info("Imported leaders csv at #{path}") + # else + # Logger.error("Cannot find leaders csv at #{path}") + # end + # end + + # # :TODO check mentor is staff before update group and add enroll course logit + # defp process_mentors_csv(path) when is_binary(path) do + # if File.exists?(path) do + # csv_stream = path |> File.stream!() |> CSV.decode(strip_fields: true) + + # for {:ok, [name, username, group_name]} <- csv_stream do + # with {:ok, mentor = %User{}} <- + # Accounts.insert_or_update_user(%{username: username, name: name}), + # {:ok, %Group{}} <- + # Courses.insert_or_update_group(%{name: group_name, mentor: mentor}) do + # :ok + # else + # error -> + # Logger.error( + # "Unable to insert mentor (name: #{name}, username: #{username}, " <> + # "group_name: #{group_name})" + # ) + + # Logger.error("error: #{inspect(error, pretty: true)}") + + # Repo.rollback(error) + # end + # end + + # Logger.info("Imported mentors csv at #{path}") + # else + # Logger.error("Cannot find mentors csv at #{path}") + # end + # end + + # @spec trimmed_gets(String.t()) :: String.t() + # defp trimmed_gets(prompt) when is_binary(prompt) do + # prompt + # |> IO.gets() + # |> String.trim() + # end end diff --git a/test/cadet/accounts/course_registration_test.exs b/test/cadet/accounts/course_registration_test.exs index c592b7f4b..fc2c13061 100644 --- a/test/cadet/accounts/course_registration_test.exs +++ b/test/cadet/accounts/course_registration_test.exs @@ -276,62 +276,6 @@ defmodule Cadet.Accounts.CourseRegistrationTest do end end - # TODO: Remove eventually (duplicate of delete_course_registration) - # describe "delete record" do - # test "succeeds", %{course1: course1, user1: user1} do - # assert length(CourseRegistrations.get_users(course1.id)) == 1 - - # {:ok, _course_reg} = - # CourseRegistrations.delete_record(%{ - # user_id: user1.id, - # course_id: course1.id, - # role: :student - # }) - - # assert CourseRegistrations.get_users(course1.id) == [] - # end - - # test "failed due to repeated removal", %{course1: course1, user1: user1} do - # assert length(CourseRegistrations.get_users(course1.id)) == 1 - - # {:ok, _course_reg} = - # CourseRegistrations.delete_record(%{ - # user_id: user1.id, - # course_id: course1.id, - # role: :student - # }) - - # assert CourseRegistrations.get_users(course1.id) == [] - - # assert {:error, :no_such_enrty} == - # CourseRegistrations.delete_record(%{ - # user_id: user1.id, - # course_id: course1.id, - # role: :student - # }) - # end - - # test "failed due to non existing entry", %{course1: course1, user2: user2} do - # assert length(CourseRegistrations.get_users(course1.id)) == 1 - - # assert {:error, :no_such_enrty} == - # CourseRegistrations.delete_record(%{ - # user_id: user2.id, - # course_id: course1.id, - # role: :student - # }) - # end - - # test "failed due to invalid changeset", %{course1: course1, user2: user2} do - # assert length(CourseRegistrations.get_users(course1.id)) == 1 - - # {:error, :no_such_enrty} = - # CourseRegistrations.delete_record(%{user_id: user2.id, course_id: course1.id}) - - # assert length(CourseRegistrations.get_users(course1.id)) == 1 - # end - # end - describe "update_role" do setup do student = insert(:course_registration, %{role: :student}) From 57dbadd5ff2f4c28d1aef66206a2f5a01307658e Mon Sep 17 00:00:00 2001 From: YaleChen299 Date: Sun, 11 Jul 2021 21:12:21 +0800 Subject: [PATCH 151/174] set latest_viewed_id to nil in users for non-viewable course --- lib/cadet/courses/courses.ex | 15 +++++++++++++++ test/cadet/courses/courses_test.exs | 19 +++++++++++++++++++ 2 files changed, 34 insertions(+) diff --git a/lib/cadet/courses/courses.ex b/lib/cadet/courses/courses.ex index 997803b50..8faca0977 100644 --- a/lib/cadet/courses/courses.ex +++ b/lib/cadet/courses/courses.ex @@ -77,6 +77,10 @@ defmodule Cadet.Courses do {:error, {:bad_request, "Invalid course id"}} course -> + if Map.has_key?(params, :viewable) and not params.viewable do + remove_latest_viewed_id(course_id) + end + course |> Course.changeset(params) |> Repo.update() @@ -89,6 +93,17 @@ defmodule Cadet.Courses do |> Repo.one() end + defp remove_latest_viewed_id(course_id) do + User + |> where(latest_viewed_id: ^course_id) + |> Repo.all() + |> Enum.each(fn user -> + user + |> User.changeset(%{latest_viewed_id: nil}) + |> Repo.update() + end) + end + def get_assessment_configs(course_id) when is_ecto_id(course_id) do AssessmentConfig |> where([at], at.course_id == ^course_id) diff --git a/test/cadet/courses/courses_test.exs b/test/cadet/courses/courses_test.exs index b2118e2d3..25ab35cba 100644 --- a/test/cadet/courses/courses_test.exs +++ b/test/cadet/courses/courses_test.exs @@ -123,6 +123,25 @@ defmodule Cadet.CoursesTest do assert updated_course.module_help_text == "help" end + test "succeeds (removes latest_viewed_id)" do + course = insert(:course) + user = insert(:user, %{latest_viewed: course}) + + {:ok, updated_course} = + Courses.update_course_config(course.id, %{ + course_name: "Data Structures and Algorithms", + course_short_name: "CS2040S", + viewable: false, + enable_game: false, + enable_achievements: false, + enable_sourcecast: false, + module_help_text: "help" + }) + + assert updated_course.viewable == false + assert is_nil(Repo.get(User, user.id).latest_viewed_id) + end + test "returns with error for invalid course id" do course = insert(:course) new_chapter = Enum.random(1..4) From aad9ed690f9e471c7ff0857463802a9fecb76633 Mon Sep 17 00:00:00 2001 From: YaleChen299 Date: Tue, 13 Jul 2021 19:27:33 +0800 Subject: [PATCH 152/174] update question testcases format --- lib/cadet/assessments/question.ex | 4 +-- .../question_types/programming_question.ex | 6 ++-- .../programming_question_testcases.ex | 2 +- lib/cadet/jobs/autograder/lambda_worker.ex | 3 +- lib/cadet/jobs/xml_parser.ex | 13 +++++--- .../controllers/assessments_controller.ex | 4 +-- lib/cadet_web/views/assessments_helpers.ex | 33 +++++++------------ .../jobs/autograder/lambda_worker_test.exs | 11 +++++-- test/cadet/updater/xml_parser_test.exs | 2 +- .../admin_grading_controller_test.exs | 24 +++++++++++--- .../assessments_controller_test.exs | 27 +++++---------- .../factories/assessments/question_factory.ex | 9 ++++- test/support/seeds.ex | 3 -- test/support/xml_generator.ex | 17 +++++++--- 14 files changed, 89 insertions(+), 69 deletions(-) diff --git a/lib/cadet/assessments/question.ex b/lib/cadet/assessments/question.ex index 695abcb78..7ab6e45c5 100644 --- a/lib/cadet/assessments/question.ex +++ b/lib/cadet/assessments/question.ex @@ -14,7 +14,6 @@ defmodule Cadet.Assessments.Question do field(:type, QuestionType) field(:max_xp, :integer) field(:show_solution, :boolean, default: false) - field(:build_hidden_testcases, :boolean, default: false) field(:blocking, :boolean, default: false) field(:answer, :map, virtual: true) embeds_one(:library, Library, on_replace: :update) @@ -24,8 +23,7 @@ defmodule Cadet.Assessments.Question do end @required_fields ~w(question type assessment_id)a - @optional_fields ~w(display_order max_xp show_solution - build_hidden_testcases blocking)a + @optional_fields ~w(display_order max_xp show_solution blocking)a @required_embeds ~w(library)a def changeset(question, params) do diff --git a/lib/cadet/assessments/question_types/programming_question.ex b/lib/cadet/assessments/question_types/programming_question.ex index 168950d33..a39625de3 100644 --- a/lib/cadet/assessments/question_types/programming_question.ex +++ b/lib/cadet/assessments/question_types/programming_question.ex @@ -14,7 +14,8 @@ defmodule Cadet.Assessments.QuestionTypes.ProgrammingQuestion do field(:postpend, :string, default: "") field(:solution, :string) embeds_many(:public, Testcase) - embeds_many(:private, Testcase) + embeds_many(:opaque, Testcase) + embeds_many(:secret, Testcase) end @required_fields ~w(content template)a @@ -24,7 +25,8 @@ defmodule Cadet.Assessments.QuestionTypes.ProgrammingQuestion do question |> cast(params, @required_fields ++ @optional_fields) |> cast_embed(:public, with: &Testcase.changeset/2) - |> cast_embed(:private, with: &Testcase.changeset/2) + |> cast_embed(:opaque, with: &Testcase.changeset/2) + |> cast_embed(:secret, with: &Testcase.changeset/2) |> validate_required(@required_fields) end end diff --git a/lib/cadet/assessments/question_types/programming_question_testcases.ex b/lib/cadet/assessments/question_types/programming_question_testcases.ex index 6aebafb23..338abe854 100644 --- a/lib/cadet/assessments/question_types/programming_question_testcases.ex +++ b/lib/cadet/assessments/question_types/programming_question_testcases.ex @@ -1,6 +1,6 @@ defmodule Cadet.Assessments.QuestionTypes.Testcase do @moduledoc """ - The Assessments.QuestionTypes.Testcase entity represents a public/private testcase. + The Assessments.QuestionTypes.Testcase entity represents a public/opaque/secret testcase. """ use Cadet, :model diff --git a/lib/cadet/jobs/autograder/lambda_worker.ex b/lib/cadet/jobs/autograder/lambda_worker.ex index e2b9c20e2..3ef4a75fd 100644 --- a/lib/cadet/jobs/autograder/lambda_worker.ex +++ b/lib/cadet/jobs/autograder/lambda_worker.ex @@ -92,7 +92,8 @@ defmodule Cadet.Autograder.LambdaWorker do studentProgram: Map.get(answer.answer, "code"), postpendProgram: Map.get(question_content, "postpend", ""), testcases: - Map.get(question_content, "public", []) ++ Map.get(question_content, "private", []), + Map.get(question_content, "public", []) ++ + Map.get(question_content, "opaque", []) ++ Map.get(question_content, "secret", []), library: %{ chapter: question.grading_library.chapter, external: upcased_name_external, diff --git a/lib/cadet/jobs/xml_parser.ex b/lib/cadet/jobs/xml_parser.ex index 07b471479..120aada84 100644 --- a/lib/cadet/jobs/xml_parser.ex +++ b/lib/cadet/jobs/xml_parser.ex @@ -146,7 +146,6 @@ defmodule Cadet.Updater.XMLParser do # max_grade: ~x"./@maxgrade"oi, max_xp: ~x"./@maxxp"oi, show_solution: ~x"./@showsolution"os, - build_hidden_testcases: ~x"./@buildhiddentestcases"os, blocking: ~x"./@blocking"os, entity: ~x"." ) @@ -185,7 +184,7 @@ defmodule Cadet.Updater.XMLParser do @spec process_question_booleans(map()) :: map() defp process_question_booleans(question) do - flags = [:show_solution, :build_hidden_testcases, :blocking] + flags = [:show_solution, :blocking] flags |> Enum.reduce(question, fn flag, acc -> @@ -225,8 +224,14 @@ defmodule Cadet.Updater.XMLParser do answer: ~x"./@answer" |> transform_by(&process_charlist/1), program: ~x"./text()" |> transform_by(&process_charlist/1) ], - private: [ - ~x"./SNIPPET/TESTCASES/PRIVATE"l, + opaque: [ + ~x"./SNIPPET/TESTCASES/OPAQUE"l, + score: ~x"./@score"oi, + answer: ~x"./@answer" |> transform_by(&process_charlist/1), + program: ~x"./text()" |> transform_by(&process_charlist/1) + ], + secret: [ + ~x"./SNIPPET/TESTCASES/SECRET"l, score: ~x"./@score"oi, answer: ~x"./@answer" |> transform_by(&process_charlist/1), program: ~x"./text()" |> transform_by(&process_charlist/1) diff --git a/lib/cadet_web/controllers/assessments_controller.ex b/lib/cadet_web/controllers/assessments_controller.ex index 4b8b6053d..dc790e176 100644 --- a/lib/cadet_web/controllers/assessments_controller.ex +++ b/lib/cadet_web/controllers/assessments_controller.ex @@ -362,13 +362,13 @@ defmodule CadetWeb.AssessmentsController do answer(:string) score(:integer) program(:string) - type(Schema.ref(:TestcaseType), "One of public/hidden/private") + type(Schema.ref(:TestcaseType), "One of public/opaque/secret") end end, TestcaseType: swagger_schema do type(:string) - enum([:public, :hidden, :private]) + enum([:public, :opaque, :secret]) end, AutogradingResult: swagger_schema do diff --git a/lib/cadet_web/views/assessments_helpers.ex b/lib/cadet_web/views/assessments_helpers.ex index 7c473790a..361504156 100644 --- a/lib/cadet_web/views/assessments_helpers.ex +++ b/lib/cadet_web/views/assessments_helpers.ex @@ -163,32 +163,22 @@ defmodule CadetWeb.AssessmentsHelpers do }) end - defp build_testcases(%{build_hidden: build_hidden}, all_testcases?) do + defp build_testcases(all_testcases?) do cond do all_testcases? -> &Enum.concat( - Enum.map(&1["public"], fn testcase -> build_testcase(testcase, "public") end), - Enum.map(&1["private"], fn testcase -> build_testcase(testcase, "private") end) + Enum.concat( + Enum.map(&1["public"], fn testcase -> build_testcase(testcase, "public") end), + Enum.map(&1["opaque"], fn testcase -> build_testcase(testcase, "opaque") end) + ), + Enum.map(&1["secret"], fn testcase -> build_testcase(testcase, "secret") end) ) - # build hidden testcases if ungraded - build_hidden -> + true -> &Enum.concat( Enum.map(&1["public"], fn testcase -> build_testcase(testcase, "public") end), - Enum.map(&1["private"], fn testcase -> build_testcase(testcase, "hidden") end) + Enum.map(&1["opaque"], fn testcase -> build_testcase(testcase, "opaque") end) ) - - true -> - &Enum.map(&1["public"], fn testcase -> build_testcase(testcase, "public") end) - end - end - - defp build_postpend(%{build_hidden: build_hidden}, all_testcases?) do - case {all_testcases?, build_hidden} do - {true, _} -> & &1["postpend"] - {_, true} -> & &1["postpend"] - # Create a 1-arity function to return an empty postpend for non-paths - _ -> fn _question -> "" end end end @@ -196,8 +186,7 @@ defmodule CadetWeb.AssessmentsHelpers do %{ question: %{ question: question, - type: question_type, - build_hidden_testcases: build_hidden_testcases + type: question_type } }, all_testcases? @@ -208,8 +197,8 @@ defmodule CadetWeb.AssessmentsHelpers do content: "content", prepend: "prepend", solutionTemplate: "template", - postpend: build_postpend(%{build_hidden: build_hidden_testcases}, all_testcases?), - testcases: build_testcases(%{build_hidden: build_hidden_testcases}, all_testcases?) + postpend: "postpend", + testcases: build_testcases(all_testcases?) }) :mcq -> diff --git a/test/cadet/jobs/autograder/lambda_worker_test.exs b/test/cadet/jobs/autograder/lambda_worker_test.exs index faa73cd22..6ecb9cd49 100644 --- a/test/cadet/jobs/autograder/lambda_worker_test.exs +++ b/test/cadet/jobs/autograder/lambda_worker_test.exs @@ -23,7 +23,10 @@ defmodule Cadet.Autograder.LambdaWorkerTest do public: [ %{"score" => 1, "answer" => "1", "program" => "f(1);"} ], - private: [ + opaque: [ + %{"score" => 1, "answer" => "45", "program" => "f(10);"} + ], + secret: [ %{"score" => 1, "answer" => "45", "program" => "f(10);"} ] }) @@ -165,7 +168,8 @@ defmodule Cadet.Autograder.LambdaWorkerTest do question: build(:programming_question_content, %{ public: [], - private: [] + opaque: [], + secret: [] }) } ) @@ -233,7 +237,8 @@ defmodule Cadet.Autograder.LambdaWorkerTest do expected = %{ prependProgram: question.question.prepend, postpendProgram: question.question.postpend, - testcases: question.question.public ++ question.question.private, + testcases: + question.question.public ++ question.question.opaque ++ question.question.secret, studentProgram: answer.answer.code, library: %{ chapter: question.grading_library.chapter, diff --git a/test/cadet/updater/xml_parser_test.exs b/test/cadet/updater/xml_parser_test.exs index ef5a6a824..4e6183fa3 100644 --- a/test/cadet/updater/xml_parser_test.exs +++ b/test/cadet/updater/xml_parser_test.exs @@ -41,7 +41,7 @@ defmodule Cadet.Updater.XMLParserTest do assessments_with_config = Enum.into(assessments, %{}, &{&1, &1.config}) - questions = build_list(5, :question, assessment: nil) + questions = [build(:programming_question), build(:mcq_question), build(:voting_question)] %{ assessments: assessments, diff --git a/test/cadet_web/admin_controllers/admin_grading_controller_test.exs b/test/cadet_web/admin_controllers/admin_grading_controller_test.exs index cde9621e9..ae3a1b29c 100644 --- a/test/cadet_web/admin_controllers/admin_grading_controller_test.exs +++ b/test/cadet_web/admin_controllers/admin_grading_controller_test.exs @@ -243,10 +243,18 @@ defmodule CadetWeb.AdminGradingControllerTest do end ) ++ Enum.map( - &1.question.question.private, + &1.question.question.opaque, fn testcase -> for {k, v} <- testcase, - into: %{"type" => "private"}, + into: %{"type" => "opaque"}, + do: {Atom.to_string(k), v} + end + ) ++ + Enum.map( + &1.question.question.secret, + fn testcase -> + for {k, v} <- testcase, + into: %{"type" => "secret"}, do: {Atom.to_string(k), v} end ), @@ -861,10 +869,18 @@ defmodule CadetWeb.AdminGradingControllerTest do end ) ++ Enum.map( - &1.question.question.private, + &1.question.question.opaque, + fn testcase -> + for {k, v} <- testcase, + into: %{"type" => "opaque"}, + do: {Atom.to_string(k), v} + end + ) ++ + Enum.map( + &1.question.question.secret, fn testcase -> for {k, v} <- testcase, - into: %{"type" => "private"}, + into: %{"type" => "secret"}, do: {Atom.to_string(k), v} end ), diff --git a/test/cadet_web/controllers/assessments_controller_test.exs b/test/cadet_web/controllers/assessments_controller_test.exs index 276fc53ac..f7980ea86 100644 --- a/test/cadet_web/controllers/assessments_controller_test.exs +++ b/test/cadet_web/controllers/assessments_controller_test.exs @@ -347,12 +347,7 @@ defmodule CadetWeb.AssessmentsControllerTest do "content" => &1.question.content, "solutionTemplate" => &1.question.template, "prepend" => &1.question.prepend, - "postpend" => - if &1.build_hidden_testcases do - &1.question.postpend - else - "" - end, + "postpend" => &1.question.postpend, "testcases" => Enum.map( &1.question.public, @@ -362,18 +357,14 @@ defmodule CadetWeb.AssessmentsControllerTest do do: {Atom.to_string(k), v} end ) ++ - if &1.build_hidden_testcases do - Enum.map( - &1.question.private, - fn testcase -> - for {k, v} <- testcase, - into: %{"type" => "hidden"}, - do: {Atom.to_string(k), v} - end - ) - else - [] - end + Enum.map( + &1.question.opaque, + fn testcase -> + for {k, v} <- testcase, + into: %{"type" => "opaque"}, + do: {Atom.to_string(k), v} + end + ) } ) diff --git a/test/factories/assessments/question_factory.ex b/test/factories/assessments/question_factory.ex index 945508581..f65ad05ec 100644 --- a/test/factories/assessments/question_factory.ex +++ b/test/factories/assessments/question_factory.ex @@ -38,7 +38,14 @@ defmodule Cadet.Assessments.QuestionFactory do program: Faker.Lorem.Shakespeare.king_richard_iii() } ], - private: [ + opaque: [ + %{ + score: :rand.uniform(5), + answer: Faker.StarWars.character(), + program: Faker.Lorem.Shakespeare.king_richard_iii() + } + ], + secret: [ %{ score: :rand.uniform(5), answer: Faker.StarWars.character(), diff --git a/test/support/seeds.ex b/test/support/seeds.ex index 6631061be..f2acbd770 100644 --- a/test/support/seeds.ex +++ b/test/support/seeds.ex @@ -143,7 +143,6 @@ defmodule Cadet.Test.Seeds do display_order: id, assessment: assessment, max_xp: 1000, - build_hidden_testcases: assessment.config.type == "path", show_solution: assessment.config.type == "path" }) end) @@ -154,7 +153,6 @@ defmodule Cadet.Test.Seeds do display_order: id, assessment: assessment, max_xp: 500, - build_hidden_testcases: assessment.config.type == "path", show_solution: assessment.config.type == "path" }) end) @@ -165,7 +163,6 @@ defmodule Cadet.Test.Seeds do display_order: id, assessment: assessment, max_xp: 100, - build_hidden_testcases: assessment.config.type == "path", show_solution: assessment.config.type == "path" }) end) diff --git a/test/support/xml_generator.ex b/test/support/xml_generator.ex index 0b56b6409..b5df37648 100644 --- a/test/support/xml_generator.ex +++ b/test/support/xml_generator.ex @@ -130,8 +130,13 @@ defmodule Cadet.Test.XMLGenerator do end ] ++ [ - for testcase <- question.question[:private] do - private(%{score: testcase.score, answer: testcase.answer}, testcase.program) + for testcase <- question.question[:opaque] do + opaque(%{score: testcase.score, answer: testcase.answer}, testcase.program) + end + ] ++ + [ + for testcase <- question.question[:secret] do + secret(%{score: testcase.score, answer: testcase.answer}, testcase.program) end ] ) @@ -283,8 +288,12 @@ defmodule Cadet.Test.XMLGenerator do {"PUBLIC", map_permit_keys(raw_attrs, ~w(score answer)a), content} end - defp private(raw_attrs, content) do - {"PRIVATE", map_permit_keys(raw_attrs, ~w(score answer)a), content} + defp opaque(raw_attrs, content) do + {"OPAQUE", map_permit_keys(raw_attrs, ~w(score answer)a), content} + end + + defp secret(raw_attrs, content) do + {"SECRET", map_permit_keys(raw_attrs, ~w(score answer)a), content} end defp map_permit_keys(map, keys) when is_map(map) and is_list(keys) do From 54ce4cd91169bbd77d84874cdf03481eed5b4806 Mon Sep 17 00:00:00 2001 From: YaleChen299 Date: Tue, 13 Jul 2021 19:32:18 +0800 Subject: [PATCH 153/174] add migration for update testcase format --- .../migrations/20210713113032_update_testcase_format.exs | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 priv/repo/migrations/20210713113032_update_testcase_format.exs diff --git a/priv/repo/migrations/20210713113032_update_testcase_format.exs b/priv/repo/migrations/20210713113032_update_testcase_format.exs new file mode 100644 index 000000000..e004f74b7 --- /dev/null +++ b/priv/repo/migrations/20210713113032_update_testcase_format.exs @@ -0,0 +1,9 @@ +defmodule Cadet.Repo.Migrations.UpdateTestcaseFormat do + use Ecto.Migration + + def change do + alter table(:questions) do + remove(:build_hidden_testcases) + end + end +end From 7b008346479147aa88f0cc6fd1f4edeebc75758b Mon Sep 17 00:00:00 2001 From: YaleChen299 Date: Tue, 13 Jul 2021 21:38:48 +0800 Subject: [PATCH 154/174] update migration --- .../migrations/20210713113032_update_testcase_format.exs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/priv/repo/migrations/20210713113032_update_testcase_format.exs b/priv/repo/migrations/20210713113032_update_testcase_format.exs index e004f74b7..0fb57f0e2 100644 --- a/priv/repo/migrations/20210713113032_update_testcase_format.exs +++ b/priv/repo/migrations/20210713113032_update_testcase_format.exs @@ -2,6 +2,14 @@ defmodule Cadet.Repo.Migrations.UpdateTestcaseFormat do use Ecto.Migration def change do + execute( + "update questions set question = (question - 'private' || jsonb_build_object('opaque', question->'private', 'secret', '[]'::jsonb)) where type = 'programming' and build_hidden_testcases;" + ) + + execute( + "update questions set question = (question - 'private' || jsonb_build_object('secret', question->'private', 'opaque', '[]'::jsonb)) where type = 'programming' and not build_hidden_testcases;" + ) + alter table(:questions) do remove(:build_hidden_testcases) end From 6645ebf295022a5d91c28df9540d875377a3bd93 Mon Sep 17 00:00:00 2001 From: Chow En Rong <53928333+chownces@users.noreply.github.com> Date: Wed, 14 Jul 2021 01:20:03 +0800 Subject: [PATCH 155/174] Increase upsert users and groups transaction timeout to handle large requests of up to 1000 entries --- lib/cadet_web/admin_controllers/admin_user_controller.ex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/cadet_web/admin_controllers/admin_user_controller.ex b/lib/cadet_web/admin_controllers/admin_user_controller.ex index 29b1b751e..9532810f5 100644 --- a/lib/cadet_web/admin_controllers/admin_user_controller.ex +++ b/lib/cadet_web/admin_controllers/admin_user_controller.ex @@ -84,7 +84,7 @@ defmodule CadetWeb.AdminUserController do {:upsert_groups, {:error, {status, message}}} -> conn |> put_status(status) |> text(message) end - end) + end, timeout: 20_000) conn end From 3c92f55e08415d0926baea87be608ef71b495c05 Mon Sep 17 00:00:00 2001 From: YaleChen299 Date: Wed, 14 Jul 2021 23:45:39 +0800 Subject: [PATCH 156/174] update admin assets controller test --- .../admin_user_controller.ex | 119 +++++++++--------- .../admin_assets_controller_test.exs | 55 +++++--- 2 files changed, 97 insertions(+), 77 deletions(-) diff --git a/lib/cadet_web/admin_controllers/admin_user_controller.ex b/lib/cadet_web/admin_controllers/admin_user_controller.ex index 9532810f5..44c9778eb 100644 --- a/lib/cadet_web/admin_controllers/admin_user_controller.ex +++ b/lib/cadet_web/admin_controllers/admin_user_controller.ex @@ -27,64 +27,67 @@ defmodule CadetWeb.AdminUserController do %{role: admin_role} = conn.assigns.course_reg {:ok, conn} = - Repo.transaction(fn -> - # Note: Usernames from frontend have not been namespaced yet - with {:validate_role, true} <- {:validate_role, admin_role in @add_users_role}, - {:validate_provider, true} <- - {:validate_provider, - Map.has_key?(Application.get_env(:cadet, :identity_providers, %{}), provider)}, - {:atomify_keys, usernames_roles_groups} <- - {:atomify_keys, - Enum.map(usernames_roles_groups, fn x -> - for({key, val} <- x, into: %{}, do: {String.to_atom(key), val}) - end)}, - {:validate_usernames, true} <- - {:validate_usernames, - Enum.reduce(usernames_roles_groups, true, fn x, acc -> - acc and Map.has_key?(x, :username) and is_binary(x.username) and - x.username != "" - end)}, - {:validate_roles, true} <- - {:validate_roles, - Enum.reduce(usernames_roles_groups, true, fn x, acc -> - acc and Map.has_key?(x, :role) and - String.to_atom(x.role) in Cadet.Accounts.Role.__enums__() - end)}, - {:namespace, usernames_roles_groups} <- - {:namespace, - Enum.map(usernames_roles_groups, fn x -> - %{x | username: Provider.namespace(x.username, provider)} - end)}, - {:upsert_users, :ok} <- - {:upsert_users, - Accounts.CourseRegistrations.upsert_users_in_course( - usernames_roles_groups, - course_id - )}, - {:upsert_groups, :ok} <- - {:upsert_groups, - Courses.upsert_groups_in_course(usernames_roles_groups, course_id)} do - text(conn, "OK") - else - {:validate_role, false} -> - conn |> put_status(:forbidden) |> text("User is not permitted to add users") - - {:validate_provider, false} -> - conn |> put_status(:bad_request) |> text("Invalid authentication provider") - - {:validate_usernames, false} -> - conn |> put_status(:bad_request) |> text("Invalid username(s) provided") - - {:validate_roles, false} -> - conn |> put_status(:bad_request) |> text("Invalid role(s) provided") - - {:upsert_users, {:error, {status, message}}} -> - conn |> put_status(status) |> text(message) - - {:upsert_groups, {:error, {status, message}}} -> - conn |> put_status(status) |> text(message) - end - end, timeout: 20_000) + Repo.transaction( + fn -> + # Note: Usernames from frontend have not been namespaced yet + with {:validate_role, true} <- {:validate_role, admin_role in @add_users_role}, + {:validate_provider, true} <- + {:validate_provider, + Map.has_key?(Application.get_env(:cadet, :identity_providers, %{}), provider)}, + {:atomify_keys, usernames_roles_groups} <- + {:atomify_keys, + Enum.map(usernames_roles_groups, fn x -> + for({key, val} <- x, into: %{}, do: {String.to_atom(key), val}) + end)}, + {:validate_usernames, true} <- + {:validate_usernames, + Enum.reduce(usernames_roles_groups, true, fn x, acc -> + acc and Map.has_key?(x, :username) and is_binary(x.username) and + x.username != "" + end)}, + {:validate_roles, true} <- + {:validate_roles, + Enum.reduce(usernames_roles_groups, true, fn x, acc -> + acc and Map.has_key?(x, :role) and + String.to_atom(x.role) in Cadet.Accounts.Role.__enums__() + end)}, + {:namespace, usernames_roles_groups} <- + {:namespace, + Enum.map(usernames_roles_groups, fn x -> + %{x | username: Provider.namespace(x.username, provider)} + end)}, + {:upsert_users, :ok} <- + {:upsert_users, + Accounts.CourseRegistrations.upsert_users_in_course( + usernames_roles_groups, + course_id + )}, + {:upsert_groups, :ok} <- + {:upsert_groups, + Courses.upsert_groups_in_course(usernames_roles_groups, course_id)} do + text(conn, "OK") + else + {:validate_role, false} -> + conn |> put_status(:forbidden) |> text("User is not permitted to add users") + + {:validate_provider, false} -> + conn |> put_status(:bad_request) |> text("Invalid authentication provider") + + {:validate_usernames, false} -> + conn |> put_status(:bad_request) |> text("Invalid username(s) provided") + + {:validate_roles, false} -> + conn |> put_status(:bad_request) |> text("Invalid role(s) provided") + + {:upsert_users, {:error, {status, message}}} -> + conn |> put_status(status) |> text(message) + + {:upsert_groups, {:error, {status, message}}} -> + conn |> put_status(status) |> text(message) + end + end, + timeout: 20_000 + ) conn end diff --git a/test/cadet_web/admin_controllers/admin_assets_controller_test.exs b/test/cadet_web/admin_controllers/admin_assets_controller_test.exs index 04e424275..5c3316de9 100644 --- a/test/cadet_web/admin_controllers/admin_assets_controller_test.exs +++ b/test/cadet_web/admin_controllers/admin_assets_controller_test.exs @@ -17,17 +17,20 @@ defmodule CadetWeb.AdminAssetsControllerTest do describe "public access, unauthenticated" do test "GET /assets/:foldername", %{conn: conn} do - conn = get(conn, build_url("random_folder"), %{}) + course = insert(:course) + conn = get(conn, build_url(course.id, "random_folder"), %{}) assert response(conn, 401) =~ "Unauthorised" end test "DELETE /assets/:foldername/*filename", %{conn: conn} do - conn = delete(conn, build_url("random_folder/random_file"), %{}) + course = insert(:course) + conn = delete(conn, build_url(course.id, "random_folder/random_file"), %{}) assert response(conn, 401) =~ "Unauthorised" end test "POST /assets/:foldername/*filename", %{conn: conn} do - conn = post(conn, build_url("random_folder/random_file"), %{}) + course = insert(:course) + conn = post(conn, build_url(course.id, "random_folder/random_file"), %{}) assert response(conn, 401) =~ "Unauthorised" end end @@ -35,21 +38,24 @@ defmodule CadetWeb.AdminAssetsControllerTest do describe "student permission, forbidden" do @tag authenticate: :student test "GET /assets/:foldername", %{conn: conn} do - conn = get(conn, build_url("testFolder"), %{}) + course_id = conn.assigns.course_id + conn = get(conn, build_url(course_id, "testFolder"), %{}) assert response(conn, 403) =~ "Forbidden" end @tag authenticate: :student test "DELETE /assets/:foldername/*filename", %{conn: conn} do - conn = delete(conn, build_url("testFolder/testFile.png")) + course_id = conn.assigns.course_id + conn = delete(conn, build_url(course_id, "testFolder/testFile.png")) assert response(conn, 403) =~ "Forbidden" end @tag authenticate: :student test "POST /assets/:foldername/*filename", %{conn: conn} do + course_id = conn.assigns.course_id conn = - post(conn, build_url("testFolder/testFile.png"), %{ + post(conn, build_url(course_id, "testFolder/testFile.png"), %{ :upload => build_upload("test/fixtures/upload.png") }) @@ -60,21 +66,24 @@ defmodule CadetWeb.AdminAssetsControllerTest do describe "inaccessible folder name" do @tag authenticate: :staff test "index files", %{conn: conn} do - conn = get(conn, build_url("wrongFolder")) + course_id = conn.assigns.course_id + conn = get(conn, build_url(course_id, "wrongFolder")) assert response(conn, 400) =~ "Invalid top-level folder name" end @tag authenticate: :staff test "delete file", %{conn: conn} do - conn = delete(conn, build_url("wrongFolder/randomFile")) + course_id = conn.assigns.course_id + conn = delete(conn, build_url(course_id, "wrongFolder/randomFile")) assert response(conn, 400) =~ "Invalid top-level folder name" end @tag authenticate: :staff test "upload file", %{conn: conn} do + course_id = conn.assigns.course_id conn = - post(conn, build_url("wrongFolder/wrongUpload.png"), %{ + post(conn, build_url(course_id, "wrongFolder/wrongUpload.png"), %{ "upload" => build_upload("test/fixtures/upload.png") }) @@ -85,8 +94,9 @@ defmodule CadetWeb.AdminAssetsControllerTest do describe "ok request" do @tag authenticate: :staff test "index file", %{conn: conn} do + course_id = conn.assigns.course_id use_cassette "aws/controller_list_assets#1" do - conn = get(conn, build_url("testFolder"), %{}) + conn = get(conn, build_url(course_id, "testFolder"), %{}) assert json_response(conn, 200) === ["testFolder/", "testFolder/test.png", "testFolder/test2.png"] @@ -95,8 +105,9 @@ defmodule CadetWeb.AdminAssetsControllerTest do @tag authenticate: :staff test "delete file", %{conn: conn} do + course_id = conn.assigns.course_id use_cassette "aws/controller_delete_asset#1" do - conn = delete(conn, build_url("testFolder/test2.png")) + conn = delete(conn, build_url(course_id, "testFolder/test2.png")) assert response(conn, 204) end @@ -104,9 +115,10 @@ defmodule CadetWeb.AdminAssetsControllerTest do @tag authenticate: :staff test "upload file", %{conn: conn} do + course_id = conn.assigns.course_id use_cassette "aws/controller_upload_asset#1" do conn = - post(conn, build_url("testFolder/test.png"), %{ + post(conn, build_url(course_id, "testFolder/test.png"), %{ "upload" => build_upload("test/fixtures/upload.png") }) @@ -119,8 +131,9 @@ defmodule CadetWeb.AdminAssetsControllerTest do describe "wrong file type" do @tag authenticate: :staff test "upload file", %{conn: conn} do + course_id = conn.assigns.course_id conn = - post(conn, build_url("testFolder/test.pdf"), %{ + post(conn, build_url(course_id, "testFolder/test.pdf"), %{ "upload" => build_upload("test/fixtures/upload.pdf") }) @@ -131,8 +144,9 @@ defmodule CadetWeb.AdminAssetsControllerTest do describe "empty file name" do @tag authenticate: :staff test "upload file", %{conn: conn} do + course_id = conn.assigns.course_id conn = - post(conn, build_url("testFolder"), %{ + post(conn, build_url(course_id, "testFolder"), %{ "upload" => build_upload("test/fixtures/upload.png") }) @@ -141,7 +155,8 @@ defmodule CadetWeb.AdminAssetsControllerTest do @tag authenticate: :staff test "delete file", %{conn: conn} do - conn = delete(conn, build_url("testFolder")) + course_id = conn.assigns.course_id + conn = delete(conn, build_url(course_id, "testFolder")) assert response(conn, 400) =~ "Empty file name" end end @@ -149,8 +164,9 @@ defmodule CadetWeb.AdminAssetsControllerTest do describe "nested filename request" do @tag authenticate: :staff test "delete file", %{conn: conn} do + course_id = conn.assigns.course_id use_cassette "aws/controller_delete_asset#2" do - conn = delete(conn, build_url("testFolder/nestedFolder/test2.png")) + conn = delete(conn, build_url(course_id, "testFolder/nestedFolder/test2.png")) assert response(conn, 204) end @@ -158,9 +174,10 @@ defmodule CadetWeb.AdminAssetsControllerTest do @tag authenticate: :staff test "upload file", %{conn: conn} do + course_id = conn.assigns.course_id use_cassette "aws/controller_upload_asset#2" do conn = - post(conn, build_url("testFolder/nestedFolder/test.png"), %{ + post(conn, build_url(course_id, "testFolder/nestedFolder/test.png"), %{ "upload" => build_upload("test/fixtures/upload.png") }) @@ -170,8 +187,8 @@ defmodule CadetWeb.AdminAssetsControllerTest do end end - defp build_url, do: "/v2/admin/assets/" - defp build_url(url), do: "#{build_url()}/#{url}" + defp build_url(course_id), do: "/v2/courses/#{course_id}/admin/assets/" + defp build_url(course_id, url), do: "#{build_url(course_id)}/#{url}" defp build_upload(path, content_type \\ "image/png") do %Plug.Upload{path: path, filename: Path.basename(path), content_type: content_type} From 783b1d7eef7cd39485fcc08546d9944b6ca1fb86 Mon Sep 17 00:00:00 2001 From: YaleChen299 Date: Sat, 17 Jul 2021 11:51:46 +0800 Subject: [PATCH 157/174] update achievement for multitenant upgrade --- lib/cadet/incentives/achievement.ex | 5 +- lib/cadet/incentives/achievements.ex | 6 +- lib/cadet/incentives/goal.ex | 5 +- lib/cadet/incentives/goal_progress.ex | 8 +- lib/cadet/incentives/goals.ex | 49 ++++--- .../admin_achievements_controller.ex | 9 +- .../admin_goals_controller.ex | 30 +++-- .../controllers/incentives_controller.ex | 15 ++- lib/cadet_web/router.ex | 4 +- .../20210716073359_update_achievement.exs | 18 +++ test/cadet/incentives/achievement_test.exs | 3 + test/cadet/incentives/achievements_test.exs | 23 +++- test/cadet/incentives/goal_progress_test.exs | 4 +- test/cadet/incentives/goal_test.exs | 3 + test/cadet/incentives/goals_test.exs | 32 +++-- .../admin_achievements_controller_test.exs | 59 +++++---- .../admin_assets_controller_test.exs | 9 ++ .../admin_goals_controller_test.exs | 124 ++++++++++-------- .../incentives_controller_test.exs | 88 +++++++++---- .../achievements/achievement_factory.ex | 1 + test/factories/achievements/goal_factory.ex | 1 + 21 files changed, 313 insertions(+), 183 deletions(-) create mode 100644 priv/repo/migrations/20210716073359_update_achievement.exs diff --git a/lib/cadet/incentives/achievement.ex b/lib/cadet/incentives/achievement.ex index 886fb1480..d6ddaa057 100644 --- a/lib/cadet/incentives/achievement.ex +++ b/lib/cadet/incentives/achievement.ex @@ -4,6 +4,7 @@ defmodule Cadet.Incentives.Achievement do """ use Cadet, :model + alias Cadet.Courses.Course alias Cadet.Incentives.{AchievementPrerequisite, AchievementToGoal} @valid_abilities ~w(Core Community Effort Exploration Flex) @@ -25,6 +26,7 @@ defmodule Cadet.Incentives.Achievement do field(:description, :string) field(:completion_text, :string) + belongs_to(:course, Course) has_many(:prerequisites, AchievementPrerequisite, on_replace: :delete) has_many(:goals, AchievementToGoal, on_replace: :delete_if_exists) @@ -32,7 +34,7 @@ defmodule Cadet.Incentives.Achievement do field(:goal_uuids, {:array, :binary_id}, virtual: true) end - @required_fields ~w(uuid title ability is_task position xp is_variable_xp)a + @required_fields ~w(uuid title ability is_task position xp is_variable_xp course_id)a @optional_fields ~w(card_tile_url open_at close_at canvas_url description completion_text prerequisite_uuids goal_uuids)a @@ -46,6 +48,7 @@ defmodule Cadet.Incentives.Achievement do |> cast(params, @required_fields ++ @optional_fields) |> validate_required(@required_fields) |> validate_inclusion(:ability, @valid_abilities) + |> foreign_key_constraint(:course_id) |> cast_join_ids( :prerequisite_uuids, :prerequisites, diff --git a/lib/cadet/incentives/achievements.ex b/lib/cadet/incentives/achievements.ex index 80c5729d7..5fa93c0a4 100644 --- a/lib/cadet/incentives/achievements.ex +++ b/lib/cadet/incentives/achievements.ex @@ -13,9 +13,10 @@ defmodule Cadet.Incentives.Achievements do This returns Achievement structs with prerequisites and goal association maps pre-loaded. """ - @spec get() :: [%Achievement{}] - def get do + @spec get(integer()) :: [%Achievement{}] + def get(course_id) when is_ecto_id(course_id) do Achievement + |> where(course_id: ^course_id) |> preload([:prerequisites, :goals]) |> Repo.all() end @@ -25,6 +26,7 @@ defmodule Cadet.Incentives.Achievements do Inserts a new achievement, or updates it if it already exists. """ def upsert(attrs) when is_map(attrs) do + # course_id not nil check is left to the changeset case attrs[:uuid] || attrs["uuid"] do nil -> {:error, {:bad_request, "No UUID specified in Achievement"}} diff --git a/lib/cadet/incentives/goal.ex b/lib/cadet/incentives/goal.ex index 26b21fe75..7a30664e9 100644 --- a/lib/cadet/incentives/goal.ex +++ b/lib/cadet/incentives/goal.ex @@ -4,6 +4,7 @@ defmodule Cadet.Incentives.Goal do """ use Cadet, :model + alias Cadet.Courses.Course alias Cadet.Incentives.{AchievementToGoal, GoalProgress} @primary_key {:uuid, :binary_id, autogenerate: false} @@ -14,15 +15,17 @@ defmodule Cadet.Incentives.Goal do field(:type, :string) field(:meta, :map) + belongs_to(:course, Course) has_many(:progress, GoalProgress, foreign_key: :goal_uuid) has_many(:achievements, AchievementToGoal, on_replace: :delete_if_exists) end - @required_fields ~w(uuid text target_count type meta)a + @required_fields ~w(uuid text target_count type meta course_id)a def changeset(goal, params) do goal |> cast(params, @required_fields) |> validate_required(@required_fields) + |> foreign_key_constraint(:course_id) end end diff --git a/lib/cadet/incentives/goal_progress.ex b/lib/cadet/incentives/goal_progress.ex index f7c552486..3824a4f41 100644 --- a/lib/cadet/incentives/goal_progress.ex +++ b/lib/cadet/incentives/goal_progress.ex @@ -5,14 +5,14 @@ defmodule Cadet.Incentives.GoalProgress do use Cadet, :model alias Cadet.Incentives.Goal - alias Cadet.Accounts.User + alias Cadet.Accounts.CourseRegistration @primary_key false schema "goal_progress" do field(:count, :integer) field(:completed, :boolean) - belongs_to(:user, User, primary_key: true) + belongs_to(:course_reg, CourseRegistration, primary_key: true) belongs_to(:goal, Goal, primary_key: true, @@ -24,13 +24,13 @@ defmodule Cadet.Incentives.GoalProgress do timestamps() end - @required_fields ~w(count completed user_id goal_uuid)a + @required_fields ~w(count completed course_reg_id goal_uuid)a def changeset(progress, params) do progress |> cast(params, @required_fields) |> validate_required(@required_fields) - |> foreign_key_constraint(:user_id) + |> foreign_key_constraint(:course_reg_id) |> foreign_key_constraint(:goal_uuid) end end diff --git a/lib/cadet/incentives/goals.ex b/lib/cadet/incentives/goals.ex index 96566bb17..4055b3fb8 100644 --- a/lib/cadet/incentives/goals.ex +++ b/lib/cadet/incentives/goals.ex @@ -6,24 +6,26 @@ defmodule Cadet.Incentives.Goals do alias Cadet.Incentives.{Goal, GoalProgress} - alias Cadet.Accounts.User + alias Cadet.Accounts.CourseRegistration import Ecto.Query @doc """ Returns all goals. """ - @spec get() :: [%Goal{}] - def get do - Repo.all(Goal) + @spec get(integer()) :: [%Goal{}] + def get(course_id) when is_ecto_id(course_id) do + Goal + |> where(course_id: ^course_id) + |> Repo.all() end @doc """ - Returns goals with user progress. + Returns goals with progress for each course_registration. """ - def get_with_progress(%User{id: user_id}) do + def get_with_progress(%CourseRegistration{id: course_reg_id}) do Goal - |> join(:left, [g], p in assoc(g, :progress), on: p.user_id == ^user_id) + |> join(:left, [g], p in assoc(g, :progress), on: p.course_reg_id == ^course_reg_id) |> preload([g, p], [:achievements, progress: p]) |> Repo.all() end @@ -79,21 +81,30 @@ defmodule Cadet.Incentives.Goals do end end - def upsert_progress(attrs, goal_uuid, user_id) do - if goal_uuid == nil or user_id == nil do + def upsert_progress(attrs, goal_uuid, course_reg_id) do + if goal_uuid == nil or course_reg_id == nil do {:error, {:bad_request, "No UUID specified in Goal"}} else - GoalProgress - |> Repo.get_by(goal_uuid: goal_uuid, user_id: user_id) - |> (&(&1 || %GoalProgress{})).() - |> GoalProgress.changeset(attrs) - |> Repo.insert_or_update() - |> case do - result = {:ok, _} -> - result + course_reg = Repo.get(CourseRegistration, course_reg_id) + goal = Repo.get_by(Goal, uuid: goal_uuid, course_id: course_reg.course_id) + + case goal do + nil -> + {:error, {:bad_request, "User and goal are not in the same course"}} + + _ -> + GoalProgress + |> Repo.get_by(goal_uuid: goal_uuid, course_reg_id: course_reg_id) + |> (&(&1 || %GoalProgress{})).() + |> GoalProgress.changeset(attrs) + |> Repo.insert_or_update() + |> case do + result = {:ok, _} -> + result - {:error, changeset} -> - {:error, {:bad_request, full_error_messages(changeset)}} + {:error, changeset} -> + {:error, {:bad_request, full_error_messages(changeset)}} + end end end end diff --git a/lib/cadet_web/admin_controllers/admin_achievements_controller.ex b/lib/cadet_web/admin_controllers/admin_achievements_controller.ex index f73de07f4..83719daab 100644 --- a/lib/cadet_web/admin_controllers/admin_achievements_controller.ex +++ b/lib/cadet_web/admin_controllers/admin_achievements_controller.ex @@ -6,15 +6,17 @@ defmodule CadetWeb.AdminAchievementsController do alias Cadet.Incentives.Achievements def bulk_update(conn, %{"achievements" => achievements}) do + course_reg = conn.assigns.course_reg achievements - |> Enum.map(&json_to_achievement(&1)) + |> Enum.map(&json_to_achievement(&1, course_reg.course_id)) |> Achievements.upsert_many() |> handle_standard_result(conn) end def update(conn, %{"uuid" => uuid, "achievement" => achievement}) do + course_reg = conn.assigns.course_reg achievement - |> json_to_achievement(uuid) + |> json_to_achievement(course_reg.course_id, uuid) |> Achievements.upsert() |> handle_standard_result(conn) end @@ -25,7 +27,7 @@ defmodule CadetWeb.AdminAchievementsController do |> handle_standard_result(conn) end - defp json_to_achievement(json, uuid \\ nil) do + defp json_to_achievement(json, course_id, uuid \\ nil) do json = json |> snake_casify_string_keys_recursive() @@ -34,6 +36,7 @@ defmodule CadetWeb.AdminAchievementsController do {"release", "open_at"}, {"card_background", "card_tile_url"} ]) + |> Map.put("course_id", course_id) |> case do map = %{"view" => view} -> map diff --git a/lib/cadet_web/admin_controllers/admin_goals_controller.ex b/lib/cadet_web/admin_controllers/admin_goals_controller.ex index 0ed49c929..e609937fe 100644 --- a/lib/cadet_web/admin_controllers/admin_goals_controller.ex +++ b/lib/cadet_web/admin_controllers/admin_goals_controller.ex @@ -6,29 +6,38 @@ defmodule CadetWeb.AdminGoalsController do alias Cadet.Incentives.Goals def index(conn, _) do - render(conn, "index.json", goals: Goals.get()) + course_id = conn.assigns.course_reg.course_id + render(conn, "index.json", goals: Goals.get(course_id)) end def bulk_update(conn, %{"goals" => goals}) do + course_reg = conn.assigns.course_reg + goals - |> Enum.map(&json_to_goal(&1)) + |> Enum.map(&json_to_goal(&1, course_reg.course_id)) |> Goals.upsert_many() |> handle_standard_result(conn) end def update(conn, %{"uuid" => uuid, "goal" => goal}) do + course_reg = conn.assigns.course_reg + goal - |> json_to_goal(uuid) + |> json_to_goal(course_reg.course_id, uuid) |> Goals.upsert() |> handle_standard_result(conn) end - def update_progress(conn, %{"uuid" => uuid, "userid" => user_id, "progress" => progress}) do - user_id = String.to_integer(user_id) + def update_progress(conn, %{ + "uuid" => uuid, + "course_reg_id" => course_reg_id, + "progress" => progress + }) do + course_reg_id = String.to_integer(course_reg_id) progress - |> json_to_progress(uuid, user_id) - |> Goals.upsert_progress(uuid, user_id) + |> json_to_progress(uuid, course_reg_id) + |> Goals.upsert_progress(uuid, course_reg_id) |> handle_standard_result(conn) end @@ -38,13 +47,14 @@ defmodule CadetWeb.AdminGoalsController do |> handle_standard_result(conn) end - defp json_to_goal(json, uuid \\ nil) do + defp json_to_goal(json, course_id, uuid \\ nil) do original_meta = json["meta"] json = json |> snake_casify_string_keys_recursive() |> Map.put("meta", original_meta) + |> Map.put("course_id", course_id) if is_nil(uuid) do json @@ -53,7 +63,7 @@ defmodule CadetWeb.AdminGoalsController do end end - defp json_to_progress(json, uuid, user_id) do + defp json_to_progress(json, uuid, course_reg_id) do json = json |> snake_casify_string_keys_recursive() @@ -62,7 +72,7 @@ defmodule CadetWeb.AdminGoalsController do count: Map.get(json, "count"), completed: Map.get(json, "completed"), goal_uuid: uuid, - user_id: user_id + course_reg_id: course_reg_id } end diff --git a/lib/cadet_web/controllers/incentives_controller.ex b/lib/cadet_web/controllers/incentives_controller.ex index a375b8d60..4e0765dd3 100644 --- a/lib/cadet_web/controllers/incentives_controller.ex +++ b/lib/cadet_web/controllers/incentives_controller.ex @@ -6,25 +6,26 @@ defmodule CadetWeb.IncentivesController do alias Cadet.Incentives.{Achievements, Goals} def index_achievements(conn, _) do - render(conn, "index_achievements.json", achievements: Achievements.get()) + course_id = conn.assigns.course_reg.course_id + render(conn, "index_achievements.json", achievements: Achievements.get(course_id)) end def index_goals(conn, _) do render(conn, "index_goals_with_progress.json", - goals: Goals.get_with_progress(conn.assigns.current_user) + goals: Goals.get_with_progress(conn.assigns.course_reg) ) end def update_progress(conn, %{"uuid" => uuid, "progress" => progress}) do - user_id = conn.assigns.current_user.id + course_reg_id = conn.assigns.course_reg.id progress - |> json_to_progress(uuid, user_id) - |> Goals.upsert_progress(uuid, user_id) + |> json_to_progress(uuid, course_reg_id) + |> Goals.upsert_progress(uuid, course_reg_id) |> handle_standard_result(conn) end - defp json_to_progress(json, uuid, user_id) do + defp json_to_progress(json, uuid, course_reg_id) do json = json |> snake_casify_string_keys_recursive() @@ -33,7 +34,7 @@ defmodule CadetWeb.IncentivesController do count: Map.get(json, "count"), completed: Map.get(json, "completed"), goal_uuid: uuid, - user_id: user_id + course_reg_id: course_reg_id } end diff --git a/lib/cadet_web/router.ex b/lib/cadet_web/router.ex index 1a6d5ca5f..6c4ee73a0 100644 --- a/lib/cadet_web/router.ex +++ b/lib/cadet_web/router.ex @@ -132,12 +132,12 @@ defmodule CadetWeb.Router do put("/users", AdminUserController, :upsert_users_and_groups) put("/users/role", AdminUserController, :update_role) delete("/users", AdminUserController, :delete_user) - post("/users/:userid/goals/:uuid/progress", AdminGoalsController, :update_progress) put("/achievements", AdminAchievementsController, :bulk_update) - # put("/achievements/:uuid", AdminAchievementsController, :update) + put("/achievements/:uuid", AdminAchievementsController, :update) # may be unused delete("/achievements/:uuid", AdminAchievementsController, :delete) + post("goals/:uuid/progress/:course_reg_id", AdminGoalsController, :update_progress) get("/goals", AdminGoalsController, :index) put("/goals", AdminGoalsController, :bulk_update) put("/goals/:uuid", AdminGoalsController, :update) diff --git a/priv/repo/migrations/20210716073359_update_achievement.exs b/priv/repo/migrations/20210716073359_update_achievement.exs new file mode 100644 index 000000000..08851e719 --- /dev/null +++ b/priv/repo/migrations/20210716073359_update_achievement.exs @@ -0,0 +1,18 @@ +defmodule Cadet.Repo.Migrations.UpdateAchievement do + use Ecto.Migration + + def change do + alter table(:achievements) do + add(:course_id, references(:courses), null: false) + end + + alter table(:goals) do + add(:course_id, references(:courses), null: false) + end + + alter table(:goal_progress) do + remove(:user_id) + add(:course_reg_id, references(:course_registrations)) + end + end +end diff --git a/test/cadet/incentives/achievement_test.exs b/test/cadet/incentives/achievement_test.exs index b6d8e64c0..3b7a664bc 100644 --- a/test/cadet/incentives/achievement_test.exs +++ b/test/cadet/incentives/achievement_test.exs @@ -5,10 +5,13 @@ defmodule Cadet.Incentives.AchievementTest do describe "Changesets" do test "valid changesets" do + course = insert(:course) + assert_changeset( %{ uuid: "d1fdae3f-2775-4503-ab6b-e043149d4a15", title: "Hello World", + course_id: course.id, ability: "Core", open_at: DateTime.from_naive!(~N[2016-05-24 13:26:08.003], "Etc/UTC"), close_at: DateTime.from_naive!(~N[2016-05-27 13:26:08.003], "Etc/UTC"), diff --git a/test/cadet/incentives/achievements_test.exs b/test/cadet/incentives/achievements_test.exs index f9f738c12..e061c8d13 100644 --- a/test/cadet/incentives/achievements_test.exs +++ b/test/cadet/incentives/achievements_test.exs @@ -13,10 +13,13 @@ defmodule Cadet.Incentives.AchievementsTest do import Cadet.TestEntityHelper test "create achievements" do + course = insert(:course) + for ability <- Achievement.valid_abilities() do {:ok, %{uuid: uuid}} = Achievements.upsert(%{ uuid: Ecto.UUID.generate(), + course_id: course.id, title: ability, ability: ability, is_task: false, @@ -30,8 +33,9 @@ defmodule Cadet.Incentives.AchievementsTest do end test "create achievement with prerequisites as id" do - a1 = insert(:achievement) - a2 = insert(:achievement) + course = insert(:course) + a1 = insert(:achievement, %{course: course}) + a2 = insert(:achievement, %{course: course}) prerequisite_uuids = [a1.uuid, a2.uuid] a_uuid = UUID.generate() attrs = achievement_literal(0) @@ -39,6 +43,7 @@ defmodule Cadet.Incentives.AchievementsTest do {:ok, _} = attrs |> Map.merge(%{ + course_id: course.id, uuid: a_uuid, prerequisite_uuids: prerequisite_uuids }) @@ -55,6 +60,7 @@ defmodule Cadet.Incentives.AchievementsTest do {:ok, _} = attrs |> Map.merge(%{ + course_id: g.course_id, uuid: a_uuid, goal_uuids: [g.uuid] }) @@ -64,9 +70,10 @@ defmodule Cadet.Incentives.AchievementsTest do end test "get achievements" do - goal = insert(:goal) - prereq = insert(:achievement) - achievement = insert(:achievement, achievement_literal(0)) + course = insert(:course) + goal = insert(:goal, %{course: course}) + prereq = insert(:achievement, %{course: course}) + achievement = insert(:achievement, Map.merge(achievement_literal(0), %{course: course})) Repo.insert(%AchievementPrerequisite{ prerequisite_uuid: prereq.uuid, @@ -80,7 +87,7 @@ defmodule Cadet.Incentives.AchievementsTest do goal_uuid = goal.uuid prereq_uuid = prereq.uuid - achievement = Enum.find(Achievements.get(), &(&1.uuid == achievement.uuid)) + achievement = Enum.find(Achievements.get(course.id), &(&1.uuid == achievement.uuid)) assert achievement_literal(0) = achievement assert [%{goal_uuid: ^goal_uuid}] = achievement.goals @@ -125,9 +132,11 @@ defmodule Cadet.Incentives.AchievementsTest do end test "bulk insert succeeds" do + course = insert(:course) + attrs = [achievement_literal(0), achievement_literal(1)] - |> Enum.map(&Map.merge(&1, %{uuid: UUID.generate()})) + |> Enum.map(&Map.merge(&1, %{course_id: course.id, uuid: UUID.generate()})) assert {:ok, result} = Achievements.upsert_many(attrs) assert [achievement_literal(0), achievement_literal(1)] = result diff --git a/test/cadet/incentives/goal_progress_test.exs b/test/cadet/incentives/goal_progress_test.exs index 1e12c6663..dbd4a4e2e 100644 --- a/test/cadet/incentives/goal_progress_test.exs +++ b/test/cadet/incentives/goal_progress_test.exs @@ -5,13 +5,13 @@ defmodule Cadet.Incentives.GoalProgressTest do describe "Changesets" do test "valid params" do - user = insert(:user) + course_reg = insert(:course_registration) goal = insert(:goal) assert_changeset_db( %{ goal_uuid: goal.uuid, - user_id: user.id, + course_reg_id: course_reg.id, count: 500, completed: false }, diff --git a/test/cadet/incentives/goal_test.exs b/test/cadet/incentives/goal_test.exs index d97786068..48b014752 100644 --- a/test/cadet/incentives/goal_test.exs +++ b/test/cadet/incentives/goal_test.exs @@ -6,9 +6,12 @@ defmodule Cadet.Incentives.GoalTest do describe "Changesets" do test "valid params" do + course = insert(:course) + assert_changeset_db( %{ uuid: UUID.generate(), + course_id: course.id, target_count: 1000, text: "Sample Text", type: "test_type", diff --git a/test/cadet/incentives/goals_test.exs b/test/cadet/incentives/goals_test.exs index 427823961..46ce0fca3 100644 --- a/test/cadet/incentives/goals_test.exs +++ b/test/cadet/incentives/goals_test.exs @@ -7,28 +7,29 @@ defmodule Cadet.Incentives.GoalssTest do import Cadet.TestEntityHelper test "create goal" do + course = insert(:course) uuid = UUID.generate() - Goals.upsert(Map.merge(goal_literal(0), %{uuid: uuid})) + Goals.upsert(Map.merge(goal_literal(0), %{course_id: course.id, uuid: uuid})) assert goal_literal(0) = Repo.get(Goal, uuid) end test "get goals" do - insert(:goal, goal_literal(0)) - assert [goal_literal(0)] = Goals.get() + goal = insert(:goal, goal_literal(0)) + assert [goal_literal(0)] = Goals.get(goal.course_id) end test "get goals with progress" do goal = insert(:goal, goal_literal(0)) - user = insert(:user) + course_reg = insert(:course_registration) Repo.insert(%GoalProgress{ count: 500, completed: false, - user_id: user.id, + course_reg_id: course_reg.id, goal_uuid: goal.uuid }) - retrieved_goal = Goals.get_with_progress(user) + retrieved_goal = Goals.get_with_progress(course_reg) assert [goal_literal(0)] = retrieved_goal assert [%{progress: [%{count: 500, completed: false}]}] = retrieved_goal @@ -48,8 +49,11 @@ defmodule Cadet.Incentives.GoalssTest do end test "bulk insert succeeds" do + course = insert(:course) + attrs = - [goal_literal(0), goal_literal(1)] |> Enum.map(&Map.merge(&1, %{uuid: UUID.generate()})) + [goal_literal(0), goal_literal(1)] + |> Enum.map(&Map.merge(&1, %{course_id: course.id, uuid: UUID.generate()})) assert {:ok, result} = Goals.upsert_many(attrs) assert [goal_literal(0), goal_literal(1)] = result @@ -76,7 +80,7 @@ defmodule Cadet.Incentives.GoalssTest do test "upsert progress" do goal = insert(:goal, goal_literal(0)) - user = insert(:user) + course_reg = insert(:course_registration, %{course: goal.course}) assert {:ok, _} = Goals.upsert_progress( @@ -84,13 +88,13 @@ defmodule Cadet.Incentives.GoalssTest do count: 100, completed: false, goal_uuid: goal.uuid, - user_id: user.id + course_reg_id: course_reg.id }, goal.uuid, - user.id + course_reg.id ) - retrieved_goal = Goals.get_with_progress(user) + retrieved_goal = Goals.get_with_progress(course_reg) assert [%{progress: [%{count: 100, completed: false}]}] = retrieved_goal assert {:ok, _} = @@ -99,13 +103,13 @@ defmodule Cadet.Incentives.GoalssTest do count: 200, completed: true, goal_uuid: goal.uuid, - user_id: user.id + course_reg_id: course_reg.id }, goal.uuid, - user.id + course_reg.id ) - retrieved_goal = Goals.get_with_progress(user) + retrieved_goal = Goals.get_with_progress(course_reg) assert [%{progress: [%{count: 200, completed: true}]}] = retrieved_goal end end diff --git a/test/cadet_web/admin_controllers/admin_achievements_controller_test.exs b/test/cadet_web/admin_controllers/admin_achievements_controller_test.exs index 92eb3d502..bbfb3a12f 100644 --- a/test/cadet_web/admin_controllers/admin_achievements_controller_test.exs +++ b/test/cadet_web/admin_controllers/admin_achievements_controller_test.exs @@ -14,13 +14,14 @@ defmodule CadetWeb.AdminAchievementsControllerTest do assert is_map(AdminAchievementsController.swagger_path_delete(nil)) end - describe "PUT /admin/achievements/:uuid" do + describe "PUT v2/courses/:course_id/admin/achievements/:uuid" do @tag authenticate: :staff test "succeeds for staff", %{conn: conn} do + course_id = conn.assigns.course_id uuid = UUID.generate() conn - |> put(build_path(uuid), %{"achievement" => achievement_json_literal(0)}) + |> put(build_path(course_id, uuid), %{"achievement" => achievement_json_literal(0)}) |> response(204) ach = Repo.get(Achievement, uuid) @@ -30,10 +31,11 @@ defmodule CadetWeb.AdminAchievementsControllerTest do @tag authenticate: :staff test "succeeds without view", %{conn: conn} do + course_id = conn.assigns.course_id uuid = UUID.generate() conn - |> put(build_path(uuid), %{"achievement" => Map.drop(achievement_json_literal(0), ["view"])}) + |> put(build_path(course_id, uuid), %{"achievement" => Map.drop(achievement_json_literal(0), ["view"])}) |> response(204) ach = Repo.get(Achievement, uuid) @@ -47,10 +49,11 @@ defmodule CadetWeb.AdminAchievementsControllerTest do @tag authenticate: :student test "403 for student", %{conn: conn} do + course = insert(:course) uuid = UUID.generate() conn - |> put(build_path(uuid), %{"achievement" => achievement_json_literal(0)}) + |> put(build_path(course.id, uuid), %{"achievement" => achievement_json_literal(0)}) |> response(403) assert Achievement |> Repo.get(uuid) |> is_nil() @@ -67,7 +70,7 @@ defmodule CadetWeb.AdminAchievementsControllerTest do end end - describe "PUT /admin/achievements" do + describe "PUT v2/courses/:course_id/admin/achievements" do setup do %{ achievements: [ @@ -79,8 +82,9 @@ defmodule CadetWeb.AdminAchievementsControllerTest do @tag authenticate: :staff test "succeeds for staff", %{conn: conn, achievements: achs = [a1, a2]} do + course_id = conn.assigns.course_id conn - |> put(build_path(), %{ + |> put(build_path(course_id), %{ "achievements" => achs }) |> response(204) @@ -91,8 +95,9 @@ defmodule CadetWeb.AdminAchievementsControllerTest do @tag authenticate: :student test "403 for student", %{conn: conn, achievements: achs = [a1, a2]} do + course_id = conn.assigns.course_id conn - |> put(build_path(), %{ + |> put(build_path(course_id), %{ "achievements" => achs }) |> response(403) @@ -102,8 +107,9 @@ defmodule CadetWeb.AdminAchievementsControllerTest do end test "401 if unauthenticated", %{conn: conn, achievements: achs = [a1, a2]} do + course = insert(:course) conn - |> put(build_path(), %{ + |> put(build_path(course.id), %{ "achievements" => achs }) |> response(401) @@ -113,48 +119,47 @@ defmodule CadetWeb.AdminAchievementsControllerTest do end end - describe "DELETE /admin/achievements/:uuid" do - setup do - {:ok, a} = - %Achievement{uuid: UUID.generate()} |> Map.merge(achievement_literal(5)) |> Repo.insert() - - %{achievement: a} - end - + describe "DELETE v2/courses/:course_id/admin/achievements/:uuid" do @tag authenticate: :staff - test "succeeds for staff", %{conn: conn, achievement: a} do + test "succeeds for staff", %{conn: conn} do + course_id = conn.assigns.course_id + {:ok, a} = %Achievement{course_id: course_id, uuid: UUID.generate()} |> Map.merge(achievement_literal(5)) |> Repo.insert() conn - |> delete(build_path(a.uuid)) + |> delete(build_path(course_id, a.uuid)) |> response(204) assert Achievement |> Repo.get(a.uuid) |> is_nil() end @tag authenticate: :student - test "403 for student", %{conn: conn, achievement: a} do + test "403 for student", %{conn: conn} do + course_id = conn.assigns.course_id + {:ok, a} = %Achievement{course_id: course_id, uuid: UUID.generate()} |> Map.merge(achievement_literal(5)) |> Repo.insert() conn - |> delete(build_path(a.uuid)) + |> delete(build_path(course_id, a.uuid)) |> response(403) assert achievement_literal(5) = Repo.get(Achievement, a.uuid) end - test "401 if unauthenticated", %{conn: conn, achievement: a} do + test "401 if unauthenticated", %{conn: conn} do + course = insert(:course) + {:ok, a} = %Achievement{course_id: course.id, uuid: UUID.generate()} |> Map.merge(achievement_literal(5)) |> Repo.insert() conn - |> delete(build_path(a.uuid)) + |> delete(build_path(course.id, a.uuid)) |> response(401) assert achievement_literal(5) = Repo.get(Achievement, a.uuid) end end - defp build_path(uuid \\ nil) + defp build_path(course_id, uuid \\ nil) - defp build_path(nil) do - "/v2/admin/achievements" + defp build_path(course_id, nil) do + "/v2/courses/#{course_id}/admin/achievements" end - defp build_path(uuid) do - "/v2/admin/achievements/#{uuid}" + defp build_path(course_id, uuid) do + "/v2/courses/#{course_id}/admin/achievements/#{uuid}" end end diff --git a/test/cadet_web/admin_controllers/admin_assets_controller_test.exs b/test/cadet_web/admin_controllers/admin_assets_controller_test.exs index 5c3316de9..7cddf0562 100644 --- a/test/cadet_web/admin_controllers/admin_assets_controller_test.exs +++ b/test/cadet_web/admin_controllers/admin_assets_controller_test.exs @@ -54,6 +54,7 @@ defmodule CadetWeb.AdminAssetsControllerTest do @tag authenticate: :student test "POST /assets/:foldername/*filename", %{conn: conn} do course_id = conn.assigns.course_id + conn = post(conn, build_url(course_id, "testFolder/testFile.png"), %{ :upload => build_upload("test/fixtures/upload.png") @@ -82,6 +83,7 @@ defmodule CadetWeb.AdminAssetsControllerTest do @tag authenticate: :staff test "upload file", %{conn: conn} do course_id = conn.assigns.course_id + conn = post(conn, build_url(course_id, "wrongFolder/wrongUpload.png"), %{ "upload" => build_upload("test/fixtures/upload.png") @@ -95,6 +97,7 @@ defmodule CadetWeb.AdminAssetsControllerTest do @tag authenticate: :staff test "index file", %{conn: conn} do course_id = conn.assigns.course_id + use_cassette "aws/controller_list_assets#1" do conn = get(conn, build_url(course_id, "testFolder"), %{}) @@ -106,6 +109,7 @@ defmodule CadetWeb.AdminAssetsControllerTest do @tag authenticate: :staff test "delete file", %{conn: conn} do course_id = conn.assigns.course_id + use_cassette "aws/controller_delete_asset#1" do conn = delete(conn, build_url(course_id, "testFolder/test2.png")) @@ -116,6 +120,7 @@ defmodule CadetWeb.AdminAssetsControllerTest do @tag authenticate: :staff test "upload file", %{conn: conn} do course_id = conn.assigns.course_id + use_cassette "aws/controller_upload_asset#1" do conn = post(conn, build_url(course_id, "testFolder/test.png"), %{ @@ -132,6 +137,7 @@ defmodule CadetWeb.AdminAssetsControllerTest do @tag authenticate: :staff test "upload file", %{conn: conn} do course_id = conn.assigns.course_id + conn = post(conn, build_url(course_id, "testFolder/test.pdf"), %{ "upload" => build_upload("test/fixtures/upload.pdf") @@ -145,6 +151,7 @@ defmodule CadetWeb.AdminAssetsControllerTest do @tag authenticate: :staff test "upload file", %{conn: conn} do course_id = conn.assigns.course_id + conn = post(conn, build_url(course_id, "testFolder"), %{ "upload" => build_upload("test/fixtures/upload.png") @@ -165,6 +172,7 @@ defmodule CadetWeb.AdminAssetsControllerTest do @tag authenticate: :staff test "delete file", %{conn: conn} do course_id = conn.assigns.course_id + use_cassette "aws/controller_delete_asset#2" do conn = delete(conn, build_url(course_id, "testFolder/nestedFolder/test2.png")) @@ -175,6 +183,7 @@ defmodule CadetWeb.AdminAssetsControllerTest do @tag authenticate: :staff test "upload file", %{conn: conn} do course_id = conn.assigns.course_id + use_cassette "aws/controller_upload_asset#2" do conn = post(conn, build_url(course_id, "testFolder/nestedFolder/test.png"), %{ diff --git a/test/cadet_web/admin_controllers/admin_goals_controller_test.exs b/test/cadet_web/admin_controllers/admin_goals_controller_test.exs index 7a13d6bb4..25fd62957 100644 --- a/test/cadet_web/admin_controllers/admin_goals_controller_test.exs +++ b/test/cadet_web/admin_controllers/admin_goals_controller_test.exs @@ -4,7 +4,6 @@ defmodule CadetWeb.AdminGoalsControllerTest do import Cadet.TestEntityHelper alias Cadet.Repo - alias Cadet.Accounts.User alias Cadet.Incentives.{Goal, Goals} alias CadetWeb.AdminGoalsController alias Ecto.UUID @@ -18,18 +17,14 @@ defmodule CadetWeb.AdminGoalsControllerTest do assert is_map(AdminGoalsController.swagger_path_update_progress(nil)) end - describe "GET /admin/goals" do - setup do - {:ok, g} = %Goal{uuid: UUID.generate()} |> Map.merge(goal_literal(5)) |> Repo.insert() - - %{goal: g} - end - + describe "GET v2/courses/:course_id/admin/goals" do @tag authenticate: :staff - test "succeeds for staff", %{conn: conn, goal: goal} do + test "succeeds for staff", %{conn: conn} do + course_id = conn.assigns.course_id + {:ok, goal} = %Goal{course_id: course_id, uuid: UUID.generate()} |> Map.merge(goal_literal(5)) |> Repo.insert() [resp_goal] = conn - |> get(build_path()) + |> get(build_path(course_id)) |> json_response(200) assert goal_json_literal(5) = resp_goal @@ -38,25 +33,28 @@ defmodule CadetWeb.AdminGoalsControllerTest do @tag authenticate: :student test "403 for student", %{conn: conn} do + course_id = conn.assigns.course_id conn - |> get(build_path()) + |> get(build_path(course_id)) |> response(403) end test "401 if unauthenticated", %{conn: conn} do + course = insert(:course) conn - |> get(build_path()) + |> get(build_path(course.id)) |> response(401) end end - describe "PUT /admin/goals/:uuid" do + describe "PUT v2/courses/:course_id/admin/goals/:uuid" do @tag authenticate: :staff test "succeeds for staff", %{conn: conn} do + course_id = conn.assigns.course_id uuid = UUID.generate() conn - |> put(build_path(uuid), %{"goal" => goal_json_literal(0)}) + |> put(build_path(course_id, uuid), %{"goal" => goal_json_literal(0)}) |> response(204) ach = Repo.get(Goal, uuid) @@ -66,27 +64,29 @@ defmodule CadetWeb.AdminGoalsControllerTest do @tag authenticate: :student test "403 for student", %{conn: conn} do + course_id = conn.assigns.course_id uuid = UUID.generate() conn - |> put(build_path(uuid), %{"goal" => goal_json_literal(0)}) + |> put(build_path(course_id, uuid), %{"goal" => goal_json_literal(0)}) |> response(403) assert Goal |> Repo.get(uuid) |> is_nil() end test "401 if unauthenticated", %{conn: conn} do + course = insert(:course) uuid = UUID.generate() conn - |> put(build_path(uuid), %{"goal" => goal_json_literal(0)}) + |> put(build_path(course.id, uuid), %{"goal" => goal_json_literal(0)}) |> response(401) assert Goal |> Repo.get(uuid) |> is_nil() end end - describe "PUT /admin/goals" do + describe "PUT v2/courses/:course_id/admin/goals" do setup do %{ goals: [ @@ -98,8 +98,9 @@ defmodule CadetWeb.AdminGoalsControllerTest do @tag authenticate: :staff test "succeeds for staff", %{conn: conn, goals: goals = [a1, a2]} do + course_id = conn.assigns.course_id conn - |> put(build_path(), %{ + |> put(build_path(course_id), %{ "goals" => goals }) |> response(204) @@ -110,8 +111,9 @@ defmodule CadetWeb.AdminGoalsControllerTest do @tag authenticate: :student test "403 for student", %{conn: conn, goals: goals = [a1, a2]} do + course_id = conn.assigns.course_id conn - |> put(build_path(), %{ + |> put(build_path(course_id), %{ "goals" => goals }) |> response(403) @@ -121,8 +123,9 @@ defmodule CadetWeb.AdminGoalsControllerTest do end test "401 if unauthenticated", %{conn: conn, goals: goals = [a1, a2]} do + course = insert(:course) conn - |> put(build_path(), %{ + |> put(build_path(course.id), %{ "goals" => goals }) |> response(401) @@ -132,85 +135,94 @@ defmodule CadetWeb.AdminGoalsControllerTest do end end - describe "DELETE /admin/goals/:uuid" do - setup do - {:ok, a} = %Goal{uuid: UUID.generate()} |> Map.merge(goal_literal(5)) |> Repo.insert() - - %{goal: a} - end - + describe "DELETE v2/courses/:course_id/admin/goals/:uuid" do @tag authenticate: :staff - test "succeeds for staff", %{conn: conn, goal: a} do + test "succeeds for staff", %{conn: conn} do + course_id = conn.assigns.course_id + {:ok, a} = %Goal{course_id: course_id, uuid: UUID.generate()} |> Map.merge(goal_literal(5)) |> Repo.insert() conn - |> delete(build_path(a.uuid)) + |> delete(build_path(course_id, a.uuid)) |> response(204) assert Goal |> Repo.get(a.uuid) |> is_nil() end @tag authenticate: :student - test "403 for student", %{conn: conn, goal: a} do + test "403 for student", %{conn: conn} do + course_id = conn.assigns.course_id + {:ok, a} = %Goal{course_id: course_id, uuid: UUID.generate()} |> Map.merge(goal_literal(5)) |> Repo.insert() conn - |> delete(build_path(a.uuid)) + |> delete(build_path(course_id, a.uuid)) |> response(403) assert goal_literal(5) = Repo.get(Goal, a.uuid) end - test "401 if unauthenticated", %{conn: conn, goal: a} do + test "401 if unauthenticated", %{conn: conn} do + course = insert(:course) + {:ok, a} = %Goal{course_id: course.id, uuid: UUID.generate()} |> Map.merge(goal_literal(5)) |> Repo.insert() conn - |> delete(build_path(a.uuid)) + |> delete(build_path(course.id, a.uuid)) |> response(401) assert goal_literal(5) = Repo.get(Goal, a.uuid) end end - describe "POST /admin/users/:userid/goals/:uuid/progress" do - setup do - {:ok, g} = %Goal{uuid: UUID.generate()} |> Map.merge(goal_literal(5)) |> Repo.insert() - u = insert(:course_registration, %{role: :student}) - - %{goal: g, user: u} - end - + describe "POST v2/courses/:course_id/goals/:uuid/progress/:course_reg_id" do @tag authenticate: :staff - test "succeeds for staff", %{conn: conn, goal: g, user: u} do + test "succeeds for staff", %{conn: conn} do + course = conn.assigns.test_cr.course + {:ok, g} = %Goal{course_id: course.id, uuid: UUID.generate()} |> Map.merge(goal_literal(5)) |> Repo.insert() + course_reg = insert(:course_registration, %{course: course, role: :student}) + conn - |> post("/v2/admin/users/#{u.id}/goals/#{g.uuid}/progress", %{ - "progress" => %{count: 100, completed: false, userid: u.id, uuid: g.uuid} + |> post(build_path(course.id, g.uuid, course_reg.id), %{ + "progress" => %{count: 100, completed: false, course_reg_id: course_reg.id, uuid: g.uuid} }) |> response(204) - retrieved_goal = Goals.get_with_progress(u) + retrieved_goal = Goals.get_with_progress(course_reg) assert [%{progress: [%{count: 100, completed: false}]}] = retrieved_goal end @tag authenticate: :student - test "403 for student", %{conn: conn, goal: g, user: u} do + test "403 for student", %{conn: conn} do + course = conn.assigns.test_cr.course + {:ok, g} = %Goal{course_id: course.id, uuid: UUID.generate()} |> Map.merge(goal_literal(5)) |> Repo.insert() + course_reg = insert(:course_registration, %{course: course, role: :student}) + conn - |> post("/v2/admin/users/#{u.id}/goals/#{g.uuid}/progress", %{ - "progress" => %{count: 100, completed: false, userid: u.id, uuid: g.uuid} + |> post(build_path(course.id, g.uuid, course_reg.id), %{ + "progress" => %{count: 100, completed: false, course_reg_id: course_reg.id, uuid: g.uuid} }) |> response(403) end - test "401 if unauthenticated", %{conn: conn, goal: g, user: u} do + test "401 if unauthenticated", %{conn: conn} do + course = insert(:course) + {:ok, g} = %Goal{course_id: course.id, uuid: UUID.generate()} |> Map.merge(goal_literal(5)) |> Repo.insert() + course_reg = insert(:course_registration, %{course: course, role: :student}) + conn - |> post("/v2/admin/users/#{u.id}/goals/#{g.uuid}/progress", %{ - "progress" => %{count: 100, completed: false, userid: u.id, uuid: g.uuid} + |> post(build_path(course.id, g.uuid, course_reg.id), %{ + "progress" => %{count: 100, completed: false, course_reg_id: course_reg.id, uuid: g.uuid} }) |> response(401) end end - defp build_path(uuid \\ nil) + defp build_path(course_id, uuid \\ nil) + + defp build_path(course_id, nil) do + "/v2/courses/#{course_id}/admin/goals" + end - defp build_path(nil) do - "/v2/admin/goals" + defp build_path(course_id, uuid) do + "/v2/courses/#{course_id}/admin/goals/#{uuid}" end - defp build_path(uuid) do - "/v2/admin/goals/#{uuid}" + defp build_path(course_id, uuid, course_reg_id) do + "v2/courses/#{course_id}/admin/goals/#{uuid}/progress/#{course_reg_id}" end end diff --git a/test/cadet_web/controllers/incentives_controller_test.exs b/test/cadet_web/controllers/incentives_controller_test.exs index e66e78242..4883f9d1f 100644 --- a/test/cadet_web/controllers/incentives_controller_test.exs +++ b/test/cadet_web/controllers/incentives_controller_test.exs @@ -5,7 +5,7 @@ defmodule CadetWeb.IncentivesControllerTest do alias Cadet.Repo alias CadetWeb.IncentivesController - alias Cadet.Incentives.{Goal, Goals, GoalProgress} + alias Cadet.Incentives.{Goals, GoalProgress} alias Ecto.UUID test "swagger" do @@ -15,46 +15,49 @@ defmodule CadetWeb.IncentivesControllerTest do assert is_map(IncentivesController.swagger_path_update_progress(nil)) end - describe "GET /achievements" do + describe "GET v2/coures/:course_id/achievements" do @tag authenticate: :student test "succeeds if authenticated", %{conn: conn} do - insert(:achievement, achievement_literal(0)) + course = conn.assigns.test_cr.course + insert(:achievement, Map.merge(achievement_literal(0), %{course: course})) - resp = conn |> get("/v2/achievements") |> json_response(200) + resp = conn |> get(build_url_achievements(course.id)) |> json_response(200) assert [achievement_json_literal(0)] = resp end test "401 if unauthenticated", %{conn: conn} do - conn |> get("/v2/achievements") |> response(401) + course = insert(:course) + conn |> get(build_url_achievements(course.id)) |> response(401) end end - describe "GET /self/goals" do + describe "GET v2/coures/:course_id/self/goals" do @tag authenticate: :student test "succeeds if authenticated", %{conn: conn} do - insert(:goal, goal_literal(0)) + course = conn.assigns.test_cr.course + insert(:goal, Map.merge(goal_literal(0), %{course: course})) - resp = conn |> get("/v2/self/goals") |> json_response(200) + resp = conn |> get(build_url_goals(course.id)) |> json_response(200) assert [goal_json_literal(0)] = resp end @tag authenticate: :student test "includes user's progress", %{conn: conn} do - user = conn.assigns.current_user - goal = insert(:goal, goal_literal(0)) + course_reg = conn.assigns.test_cr + goal = insert(:goal, Map.merge(goal_literal(0), %{course: course_reg.course})) {:ok, progress} = %GoalProgress{ goal_uuid: goal.uuid, - user_id: user.id, + course_reg_id: course_reg.id, count: 123, completed: true } |> Repo.insert() - [resp_goal] = conn |> get("/v2/self/goals") |> json_response(200) + [resp_goal] = conn |> get(build_url_goals(course_reg.course_id)) |> json_response(200) assert goal_json_literal(0) = resp_goal assert resp_goal["count"] == progress.count @@ -62,37 +65,66 @@ defmodule CadetWeb.IncentivesControllerTest do end test "401 if unauthenticated", %{conn: conn} do - conn |> get("/v2/self/goals") |> response(401) + course = insert(:course) + conn |> get(build_url_goals(course.id)) |> response(401) end end - describe "POST /self/goals/:uuid/progress" do - setup do - {:ok, g} = %Goal{uuid: UUID.generate()} |> Map.merge(goal_literal(5)) |> Repo.insert() - - %{goal: g} - end - + describe "POST v2/coures/:course_id/self/goals/:uuid/progress" do @tag authenticate: :student - test "succeeds if authenticated", %{conn: conn, goal: g} do - user = conn.assigns.current_user + test "succeeds if authenticated", %{conn: conn} do + course_reg = conn.assigns.test_cr + uuid = UUID.generate() + + goal = + insert( + :goal, + Map.merge(goal_literal(5), %{ + course: course_reg.course, + course_id: course_reg.course_id, + uuid: uuid + }) + ) conn - |> post("/v2/self/goals/#{g.uuid}/progress", %{ - "progress" => %{count: 100, completed: false, userid: user.id, uuid: g.uuid} + |> post(build_url_goals(course_reg.course_id, goal.uuid), %{ + "progress" => %{ + count: 100, + completed: false, + course_reg_id: course_reg.id, + uuid: goal.uuid + } }) |> response(204) - retrieved_goal = Goals.get_with_progress(user) + retrieved_goal = Goals.get_with_progress(course_reg) assert [%{progress: [%{count: 100, completed: false}]}] = retrieved_goal end - test "401 if unauthenticated", %{conn: conn, goal: g} do + test "401 if unauthenticated", %{conn: conn} do + course = insert(:course) + uuid = UUID.generate() + + goal = + insert( + :goal, + Map.merge(goal_literal(5), %{course: course, course_id: course.id, uuid: uuid}) + ) + conn - |> post("/v2/self/goals/#{g.uuid}/progress", %{ - "progress" => %{count: 100, completed: false, userid: 1, uuid: g.uuid} + |> post(build_url_goals(course.id, goal.uuid), %{ + "progress" => %{count: 100, completed: false, course_reg_id: 1, uuid: goal.uuid} }) |> response(401) end end + + defp build_url_achievements(course_id), + do: "/v2/courses/#{course_id}/achievements" + + defp build_url_goals(course_id), + do: "/v2/courses/#{course_id}/self/goals" + + defp build_url_goals(course_id, uuid), + do: "/v2/courses/#{course_id}/self/goals/#{uuid}/progress" end diff --git a/test/factories/achievements/achievement_factory.ex b/test/factories/achievements/achievement_factory.ex index 6d2ab4761..f859cd956 100644 --- a/test/factories/achievements/achievement_factory.ex +++ b/test/factories/achievements/achievement_factory.ex @@ -11,6 +11,7 @@ defmodule Cadet.Incentives.AchievementFactory do def achievement_factory do %Achievement{ uuid: UUID.generate(), + course: insert(:course), title: Faker.Food.dish(), ability: Enum.random(Achievement.valid_abilities()), is_task: false, diff --git a/test/factories/achievements/goal_factory.ex b/test/factories/achievements/goal_factory.ex index 4305117d5..c5238da36 100644 --- a/test/factories/achievements/goal_factory.ex +++ b/test/factories/achievements/goal_factory.ex @@ -14,6 +14,7 @@ defmodule Cadet.Incentives.GoalFactory do text: "Score earned from Curve Introduction mission", target_count: Faker.random_between(1, 1000), type: "test_type", + course: insert(:course), meta: %{} } end From 99df99612caa0444d1a950f3a576a014424de294 Mon Sep 17 00:00:00 2001 From: YaleChen299 Date: Sat, 17 Jul 2021 16:59:07 +0800 Subject: [PATCH 158/174] fix formatting --- .../admin_achievements_controller.ex | 2 + lib/cadet_web/router.ex | 3 +- lib/cadet_web/views/assessments_helpers.ex | 24 ++++----- .../admin_achievements_controller_test.exs | 28 ++++++++-- .../admin_goals_controller_test.exs | 54 ++++++++++++++++--- 5 files changed, 86 insertions(+), 25 deletions(-) diff --git a/lib/cadet_web/admin_controllers/admin_achievements_controller.ex b/lib/cadet_web/admin_controllers/admin_achievements_controller.ex index 83719daab..36e85c627 100644 --- a/lib/cadet_web/admin_controllers/admin_achievements_controller.ex +++ b/lib/cadet_web/admin_controllers/admin_achievements_controller.ex @@ -7,6 +7,7 @@ defmodule CadetWeb.AdminAchievementsController do def bulk_update(conn, %{"achievements" => achievements}) do course_reg = conn.assigns.course_reg + achievements |> Enum.map(&json_to_achievement(&1, course_reg.course_id)) |> Achievements.upsert_many() @@ -15,6 +16,7 @@ defmodule CadetWeb.AdminAchievementsController do def update(conn, %{"uuid" => uuid, "achievement" => achievement}) do course_reg = conn.assigns.course_reg + achievement |> json_to_achievement(course_reg.course_id, uuid) |> Achievements.upsert() diff --git a/lib/cadet_web/router.ex b/lib/cadet_web/router.ex index 6c4ee73a0..decd248d5 100644 --- a/lib/cadet_web/router.ex +++ b/lib/cadet_web/router.ex @@ -134,7 +134,8 @@ defmodule CadetWeb.Router do delete("/users", AdminUserController, :delete_user) put("/achievements", AdminAchievementsController, :bulk_update) - put("/achievements/:uuid", AdminAchievementsController, :update) # may be unused + # update may be unused + put("/achievements/:uuid", AdminAchievementsController, :update) delete("/achievements/:uuid", AdminAchievementsController, :delete) post("goals/:uuid/progress/:course_reg_id", AdminGoalsController, :update_progress) diff --git a/lib/cadet_web/views/assessments_helpers.ex b/lib/cadet_web/views/assessments_helpers.ex index 361504156..f534a9f3b 100644 --- a/lib/cadet_web/views/assessments_helpers.ex +++ b/lib/cadet_web/views/assessments_helpers.ex @@ -164,21 +164,19 @@ defmodule CadetWeb.AssessmentsHelpers do end defp build_testcases(all_testcases?) do - cond do - all_testcases? -> - &Enum.concat( - Enum.concat( - Enum.map(&1["public"], fn testcase -> build_testcase(testcase, "public") end), - Enum.map(&1["opaque"], fn testcase -> build_testcase(testcase, "opaque") end) - ), - Enum.map(&1["secret"], fn testcase -> build_testcase(testcase, "secret") end) - ) - - true -> - &Enum.concat( + if all_testcases? do + &Enum.concat( + Enum.concat( Enum.map(&1["public"], fn testcase -> build_testcase(testcase, "public") end), Enum.map(&1["opaque"], fn testcase -> build_testcase(testcase, "opaque") end) - ) + ), + Enum.map(&1["secret"], fn testcase -> build_testcase(testcase, "secret") end) + ) + else + &Enum.concat( + Enum.map(&1["public"], fn testcase -> build_testcase(testcase, "public") end), + Enum.map(&1["opaque"], fn testcase -> build_testcase(testcase, "opaque") end) + ) end end diff --git a/test/cadet_web/admin_controllers/admin_achievements_controller_test.exs b/test/cadet_web/admin_controllers/admin_achievements_controller_test.exs index bbfb3a12f..7bedaaaed 100644 --- a/test/cadet_web/admin_controllers/admin_achievements_controller_test.exs +++ b/test/cadet_web/admin_controllers/admin_achievements_controller_test.exs @@ -35,7 +35,9 @@ defmodule CadetWeb.AdminAchievementsControllerTest do uuid = UUID.generate() conn - |> put(build_path(course_id, uuid), %{"achievement" => Map.drop(achievement_json_literal(0), ["view"])}) + |> put(build_path(course_id, uuid), %{ + "achievement" => Map.drop(achievement_json_literal(0), ["view"]) + }) |> response(204) ach = Repo.get(Achievement, uuid) @@ -83,6 +85,7 @@ defmodule CadetWeb.AdminAchievementsControllerTest do @tag authenticate: :staff test "succeeds for staff", %{conn: conn, achievements: achs = [a1, a2]} do course_id = conn.assigns.course_id + conn |> put(build_path(course_id), %{ "achievements" => achs @@ -96,6 +99,7 @@ defmodule CadetWeb.AdminAchievementsControllerTest do @tag authenticate: :student test "403 for student", %{conn: conn, achievements: achs = [a1, a2]} do course_id = conn.assigns.course_id + conn |> put(build_path(course_id), %{ "achievements" => achs @@ -108,6 +112,7 @@ defmodule CadetWeb.AdminAchievementsControllerTest do test "401 if unauthenticated", %{conn: conn, achievements: achs = [a1, a2]} do course = insert(:course) + conn |> put(build_path(course.id), %{ "achievements" => achs @@ -123,7 +128,12 @@ defmodule CadetWeb.AdminAchievementsControllerTest do @tag authenticate: :staff test "succeeds for staff", %{conn: conn} do course_id = conn.assigns.course_id - {:ok, a} = %Achievement{course_id: course_id, uuid: UUID.generate()} |> Map.merge(achievement_literal(5)) |> Repo.insert() + + {:ok, a} = + %Achievement{course_id: course_id, uuid: UUID.generate()} + |> Map.merge(achievement_literal(5)) + |> Repo.insert() + conn |> delete(build_path(course_id, a.uuid)) |> response(204) @@ -134,7 +144,12 @@ defmodule CadetWeb.AdminAchievementsControllerTest do @tag authenticate: :student test "403 for student", %{conn: conn} do course_id = conn.assigns.course_id - {:ok, a} = %Achievement{course_id: course_id, uuid: UUID.generate()} |> Map.merge(achievement_literal(5)) |> Repo.insert() + + {:ok, a} = + %Achievement{course_id: course_id, uuid: UUID.generate()} + |> Map.merge(achievement_literal(5)) + |> Repo.insert() + conn |> delete(build_path(course_id, a.uuid)) |> response(403) @@ -144,7 +159,12 @@ defmodule CadetWeb.AdminAchievementsControllerTest do test "401 if unauthenticated", %{conn: conn} do course = insert(:course) - {:ok, a} = %Achievement{course_id: course.id, uuid: UUID.generate()} |> Map.merge(achievement_literal(5)) |> Repo.insert() + + {:ok, a} = + %Achievement{course_id: course.id, uuid: UUID.generate()} + |> Map.merge(achievement_literal(5)) + |> Repo.insert() + conn |> delete(build_path(course.id, a.uuid)) |> response(401) diff --git a/test/cadet_web/admin_controllers/admin_goals_controller_test.exs b/test/cadet_web/admin_controllers/admin_goals_controller_test.exs index 25fd62957..62417ad79 100644 --- a/test/cadet_web/admin_controllers/admin_goals_controller_test.exs +++ b/test/cadet_web/admin_controllers/admin_goals_controller_test.exs @@ -21,7 +21,12 @@ defmodule CadetWeb.AdminGoalsControllerTest do @tag authenticate: :staff test "succeeds for staff", %{conn: conn} do course_id = conn.assigns.course_id - {:ok, goal} = %Goal{course_id: course_id, uuid: UUID.generate()} |> Map.merge(goal_literal(5)) |> Repo.insert() + + {:ok, goal} = + %Goal{course_id: course_id, uuid: UUID.generate()} + |> Map.merge(goal_literal(5)) + |> Repo.insert() + [resp_goal] = conn |> get(build_path(course_id)) @@ -34,6 +39,7 @@ defmodule CadetWeb.AdminGoalsControllerTest do @tag authenticate: :student test "403 for student", %{conn: conn} do course_id = conn.assigns.course_id + conn |> get(build_path(course_id)) |> response(403) @@ -41,6 +47,7 @@ defmodule CadetWeb.AdminGoalsControllerTest do test "401 if unauthenticated", %{conn: conn} do course = insert(:course) + conn |> get(build_path(course.id)) |> response(401) @@ -99,6 +106,7 @@ defmodule CadetWeb.AdminGoalsControllerTest do @tag authenticate: :staff test "succeeds for staff", %{conn: conn, goals: goals = [a1, a2]} do course_id = conn.assigns.course_id + conn |> put(build_path(course_id), %{ "goals" => goals @@ -112,6 +120,7 @@ defmodule CadetWeb.AdminGoalsControllerTest do @tag authenticate: :student test "403 for student", %{conn: conn, goals: goals = [a1, a2]} do course_id = conn.assigns.course_id + conn |> put(build_path(course_id), %{ "goals" => goals @@ -124,6 +133,7 @@ defmodule CadetWeb.AdminGoalsControllerTest do test "401 if unauthenticated", %{conn: conn, goals: goals = [a1, a2]} do course = insert(:course) + conn |> put(build_path(course.id), %{ "goals" => goals @@ -139,7 +149,12 @@ defmodule CadetWeb.AdminGoalsControllerTest do @tag authenticate: :staff test "succeeds for staff", %{conn: conn} do course_id = conn.assigns.course_id - {:ok, a} = %Goal{course_id: course_id, uuid: UUID.generate()} |> Map.merge(goal_literal(5)) |> Repo.insert() + + {:ok, a} = + %Goal{course_id: course_id, uuid: UUID.generate()} + |> Map.merge(goal_literal(5)) + |> Repo.insert() + conn |> delete(build_path(course_id, a.uuid)) |> response(204) @@ -150,7 +165,12 @@ defmodule CadetWeb.AdminGoalsControllerTest do @tag authenticate: :student test "403 for student", %{conn: conn} do course_id = conn.assigns.course_id - {:ok, a} = %Goal{course_id: course_id, uuid: UUID.generate()} |> Map.merge(goal_literal(5)) |> Repo.insert() + + {:ok, a} = + %Goal{course_id: course_id, uuid: UUID.generate()} + |> Map.merge(goal_literal(5)) + |> Repo.insert() + conn |> delete(build_path(course_id, a.uuid)) |> response(403) @@ -160,7 +180,12 @@ defmodule CadetWeb.AdminGoalsControllerTest do test "401 if unauthenticated", %{conn: conn} do course = insert(:course) - {:ok, a} = %Goal{course_id: course.id, uuid: UUID.generate()} |> Map.merge(goal_literal(5)) |> Repo.insert() + + {:ok, a} = + %Goal{course_id: course.id, uuid: UUID.generate()} + |> Map.merge(goal_literal(5)) + |> Repo.insert() + conn |> delete(build_path(course.id, a.uuid)) |> response(401) @@ -173,7 +198,12 @@ defmodule CadetWeb.AdminGoalsControllerTest do @tag authenticate: :staff test "succeeds for staff", %{conn: conn} do course = conn.assigns.test_cr.course - {:ok, g} = %Goal{course_id: course.id, uuid: UUID.generate()} |> Map.merge(goal_literal(5)) |> Repo.insert() + + {:ok, g} = + %Goal{course_id: course.id, uuid: UUID.generate()} + |> Map.merge(goal_literal(5)) + |> Repo.insert() + course_reg = insert(:course_registration, %{course: course, role: :student}) conn @@ -189,7 +219,12 @@ defmodule CadetWeb.AdminGoalsControllerTest do @tag authenticate: :student test "403 for student", %{conn: conn} do course = conn.assigns.test_cr.course - {:ok, g} = %Goal{course_id: course.id, uuid: UUID.generate()} |> Map.merge(goal_literal(5)) |> Repo.insert() + + {:ok, g} = + %Goal{course_id: course.id, uuid: UUID.generate()} + |> Map.merge(goal_literal(5)) + |> Repo.insert() + course_reg = insert(:course_registration, %{course: course, role: :student}) conn @@ -201,7 +236,12 @@ defmodule CadetWeb.AdminGoalsControllerTest do test "401 if unauthenticated", %{conn: conn} do course = insert(:course) - {:ok, g} = %Goal{course_id: course.id, uuid: UUID.generate()} |> Map.merge(goal_literal(5)) |> Repo.insert() + + {:ok, g} = + %Goal{course_id: course.id, uuid: UUID.generate()} + |> Map.merge(goal_literal(5)) + |> Repo.insert() + course_reg = insert(:course_registration, %{course: course, role: :student}) conn From 4979a563d4ef58f869f378b3992c2b3d379cd5dd Mon Sep 17 00:00:00 2001 From: YaleChen299 Date: Mon, 19 Jul 2021 14:35:38 +0800 Subject: [PATCH 159/174] renaming and cleaning up code --- lib/cadet/accounts/accounts.ex | 2 +- lib/cadet/courses/assessment_config.ex | 2 +- lib/cadet/courses/group.ex | 20 +-- lib/cadet/jobs/xml_parser.ex | 10 -- .../admin_courses_controller.ex | 8 +- .../controllers/stories_controller.ex | 2 +- lib/mix/tasks/users/import.ex | 144 ------------------ test/cadet/courses/courses_test.exs | 28 ---- test/cadet/courses/group_test.exs | 24 ++- .../admin_courses_controller_test.exs | 7 +- 10 files changed, 41 insertions(+), 206 deletions(-) delete mode 100644 lib/mix/tasks/users/import.ex diff --git a/lib/cadet/accounts/accounts.ex b/lib/cadet/accounts/accounts.ex index 8284b7ecd..90cf2bc38 100644 --- a/lib/cadet/accounts/accounts.ex +++ b/lib/cadet/accounts/accounts.ex @@ -73,7 +73,7 @@ defmodule Cadet.Accounts do Sign in using given user ID """ def sign_in(username, token, provider) do - user = Repo.one(Query.username(username)) + user = username |> Query.username() |> Repo.one() if is_nil(user) or is_nil(user.name) do # user is not registered in our database or does not have a name diff --git a/lib/cadet/courses/assessment_config.ex b/lib/cadet/courses/assessment_config.ex index 00c1d55a9..69326c51b 100644 --- a/lib/cadet/courses/assessment_config.ex +++ b/lib/cadet/courses/assessment_config.ex @@ -1,6 +1,6 @@ defmodule Cadet.Courses.AssessmentConfig do @moduledoc """ - The AssessmentConfig entity stores the assessment tyoes in a + The AssessmentConfig entity stores the assessment types in a particular course. """ use Cadet, :model diff --git a/lib/cadet/courses/group.ex b/lib/cadet/courses/group.ex index a6b3fef58..838de20cf 100644 --- a/lib/cadet/courses/group.ex +++ b/lib/cadet/courses/group.ex @@ -26,8 +26,7 @@ defmodule Cadet.Courses.Group do |> validate_required(@required_fields) |> add_belongs_to_id_from_model([:leader, :course], attrs) |> validate_role - - # |> validate_course + |> validate_course end defp validate_role(changeset) do @@ -41,13 +40,14 @@ defmodule Cadet.Courses.Group do end end - # defp validate_course(changeset) do - # course_id = get_field(changeset, :course_id) - # leader_id = get_field(changeset, :leader_id) - - # if leader_id != nil && Repo.get(CourseRegistration, leader_id).course_id != course_id do - # add_error(changeset, :leader, "does not belong to the same course ") - # end + defp validate_course(changeset) do + course_id = get_field(changeset, :course_id) + leader_id = get_field(changeset, :leader_id) - # end + if leader_id != nil && Repo.get(CourseRegistration, leader_id).course_id != course_id do + add_error(changeset, :leader, "does not belong to the same course ") + else + changeset + end + end end diff --git a/lib/cadet/jobs/xml_parser.ex b/lib/cadet/jobs/xml_parser.ex index 120aada84..a8458bb57 100644 --- a/lib/cadet/jobs/xml_parser.ex +++ b/lib/cadet/jobs/xml_parser.ex @@ -124,15 +124,6 @@ defmodule Cadet.Updater.XMLParser do "public" end - # @spec change_quest_to_sidequest(String.t()) :: String.t() - # defp change_quest_to_sidequest("quest") do - # "sidequest" - # end - - # defp change_quest_to_sidequest(type) when is_binary(type) do - # type - # end - @spec process_questions(String.t()) :: {:ok, [map()]} | {:error, String.t()} defp process_questions(xml) do default_library = xpath(xml, ~x"//TASK/DEPLOYMENT"e) @@ -143,7 +134,6 @@ defmodule Cadet.Updater.XMLParser do |> xpath( ~x"//PROBLEMS/PROBLEM"el, type: ~x"./@type"o |> transform_by(&process_charlist/1), - # max_grade: ~x"./@maxgrade"oi, max_xp: ~x"./@maxxp"oi, show_solution: ~x"./@showsolution"os, blocking: ~x"./@blocking"os, diff --git a/lib/cadet_web/admin_controllers/admin_courses_controller.ex b/lib/cadet_web/admin_controllers/admin_courses_controller.ex index fda2aa9fe..41d8b7e97 100644 --- a/lib/cadet_web/admin_controllers/admin_courses_controller.ex +++ b/lib/cadet_web/admin_controllers/admin_courses_controller.ex @@ -52,12 +52,16 @@ defmodule CadetWeb.AdminCoursesController do |> text(message) end else - send_resp(conn, :bad_request, "List parameter does not contain all maps") + send_resp( + conn, + :bad_request, + "assessmentConfigs should be a list of assessment configuration objects" + ) end end def update_assessment_configs(conn, _) do - send_resp(conn, :bad_request, "Missing List parameter(s)") + send_resp(conn, :bad_request, "missing assessmentConfig") end def delete_assessment_config(conn, %{ diff --git a/lib/cadet_web/controllers/stories_controller.ex b/lib/cadet_web/controllers/stories_controller.ex index 214cf462f..5d509eac3 100644 --- a/lib/cadet_web/controllers/stories_controller.ex +++ b/lib/cadet_web/controllers/stories_controller.ex @@ -73,7 +73,7 @@ defmodule CadetWeb.StoriesController do end swagger_path :create do - post("/v2s{course_id}/stories") + post("/v2{course_id}/stories") summary("Creates a new story") diff --git a/lib/mix/tasks/users/import.ex b/lib/mix/tasks/users/import.ex deleted file mode 100644 index eca2e0326..000000000 --- a/lib/mix/tasks/users/import.ex +++ /dev/null @@ -1,144 +0,0 @@ -defmodule Mix.Tasks.Cadet.Users.Import do - @moduledoc """ - Import user and grouping information from several csv files. - - To use this, you need to prepare 3 csv files: - 1. List of all the students together with their group names - 2. List of all the leaders together with their group names - 3. List of all the mentors together with their group names - - For all the files, they must be comma-separated csv and in this format: - - ``` - name,username,group_name - ``` - - (Username could be e.g. NUSNET ID) - - Note that group names must be unique. - """ - - # @shortdoc "Import user and grouping information from csv files." - - # use Mix.Task - - # require Logger - - # alias Cadet.{Accounts, Courses, Repo} - # alias Cadet.Courses.Group - # alias Cadet.Accounts.User - - # def run(_args) do - # # Required for Ecto to work properly, from Mix.Ecto - # if function_exported?(Mix.Task, :run, 2), do: Mix.Task.run("app.start") - - # students_csv_path = trimmed_gets("Path to students csv (leave blank to skip): ") - # leaders_csv_path = trimmed_gets("Path to leaders csv (leave blank to skip): ") - # mentors_csv_path = trimmed_gets("Path to mentors csv (leave blank to skip): ") - - # Repo.transaction(fn -> - # students_csv_path != "" && process_students_csv(students_csv_path) - # leaders_csv_path != "" && process_leaders_csv(leaders_csv_path) - # mentors_csv_path != "" && process_mentors_csv(mentors_csv_path) - # end) - # end - - # defp process_students_csv(path) when is_binary(path) do - # if File.exists?(path) do - # csv_stream = path |> File.stream!() |> CSV.decode(strip_fields: true) - - # for {:ok, [name, username, group_name]} <- csv_stream do - # with {:ok, _group = %Group{}} <- Courses.get_or_create_group(group_name), - # {:ok, %User{}} <- - # Accounts.insert_or_update_user(%{ - # username: username, - # name: name - # # role: :student, - # # group: group - # }) do - # :ok - # else - # error -> - # Logger.error( - # "Unable to insert student (name: #{name}, username: #{username}, " <> - # "group_name: #{group_name})" - # ) - - # Logger.error("error: #{inspect(error, pretty: true)}") - - # Repo.rollback(error) - # end - # end - - # Logger.info("Imported students csv at #{path}") - # else - # Logger.error("Cannot find students csv at #{path}") - # end - # end - - # defp process_leaders_csv(path) when is_binary(path) do - # if File.exists?(path) do - # csv_stream = path |> File.stream!() |> CSV.decode(strip_fields: true) - - # for {:ok, [name, username, group_name]} <- csv_stream do - # with {:ok, leader = %User{}} <- - # Accounts.insert_or_update_user(%{username: username, name: name}), - # {:ok, %Group{}} <- - # Courses.insert_or_update_group(%{name: group_name, leader: leader}) do - # :ok - # else - # error -> - # Logger.error( - # "Unable to insert leader (name: #{name}, username: #{username}, " <> - # "group_name: #{group_name})" - # ) - - # Logger.error("error: #{inspect(error, pretty: true)}") - - # Repo.rollback(error) - # end - # end - - # Logger.info("Imported leaders csv at #{path}") - # else - # Logger.error("Cannot find leaders csv at #{path}") - # end - # end - - # # :TODO check mentor is staff before update group and add enroll course logit - # defp process_mentors_csv(path) when is_binary(path) do - # if File.exists?(path) do - # csv_stream = path |> File.stream!() |> CSV.decode(strip_fields: true) - - # for {:ok, [name, username, group_name]} <- csv_stream do - # with {:ok, mentor = %User{}} <- - # Accounts.insert_or_update_user(%{username: username, name: name}), - # {:ok, %Group{}} <- - # Courses.insert_or_update_group(%{name: group_name, mentor: mentor}) do - # :ok - # else - # error -> - # Logger.error( - # "Unable to insert mentor (name: #{name}, username: #{username}, " <> - # "group_name: #{group_name})" - # ) - - # Logger.error("error: #{inspect(error, pretty: true)}") - - # Repo.rollback(error) - # end - # end - - # Logger.info("Imported mentors csv at #{path}") - # else - # Logger.error("Cannot find mentors csv at #{path}") - # end - # end - - # @spec trimmed_gets(String.t()) :: String.t() - # defp trimmed_gets(prompt) when is_binary(prompt) do - # prompt - # |> IO.gets() - # |> String.trim() - # end -end diff --git a/test/cadet/courses/courses_test.exs b/test/cadet/courses/courses_test.exs index 25ab35cba..bae06cac6 100644 --- a/test/cadet/courses/courses_test.exs +++ b/test/cadet/courses/courses_test.exs @@ -554,32 +554,4 @@ defmodule Cadet.CoursesTest do refute is_nil(group_db) end end - - # describe "insert_or_update_group" do - # test "existing group" do - # group = insert(:group) - # group_params = params_with_assocs(:group, name: group.name) - # Courses.insert_or_update_group(group_params) - - # updated_group = - # Group - # |> where(name: ^group.name) - # |> Repo.one() - - # assert updated_group.id == group.id - # assert updated_group.leader_id == group_params.leader_id - # end - - # test "non-existent group" do - # group_params = params_with_assocs(:group) - # Courses.insert_or_update_group(group_params) - - # updated_group = - # Group - # |> where(name: ^group_params.name) - # |> Repo.one() - - # assert updated_group.leader_id == group_params.leader_id - # end - # end end diff --git a/test/cadet/courses/group_test.exs b/test/cadet/courses/group_test.exs index 945ac962f..a8e4495c6 100644 --- a/test/cadet/courses/group_test.exs +++ b/test/cadet/courses/group_test.exs @@ -10,13 +10,25 @@ defmodule Cadet.Courses.GroupTest do end test "validate role" do - student = insert(:course_registration, %{role: :student}) - staff = insert(:course_registration, %{role: :staff}) - admin = insert(:course_registration, %{role: :admin}) + course = insert(:course) + student = insert(:course_registration, %{course: course, role: :student}) + staff = insert(:course_registration, %{course: course, role: :staff}) + admin = insert(:course_registration, %{course: course, role: :admin}) - assert_changeset(%{name: "test", course_id: 1, leader_id: staff.id}, :valid) - assert_changeset(%{name: "test", course_id: 1, leader_id: admin.id}, :valid) - assert_changeset(%{name: "test", course_id: 1, leader_id: student.id}, :invalid) + assert_changeset(%{name: "test", course_id: course.id, leader_id: staff.id}, :valid) + assert_changeset(%{name: "test", course_id: course.id, leader_id: admin.id}, :valid) + assert_changeset(%{name: "test", course_id: course.id, leader_id: student.id}, :invalid) + end + + test "validate course" do + course = insert(:course) + student = insert(:course_registration, %{course: course, role: :student}) + staff = insert(:course_registration, %{course: course, role: :staff}) + admin = insert(:course_registration, %{course: course, role: :admin}) + + assert_changeset(%{name: "test", course_id: course.id + 1, leader_id: staff.id}, :invalid) + assert_changeset(%{name: "test", course_id: course.id + 1, leader_id: admin.id}, :invalid) + assert_changeset(%{name: "test", course_id: course.id + 1, leader_id: student.id}, :invalid) end end end diff --git a/test/cadet_web/admin_controllers/admin_courses_controller_test.exs b/test/cadet_web/admin_controllers/admin_courses_controller_test.exs index 6a16128f4..6c552e111 100644 --- a/test/cadet_web/admin_controllers/admin_courses_controller_test.exs +++ b/test/cadet_web/admin_controllers/admin_courses_controller_test.exs @@ -276,7 +276,7 @@ defmodule CadetWeb.AdminCoursesControllerTest do "assessmentConfigs" => "Missions" }) - assert response(conn, 400) == "Missing List parameter(s)" + assert response(conn, 400) == "missing assessmentConfig" end @tag authenticate: :staff @@ -288,7 +288,8 @@ defmodule CadetWeb.AdminCoursesControllerTest do "assessmentConfigs" => [1, "Missions", "Quests"] }) - assert response(conn, 400) == "List parameter does not contain all maps" + assert response(conn, 400) == + "assessmentConfigs should be a list of assessment configuration objects" end @tag authenticate: :staff @@ -297,7 +298,7 @@ defmodule CadetWeb.AdminCoursesControllerTest do conn = put(conn, build_url_assessment_configs(course_id), %{}) - assert response(conn, 400) == "Missing List parameter(s)" + assert response(conn, 400) == "missing assessmentConfig" end end From 58c9b6aec625bc5b2317a7af18eb761f5ba59d58 Mon Sep 17 00:00:00 2001 From: YaleChen299 Date: Tue, 20 Jul 2021 01:38:34 +0800 Subject: [PATCH 160/174] rename latest viewed course --- lib/cadet/accounts/accounts.ex | 26 +++++++++++++---- lib/cadet/accounts/course_registrations.ex | 2 +- lib/cadet/accounts/query.ex | 2 +- lib/cadet/accounts/user.ex | 8 +++--- lib/cadet/courses/courses.ex | 12 ++++---- lib/cadet_web/controllers/user_controller.ex | 12 ++++---- lib/cadet_web/router.ex | 5 ++-- lib/cadet_web/views/sourcecast_view.ex | 2 +- .../20210531155751_multitenant_upgrade.exs | 8 +----- ...20210719091011_rename_latest_viewed_id.exs | 7 +++++ priv/repo/seeds.exs | 10 +++---- .../accounts/course_registration_test.exs | 2 +- test/cadet/accounts/user_test.exs | 4 +-- test/cadet/auth/guardian_test.exs | 2 +- test/cadet/courses/courses_test.exs | 10 +++---- .../sourcecast_controller_test.exs | 8 +++--- .../controllers/user_controller_test.exs | 28 +++++++++---------- test/factories/accounts/user_factory.ex | 4 +-- test/support/conn_case.ex | 2 +- test/support/seeds.ex | 10 +++---- 20 files changed, 89 insertions(+), 75 deletions(-) create mode 100644 priv/repo/migrations/20210719091011_rename_latest_viewed_id.exs diff --git a/lib/cadet/accounts/accounts.ex b/lib/cadet/accounts/accounts.ex index 90cf2bc38..14987985e 100644 --- a/lib/cadet/accounts/accounts.ex +++ b/lib/cadet/accounts/accounts.ex @@ -97,12 +97,26 @@ defmodule Cadet.Accounts do end end - def update_latest_viewed(user = %User{}, latest_viewed_id) when is_ecto_id(latest_viewed_id) do - case user - |> User.changeset(%{latest_viewed_id: latest_viewed_id}) - |> Repo.update() do - result = {:ok, _} -> result - {:error, changeset} -> {:error, {:internal_server_error, full_error_messages(changeset)}} + def update_latest_viewed(user = %User{id: user_id}, latest_viewed_course_id) + when is_ecto_id(latest_viewed_course_id) do + CourseRegistration + |> where(user_id: ^user_id) + |> where(course_id: ^latest_viewed_course_id) + |> Repo.one() + |> case do + nil -> + {:error, {:bad_request, "user is not in the course"}} + + _ -> + case user + |> User.changeset(%{latest_viewed_course_id: latest_viewed_course_id}) + |> Repo.update() do + result = {:ok, _} -> + result + + {:error, changeset} -> + {:error, {:internal_server_error, full_error_messages(changeset)}} + end end end end diff --git a/lib/cadet/accounts/course_registrations.ex b/lib/cadet/accounts/course_registrations.ex index db0dbf2f9..feb7f621b 100644 --- a/lib/cadet/accounts/course_registrations.ex +++ b/lib/cadet/accounts/course_registrations.ex @@ -110,7 +110,7 @@ defmodule Cadet.Accounts.CourseRegistrations do User |> where(id: ^user_id) |> Repo.one() - |> User.changeset(%{latest_viewed_id: course_id}) + |> User.changeset(%{latest_viewed_course_id: course_id}) |> Repo.update() ok diff --git a/lib/cadet/accounts/query.ex b/lib/cadet/accounts/query.ex index 15d5d094a..3c368d1f1 100644 --- a/lib/cadet/accounts/query.ex +++ b/lib/cadet/accounts/query.ex @@ -17,7 +17,7 @@ defmodule Cadet.Accounts.Query do def username(username) do User |> of_username(username) - |> preload(:latest_viewed) + |> preload(:latest_viewed_course) end @spec students_of(%CourseRegistration{}) :: Ecto.Query.t() diff --git a/lib/cadet/accounts/user.ex b/lib/cadet/accounts/user.ex index ac06d4382..99f19f9bb 100644 --- a/lib/cadet/accounts/user.ex +++ b/lib/cadet/accounts/user.ex @@ -12,19 +12,19 @@ defmodule Cadet.Accounts.User do field(:name, :string) field(:username, :string) - belongs_to(:latest_viewed, Course) - has_many(:course_registration, CourseRegistration) + belongs_to(:latest_viewed_course, Course) + has_many(:courses, CourseRegistration) timestamps() end @required_fields ~w(username)a - @optional_fields ~w(name latest_viewed_id)a + @optional_fields ~w(name latest_viewed_course_id)a def changeset(user, params \\ %{}) do user |> cast(params, @required_fields ++ @optional_fields) |> validate_required(@required_fields) - |> foreign_key_constraint(:latest_viewed_id) + |> foreign_key_constraint(:latest_viewed_course_id) end end diff --git a/lib/cadet/courses/courses.ex b/lib/cadet/courses/courses.ex index 8faca0977..fdb639c94 100644 --- a/lib/cadet/courses/courses.ex +++ b/lib/cadet/courses/courses.ex @@ -35,11 +35,11 @@ defmodule Cadet.Courses do role: :admin }) end) - |> Multi.update(:latest_viewed_id, fn %{course: course} -> + |> Multi.update(:latest_viewed_course_id, fn %{course: course} -> User |> where(id: ^user.id) |> Repo.one() - |> User.changeset(%{latest_viewed_id: course.id}) + |> User.changeset(%{latest_viewed_course_id: course.id}) end) |> Repo.transaction() end @@ -78,7 +78,7 @@ defmodule Cadet.Courses do course -> if Map.has_key?(params, :viewable) and not params.viewable do - remove_latest_viewed_id(course_id) + remove_latest_viewed_course_id(course_id) end course @@ -93,13 +93,13 @@ defmodule Cadet.Courses do |> Repo.one() end - defp remove_latest_viewed_id(course_id) do + defp remove_latest_viewed_course_id(course_id) do User - |> where(latest_viewed_id: ^course_id) + |> where(latest_viewed_course_id: ^course_id) |> Repo.all() |> Enum.each(fn user -> user - |> User.changeset(%{latest_viewed_id: nil}) + |> User.changeset(%{latest_viewed_course_id: nil}) |> Repo.update() end) end diff --git a/lib/cadet_web/controllers/user_controller.ex b/lib/cadet_web/controllers/user_controller.ex index 2575fee78..e5031b863 100644 --- a/lib/cadet_web/controllers/user_controller.ex +++ b/lib/cadet_web/controllers/user_controller.ex @@ -13,8 +13,8 @@ defmodule CadetWeb.UserController do user = conn.assigns.current_user courses = CourseRegistrations.get_courses(conn.assigns.current_user) - if user.latest_viewed_id do - latest = CourseRegistrations.get_user_course(user.id, user.latest_viewed_id) + if user.latest_viewed_course_id do + latest = CourseRegistrations.get_user_course(user.id, user.latest_viewed_course_id) xp = user_total_xp(latest) max_xp = user_max_xp(latest) story = user_current_story(latest) @@ -45,9 +45,9 @@ defmodule CadetWeb.UserController do user = conn.assigns.current_user latest = - case user.latest_viewed_id do + case user.latest_viewed_course_id do nil -> nil - _ -> CourseRegistrations.get_user_course(user.id, user.latest_viewed_id) + _ -> CourseRegistrations.get_user_course(user.id, user.latest_viewed_course_id) end get_course_reg_config(conn, latest) @@ -115,7 +115,7 @@ defmodule CadetWeb.UserController do end swagger_path :get_latest_viewed do - get("/v2/user/latest_viewed") + get("/v2/user/latest_viewed_course") summary("Get the latest_viewed_course of a user") @@ -126,7 +126,7 @@ defmodule CadetWeb.UserController do end swagger_path :update_latest_viewed do - put("/v2/user/latest_viewed") + put("/v2/user/latest_viewed_course") summary("Update user's latest viewed course") security([%{JWT: []}]) consumes("application/json") diff --git a/lib/cadet_web/router.ex b/lib/cadet_web/router.ex index decd248d5..c4599c9da 100644 --- a/lib/cadet_web/router.ex +++ b/lib/cadet_web/router.ex @@ -54,8 +54,8 @@ defmodule CadetWeb.Router do pipe_through([:api, :auth, :ensure_auth]) get("/user", UserController, :index) - get("/user/latest_viewed", UserController, :get_latest_viewed) - put("/user/latest_viewed", UserController, :update_latest_viewed) + get("/user/latest_viewed_course", UserController, :get_latest_viewed) + put("/user/latest_viewed_course", UserController, :update_latest_viewed) post("/config/create", CoursesController, :create) @@ -134,7 +134,6 @@ defmodule CadetWeb.Router do delete("/users", AdminUserController, :delete_user) put("/achievements", AdminAchievementsController, :bulk_update) - # update may be unused put("/achievements/:uuid", AdminAchievementsController, :update) delete("/achievements/:uuid", AdminAchievementsController, :delete) diff --git a/lib/cadet_web/views/sourcecast_view.ex b/lib/cadet_web/views/sourcecast_view.ex index 0c9ca2f75..8c6af3e07 100644 --- a/lib/cadet_web/views/sourcecast_view.ex +++ b/lib/cadet_web/views/sourcecast_view.ex @@ -17,7 +17,7 @@ defmodule CadetWeb.SourcecastView do playbackData: :playbackData, uploader: &transform_map_for_view(&1.uploader, [:name, :id]), url: &Cadet.Courses.SourcecastUpload.url({&1.audio, &1}), - course_id: :course_id + courseId: :course_id }) end end diff --git a/priv/repo/migrations/20210531155751_multitenant_upgrade.exs b/priv/repo/migrations/20210531155751_multitenant_upgrade.exs index cc438efd4..8892255bf 100644 --- a/priv/repo/migrations/20210531155751_multitenant_upgrade.exs +++ b/priv/repo/migrations/20210531155751_multitenant_upgrade.exs @@ -173,13 +173,7 @@ defmodule Cadet.Repo.Migrations.MultitenantUpgrade do end) # Add latest_viewed_id to existing users - User - |> Repo.all() - |> Enum.each(fn user -> - user - |> User.changeset(%{latest_viewed_id: course.id}) - |> Repo.update() - end) + repo().update_all("users", set: [latest_viewed_id: course.id]) # Handle groups (adding course_id, and updating leader_id to course registrations) from(g in "groups", select: {g.id, g.temp_leader_id}) diff --git a/priv/repo/migrations/20210719091011_rename_latest_viewed_id.exs b/priv/repo/migrations/20210719091011_rename_latest_viewed_id.exs new file mode 100644 index 000000000..33d6ac29a --- /dev/null +++ b/priv/repo/migrations/20210719091011_rename_latest_viewed_id.exs @@ -0,0 +1,7 @@ +defmodule Cadet.Repo.Migrations.RenameLatestViewedId do + use Ecto.Migration + + def change do + rename(table(:users), :latest_viewed_id, to: :latest_viewed_course_id) + end +end diff --git a/priv/repo/seeds.exs b/priv/repo/seeds.exs index 7bb65997a..7c65ed19d 100644 --- a/priv/repo/seeds.exs +++ b/priv/repo/seeds.exs @@ -21,13 +21,13 @@ if Cadet.Env.env() == :dev do course1 = insert(:course) course2 = insert(:course, %{course_name: "Algorithm", course_short_name: "CS2040S"}) # Users - avenger1 = insert(:user, %{name: "avenger", latest_viewed: course1}) - admin1 = insert(:user, %{name: "admin", latest_viewed: course1}) + avenger1 = insert(:user, %{name: "avenger", latest_viewed_course: course1}) + admin1 = insert(:user, %{name: "admin", latest_viewed_course: course1}) - studenta1admin2 = insert(:user, %{name: "student a", latest_viewed: course1}) + studenta1admin2 = insert(:user, %{name: "student a", latest_viewed_course: course1}) - studentb1 = insert(:user, %{latest_viewed: course1}) - studentc1 = insert(:user, %{latest_viewed: course1}) + studentb1 = insert(:user, %{latest_viewed_course: course1}) + studentc1 = insert(:user, %{latest_viewed_course: course1}) # CourseRegistration and Group avenger1_cr = insert(:course_registration, %{user: avenger1, course: course1, role: :staff}) admin1_cr = insert(:course_registration, %{user: admin1, course: course1, role: :admin}) diff --git a/test/cadet/accounts/course_registration_test.exs b/test/cadet/accounts/course_registration_test.exs index fc2c13061..49e07b499 100644 --- a/test/cadet/accounts/course_registration_test.exs +++ b/test/cadet/accounts/course_registration_test.exs @@ -206,7 +206,7 @@ defmodule Cadet.Accounts.CourseRegistrationTest do assert course_reg.user_id == user2.id assert course_reg.course_id == course1.id - assert User |> where(id: ^user2.id) |> Repo.one() |> Map.fetch!(:latest_viewed_id) == + assert User |> where(id: ^user2.id) |> Repo.one() |> Map.fetch!(:latest_viewed_course_id) == course1.id end end diff --git a/test/cadet/accounts/user_test.exs b/test/cadet/accounts/user_test.exs index 8603af1d0..97318b44a 100644 --- a/test/cadet/accounts/user_test.exs +++ b/test/cadet/accounts/user_test.exs @@ -7,12 +7,12 @@ defmodule Cadet.Accounts.UserTest do test "valid changeset" do assert_changeset(%{username: "luminus/E0000000"}, :valid) assert_changeset(%{username: "luminus/E0000001", name: "Avenger"}, :valid) - assert_changeset(%{username: "happy", latest_viewed_id: 1}, :valid) + assert_changeset(%{username: "happy", latest_viewed_course_id: 1}, :valid) end test "invalid changeset" do assert_changeset(%{name: "people"}, :invalid) - assert_changeset(%{latest_viewed_id: 1}, :invalid) + assert_changeset(%{latest_viewed_course_id: 1}, :invalid) end end end diff --git a/test/cadet/auth/guardian_test.exs b/test/cadet/auth/guardian_test.exs index e445a96e2..2884be4e6 100644 --- a/test/cadet/auth/guardian_test.exs +++ b/test/cadet/auth/guardian_test.exs @@ -21,7 +21,7 @@ defmodule Cadet.Auth.GuardianTest do } assert Guardian.resource_from_claims(good_claims) == - {:ok, remove_preload(user, :latest_viewed)} + {:ok, remove_preload(user, :latest_viewed_course)} assert Guardian.resource_from_claims(bad_claims) == {:error, :not_found} end diff --git a/test/cadet/courses/courses_test.exs b/test/cadet/courses/courses_test.exs index bae06cac6..5b398d021 100644 --- a/test/cadet/courses/courses_test.exs +++ b/test/cadet/courses/courses_test.exs @@ -36,8 +36,8 @@ defmodule Cadet.CoursesTest do assert length(course_regs) == 1 assert Enum.at(course_regs, 0).role == :admin - # User's latest_viewed course is updated - assert User |> where(id: ^user.id) |> Repo.one() |> Map.fetch!(:latest_viewed_id) == + # User's latest_viewed_course is updated + assert User |> where(id: ^user.id) |> Repo.one() |> Map.fetch!(:latest_viewed_course_id) == Enum.at(course_regs, 0).course_id end end @@ -123,9 +123,9 @@ defmodule Cadet.CoursesTest do assert updated_course.module_help_text == "help" end - test "succeeds (removes latest_viewed_id)" do + test "succeeds (removes latest_viewed_course_id)" do course = insert(:course) - user = insert(:user, %{latest_viewed: course}) + user = insert(:user, %{latest_viewed_course: course}) {:ok, updated_course} = Courses.update_course_config(course.id, %{ @@ -139,7 +139,7 @@ defmodule Cadet.CoursesTest do }) assert updated_course.viewable == false - assert is_nil(Repo.get(User, user.id).latest_viewed_id) + assert is_nil(Repo.get(User, user.id).latest_viewed_course_id) end test "returns with error for invalid course id" do diff --git a/test/cadet_web/controllers/sourcecast_controller_test.exs b/test/cadet_web/controllers/sourcecast_controller_test.exs index 25e9d7bb2..6ba74aa20 100644 --- a/test/cadet_web/controllers/sourcecast_controller_test.exs +++ b/test/cadet_web/controllers/sourcecast_controller_test.exs @@ -36,7 +36,7 @@ defmodule CadetWeb.SourcecastControllerTest do "id" => &1.uploader.id }, "url" => Cadet.Courses.SourcecastUpload.url({&1.audio, &1}), - "course_id" => nil + "courseId" => nil } ) @@ -92,7 +92,7 @@ defmodule CadetWeb.SourcecastControllerTest do "id" => &1.uploader.id }, "url" => Cadet.Courses.SourcecastUpload.url({&1.audio, &1}), - "course_id" => course_id + "courseId" => course_id } ) @@ -176,7 +176,7 @@ defmodule CadetWeb.SourcecastControllerTest do "id" => conn.assigns[:current_user].id, "name" => conn.assigns[:current_user].name }, - "course_id" => nil + "courseId" => nil } ] @@ -225,7 +225,7 @@ defmodule CadetWeb.SourcecastControllerTest do "id" => conn.assigns[:current_user].id, "name" => conn.assigns[:current_user].name }, - "course_id" => course_id + "courseId" => course_id } ] diff --git a/test/cadet_web/controllers/user_controller_test.exs b/test/cadet_web/controllers/user_controller_test.exs index a3c7dd8ea..624400551 100644 --- a/test/cadet_web/controllers/user_controller_test.exs +++ b/test/cadet_web/controllers/user_controller_test.exs @@ -17,7 +17,7 @@ defmodule CadetWeb.UserControllerTest do @tag authenticate: :student test "success, student non-story fields", %{conn: conn} do user = conn.assigns.current_user - course = user.latest_viewed + course = user.latest_viewed_course config2 = insert(:assessment_config, %{order: 2, type: "test type 2", course: course}) config3 = insert(:assessment_config, %{order: 3, type: "test type 3", course: course}) config1 = insert(:assessment_config, %{order: 1, type: "test type 1", course: course}) @@ -65,7 +65,7 @@ defmodule CadetWeb.UserControllerTest do "name" => user.name, "courses" => [ %{ - "courseId" => user.latest_viewed_id, + "courseId" => user.latest_viewed_course_id, "courseShortName" => "CS1101S", "courseName" => "Programming Methodology", "viewable" => true, @@ -132,8 +132,8 @@ defmodule CadetWeb.UserControllerTest do assert expected == resp end - @tag sign_in: %{latest_viewed: nil} - test "success, no latest_viewed course", %{conn: conn} do + @tag sign_in: %{latest_viewed_course: nil} + test "success, no latest_viewed_course", %{conn: conn} do user = conn.assigns.current_user resp = @@ -335,11 +335,11 @@ defmodule CadetWeb.UserControllerTest do # end end - describe "GET /v2/user/latest_viewed" do + describe "GET /v2/user/latest_viewed_course" do @tag authenticate: :student test "success, student non-story fields", %{conn: conn} do user = conn.assigns.current_user - course = user.latest_viewed + course = user.latest_viewed_course cr = Repo.get_by(CourseRegistration, course_id: course.id, user_id: user.id) _another_cr = insert(:course_registration, %{user: user}) assessment = insert(:assessment, %{is_published: true, course: course}) @@ -374,7 +374,7 @@ defmodule CadetWeb.UserControllerTest do resp = conn - |> get("/v2/user/latest_viewed") + |> get("/v2/user/latest_viewed_course") |> json_response(200) |> put_in(["courseRegistration", "story"], nil) @@ -406,11 +406,11 @@ defmodule CadetWeb.UserControllerTest do assert expected == resp end - @tag sign_in: %{latest_viewed: nil} - test "success, no latest_viewed course", %{conn: conn} do + @tag sign_in: %{latest_viewed_course: nil} + test "success, no latest_viewed_course", %{conn: conn} do resp = conn - |> get("/v2/user/latest_viewed") + |> get("/v2/user/latest_viewed_course") |> json_response(200) expected = %{ @@ -423,12 +423,12 @@ defmodule CadetWeb.UserControllerTest do end test "unauthorized", %{conn: conn} do - conn = get(conn, "/v2/user/latest_viewed", nil) + conn = get(conn, "/v2/user/latest_viewed_course", nil) assert response(conn, 401) =~ "Unauthorised" end end - describe "PUT /v2/user/latest_viewed/{course_id}" do + describe "PUT /v2/user/latest_viewed_course/{course_id}" do @tag authenticate: :student test "success, updating game state", %{conn: conn} do user = conn.assigns.current_user @@ -436,12 +436,12 @@ defmodule CadetWeb.UserControllerTest do insert(:course_registration, %{user: user, course: new_course}) conn - |> put("/v2/user/latest_viewed", %{"courseId" => new_course.id}) + |> put("/v2/user/latest_viewed_course", %{"courseId" => new_course.id}) |> response(200) updated_user = Repo.get(User, user.id) - assert new_course.id == updated_user.latest_viewed_id + assert new_course.id == updated_user.latest_viewed_course_id end end diff --git a/test/factories/accounts/user_factory.ex b/test/factories/accounts/user_factory.ex index 52518205e..4dc203262 100644 --- a/test/factories/accounts/user_factory.ex +++ b/test/factories/accounts/user_factory.ex @@ -16,7 +16,7 @@ defmodule Cadet.Accounts.UserFactory do :nusnet_id, &"test/E#{&1 |> Integer.to_string() |> String.pad_leading(7, "0")}" ), - latest_viewed: build(:course) + latest_viewed_course: build(:course) } end @@ -28,7 +28,7 @@ defmodule Cadet.Accounts.UserFactory do :nusnet_id, &"test/E#{&1 |> Integer.to_string() |> String.pad_leading(7, "0")}" ), - latest_viewed: build(:course) + latest_viewed_course: build(:course) } end end diff --git a/test/support/conn_case.ex b/test/support/conn_case.ex index 4abea05db..ef8da28c5 100644 --- a/test/support/conn_case.ex +++ b/test/support/conn_case.ex @@ -53,7 +53,7 @@ defmodule CadetWeb.ConnCase do if tags[:authenticate] do course = Factory.insert(:course) - user = Factory.insert(:user, %{latest_viewed: course}) + user = Factory.insert(:user, %{latest_viewed_course: course}) course_registration = cond do diff --git a/test/support/seeds.ex b/test/support/seeds.ex index f2acbd770..eca9847f7 100644 --- a/test/support/seeds.ex +++ b/test/support/seeds.ex @@ -41,13 +41,13 @@ defmodule Cadet.Test.Seeds do course1 = insert(:course) course2 = insert(:course, %{course_name: "Algorithm", course_short_name: "CS2040S"}) # Users - avenger1 = insert(:user, %{name: "avenger", latest_viewed: course1}) - admin1 = insert(:user, %{name: "admin", latest_viewed: course1}) + avenger1 = insert(:user, %{name: "avenger", latest_viewed_course: course1}) + admin1 = insert(:user, %{name: "admin", latest_viewed_course: course1}) - studenta1admin2 = insert(:user, %{name: "student a", latest_viewed: course1}) + studenta1admin2 = insert(:user, %{name: "student a", latest_viewed_course: course1}) - studentb1 = insert(:user, %{latest_viewed: course1}) - studentc1 = insert(:user, %{latest_viewed: course1}) + studentb1 = insert(:user, %{latest_viewed_course: course1}) + studentc1 = insert(:user, %{latest_viewed_course: course1}) # CourseRegistration and Group avenger1_cr = insert(:course_registration, %{user: avenger1, course: course1, role: :staff}) admin1_cr = insert(:course_registration, %{user: admin1, course: course1, role: :admin}) From f99bbe7d3c24e3bb3e81578b171cbcea62b6ffcf Mon Sep 17 00:00:00 2001 From: YaleChen299 Date: Tue, 20 Jul 2021 16:03:15 +0800 Subject: [PATCH 161/174] add course_reg_id to admin user routes --- .../admin_user_controller.ex | 20 ++-- lib/cadet_web/router.ex | 6 +- .../admin_goals_controller_test.exs | 2 +- .../admin_user_controller_test.exs | 109 ++++++------------ 4 files changed, 53 insertions(+), 84 deletions(-) diff --git a/lib/cadet_web/admin_controllers/admin_user_controller.ex b/lib/cadet_web/admin_controllers/admin_user_controller.ex index 44c9778eb..2a2d6ea7d 100644 --- a/lib/cadet_web/admin_controllers/admin_user_controller.ex +++ b/lib/cadet_web/admin_controllers/admin_user_controller.ex @@ -93,17 +93,19 @@ defmodule CadetWeb.AdminUserController do end @update_role_roles ~w(admin)a - def update_role(conn, %{"role" => role, "courseRegId" => coursereg_id}) do + def update_role(conn, %{"role" => role, "course_reg_id" => course_reg_id}) do + course_reg_id = course_reg_id |> String.to_integer() + %{id: admin_course_reg_id, role: admin_role, course_id: admin_course_id} = conn.assigns.course_reg with {:validate_role, true} <- {:validate_role, admin_role in @update_role_roles}, - {:validate_not_self, true} <- {:validate_not_self, admin_course_reg_id != coursereg_id}, + {:validate_not_self, true} <- {:validate_not_self, admin_course_reg_id != course_reg_id}, {:get_cr, user_course_reg} when not is_nil(user_course_reg) <- - {:get_cr, CourseRegistration |> where(id: ^coursereg_id) |> Repo.one()}, + {:get_cr, CourseRegistration |> where(id: ^course_reg_id) |> Repo.one()}, {:validate_same_course, true} <- {:validate_same_course, user_course_reg.course_id == admin_course_id} do - case CourseRegistrations.update_role(role, coursereg_id) do + case CourseRegistrations.update_role(role, course_reg_id) do {:ok, %{}} -> text(conn, "OK") @@ -128,18 +130,20 @@ defmodule CadetWeb.AdminUserController do end @delete_user_roles ~w(admin)a - def delete_user(conn, %{"courseRegId" => coursereg_id}) do + def delete_user(conn, %{"course_reg_id" => course_reg_id}) do + course_reg_id = course_reg_id |> String.to_integer() + %{id: admin_course_reg_id, role: admin_role, course_id: admin_course_id} = conn.assigns.course_reg with {:validate_role, true} <- {:validate_role, admin_role in @delete_user_roles}, - {:validate_not_self, true} <- {:validate_not_self, admin_course_reg_id != coursereg_id}, + {:validate_not_self, true} <- {:validate_not_self, admin_course_reg_id != course_reg_id}, {:get_cr, user_course_reg} when not is_nil(user_course_reg) <- - {:get_cr, CourseRegistration |> where(id: ^coursereg_id) |> Repo.one()}, + {:get_cr, CourseRegistration |> where(id: ^course_reg_id) |> Repo.one()}, {:prevent_delete_admin, true} <- {:prevent_delete_admin, user_course_reg.role != :admin}, {:validate_same_course, true} <- {:validate_same_course, user_course_reg.course_id == admin_course_id} do - case CourseRegistrations.delete_course_registration(coursereg_id) do + case CourseRegistrations.delete_course_registration(course_reg_id) do {:ok, %{}} -> text(conn, "OK") diff --git a/lib/cadet_web/router.ex b/lib/cadet_web/router.ex index c4599c9da..4ae3ece67 100644 --- a/lib/cadet_web/router.ex +++ b/lib/cadet_web/router.ex @@ -130,14 +130,14 @@ defmodule CadetWeb.Router do get("/users", AdminUserController, :index) put("/users", AdminUserController, :upsert_users_and_groups) - put("/users/role", AdminUserController, :update_role) - delete("/users", AdminUserController, :delete_user) + put("/users/:course_reg_id/role", AdminUserController, :update_role) + delete("/users/:course_reg_id", AdminUserController, :delete_user) + post("/users/:course_reg_id/goals/:uuid/progress", AdminGoalsController, :update_progress) put("/achievements", AdminAchievementsController, :bulk_update) put("/achievements/:uuid", AdminAchievementsController, :update) delete("/achievements/:uuid", AdminAchievementsController, :delete) - post("goals/:uuid/progress/:course_reg_id", AdminGoalsController, :update_progress) get("/goals", AdminGoalsController, :index) put("/goals", AdminGoalsController, :bulk_update) put("/goals/:uuid", AdminGoalsController, :update) diff --git a/test/cadet_web/admin_controllers/admin_goals_controller_test.exs b/test/cadet_web/admin_controllers/admin_goals_controller_test.exs index 62417ad79..11075d90d 100644 --- a/test/cadet_web/admin_controllers/admin_goals_controller_test.exs +++ b/test/cadet_web/admin_controllers/admin_goals_controller_test.exs @@ -263,6 +263,6 @@ defmodule CadetWeb.AdminGoalsControllerTest do end defp build_path(course_id, uuid, course_reg_id) do - "v2/courses/#{course_id}/admin/goals/#{uuid}/progress/#{course_reg_id}" + "v2/courses/#{course_id}/admin/users/#{course_reg_id}/goals/#{uuid}/progress/" end end diff --git a/test/cadet_web/admin_controllers/admin_user_controller_test.exs b/test/cadet_web/admin_controllers/admin_user_controller_test.exs index cbba73182..e70b3508f 100644 --- a/test/cadet_web/admin_controllers/admin_user_controller_test.exs +++ b/test/cadet_web/admin_controllers/admin_user_controller_test.exs @@ -74,12 +74,13 @@ defmodule CadetWeb.AdminUserControllerTest do |> response(403) end - # test "401 when not logged in", %{conn: conn} do - # course_id = conn.assigns[:course_id] - # assert conn - # |> get(build_url(course_id)) - # |> response(401) - # end + test "401 when not logged in", %{conn: conn} do + course = insert(:course) + + assert conn + |> get(build_url_users(course.id)) + |> response(401) + end end describe "PUT /v2/courses/{course_id}/admin/users" do @@ -285,7 +286,7 @@ defmodule CadetWeb.AdminUserControllerTest do end end - describe "PUT /v2/courses/{course_id}/admin/users/role" do + describe "PUT /v2/courses/{course_id}/admin/users/{course_reg_id}/role" do @tag authenticate: :admin test "success (student to staff), when admin is admin of the course the user is in", %{ conn: conn @@ -295,11 +296,10 @@ defmodule CadetWeb.AdminUserControllerTest do user_course_reg = insert(:course_registration, %{role: :student, course: course}) params = %{ - "role" => "staff", - "courseRegId" => user_course_reg.id + "role" => "staff" } - resp = put(conn, build_url_users_role(course_id), params) + resp = put(conn, build_url_users_role(course_id, user_course_reg.id), params) assert response(resp, 200) == "OK" updated_course_reg = Repo.get(CourseRegistration, user_course_reg.id) @@ -315,11 +315,10 @@ defmodule CadetWeb.AdminUserControllerTest do user_course_reg = insert(:course_registration, %{role: :staff, course: course}) params = %{ - "role" => "student", - "courseRegId" => user_course_reg.id + "role" => "student" } - resp = put(conn, build_url_users_role(course_id), params) + resp = put(conn, build_url_users_role(course_id, user_course_reg.id), params) assert response(resp, 200) == "OK" updated_course_reg = Repo.get(CourseRegistration, user_course_reg.id) @@ -335,11 +334,10 @@ defmodule CadetWeb.AdminUserControllerTest do user_course_reg = insert(:course_registration, %{role: :admin, course: course}) params = %{ - "role" => "staff", - "courseRegId" => user_course_reg.id + "role" => "staff" } - resp = put(conn, build_url_users_role(course_id), params) + resp = put(conn, build_url_users_role(course_id, user_course_reg.id), params) assert response(resp, 200) == "OK" updated_course_reg = Repo.get(CourseRegistration, user_course_reg.id) @@ -351,11 +349,10 @@ defmodule CadetWeb.AdminUserControllerTest do course_id = conn.assigns[:course_id] params = %{ - "role" => "staff", - "courseRegId" => 10_000 + "role" => "staff" } - conn = put(conn, build_url_users_role(course_id), params) + conn = put(conn, build_url_users_role(course_id, 10_000), params) assert response(conn, 400) == "User course registration does not exist" end @@ -366,11 +363,10 @@ defmodule CadetWeb.AdminUserControllerTest do user_course_reg = insert(:course_registration, %{role: :student}) params = %{ - "role" => "staff", - "courseRegId" => user_course_reg.id + "role" => "staff" } - conn = put(conn, build_url_users_role(course_id), params) + conn = put(conn, build_url_users_role(course_id, user_course_reg.id), params) assert response(conn, 403) == "User is in a different course" unchanged_course_reg = Repo.get(CourseRegistration, user_course_reg.id) @@ -384,11 +380,10 @@ defmodule CadetWeb.AdminUserControllerTest do user_course_reg = insert(:course_registration, %{role: :student, course: course}) params = %{ - "role" => "staff", - "courseRegId" => user_course_reg.id + "role" => "staff" } - conn = put(conn, build_url_users_role(course_id), params) + conn = put(conn, build_url_users_role(course_id, user_course_reg.id), params) assert response(conn, 403) == "User is not permitted to change others' roles" unchanged_course_reg = Repo.get(CourseRegistration, user_course_reg.id) @@ -402,11 +397,10 @@ defmodule CadetWeb.AdminUserControllerTest do user_course_reg = insert(:course_registration, %{role: :student, course: course}) params = %{ - "role" => "avenger", - "courseRegId" => user_course_reg.id + "role" => "avenger" } - conn = put(conn, build_url_users_role(course_id), params) + conn = put(conn, build_url_users_role(course_id, user_course_reg.id), params) assert response(conn, 400) == "role is invalid" unchanged_course_reg = Repo.get(CourseRegistration, user_course_reg.id) @@ -414,7 +408,7 @@ defmodule CadetWeb.AdminUserControllerTest do end end - describe "DELETE /v2/courses/{course_id}/admin/users" do + describe "DELETE /v2/courses/{course_id}/admin/users/{course_reg_id}" do @tag authenticate: :admin test "success (delete student), when admin is admin of the course the user is in", %{ conn: conn @@ -423,11 +417,7 @@ defmodule CadetWeb.AdminUserControllerTest do course = Repo.get(Course, course_id) user_course_reg = insert(:course_registration, %{role: :student, course: course}) - params = %{ - "courseRegId" => user_course_reg.id - } - - resp = delete(conn, build_url_users(course_id), params) + resp = delete(conn, build_url_users(course_id, user_course_reg.id)) assert response(resp, 200) == "OK" assert Repo.get(CourseRegistration, user_course_reg.id) == nil @@ -441,11 +431,7 @@ defmodule CadetWeb.AdminUserControllerTest do course = Repo.get(Course, course_id) user_course_reg = insert(:course_registration, %{role: :staff, course: course}) - params = %{ - "courseRegId" => user_course_reg.id - } - - resp = delete(conn, build_url_users(course_id), params) + resp = delete(conn, build_url_users(course_id, user_course_reg.id)) assert response(resp, 200) == "OK" assert Repo.get(CourseRegistration, user_course_reg.id) == nil @@ -459,11 +445,7 @@ defmodule CadetWeb.AdminUserControllerTest do course = Repo.get(Course, course_id) user_course_reg = insert(:course_registration, %{role: :student, course: course}) - params = %{ - "courseRegId" => user_course_reg.id - } - - conn = delete(conn, build_url_users(course_id), params) + conn = delete(conn, build_url_users(course_id, user_course_reg.id)) assert response(conn, 403) == "User is not permitted to delete other users" assert Repo.get(CourseRegistration, user_course_reg.id) != nil @@ -474,19 +456,9 @@ defmodule CadetWeb.AdminUserControllerTest do conn: conn } do course_id = conn.assigns[:course_id] - current_user = conn.assigns[:current_user] + own_course_reg = conn.assigns[:test_cr] - own_course_reg = - CourseRegistration - |> where(user_id: ^current_user.id) - |> where(course_id: ^course_id) - |> Repo.one() - - params = %{ - "courseRegId" => own_course_reg.id - } - - conn = delete(conn, build_url_users(course_id), params) + conn = delete(conn, build_url_users(course_id, own_course_reg.id)) assert response(conn, 400) == "Admin not allowed to delete ownself from course" assert Repo.get(CourseRegistration, own_course_reg.id) != nil @@ -498,11 +470,7 @@ defmodule CadetWeb.AdminUserControllerTest do } do course_id = conn.assigns[:course_id] - params = %{ - "courseRegId" => 1 - } - - conn = delete(conn, build_url_users(course_id), params) + conn = delete(conn, build_url_users(course_id, 1)) assert response(conn, 400) == "User course registration does not exist" end @@ -515,11 +483,7 @@ defmodule CadetWeb.AdminUserControllerTest do course = Repo.get(Course, course_id) user_course_reg = insert(:course_registration, %{role: :admin, course: course}) - params = %{ - "courseRegId" => user_course_reg.id - } - - conn = delete(conn, build_url_users(course_id), params) + conn = delete(conn, build_url_users(course_id, user_course_reg.id)) assert response(conn, 400) == "Admins cannot be deleted" end @@ -531,16 +495,17 @@ defmodule CadetWeb.AdminUserControllerTest do course_id = conn.assigns[:course_id] user_course_reg = insert(:course_registration, %{role: :student}) - params = %{ - "courseRegId" => user_course_reg.id - } - - conn = delete(conn, build_url_users(course_id), params) + conn = delete(conn, build_url_users(course_id, user_course_reg.id)) assert response(conn, 403) == "User is in a different course" end end defp build_url_users(course_id), do: "/v2/courses/#{course_id}/admin/users" - defp build_url_users_role(course_id), do: build_url_users(course_id) <> "/role" + + defp build_url_users(course_id, course_reg_id), + do: "/v2/courses/#{course_id}/admin/users/#{course_reg_id}" + + defp build_url_users_role(course_id, course_reg_id), + do: build_url_users(course_id, course_reg_id) <> "/role" end From b05b7999d392d2b2ca90b4baa9c61448aa4e33fb Mon Sep 17 00:00:00 2001 From: YaleChen299 Date: Tue, 20 Jul 2021 21:05:08 +0800 Subject: [PATCH 162/174] update migration --- .../migrations/20210716073359_update_achievement.exs | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/priv/repo/migrations/20210716073359_update_achievement.exs b/priv/repo/migrations/20210716073359_update_achievement.exs index 08851e719..06cecf795 100644 --- a/priv/repo/migrations/20210716073359_update_achievement.exs +++ b/priv/repo/migrations/20210716073359_update_achievement.exs @@ -14,5 +14,15 @@ defmodule Cadet.Repo.Migrations.UpdateAchievement do remove(:user_id) add(:course_reg_id, references(:course_registrations)) end + + execute( + fn -> + courses = from(c in "courses", select: {c.id}) |> repo().all() + course_id = courses |> Enum.at(0) |> elem(0) + repo().update_all("achievements", set: [course_id: course_id]) + repo().update_all("goals", set: [course_id: course_id]) + repo().delete_all("goal_progress") + end + ) end end From bd8c83e743f5c85280f57a638fd593bcc66e0448 Mon Sep 17 00:00:00 2001 From: YaleChen299 Date: Tue, 20 Jul 2021 21:10:44 +0800 Subject: [PATCH 163/174] update migration --- priv/repo/migrations/20210716073359_update_achievement.exs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/priv/repo/migrations/20210716073359_update_achievement.exs b/priv/repo/migrations/20210716073359_update_achievement.exs index 06cecf795..ad2d1f2a1 100644 --- a/priv/repo/migrations/20210716073359_update_achievement.exs +++ b/priv/repo/migrations/20210716073359_update_achievement.exs @@ -17,10 +17,10 @@ defmodule Cadet.Repo.Migrations.UpdateAchievement do execute( fn -> - courses = from(c in "courses", select: {c.id}) |> repo().all() + courses = "courses" |> repo().all() course_id = courses |> Enum.at(0) |> elem(0) - repo().update_all("achievements", set: [course_id: course_id]) - repo().update_all("goals", set: [course_id: course_id]) + repo().delete_all("achievements") + repo().delete_all("goals") repo().delete_all("goal_progress") end ) From b96e63275547bc8fc8de9146424e12e80ecf0c68 Mon Sep 17 00:00:00 2001 From: YaleChen299 Date: Tue, 20 Jul 2021 21:14:10 +0800 Subject: [PATCH 164/174] update migration --- priv/repo/migrations/20210716073359_update_achievement.exs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/priv/repo/migrations/20210716073359_update_achievement.exs b/priv/repo/migrations/20210716073359_update_achievement.exs index ad2d1f2a1..50f6a213e 100644 --- a/priv/repo/migrations/20210716073359_update_achievement.exs +++ b/priv/repo/migrations/20210716073359_update_achievement.exs @@ -19,8 +19,8 @@ defmodule Cadet.Repo.Migrations.UpdateAchievement do fn -> courses = "courses" |> repo().all() course_id = courses |> Enum.at(0) |> elem(0) - repo().delete_all("achievements") - repo().delete_all("goals") + repo().update_all("achievements", set: [course_id: course_id]) + repo().update_all("goals", set: [course_id: course_id]) repo().delete_all("goal_progress") end ) From 069a856790b91adee8e879a327a08a0295b4c041 Mon Sep 17 00:00:00 2001 From: YaleChen299 Date: Tue, 20 Jul 2021 22:42:02 +0800 Subject: [PATCH 165/174] update achievement migration --- .../20210531155751_multitenant_upgrade.exs | 5 +-- .../20210716073359_update_achievement.exs | 40 ++++++++++++++----- 2 files changed, 31 insertions(+), 14 deletions(-) diff --git a/priv/repo/migrations/20210531155751_multitenant_upgrade.exs b/priv/repo/migrations/20210531155751_multitenant_upgrade.exs index 8892255bf..1255e95f6 100644 --- a/priv/repo/migrations/20210531155751_multitenant_upgrade.exs +++ b/priv/repo/migrations/20210531155751_multitenant_upgrade.exs @@ -155,7 +155,7 @@ defmodule Cadet.Repo.Migrations.MultitenantUpgrade do # Namespace existing usernames from(u in "users", update: [set: [username: fragment("? || ? ", "luminus/", u.username)]]) - |> Repo.update_all([]) + |> repo().update_all([]) # Create course registrations for existing users from(u in "users", select: {u.id, u.role, u.group_id, u.game_states}) @@ -187,8 +187,7 @@ defmodule Cadet.Repo.Migrations.MultitenantUpgrade do CourseRegistration |> where(role: :staff) |> Repo.one() - - Map.fetch!(:id) + |> Map.fetch!(:id) id -> CourseRegistration diff --git a/priv/repo/migrations/20210716073359_update_achievement.exs b/priv/repo/migrations/20210716073359_update_achievement.exs index 50f6a213e..14e851b55 100644 --- a/priv/repo/migrations/20210716073359_update_achievement.exs +++ b/priv/repo/migrations/20210716073359_update_achievement.exs @@ -1,28 +1,46 @@ defmodule Cadet.Repo.Migrations.UpdateAchievement do use Ecto.Migration + import Ecto.Query, only: [from: 2, where: 2] def change do alter table(:achievements) do - add(:course_id, references(:courses), null: false) + add(:course_id, references(:courses), null: true) end alter table(:goals) do - add(:course_id, references(:courses), null: false) + add(:course_id, references(:courses), null: true) end alter table(:goal_progress) do - remove(:user_id) - add(:course_reg_id, references(:course_registrations)) + add(:course_reg_id, references(:course_registrations), null: true) end + execute(fn -> + courses = from(c in "courses", select: {c.id}) |> repo().all() + course_id = courses |> Enum.at(0) |> elem(0) + repo().update_all("achievements", set: [course_id: course_id]) + repo().update_all("goals", set: [course_id: course_id]) + end) + execute( - fn -> - courses = "courses" |> repo().all() - course_id = courses |> Enum.at(0) |> elem(0) - repo().update_all("achievements", set: [course_id: course_id]) - repo().update_all("goals", set: [course_id: course_id]) - repo().delete_all("goal_progress") - end + "update goal_progress gp set course_reg_id = (select cr.id from course_registrations cr where cr.user_id = gp.user_id)" ) + + alter table(:achievements) do + modify(:course_id, references(:courses), null: false, from: references(:courses)) + end + + alter table(:goals) do + modify(:course_id, references(:courses), null: false, from: references(:courses)) + end + + alter table(:goal_progress) do + remove(:user_id) + + modify(:course_reg_id, references(:course_registrations), + null: false, + from: references(:course_registrations) + ) + end end end From c9afc2d7c10fa33a15cd2261436cca86b85a85ea Mon Sep 17 00:00:00 2001 From: YaleChen299 Date: Wed, 21 Jul 2021 02:58:31 +0800 Subject: [PATCH 166/174] update contest leaderboard --- lib/cadet/assessments/assessments.ex | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/lib/cadet/assessments/assessments.ex b/lib/cadet/assessments/assessments.ex index 469486193..bbba89f52 100644 --- a/lib/cadet/assessments/assessments.ex +++ b/lib/cadet/assessments/assessments.ex @@ -938,6 +938,14 @@ defmodule Cadet.Assessments do def fetch_top_relative_score_answers(question_id, number_of_answers) do Answer |> where(question_id: ^question_id) + |> where( + [a], + fragment( + "?->>'code' like ?", + a.answer, + "%return%" + ) + ) |> order_by(desc: :relative_score) |> join(:left, [a], s in assoc(a, :submission)) |> join(:left, [a, s], student in assoc(s, :student)) From 47ddd966869da6885f06847adf2f96d14017035f Mon Sep 17 00:00:00 2001 From: YaleChen299 Date: Fri, 23 Jul 2021 14:05:33 +0800 Subject: [PATCH 167/174] update contest test --- test/cadet/assessments/assessments_test.exs | 26 ++++++++++++-------- test/factories/assessments/answer_factory.ex | 2 +- 2 files changed, 17 insertions(+), 11 deletions(-) diff --git a/test/cadet/assessments/assessments_test.exs b/test/cadet/assessments/assessments_test.exs index 31bf6845e..a34133597 100644 --- a/test/cadet/assessments/assessments_test.exs +++ b/test/cadet/assessments/assessments_test.exs @@ -266,6 +266,7 @@ defmodule Cadet.AssessmentsTest do fn submission -> insert( :answer, + answer: build(:programming_answer), submission: submission, question: voting_question ) @@ -301,19 +302,13 @@ defmodule Cadet.AssessmentsTest do top_x_ans = Assessments.fetch_top_relative_score_answers(question_id, 5) - assert get_answer_relative_scores(top_x_ans) == [ - 99.0, - 89.0, - 79.0, - 69.0, - 59.0 - ] + assert get_answer_relative_scores(top_x_ans) == expected_top_relative_scores(5) x = 3 top_x_ans = Assessments.fetch_top_relative_score_answers(question_id, x) # verify that top x ans are queried correctly - assert get_answer_relative_scores(top_x_ans) == [99.0, 89.0, 79.0] + assert get_answer_relative_scores(top_x_ans) == expected_top_relative_scores(3) end end @@ -412,6 +407,7 @@ defmodule Cadet.AssessmentsTest do fn submission -> insert( :answer, + answer: build(:programming_answer), submission: submission, question: current_question ) @@ -423,6 +419,7 @@ defmodule Cadet.AssessmentsTest do fn submission -> insert( :answer, + answer: build(:programming_answer), submission: submission, question: yesterday_question ) @@ -434,6 +431,7 @@ defmodule Cadet.AssessmentsTest do fn submission -> insert( :answer, + answer: build(:programming_answer), submission: submission, question: past_question ) @@ -544,7 +542,7 @@ defmodule Cadet.AssessmentsTest do assert get_answer_relative_scores( Assessments.fetch_top_relative_score_answers(yesterday_question.id, 5) - ) == [99.0, 89.0, 79.0, 69.0, 59.0] + ) == expected_top_relative_scores(5) end test "update_rolling_contest_leaderboards correcly updates leaderboards which voting is active", @@ -566,7 +564,7 @@ defmodule Cadet.AssessmentsTest do assert get_answer_relative_scores( Assessments.fetch_top_relative_score_answers(current_question.id, 5) - ) == [99.0, 89.0, 79.0, 69.0, 59.0] + ) == expected_top_relative_scores(5) end end @@ -577,4 +575,12 @@ defmodule Cadet.AssessmentsTest do defp get_question_ids(questions) do questions |> Enum.map(fn q -> q.id end) |> Enum.sort() end + + defp expected_top_relative_scores(top_x) do + # "return 0;" in the factory has 3 token + 10..0 + |> Enum.to_list() + |> Enum.map(fn score -> 10 * score - :math.pow(2, 3 / 50) end) + |> Enum.take(top_x) + end end diff --git a/test/factories/assessments/answer_factory.ex b/test/factories/assessments/answer_factory.ex index c7faf2aee..de614ca64 100644 --- a/test/factories/assessments/answer_factory.ex +++ b/test/factories/assessments/answer_factory.ex @@ -17,7 +17,7 @@ defmodule Cadet.Assessments.AnswerFactory do def programming_answer_factory do %{ - code: sequence(:code, &"alert(#{&1})") + code: sequence(:code, &"return #{&1};") } end From cbd4ce7fe849ae6e544f85b959a0f9f24ed11abb Mon Sep 17 00:00:00 2001 From: YaleChen299 Date: Wed, 28 Jul 2021 17:14:33 +0800 Subject: [PATCH 168/174] fix assessment config update --- .../admin_controllers/admin_courses_controller.ex | 11 ++++++++++- .../admin_courses_controller_test.exs | 8 ++++---- 2 files changed, 14 insertions(+), 5 deletions(-) diff --git a/lib/cadet_web/admin_controllers/admin_courses_controller.ex b/lib/cadet_web/admin_controllers/admin_courses_controller.ex index 41d8b7e97..f24571fab 100644 --- a/lib/cadet_web/admin_controllers/admin_courses_controller.ex +++ b/lib/cadet_web/admin_controllers/admin_courses_controller.ex @@ -40,7 +40,16 @@ defmodule CadetWeb.AdminCoursesController do }) when is_ecto_id(course_id) and is_list(assessment_configs) do if Enum.all?(assessment_configs, &is_map/1) do - configs = assessment_configs |> Enum.map(&to_snake_case_atom_keys/1) + configs = + assessment_configs + |> Enum.map(&to_snake_case_atom_keys/1) + |> update_in( + [Access.all()], + &with( + {v, m} <- Map.pop(&1, :display_in_dashboard), + do: Map.put(m, :show_grading_summary, v) + ) + ) case Courses.mass_upsert_and_reorder_assessment_configs(course_id, configs) do {:ok, _} -> diff --git a/test/cadet_web/admin_controllers/admin_courses_controller_test.exs b/test/cadet_web/admin_controllers/admin_courses_controller_test.exs index 6c552e111..008acf3c7 100644 --- a/test/cadet_web/admin_controllers/admin_courses_controller_test.exs +++ b/test/cadet_web/admin_controllers/admin_courses_controller_test.exs @@ -216,17 +216,17 @@ defmodule CadetWeb.AdminCoursesControllerTest do "assessmentConfigId" => config.id, "courseId" => course_id, "type" => "Missions", + "displayInDashboard" => true, "earlySubmissionXp" => 100, - "hoursBeforeEarlyXpDecay" => 24, - "decayRatePointsPerHour" => 1 + "hoursBeforeEarlyXpDecay" => 24 }, %{ "assessmentConfigId" => -1, "courseId" => course_id, "type" => "Paths", + "displayInDashboard" => true, "earlySubmissionXp" => 100, - "hoursBeforeEarlyXpDecay" => 24, - "decayRatePointsPerHour" => 1 + "hoursBeforeEarlyXpDecay" => 24 } ] } From 60b69884fa7758d81f571531dc6d22d0093b5f21 Mon Sep 17 00:00:00 2001 From: YaleChen299 Date: Thu, 29 Jul 2021 17:33:00 +0800 Subject: [PATCH 169/174] fix source chapter and variant changeset condition --- lib/cadet/courses/course.ex | 44 +++++++++++++++++++++---------------- 1 file changed, 25 insertions(+), 19 deletions(-) diff --git a/lib/cadet/courses/course.ex b/lib/cadet/courses/course.ex index 4ea190aa7..0a4c41b86 100644 --- a/lib/cadet/courses/course.ex +++ b/lib/cadet/courses/course.ex @@ -22,30 +22,36 @@ defmodule Cadet.Courses.Course do timestamps() end - @required_fields ~w(source_chapter source_variant)a - @optional_fields ~w(course_name course_short_name viewable - enable_game enable_achievements enable_sourcecast module_help_text)a + @optional_fields ~w(course_name course_short_name viewable enable_game + enable_achievements enable_sourcecast module_help_text source_chapter source_variant)a def changeset(course, params) do - if Map.has_key?(params, :source_chapter) or Map.has_key?(params, :source_variant) do - course - |> cast(params, @required_fields ++ @optional_fields) - |> validate_required(@required_fields) - |> validate_sublanguage_combination() - else - course - |> cast(params, @optional_fields) - end + course + |> cast(params, @optional_fields) + |> validate_sublanguage_combination(params) end # Validates combination of Source chapter and variant - defp validate_sublanguage_combination(changeset) do - case get_field(changeset, :source_chapter) do - 1 -> validate_inclusion(changeset, :source_variant, ["default", "lazy", "wasm"]) - 2 -> validate_inclusion(changeset, :source_variant, ["default", "lazy"]) - 3 -> validate_inclusion(changeset, :source_variant, ["default", "concurrent", "non-det"]) - 4 -> validate_inclusion(changeset, :source_variant, ["default", "gpu"]) - _ -> add_error(changeset, :source_chapter, "is invalid") + defp validate_sublanguage_combination(changeset, params) do + chap = Map.has_key?(params, :source_chapter) + var = Map.has_key?(params, :source_variant) + + # not (chap xor var) + if (chap and var) or (not chap and not var) do + case get_field(changeset, :source_chapter, nil) do + nil -> changeset + 1 -> validate_inclusion(changeset, :source_variant, ["default", "lazy", "wasm"]) + 2 -> validate_inclusion(changeset, :source_variant, ["default", "lazy"]) |> IO.inspect() + 3 -> validate_inclusion(changeset, :source_variant, ["default", "concurrent", "non-det"]) + 4 -> validate_inclusion(changeset, :source_variant, ["default", "gpu"]) + _ -> add_error(changeset, :source_chapter, "is invalid") + end + else + add_error( + changeset, + :source_chapter, + "source chapter and source variant must be present together" + ) end end end From e8f119a3108dc2a28639d541b1825da65013f668 Mon Sep 17 00:00:00 2001 From: YaleChen299 Date: Thu, 29 Jul 2021 19:11:59 +0800 Subject: [PATCH 170/174] fix course changeset and validation --- lib/cadet/courses/course.ex | 10 +- lib/cadet/courses/courses.ex | 81 +---------- .../admin_courses_controller.ex | 24 ++-- .../controllers/courses_controller.ex | 32 ++--- test/cadet/courses/course_test.exs | 127 ++++++++++++++++-- .../admin_courses_controller_test.exs | 2 +- .../controllers/courses_controller_test.exs | 3 +- 7 files changed, 142 insertions(+), 137 deletions(-) diff --git a/lib/cadet/courses/course.ex b/lib/cadet/courses/course.ex index 0a4c41b86..430f5dad4 100644 --- a/lib/cadet/courses/course.ex +++ b/lib/cadet/courses/course.ex @@ -22,12 +22,14 @@ defmodule Cadet.Courses.Course do timestamps() end - @optional_fields ~w(course_name course_short_name viewable enable_game - enable_achievements enable_sourcecast module_help_text source_chapter source_variant)a + @required_fields ~w(course_name viewable enable_game + enable_achievements enable_sourcecast source_chapter source_variant)a + @optional_fields ~w(course_short_name module_help_text)a def changeset(course, params) do course - |> cast(params, @optional_fields) + |> cast(params, @required_fields ++ @optional_fields) + |> validate_required(@required_fields) |> validate_sublanguage_combination(params) end @@ -41,7 +43,7 @@ defmodule Cadet.Courses.Course do case get_field(changeset, :source_chapter, nil) do nil -> changeset 1 -> validate_inclusion(changeset, :source_variant, ["default", "lazy", "wasm"]) - 2 -> validate_inclusion(changeset, :source_variant, ["default", "lazy"]) |> IO.inspect() + 2 -> validate_inclusion(changeset, :source_variant, ["default", "lazy"]) 3 -> validate_inclusion(changeset, :source_variant, ["default", "concurrent", "non-det"]) 4 -> validate_inclusion(changeset, :source_variant, ["default", "gpu"]) _ -> add_error(changeset, :source_chapter, "is invalid") diff --git a/lib/cadet/courses/courses.ex b/lib/cadet/courses/courses.ex index fdb639c94..6e85f8779 100644 --- a/lib/cadet/courses/courses.ex +++ b/lib/cadet/courses/courses.ex @@ -8,7 +8,7 @@ defmodule Cadet.Courses do import Ecto.Query alias Ecto.Multi - alias Cadet.Accounts.{CourseRegistration, User} + alias Cadet.Accounts.{CourseRegistration, User, CourseRegistrations} alias Cadet.Courses.{ AssessmentConfig, @@ -28,19 +28,13 @@ defmodule Cadet.Courses do def create_course_config(params, user) do Multi.new() |> Multi.insert(:course, Course.changeset(%Course{}, params)) - |> Multi.insert(:course_reg, fn %{course: course} -> - CourseRegistration.changeset(%CourseRegistration{}, %{ + |> Multi.run(:course_reg, fn _repo, %{course: course} -> + CourseRegistrations.enroll_course(%{ course_id: course.id, user_id: user.id, role: :admin }) end) - |> Multi.update(:latest_viewed_course_id, fn %{course: course} -> - User - |> where(id: ^user.id) - |> Repo.one() - |> User.changeset(%{latest_viewed_course_id: course.id}) - end) |> Repo.transaction() end @@ -313,75 +307,6 @@ defmodule Cadet.Courses do end end - # @doc """ - # Updates a group based on the group name or create one if it doesn't exist - # """ - # @spec insert_or_update_group(map()) :: {:ok, %Group{}} | {:error, Ecto.Changeset.t()} - # def insert_or_update_group(params = %{name: name}) when is_binary(name) do - # Group - # |> where(name: ^name) - # |> Repo.one() - # |> case do - # nil -> - # Group.changeset(%Group{}, params) - - # group -> - # Group.changeset(group, params) - # end - # |> Repo.insert_or_update() - # end - - # @doc """ - # Reassign a student to a discussion group - # This will un-assign student from the current discussion group - # """ - # def assign_group(leader = %User{}, student = %User{}) do - # cond do - # leader.role == :student -> - # {:error, :invalid} - - # student.role != :student -> - # {:error, :invalid} - - # true -> - # Repo.transaction(fn -> - # {:ok, _} = unassign_group(student) - - # %Group{} - # |> Group.changeset(%{}) - # |> put_assoc(:leader, leader) - # |> put_assoc(:student, student) - # |> Repo.insert!() - # end) - # end - # end - - # @doc """ - # Remove existing student from discussion group, no-op if a student - # is unassigned - # """ - # def unassign_group(student = %User{}) do - # existing_group = Repo.get_by(Group, student_id: student.id) - - # if existing_group == nil do - # {:ok, nil} - # else - # Repo.delete(existing_group) - # end - # end - - # @doc """ - # Get list of students under staff discussion group - # """ - # def list_students_by_leader(staff = %CourseRegistration{}) do - # import Cadet.Course.Query, only: [group_members: 1] - - # staff - # |> group_members() - # |> Repo.all() - # |> Repo.preload([:student]) - # end - @upload_file_roles ~w(admin staff)a @doc """ diff --git a/lib/cadet_web/admin_controllers/admin_courses_controller.ex b/lib/cadet_web/admin_controllers/admin_courses_controller.ex index f24571fab..bab104906 100644 --- a/lib/cadet_web/admin_controllers/admin_courses_controller.ex +++ b/lib/cadet_web/admin_controllers/admin_courses_controller.ex @@ -9,23 +9,17 @@ defmodule CadetWeb.AdminCoursesController do when is_ecto_id(course_id) do params = params |> to_snake_case_atom_keys() - if (Map.has_key?(params, :source_chapter) and Map.has_key?(params, :source_variant)) or - (not Map.has_key?(params, :source_chapter) and - not Map.has_key?(params, :source_variant)) do - case Courses.update_course_config(course_id, params) do - {:ok, _} -> - text(conn, "OK") + case Courses.update_course_config(course_id, params) do + {:ok, _} -> + text(conn, "OK") - {:error, {status, message}} -> - send_resp(conn, status, message) + {:error, {status, message}} -> + send_resp(conn, status, message) - {:error, _} -> - conn - |> put_status(:bad_request) - |> text("Invalid parameter(s)") - end - else - send_resp(conn, :bad_request, "Missing parameter(s)") + {:error, _} -> + conn + |> put_status(:bad_request) + |> text("Invalid parameter(s)") end end diff --git a/lib/cadet_web/controllers/courses_controller.ex b/lib/cadet_web/controllers/courses_controller.ex index 21f18ca2e..0c09c291f 100644 --- a/lib/cadet_web/controllers/courses_controller.ex +++ b/lib/cadet_web/controllers/courses_controller.ex @@ -16,30 +16,14 @@ defmodule CadetWeb.CoursesController do user = conn.assigns.current_user params = params |> to_snake_case_atom_keys() - required_keys = [ - :course_name, - :course_short_name, - :viewable, - :enable_game, - :enable_achievements, - :enable_sourcecast, - :source_chapter, - :source_variant, - :module_help_text - ] - - if Enum.reduce(required_keys, true, fn x, acc -> acc and Map.has_key?(params, x) end) do - case Courses.create_course_config(params, user) do - {:ok, _} -> - text(conn, "OK") - - {:error, _, _, _} -> - conn - |> put_status(:bad_request) - |> text("Invalid parameter(s)") - end - else - send_resp(conn, :bad_request, "Missing parameter(s)") + case Courses.create_course_config(params, user) do + {:ok, _} -> + text(conn, "OK") + + {:error, _, _, _} -> + conn + |> put_status(:bad_request) + |> text("Invalid parameter(s)") end end diff --git a/test/cadet/courses/course_test.exs b/test/cadet/courses/course_test.exs index 043e6e71e..da782b36c 100644 --- a/test/cadet/courses/course_test.exs +++ b/test/cadet/courses/course_test.exs @@ -5,30 +5,131 @@ defmodule Cadet.Courses.CourseTest do describe "Course Configuration Changesets" do test "valid changesets" do - assert_changeset(%{course_name: "Data Structures and Algorithms"}, :valid) - assert_changeset(%{course_short_name: "CS2040S"}, :valid) - assert_changeset(%{viewable: false}, :valid) - assert_changeset(%{enable_game: false}, :valid) - assert_changeset(%{enable_achievements: false}, :valid) - assert_changeset(%{enable_sourcecast: false}, :valid) - assert_changeset(%{module_help_text: ""}, :valid) - assert_changeset(%{module_help_text: "Module help text"}, :valid) + assert_changeset( + %{ + course_name: "Data Structures and Algorithms", + source_chapter: 1, + source_variant: "default" + }, + :valid + ) + + assert_changeset( + %{ + course_short_name: "CS2040S", + course_name: "Data Structures and Algorithms", + source_chapter: 1, + source_variant: "default" + }, + :valid + ) + + assert_changeset( + %{ + viewable: false, + course_name: "Data Structures and Algorithms", + source_chapter: 1, + source_variant: "default" + }, + :valid + ) + + assert_changeset( + %{ + enable_game: false, + course_name: "Data Structures and Algorithms", + source_chapter: 1, + source_variant: "default" + }, + :valid + ) + + assert_changeset( + %{ + enable_achievements: false, + course_name: "Data Structures and Algorithms", + source_chapter: 1, + source_variant: "default" + }, + :valid + ) + + assert_changeset( + %{ + enable_sourcecast: false, + course_name: "Data Structures and Algorithms", + source_chapter: 1, + source_variant: "default" + }, + :valid + ) + + assert_changeset( + %{ + module_help_text: "", + course_name: "Data Structures and Algorithms", + source_chapter: 1, + source_variant: "default" + }, + :valid + ) + + assert_changeset( + %{ + module_help_text: "Module help text", + course_name: "Data Structures and Algorithms", + source_chapter: 1, + source_variant: "default" + }, + :valid + ) assert_changeset( %{ enable_game: true, enable_achievements: true, - enable_sourcecast: true + enable_sourcecast: true, + course_name: "Data Structures and Algorithms", + source_chapter: 1, + source_variant: "default" }, :valid ) - assert_changeset(%{source_chapter: 1, source_variant: "wasm"}, :valid) - assert_changeset(%{source_chapter: 2, source_variant: "lazy"}, :valid) - assert_changeset(%{source_chapter: 3, source_variant: "non-det"}, :valid) + assert_changeset( + %{ + source_chapter: 1, + source_variant: "wasm", + course_name: "Data Structures and Algorithms" + }, + :valid + ) + + assert_changeset( + %{ + source_chapter: 2, + source_variant: "lazy", + course_name: "Data Structures and Algorithms" + }, + :valid + ) + + assert_changeset( + %{ + source_chapter: 3, + source_variant: "non-det", + course_name: "Data Structures and Algorithms" + }, + :valid + ) assert_changeset( - %{source_chapter: 4, source_variant: "default", enable_achievements: true}, + %{ + source_chapter: 4, + source_variant: "default", + enable_achievements: true, + course_name: "Data Structures and Algorithms" + }, :valid ) end diff --git a/test/cadet_web/admin_controllers/admin_courses_controller_test.exs b/test/cadet_web/admin_controllers/admin_courses_controller_test.exs index 008acf3c7..e359a48b1 100644 --- a/test/cadet_web/admin_controllers/admin_courses_controller_test.exs +++ b/test/cadet_web/admin_controllers/admin_courses_controller_test.exs @@ -136,7 +136,7 @@ defmodule CadetWeb.AdminCoursesControllerTest do "sourceVariant" => "default" }) - assert response(conn, 400) == "Missing parameter(s)" + assert response(conn, 400) == "Invalid parameter(s)" end end diff --git a/test/cadet_web/controllers/courses_controller_test.exs b/test/cadet_web/controllers/courses_controller_test.exs index 22939d622..0d8d04abf 100644 --- a/test/cadet_web/controllers/courses_controller_test.exs +++ b/test/cadet_web/controllers/courses_controller_test.exs @@ -48,14 +48,13 @@ defmodule CadetWeb.CoursesControllerTest do "viewable" => "true", "enable_achievements" => "true", "enable_sourcecast" => "true", - "source_chapter" => "1", "source_variant" => "default", "module_help_text" => "Help Text" } conn = post(conn, build_url_create(), params) - assert response(conn, 400) == "Missing parameter(s)" + assert response(conn, 400) == "Invalid parameter(s)" end @tag authenticate: :student From 20cdee2856c2044ead3c6db6621bd6277af7ac47 Mon Sep 17 00:00:00 2001 From: YaleChen299 Date: Thu, 29 Jul 2021 19:40:56 +0800 Subject: [PATCH 171/174] simplify function --- lib/cadet/courses/course.ex | 43 ++++++++++++++++++++++++------------- 1 file changed, 28 insertions(+), 15 deletions(-) diff --git a/lib/cadet/courses/course.ex b/lib/cadet/courses/course.ex index 430f5dad4..e87a62d78 100644 --- a/lib/cadet/courses/course.ex +++ b/lib/cadet/courses/course.ex @@ -39,21 +39,34 @@ defmodule Cadet.Courses.Course do var = Map.has_key?(params, :source_variant) # not (chap xor var) - if (chap and var) or (not chap and not var) do - case get_field(changeset, :source_chapter, nil) do - nil -> changeset - 1 -> validate_inclusion(changeset, :source_variant, ["default", "lazy", "wasm"]) - 2 -> validate_inclusion(changeset, :source_variant, ["default", "lazy"]) - 3 -> validate_inclusion(changeset, :source_variant, ["default", "concurrent", "non-det"]) - 4 -> validate_inclusion(changeset, :source_variant, ["default", "gpu"]) - _ -> add_error(changeset, :source_chapter, "is invalid") - end - else - add_error( - changeset, - :source_chapter, - "source chapter and source variant must be present together" - ) + case {chap, var} do + {true, true} -> + case get_field(changeset, :source_chapter) do + 1 -> + validate_inclusion(changeset, :source_variant, ["default", "lazy", "wasm"]) + + 2 -> + validate_inclusion(changeset, :source_variant, ["default", "lazy"]) + + 3 -> + validate_inclusion(changeset, :source_variant, ["default", "concurrent", "non-det"]) + + 4 -> + validate_inclusion(changeset, :source_variant, ["default", "gpu"]) + + _ -> + add_error(changeset, :source_chapter, "is invalid") + end + + {false, false} -> + changeset + + {_, _} -> + add_error( + changeset, + :source_chapter, + "source chapter and source variant must be present together" + ) end end end From a0fdd712971568e4f9fb917268f8ea528483e852 Mon Sep 17 00:00:00 2001 From: YaleChen299 Date: Thu, 29 Jul 2021 20:01:32 +0800 Subject: [PATCH 172/174] update delete route --- lib/cadet/courses/courses.ex | 4 +- .../admin_courses_controller.ex | 12 ++--- lib/cadet_web/router.ex | 7 ++- test/cadet/courses/courses_test.exs | 12 +---- .../admin_courses_controller_test.exs | 48 ++----------------- 5 files changed, 18 insertions(+), 65 deletions(-) diff --git a/lib/cadet/courses/courses.ex b/lib/cadet/courses/courses.ex index 6e85f8779..431cb4212 100644 --- a/lib/cadet/courses/courses.ex +++ b/lib/cadet/courses/courses.ex @@ -174,9 +174,9 @@ defmodule Cadet.Courses do end) end - @spec delete_assessment_config(integer(), map()) :: + @spec delete_assessment_config(integer(), integer()) :: {:ok, Ecto.Schema.t()} | {:error, Ecto.Changeset.t()} | {:error, :no_such_enrty} - def delete_assessment_config(course_id, %{assessment_config_id: assessment_config_id}) do + def delete_assessment_config(course_id, assessment_config_id) do config = AssessmentConfig |> where(course_id: ^course_id) diff --git a/lib/cadet_web/admin_controllers/admin_courses_controller.ex b/lib/cadet_web/admin_controllers/admin_courses_controller.ex index bab104906..2f0a2f7e7 100644 --- a/lib/cadet_web/admin_controllers/admin_courses_controller.ex +++ b/lib/cadet_web/admin_controllers/admin_courses_controller.ex @@ -69,12 +69,10 @@ defmodule CadetWeb.AdminCoursesController do def delete_assessment_config(conn, %{ "course_id" => course_id, - "assessmentConfig" => assessment_config + "assessment_config_id" => assessment_config_id }) - when is_ecto_id(course_id) and is_map(assessment_config) do - config = assessment_config |> to_snake_case_atom_keys() - - case Courses.delete_assessment_config(course_id, config) do + when is_ecto_id(course_id) and is_ecto_id(assessment_config_id) do + case Courses.delete_assessment_config(course_id, assessment_config_id) do {:ok, _} -> text(conn, "OK") @@ -85,10 +83,6 @@ defmodule CadetWeb.AdminCoursesController do end end - def delete_assessment_config(conn, _) do - send_resp(conn, :bad_request, "Missing Map parameter(s)") - end - swagger_path :update_course_config do put("/v2/courses/{course_id}/admin/onfig") diff --git a/lib/cadet_web/router.ex b/lib/cadet_web/router.ex index 4ae3ece67..8383f371b 100644 --- a/lib/cadet_web/router.ex +++ b/lib/cadet_web/router.ex @@ -146,7 +146,12 @@ defmodule CadetWeb.Router do put("/config", AdminCoursesController, :update_course_config) get("/config/assessment_configs", AdminCoursesController, :get_assessment_configs) put("/config/assessment_configs", AdminCoursesController, :update_assessment_configs) - delete("/config/assessment_config", AdminCoursesController, :delete_assessment_config) + + delete( + "/config/assessment_config/:assessment_config_id", + AdminCoursesController, + :delete_assessment_config + ) end # Other scopes may use custom stacks. diff --git a/test/cadet/courses/courses_test.exs b/test/cadet/courses/courses_test.exs index 5b398d021..07fd08537 100644 --- a/test/cadet/courses/courses_test.exs +++ b/test/cadet/courses/courses_test.exs @@ -380,11 +380,7 @@ defmodule Cadet.CoursesTest do old_configs = Courses.get_assessment_configs(course.id) refute Assessment |> Repo.get(assessment.id) |> is_nil() - params = %{ - assessment_config_id: config.id - } - - {:ok, _} = Courses.delete_assessment_config(course.id, params) + {:ok, _} = Courses.delete_assessment_config(course.id, config.id) new_configs = Courses.get_assessment_configs(course.id) assert length(old_configs) == 1 @@ -396,11 +392,7 @@ defmodule Cadet.CoursesTest do course = insert(:course) insert(:assessment_config, %{order: 1, course: course}) - params = %{ - assessment_config_id: -1 - } - - assert {:error, :no_such_enrty} == Courses.delete_assessment_config(course.id, params) + assert {:error, :no_such_enrty} == Courses.delete_assessment_config(course.id, -1) end end diff --git a/test/cadet_web/admin_controllers/admin_courses_controller_test.exs b/test/cadet_web/admin_controllers/admin_courses_controller_test.exs index e359a48b1..eaa8f1e4d 100644 --- a/test/cadet_web/admin_controllers/admin_courses_controller_test.exs +++ b/test/cadet_web/admin_controllers/admin_courses_controller_test.exs @@ -312,20 +312,9 @@ defmodule CadetWeb.AdminCoursesControllerTest do old_configs = course_id |> Courses.get_assessment_configs() |> Enum.map(& &1.type) - params = %{ - "assessmentConfig" => %{ - "assessmentConfigId" => config1.id, - "courseId" => course_id, - "type" => "Missions", - "earlySubmissionXp" => 100, - "hoursBeforeEarlyXpDecay" => 24, - "decayRatePointsPerHour" => 1 - } - } - resp = conn - |> delete(build_url_assessment_config(course_id), params) + |> delete(build_url_assessment_config(course_id, config1.id)) |> response(200) assert resp == "OK" @@ -339,10 +328,7 @@ defmodule CadetWeb.AdminCoursesControllerTest do test "rejects forbidden request for non-staff users", %{conn: conn} do course_id = conn.assigns[:course_id] - conn = - delete(conn, build_url_assessment_config(course_id), %{ - "assessmentConfig" => %{} - }) + conn = delete(conn, build_url_assessment_config(course_id, 1)) assert response(conn, 403) == "Forbidden" end @@ -351,34 +337,10 @@ defmodule CadetWeb.AdminCoursesControllerTest do test "rejects request if user is not in specified course", %{conn: conn} do course_id = conn.assigns[:course_id] - conn = - delete(conn, build_url_assessment_config(course_id + 1), %{ - "assessmentConfig" => %{} - }) + conn = delete(conn, build_url_assessment_config(course_id + 1, 1)) assert response(conn, 403) == "Forbidden" end - - @tag authenticate: :staff - test "rejects requests with invalid params 1", %{conn: conn} do - course_id = conn.assigns[:course_id] - - conn = - delete(conn, build_url_assessment_config(course_id), %{ - "assessmentConfigs" => "Missions" - }) - - assert response(conn, 400) == "Missing Map parameter(s)" - end - - @tag authenticate: :staff - test "rejects requests with missing params", %{conn: conn} do - course_id = conn.assigns[:course_id] - - conn = delete(conn, build_url_assessment_config(course_id), %{}) - - assert response(conn, 400) == "Missing Map parameter(s)" - end end defp build_url_course_config(course_id), do: "/v2/courses/#{course_id}/admin/config" @@ -386,8 +348,8 @@ defmodule CadetWeb.AdminCoursesControllerTest do defp build_url_assessment_configs(course_id), do: "/v2/courses/#{course_id}/admin/config/assessment_configs" - defp build_url_assessment_config(course_id), - do: "/v2/courses/#{course_id}/admin/config/assessment_config" + defp build_url_assessment_config(course_id, config_id), + do: "/v2/courses/#{course_id}/admin/config/assessment_config/#{config_id}" defp to_map(schema), do: schema |> Map.from_struct() |> Map.drop([:updated_at]) From 5dbc08a179018e827d89856db989b43ffe6566ba Mon Sep 17 00:00:00 2001 From: YaleChen299 Date: Sun, 1 Aug 2021 00:20:31 +0800 Subject: [PATCH 173/174] fix issues --- lib/cadet/helpers/model_helper.ex | 11 ++ .../20210531155751_multitenant_upgrade.exs | 2 +- test/cadet/auth/guardian_test.exs | 2 +- .../admin_goals_controller_test.exs | 2 +- .../controllers/user_controller_test.exs | 174 ------------------ test/test_helper.exs | 16 -- 6 files changed, 14 insertions(+), 193 deletions(-) diff --git a/lib/cadet/helpers/model_helper.ex b/lib/cadet/helpers/model_helper.ex index ce960cd3b..b41fc8a35 100644 --- a/lib/cadet/helpers/model_helper.ex +++ b/lib/cadet/helpers/model_helper.ex @@ -132,4 +132,15 @@ defmodule Cadet.ModelHelper do |> cast_assoc(assoc_field) end end + + def remove_preload(struct, field, cardinality \\ :one) do + %{ + struct + | field => %Ecto.Association.NotLoaded{ + __field__: field, + __owner__: struct.__struct__, + __cardinality__: cardinality + } + } + end end diff --git a/priv/repo/migrations/20210531155751_multitenant_upgrade.exs b/priv/repo/migrations/20210531155751_multitenant_upgrade.exs index 1255e95f6..acf08c8bd 100644 --- a/priv/repo/migrations/20210531155751_multitenant_upgrade.exs +++ b/priv/repo/migrations/20210531155751_multitenant_upgrade.exs @@ -185,7 +185,7 @@ defmodule Cadet.Repo.Migrations.MultitenantUpgrade do # assign a staff to be the leader_id during migration nil -> CourseRegistration - |> where(role: :staff) + |> where([cr], cr.role in [:admin, :staff]) |> Repo.one() |> Map.fetch!(:id) diff --git a/test/cadet/auth/guardian_test.exs b/test/cadet/auth/guardian_test.exs index 2884be4e6..483adf091 100644 --- a/test/cadet/auth/guardian_test.exs +++ b/test/cadet/auth/guardian_test.exs @@ -1,7 +1,7 @@ defmodule Cadet.Auth.GuardianTest do use Cadet.DataCase - import Cadet.TestHelper + import Cadet.ModelHelper alias Cadet.Auth.Guardian test "token subject is user id" do diff --git a/test/cadet_web/admin_controllers/admin_goals_controller_test.exs b/test/cadet_web/admin_controllers/admin_goals_controller_test.exs index 11075d90d..c501faf95 100644 --- a/test/cadet_web/admin_controllers/admin_goals_controller_test.exs +++ b/test/cadet_web/admin_controllers/admin_goals_controller_test.exs @@ -263,6 +263,6 @@ defmodule CadetWeb.AdminGoalsControllerTest do end defp build_path(course_id, uuid, course_reg_id) do - "v2/courses/#{course_id}/admin/users/#{course_reg_id}/goals/#{uuid}/progress/" + "/v2/courses/#{course_id}/admin/users/#{course_reg_id}/goals/#{uuid}/progress/" end end diff --git a/test/cadet_web/controllers/user_controller_test.exs b/test/cadet_web/controllers/user_controller_test.exs index 624400551..15603e316 100644 --- a/test/cadet_web/controllers/user_controller_test.exs +++ b/test/cadet_web/controllers/user_controller_test.exs @@ -155,184 +155,10 @@ defmodule CadetWeb.UserControllerTest do assert expected == resp end - # # This also tests for the case where assessment has no submission - # @tag authenticate: :student - # test "success, student story ordering", %{conn: conn} do - # early_assessments = build_assessments_starting_at(Timex.shift(Timex.now(), days: -3)) - # late_assessments = build_assessments_starting_at(Timex.shift(Timex.now(), days: -1)) - - # for assessment <- early_assessments ++ late_assessments do - # resp_story = - # conn - # |> get("/v2/user") - # |> json_response(200) - # |> Map.get("latestViewedCourse").story - - # expected_story = %{ - # "story" => assessment.story, - # "playStory" => true - # } - - # assert resp_story == expected_story - - # {:ok, _} = Repo.delete(assessment) - # end - # end - - # @tag authenticate: :student - # test "success, student story skips assessment without story", %{conn: conn} do - # assessments = build_assessments_starting_at(Timex.shift(Timex.now(), days: -1)) - - # assessments - # |> List.first() - # |> Assessment.changeset(%{story: nil}) - # |> Repo.update() - - # resp_story = - # conn - # |> get("/v2/user") - # |> json_response(200) - # |> Map.get("story") - - # expected_story = %{ - # "story" => Enum.fetch!(assessments, 1).story, - # "playStory" => true - # } - - # assert resp_story == expected_story - # end - - # @tag authenticate: :student - # test "success, student story skips unopen assessments", %{conn: conn} do - # build_assessments_starting_at(Timex.shift(Timex.now(), days: 1)) - # build_assessments_starting_at(Timex.shift(Timex.now(), months: -1)) - - # valid_assessments = build_assessments_starting_at(Timex.shift(Timex.now(), days: -1)) - - # for assessment <- valid_assessments do - # assessment - # |> Assessment.changeset(%{is_published: false}) - # |> Repo.update!() - # end - - # resp_story = - # conn - # |> get("/v2/user") - # |> json_response(200) - # |> Map.get("story") - - # expected_story = %{ - # "story" => nil, - # "playStory" => false - # } - - # assert resp_story == expected_story - # end - - # @tag authenticate: :student - # test "success, student story skips attempting/attempted/submitted", %{conn: conn} do - # user = conn.assigns.current_user - - # early_assessments = build_assessments_starting_at(Timex.shift(Timex.now(), days: -3)) - # late_assessments = build_assessments_starting_at(Timex.shift(Timex.now(), days: -1)) - - # # Submit for i-th assessment, expect (i+1)th story to be returned - # for status <- [:attempting, :attempted, :submitted] do - # for [tester, checker] <- - # Enum.chunk_every(early_assessments ++ late_assessments, 2, 1, :discard) do - # insert(:submission, %{student: user, assessment: tester, status: status}) - - # resp_story = - # conn - # |> get("/v2/user") - # |> json_response(200) - # |> Map.get("story") - - # expected_story = %{ - # "story" => checker.story, - # "playStory" => true - # } - - # assert resp_story == expected_story - # end - - # Repo.delete_all(Submission) - # end - # end - - # @tag authenticate: :student - # test "success, return most recent assessment when all are attempted", %{conn: conn} do - # user = conn.assigns.current_user - - # early_assessments = build_assessments_starting_at(Timex.shift(Timex.now(), days: -3)) - # late_assessments = build_assessments_starting_at(Timex.shift(Timex.now(), days: -1)) - - # for assessment <- early_assessments ++ late_assessments do - # insert(:submission, %{student: user, assessment: assessment, status: :attempted}) - # end - - # resp_story = - # conn - # |> get("/v2/user") - # |> json_response(200) - # |> Map.get("story") - - # expected_story = %{ - # "story" => late_assessments |> List.first() |> Map.get(:story), - # "playStory" => false - # } - - # assert resp_story == expected_story - # end - - # @tag authenticate: :staff - # test "success, staff", %{conn: conn} do - # user = conn.assigns.current_user - - # resp = - # conn - # |> get("/v2/user") - # |> json_response(200) - # |> Map.delete("story") - - # expected = %{ - # "name" => user.name, - # "role" => "#{user.role}", - # "group" => nil, - # "grade" => 0, - # "maxGrade" => 0, - # "xp" => 0, - # "gameStates" => %{}, - # "userId" => user.id - # } - - # assert expected == resp - # end - test "unauthorized", %{conn: conn} do conn = get(conn, "/v2/user", nil) assert response(conn, 401) =~ "Unauthorised" end - - # defp build_assessments_starting_at(time) do - # type_order_map = - # Assessment.assessment_types() - # |> Enum.with_index() - # |> Enum.reduce(%{}, fn {type, idx}, acc -> Map.put(acc, type, idx) end) - - # Assessment.assessment_types() - # |> Enum.map( - # &build(:assessment, %{ - # type: &1, - # is_published: true, - # open_at: time, - # close_at: Timex.shift(time, days: 10) - # }) - # ) - # |> Enum.shuffle() - # |> Enum.map(&insert(&1)) - # |> Enum.sort(&(type_order_map[&1.type] < type_order_map[&2.type])) - # end end describe "GET /v2/user/latest_viewed_course" do diff --git a/test/test_helper.exs b/test/test_helper.exs index 2a7b91426..30e668048 100644 --- a/test/test_helper.exs +++ b/test/test_helper.exs @@ -6,19 +6,3 @@ ExUnit.start() Faker.start() Ecto.Adapters.SQL.Sandbox.mode(Cadet.Repo, :manual) - -defmodule Cadet.TestHelper do - @doc """ - Removes a preloaded Ecto association. - """ - def remove_preload(struct, field, cardinality \\ :one) do - %{ - struct - | field => %Ecto.Association.NotLoaded{ - __field__: field, - __owner__: struct.__struct__, - __cardinality__: cardinality - } - } - end -end From fd5605736d439e254ab78d035b302ac2929ba0a4 Mon Sep 17 00:00:00 2001 From: YaleChen299 Date: Sun, 1 Aug 2021 00:31:59 +0800 Subject: [PATCH 174/174] fix migration --- priv/repo/migrations/20210531155751_multitenant_upgrade.exs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/priv/repo/migrations/20210531155751_multitenant_upgrade.exs b/priv/repo/migrations/20210531155751_multitenant_upgrade.exs index acf08c8bd..62c6385a9 100644 --- a/priv/repo/migrations/20210531155751_multitenant_upgrade.exs +++ b/priv/repo/migrations/20210531155751_multitenant_upgrade.exs @@ -1,6 +1,6 @@ defmodule Cadet.Repo.Migrations.MultitenantUpgrade do use Ecto.Migration - import Ecto.Query, only: [from: 2, where: 2] + import Ecto.Query alias Cadet.Accounts.{CourseRegistration, Notification, Role, User} alias Cadet.Assessments.{Answer, Assessment, Question, Submission, SubmissionVotes}