diff --git a/lib/cadet/accounts/course_registrations.ex b/lib/cadet/accounts/course_registrations.ex index c45549199..79728c0dc 100644 --- a/lib/cadet/accounts/course_registrations.ex +++ b/lib/cadet/accounts/course_registrations.ex @@ -47,6 +47,20 @@ defmodule Cadet.Accounts.CourseRegistrations do |> Repo.all() end + def get_exam_mode_course(%User{id: id}) do + CourseRegistration + |> where([cr], cr.user_id == ^id and cr.role == :student) + |> join(:inner, [cr], c in assoc(cr, :course), + on: c.enable_exam_mode == true and c.is_official_course == true + ) + |> 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() + end + def get_admin_courses_count(%User{id: id}) do CourseRegistration |> where(user_id: ^id) diff --git a/lib/cadet/accounts/user.ex b/lib/cadet/accounts/user.ex index d3f3c6026..98edf8970 100644 --- a/lib/cadet/accounts/user.ex +++ b/lib/cadet/accounts/user.ex @@ -21,6 +21,7 @@ defmodule Cadet.Accounts.User do field(:provider, :string) field(:super_admin, :boolean) field(:email, :string) + field(:is_paused, :boolean) belongs_to(:latest_viewed_course, Course) has_many(:courses, CourseRegistration) @@ -29,7 +30,7 @@ defmodule Cadet.Accounts.User do end @required_fields ~w(username provider)a - @optional_fields ~w(name latest_viewed_course_id super_admin)a + @optional_fields ~w(name latest_viewed_course_id super_admin is_paused)a def changeset(user, params \\ %{}) do user diff --git a/lib/cadet/courses/course.ex b/lib/cadet/courses/course.ex index c74d23bd7..ce977dedb 100644 --- a/lib/cadet/courses/course.ex +++ b/lib/cadet/courses/course.ex @@ -14,6 +14,9 @@ defmodule Cadet.Courses.Course do enable_achievements: boolean(), enable_sourcecast: boolean(), enable_stories: boolean(), + enable_exam_mode: boolean(), + resume_code: string(), + is_official_course: boolean(), source_chapter: integer(), source_variant: String.t(), module_help_text: String.t(), @@ -28,6 +31,9 @@ defmodule Cadet.Courses.Course do field(:enable_achievements, :boolean, default: true) field(:enable_sourcecast, :boolean, default: true) field(:enable_stories, :boolean, default: false) + field(:enable_exam_mode, :boolean, default: false) + field(:resume_code, :string) + field(:is_official_course, :boolean, default: false) field(:source_chapter, :integer) field(:source_variant, :string) field(:module_help_text, :string) @@ -41,14 +47,51 @@ defmodule Cadet.Courses.Course do end @required_fields ~w(course_name viewable enable_game - enable_achievements enable_sourcecast enable_stories source_chapter source_variant)a - @optional_fields ~w(course_short_name module_help_text)a + enable_exam_mode enable_achievements enable_sourcecast enable_stories source_chapter source_variant)a + @optional_fields ~w(course_short_name module_help_text resume_code)a def changeset(course, params) do course |> cast(params, @required_fields ++ @optional_fields) |> validate_required(@required_fields) |> validate_sublanguage_combination(params) + |> validate_exam_mode(params) + end + + # Validates combination of exam mode, resume code, and official course state + defp validate_exam_mode(changeset, params) do + has_resume_code = params |> Map.has_key?(:resume_code) + + resume_code_params = + params + |> Map.get(:resume_code, "") + |> String.trim() + + resume_code = + changeset + |> get_field(:resume_code) + + enable_exam_mode = Map.get(params, :enable_exam_mode, false) + is_official_course = get_field(changeset, :is_official_course, false) + + case {enable_exam_mode, is_official_course, has_resume_code, resume_code_params} do + {_, _, true, ""} -> + add_error( + changeset, + :resume_code, + "Resume code must not be empty." + ) + + {true, false, _, _} -> + add_error( + changeset, + :enable_exam_mode, + "Exam mode is only available for official institution course." + ) + + _ -> + changeset + end end # Validates combination of Source chapter and variant diff --git a/lib/cadet/courses/courses.ex b/lib/cadet/courses/courses.ex index 5c0464fae..ac49d43c5 100644 --- a/lib/cadet/courses/courses.ex +++ b/lib/cadet/courses/courses.ex @@ -24,11 +24,12 @@ defmodule Cadet.Courses do @doc """ Creates a new course configuration, course registration, and sets - the user's latest course id to the newly created course. + the user's latest course id to the newly created course. A 4-digit + course resume code will be randomly generated. """ def create_course_config(params, user) do Multi.new() - |> Multi.insert(:course, Course.changeset(%Course{}, params)) + |> Multi.insert(:course, Course.changeset(%Course{}, set_default_resume_code(params))) |> Multi.run(:course_reg, fn _repo, %{course: course} -> CourseRegistrations.enroll_course(%{ course_id: course.id, @@ -82,6 +83,11 @@ defmodule Cadet.Courses do end end + defp set_default_resume_code(params) do + params + |> Map.put(:resume_code, Integer.to_string(:rand.uniform(9000) + 999)) + end + def get_all_course_ids do Course |> select([c], c.id) diff --git a/lib/cadet/focus_logs/focus_log.ex b/lib/cadet/focus_logs/focus_log.ex new file mode 100644 index 000000000..47cac0bbc --- /dev/null +++ b/lib/cadet/focus_logs/focus_log.ex @@ -0,0 +1,32 @@ +defmodule Cadet.FocusLogs.FocusLog do + @moduledoc """ + The FocusLog entity represents a log of user's browser focus + while using Source Academy under exam mode. + """ + use Cadet, :model + + alias Cadet.Accounts.User + alias Cadet.Courses.Course + + @type t :: %__MODULE__{ + user: User.t(), + course: Course.t(), + focus_type: integer() + } + + schema "user_browser_focus_log" do + belongs_to(:user, User) + belongs_to(:course, Course) + field(:time, :naive_datetime) + field(:focus_type, :integer) + end + + @required_fields ~w(user_id course_id time focus_type)a + + def changeset(focus_log, params) do + focus_log + |> cast(params, @required_fields) + |> add_belongs_to_id_from_model([:user, :course], params) + |> validate_required(@required_fields) + end +end diff --git a/lib/cadet/focus_logs/focus_logs.ex b/lib/cadet/focus_logs/focus_logs.ex new file mode 100644 index 000000000..3eca5344c --- /dev/null +++ b/lib/cadet/focus_logs/focus_logs.ex @@ -0,0 +1,26 @@ +defmodule Cadet.FocusLogs do + @moduledoc """ + Contains logic to manage user's browser focus log + such as insertion + """ + alias Cadet.FocusLogs.FocusLog + + use Cadet, [:context, :display] + + def insert_log(user_id, course_id, focus_type) do + insert_result = + %FocusLog{} + |> FocusLog.changeset(%{ + user_id: user_id, + course_id: course_id, + time: DateTime.utc_now(), + focus_type: focus_type + }) + |> Repo.insert() + + case insert_result do + {:ok, log} -> {:ok, log} + {:error, changeset} -> {:error, full_error_messages(changeset)} + 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 7220a4d80..d73d4f525 100644 --- a/lib/cadet_web/admin_controllers/admin_courses_controller.ex +++ b/lib/cadet_web/admin_controllers/admin_courses_controller.ex @@ -108,6 +108,14 @@ defmodule CadetWeb.AdminCoursesController do enable_achievements(:body, :boolean, "Enable achievements") enable_sourcecast(:body, :boolean, "Enable sourcecast") enable_stories(:body, :boolean, "Enable stories") + enable_exam_mode(:body, :boolean, "Enable exam mode") + + resume_code( + :body, + :string, + "Resume code that students that attempt to open developer tools will be prompted to enter" + ) + sublanguage(:body, Schema.ref(:AdminSublanguage), "sublanguage object") module_help_text(:body, :string, "Module help text") end @@ -143,7 +151,11 @@ defmodule CadetWeb.AdminCoursesController do title("AdminSublanguage") properties do - chapter(:integer, "Chapter number from 1 to 4", required: true, minimum: 1, maximum: 4) + chapter(:integer, "Chapter number from 1 to 4", + required: true, + minimum: 1, + maximum: 4 + ) variant(Schema.ref(:SourceVariant), "Variant name", required: true) end diff --git a/lib/cadet_web/controllers/courses_controller.ex b/lib/cadet_web/controllers/courses_controller.ex index e6555bd7a..2a20ee75d 100644 --- a/lib/cadet_web/controllers/courses_controller.ex +++ b/lib/cadet_web/controllers/courses_controller.ex @@ -9,7 +9,11 @@ defmodule CadetWeb.CoursesController 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) + if conn.assigns.course_reg.role == :admin do + render(conn, "config_admin.json", config: config) + else + render(conn, "config.json", config: config) + end # coveralls-ignore-start # no course error will not happen here @@ -40,6 +44,36 @@ defmodule CadetWeb.CoursesController do end end + defp check_resume_code(course_id, resume_code) do + case Courses.get_course_config(course_id) do + {:ok, config} -> {:ok, config.resume_code == resume_code} + {:error, {status_code, message}} -> {:error, {status_code, message}} + end + end + + defp unpause_user(conn, user) do + update_result = + user + |> Cadet.Accounts.User.changeset(%{is_paused: false}) + |> Cadet.Repo.update() + + case update_result do + {:ok, _} -> conn |> send_resp(:ok, "") + {:error, _} -> conn |> send_resp(500, "") + end + end + + def try_unpause_user(conn, %{"course_id" => course_id}) when is_ecto_id(course_id) do + user = conn.assigns.current_user + resume_code = Map.get(conn.body_params, "resume_code", nil) + + case check_resume_code(course_id, resume_code) do + {:ok, true} -> unpause_user(conn, user) + {:ok, false} -> conn |> send_resp(:forbidden, "") + {:error, {status_code, message}} -> conn |> send_resp(status_code, message) + end + end + swagger_path :create do post("/config/create") @@ -56,6 +90,7 @@ defmodule CadetWeb.CoursesController do enable_achievements(:body, :boolean, "Enable achievements", required: true) enable_sourcecast(:body, :boolean, "Enable sourcecast", required: true) enable_stories(:body, :boolean, "Enable stories", required: true) + enable_exam_mode(:body, :boolean, "Enable exam mode", required: true) source_chapter(:body, :number, "Default source chapter", required: true) source_variant(:body, Schema.ref(:SourceVariant), "Default source variant name", @@ -97,6 +132,7 @@ defmodule CadetWeb.CoursesController do enable_achievements(:boolean, "Enable achievements", required: true) enable_sourcecast(:boolean, "Enable sourcecast", required: true) enable_stories(:boolean, "Enable stories", required: true) + enable_exam_mode(:boolean, "Enable exam mode", 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) diff --git a/lib/cadet_web/controllers/user_controller.ex b/lib/cadet_web/controllers/user_controller.ex index cfc162652..e6e440122 100644 --- a/lib/cadet_web/controllers/user_controller.ex +++ b/lib/cadet_web/controllers/user_controller.ex @@ -12,42 +12,68 @@ 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_course_id do - latest = CourseRegistrations.get_user_course(user.id, user.latest_viewed_course_id) - xp = Assessments.assessments_total_xp(latest) - max_xp = Assessments.user_max_xp(latest) - story = Assessments.user_current_story(latest) - - render( - conn, - "index.json", - user: user, - courses: courses, - latest: latest, - max_xp: max_xp, - story: story, - xp: xp - ) - else - render(conn, "index.json", - user: user, - courses: courses, - latest: nil, - max_xp: nil, - story: nil, - xp: nil - ) + exam_mode_course = CourseRegistrations.get_exam_mode_course(conn.assigns.current_user) + + cond do + exam_mode_course -> + xp = Assessments.assessments_total_xp(exam_mode_course) + max_xp = Assessments.user_max_xp(exam_mode_course) + story = Assessments.user_current_story(exam_mode_course) + + render( + conn, + "index.json", + user: user, + courses: [exam_mode_course], + latest: exam_mode_course, + max_xp: max_xp, + story: story, + xp: xp + ) + + user.latest_viewed_course_id -> + latest = CourseRegistrations.get_user_course(user.id, user.latest_viewed_course_id) + xp = Assessments.assessments_total_xp(latest) + max_xp = Assessments.user_max_xp(latest) + story = Assessments.user_current_story(latest) + + render( + conn, + "index.json", + user: user, + courses: courses, + latest: latest, + max_xp: max_xp, + story: story, + xp: xp + ) + + true -> + render(conn, "index.json", + user: user, + courses: courses, + latest: nil, + max_xp: nil, + story: nil, + xp: nil + ) end end def get_latest_viewed(conn, _) do user = conn.assigns.current_user + exam_mode_course = CourseRegistrations.get_exam_mode_course(conn.assigns.current_user) latest = - case user.latest_viewed_course_id do - nil -> nil - _ -> CourseRegistrations.get_user_course(user.id, user.latest_viewed_course_id) + cond do + exam_mode_course -> + CourseRegistrations.get_user_course(user.id, exam_mode_course.course_id) + + user.latest_viewed_course_id -> + CourseRegistrations.get_user_course(user.id, user.latest_viewed_course_id) + + true -> + nil end get_course_reg_config(conn, latest) @@ -98,6 +124,47 @@ defmodule CadetWeb.UserController do end end + def pause_user(conn, params) do + user = conn.assigns.current_user + + user + |> Cadet.Accounts.User.changeset(%{is_paused: true}) + |> Cadet.Repo.update() + |> case do + result = {:ok, _} -> + conn + |> put_status(:ok) + |> text("User is paused.") + + {:error, _} -> + conn + |> put_status(500) + |> text(:error) + end + end + + def log_user_focus_change(conn, %{"course_id" => course_id, "state" => state}) do + user = conn.assigns.current_user + + focus_state = + case state do + "0" -> 0 + "1" -> 1 + _ -> nil + end + + if focus_state do + case Cadet.FocusLogs.insert_log(user.id, course_id, state) do + {:ok, _} -> conn |> send_resp(:ok, "") + {:error, message} -> conn |> send_resp(500, message) + end + else + conn + |> put_status(403) + |> text("Invalid user focus state") + end + end + def update_research_agreement(conn, %{"agreedToResearch" => agreed_to_research}) do course_reg = conn.assigns[:course_reg] @@ -317,6 +384,8 @@ defmodule CadetWeb.UserController do enable_achievements(:boolean, "Enable achievements", required: true) enable_sourcecast(:boolean, "Enable sourcecast", required: true) enable_stories(:boolean, "Enable stories", required: true) + enable_exam_mode(:boolean, "Enable exam mode", required: true) + is_official_course(:boolean, "Course status (official institution course)") 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) @@ -332,6 +401,8 @@ defmodule CadetWeb.UserController do enable_achievements: true, enable_sourcecast: true, enable_stories: false, + enable_exam_mode: false, + is_official_course: true, source_chapter: 1, source_variant: "default", module_help_text: "Help text", diff --git a/lib/cadet_web/router.ex b/lib/cadet_web/router.ex index 1ee304c01..96adf1925 100644 --- a/lib/cadet_web/router.ex +++ b/lib/cadet_web/router.ex @@ -117,8 +117,11 @@ defmodule CadetWeb.Router do get("/user/total_xp", UserController, :combined_total_xp) put("/user/game_states", UserController, :update_game_states) put("/user/research_agreement", UserController, :update_research_agreement) + put("/user/pause", UserController, :pause_user) + post("/user/focus/:state", UserController, :log_user_focus_change) get("/config", CoursesController, :index) + post("/resume_code", CoursesController, :try_unpause_user) get("/team/:assessmentid", TeamController, :index) end diff --git a/lib/cadet_web/views/courses_view.ex b/lib/cadet_web/views/courses_view.ex index a6ae9c4fa..d528048b5 100644 --- a/lib/cadet_web/views/courses_view.ex +++ b/lib/cadet_web/views/courses_view.ex @@ -12,6 +12,31 @@ defmodule CadetWeb.CoursesView do enableAchievements: :enable_achievements, enableSourcecast: :enable_sourcecast, enableStories: :enable_stories, + enableExamMode: :enable_exam_mode, + isOfficialCourse: :is_official_course, + sourceChapter: :source_chapter, + sourceVariant: :source_variant, + moduleHelpText: :module_help_text, + assessmentTypes: :assessment_configs, + assetsPrefix: :assets_prefix + }) + } + end + + def render("config_admin.json", %{config: config}) do + %{ + config: + transform_map_for_view(config, %{ + courseName: :course_name, + courseShortName: :course_short_name, + viewable: :viewable, + enableGame: :enable_game, + enableAchievements: :enable_achievements, + enableSourcecast: :enable_sourcecast, + enableStories: :enable_stories, + enableExamMode: :enable_exam_mode, + isOfficialCourse: :is_official_course, + resumeCode: :resume_code, sourceChapter: :source_chapter, sourceVariant: :source_variant, moduleHelpText: :module_help_text, diff --git a/lib/cadet_web/views/user_view.ex b/lib/cadet_web/views/user_view.ex index c324d4bf0..15e07148f 100644 --- a/lib/cadet_web/views/user_view.ex +++ b/lib/cadet_web/views/user_view.ex @@ -15,6 +15,7 @@ defmodule CadetWeb.UserView do user: %{ userId: user.id, name: user.name, + isPaused: user.is_paused, courses: render_many(courses, CadetWeb.UserView, "courses.json", as: :cr) }, courseRegistration: @@ -105,6 +106,8 @@ defmodule CadetWeb.UserView do enableAchievements: :enable_achievements, enableSourcecast: :enable_sourcecast, enableStories: :enable_stories, + enableExamMode: :enable_exam_mode, + isOfficialCourse: :is_official_course, sourceChapter: :source_chapter, sourceVariant: :source_variant, moduleHelpText: :module_help_text, diff --git a/priv/repo/migrations/20250316232500_alter_users_table_add_is_paused.exs b/priv/repo/migrations/20250316232500_alter_users_table_add_is_paused.exs new file mode 100644 index 000000000..74b1ad2b5 --- /dev/null +++ b/priv/repo/migrations/20250316232500_alter_users_table_add_is_paused.exs @@ -0,0 +1,9 @@ +defmodule Cadet.Repo.Migrations.AlterUsersTableAddIsPaused do + use Ecto.Migration + + def change do + alter table(:users) do + add(:is_paused, :boolean, null: false, default: false) + end + end +end diff --git a/priv/repo/migrations/20250331171140_alter_courses_table_add_enable_exam_mode.exs b/priv/repo/migrations/20250331171140_alter_courses_table_add_enable_exam_mode.exs new file mode 100644 index 000000000..d8f66e6bb --- /dev/null +++ b/priv/repo/migrations/20250331171140_alter_courses_table_add_enable_exam_mode.exs @@ -0,0 +1,9 @@ +defmodule Cadet.Repo.Migrations.AlterCoursesTableAddEnableExamMode do + use Ecto.Migration + + def change do + alter table(:courses) do + add(:enable_exam_mode, :boolean, null: false, default: false) + end + end +end diff --git a/priv/repo/migrations/20250331232502_alter_courses_table_add_is_official_course.exs b/priv/repo/migrations/20250331232502_alter_courses_table_add_is_official_course.exs new file mode 100644 index 000000000..eb6a0f0db --- /dev/null +++ b/priv/repo/migrations/20250331232502_alter_courses_table_add_is_official_course.exs @@ -0,0 +1,9 @@ +defmodule Cadet.Repo.Migrations.AlterCoursesTableAddIsOfficialCourse do + use Ecto.Migration + + def change do + alter table(:courses) do + add(:is_official_course, :boolean, null: false, default: false) + end + end +end diff --git a/priv/repo/migrations/20250401214500_alter_courses_table_add_resume_code.exs b/priv/repo/migrations/20250401214500_alter_courses_table_add_resume_code.exs new file mode 100644 index 000000000..1ad90dceb --- /dev/null +++ b/priv/repo/migrations/20250401214500_alter_courses_table_add_resume_code.exs @@ -0,0 +1,9 @@ +defmodule Cadet.Repo.Migrations.AlterCoursesTableAddResumeCode do + use Ecto.Migration + + def change do + alter table(:courses) do + add(:resume_code, :string, null: false, default: "resume_code") + end + end +end diff --git a/priv/repo/migrations/20250404231500_create_table_user_browser_focus_log.exs b/priv/repo/migrations/20250404231500_create_table_user_browser_focus_log.exs new file mode 100644 index 000000000..237d0a102 --- /dev/null +++ b/priv/repo/migrations/20250404231500_create_table_user_browser_focus_log.exs @@ -0,0 +1,12 @@ +defmodule Cadet.Repo.Migrations.CreateUserBrowserFocusLogTable do + use Ecto.Migration + + def change do + create table(:user_browser_focus_log) do + add(:user_id, references(:users), null: false) + add(:course_id, references(:courses), null: false) + add(:time, :naive_datetime, null: false) + add(:focus_type, :integer, null: false) + end + end +end diff --git a/test/cadet/courses/course_test.exs b/test/cadet/courses/course_test.exs index 4e852596e..e23cd73ee 100644 --- a/test/cadet/courses/course_test.exs +++ b/test/cadet/courses/course_test.exs @@ -181,5 +181,13 @@ defmodule Cadet.Courses.CourseTest do test "invalid changeset with invalid chapter-variant combination" do assert_changeset(%{source_chapter: 4, source_variant: "lazy"}, :invalid) end + + test "invalid changeset with invalid exam mode and official course combination" do + assert_changeset(%{enable_exam_mode: true, is_official_course: false}, :invalid) + end + + test "invalid changeset with invalid course resume code" do + assert_changeset(%{resume_code: ""}, :invalid) + end end end diff --git a/test/cadet/courses/courses_test.exs b/test/cadet/courses/courses_test.exs index fc8880f79..e8090ffc2 100644 --- a/test/cadet/courses/courses_test.exs +++ b/test/cadet/courses/courses_test.exs @@ -21,6 +21,9 @@ defmodule Cadet.CoursesTest do enable_achievements: true, enable_sourcecast: true, enable_stories: false, + enable_exam_mode: false, + resume_code: "resume_code", + is_official_course: true, source_chapter: 1, source_variant: "default", module_help_text: "Help Text" @@ -57,6 +60,9 @@ defmodule Cadet.CoursesTest do assert course.enable_achievements == true assert course.enable_sourcecast == true assert course.enable_stories == false + assert course.enable_exam_mode == false + assert course.is_official_course == true + assert course.resume_code == "resume_code" assert course.source_chapter == 1 assert course.source_variant == "default" assert course.module_help_text == "Help Text" @@ -84,6 +90,9 @@ defmodule Cadet.CoursesTest do enable_achievements: false, enable_sourcecast: false, enable_stories: true, + enable_exam_mode: true, + is_official_course: true, + resume_code: "resume_code", module_help_text: "" }) @@ -94,6 +103,9 @@ defmodule Cadet.CoursesTest do assert updated_course.enable_achievements == false assert updated_course.enable_sourcecast == false assert updated_course.enable_stories == true + assert updated_course.enable_exam_mode == true + assert updated_course.is_official_course == true + assert updated_course.resume_code == "resume_code" assert updated_course.source_chapter == 1 assert updated_course.source_variant == "default" assert updated_course.module_help_text == nil @@ -112,6 +124,9 @@ defmodule Cadet.CoursesTest do enable_achievements: false, enable_sourcecast: false, enable_stories: true, + enable_exam_mode: false, + is_official_course: true, + resume_code: "resume_code", source_chapter: new_chapter, source_variant: "default", module_help_text: "help" @@ -124,6 +139,9 @@ defmodule Cadet.CoursesTest do assert updated_course.enable_achievements == false assert updated_course.enable_sourcecast == false assert updated_course.enable_stories == true + assert updated_course.enable_exam_mode == false + assert updated_course.is_official_course == true + assert updated_course.resume_code == "resume_code" assert updated_course.source_chapter == new_chapter assert updated_course.source_variant == "default" assert updated_course.module_help_text == "help" @@ -142,6 +160,9 @@ defmodule Cadet.CoursesTest do enable_achievements: false, enable_sourcecast: false, enable_stories: false, + enable_exam_mode: false, + is_official_course: true, + resume_code: "resume_code", module_help_text: "help" }) 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 6c9105524..72302a664 100644 --- a/test/cadet_web/admin_controllers/admin_courses_controller_test.exs +++ b/test/cadet_web/admin_controllers/admin_courses_controller_test.exs @@ -68,6 +68,7 @@ defmodule CadetWeb.AdminCoursesControllerTest do "enableGame" => false, "enableStories" => false, "enableAchievements" => false, + "resumeCode" => "spanning_tree", "enableSourcecast" => true, "moduleHelpText" => "help" } @@ -140,6 +141,30 @@ defmodule CadetWeb.AdminCoursesControllerTest do assert response(conn, 400) == "Invalid parameter(s)" end + @tag authenticate: :admin + test "rejects requests with invalid resume code", %{conn: conn} do + course_id = conn.assigns[:course_id] + + conn = + put(conn, build_url_course_config(course_id), %{ + "resumeCode" => "" + }) + + assert response(conn, 400) == "Invalid parameter(s)" + end + + @tag authenticate: :admin + test "rejects requests with invalid resume code 2", %{conn: conn} do + course_id = conn.assigns[:course_id] + + conn = + put(conn, build_url_course_config(course_id), %{ + "resumeCode" => " " + }) + + assert response(conn, 400) == "Invalid parameter(s)" + end + @tag authenticate: :admin test "rejects requests with missing params", %{conn: conn} do course_id = conn.assigns[:course_id] diff --git a/test/cadet_web/controllers/courses_controller_test.exs b/test/cadet_web/controllers/courses_controller_test.exs index 1a24caebf..d7f1931b7 100644 --- a/test/cadet_web/controllers/courses_controller_test.exs +++ b/test/cadet_web/controllers/courses_controller_test.exs @@ -28,6 +28,9 @@ defmodule CadetWeb.CoursesControllerTest do "enable_achievements" => "true", "enable_sourcecast" => "true", "enable_stories" => "true", + "enable_exam_mode" => "false", + "is_official_course" => "true", + "resume_code" => "resume_code", "source_chapter" => "1", "source_variant" => "default", "module_help_text" => "Help Text" @@ -73,6 +76,9 @@ defmodule CadetWeb.CoursesControllerTest do "enable_achievements" => "true", "enable_sourcecast" => "true", "enable_stories" => "true", + "enable_exam_mode" => "false", + "is_official_course" => "true", + "resume_code" => "resume_code", "source_chapter" => "1", "source_variant" => "default", "module_help_text" => "Help Text" @@ -96,6 +102,9 @@ defmodule CadetWeb.CoursesControllerTest do "enable_achievements" => "true", "enable_sourcecast" => "true", "enable_stories" => "true", + "enable_exam_mode" => "false", + "is_official_course" => "true", + "resume_code" => "resume_code", "source_chapter" => "1", "source_variant" => "default", "module_help_text" => "Help Text" @@ -120,6 +129,9 @@ defmodule CadetWeb.CoursesControllerTest do "enable_achievements" => "true", "enable_sourcecast" => "true", "enable_stories" => "true", + "enable_exam_mode" => "false", + "is_official_course" => "true", + "resume_code" => "resume_code", "source_chapter" => "1", "source_variant" => "default", "module_help_text" => "Help Text" diff --git a/test/cadet_web/controllers/user_controller_test.exs b/test/cadet_web/controllers/user_controller_test.exs index ceb1cc300..c5523d1e0 100644 --- a/test/cadet_web/controllers/user_controller_test.exs +++ b/test/cadet_web/controllers/user_controller_test.exs @@ -90,7 +90,8 @@ defmodule CadetWeb.UserControllerTest do "viewable" => true, "role" => "#{another_cr.role}" } - ] + ], + "isPaused" => false }, "courseRegistration" => %{ "courseRegId" => cr.id, @@ -114,6 +115,8 @@ defmodule CadetWeb.UserControllerTest do "sourceChapter" => 1, "sourceVariant" => "default", "viewable" => true, + "enableExamMode" => false, + "isOfficialCourse" => true, "assetsPrefix" => Courses.assets_prefix(course) }, "assessmentConfigurations" => [ @@ -172,7 +175,8 @@ defmodule CadetWeb.UserControllerTest do "user" => %{ "userId" => user.id, "name" => user.name, - "courses" => [] + "courses" => [], + "isPaused" => false }, "courseRegistration" => nil, "courseConfiguration" => nil, @@ -328,6 +332,8 @@ defmodule CadetWeb.UserControllerTest do "sourceChapter" => 1, "sourceVariant" => "default", "viewable" => true, + "enableExamMode" => false, + "isOfficialCourse" => true, "assetsPrefix" => Courses.assets_prefix(course) }, "assessmentConfigurations" => [] diff --git a/test/factories/accounts/user_factory.ex b/test/factories/accounts/user_factory.ex index 60df091be..7fcaafc8d 100644 --- a/test/factories/accounts/user_factory.ex +++ b/test/factories/accounts/user_factory.ex @@ -18,7 +18,8 @@ defmodule Cadet.Accounts.UserFactory do &"E#{&1 |> Integer.to_string() |> String.pad_leading(7, "0")}" ), latest_viewed_course: build(:course), - super_admin: false + super_admin: false, + is_paused: false } end @@ -31,7 +32,8 @@ defmodule Cadet.Accounts.UserFactory do :nusnet_id, &"E#{&1 |> Integer.to_string() |> String.pad_leading(7, "0")}" ), - latest_viewed_course: build(:course) + latest_viewed_course: build(:course), + is_paused: false } end end diff --git a/test/factories/courses/course_factory.ex b/test/factories/courses/course_factory.ex index 0ca2c3ec6..f2322f76a 100644 --- a/test/factories/courses/course_factory.ex +++ b/test/factories/courses/course_factory.ex @@ -16,6 +16,9 @@ defmodule Cadet.Courses.CourseFactory do enable_achievements: true, enable_sourcecast: true, enable_stories: false, + enable_exam_mode: false, + is_official_course: true, + resume_code: "resume_code", source_chapter: 1, source_variant: "default", module_help_text: "Help Text"