From fc72d2937f85cf76eff3ef08f1ad221d1c74a71a Mon Sep 17 00:00:00 2001 From: Izumi Kyouka Date: Fri, 14 Feb 2025 13:53:54 +0800 Subject: [PATCH 01/38] added enable_exam_mode field to all tests, models, and controllers --- lib/cadet/courses/course.ex | 4 +++- .../admin_controllers/admin_courses_controller.ex | 1 + lib/cadet_web/controllers/courses_controller.ex | 2 ++ lib/cadet_web/controllers/user_controller.ex | 2 ++ lib/cadet_web/views/courses_view.ex | 1 + lib/cadet_web/views/user_view.ex | 1 + .../repo/migrations/20210531155751_multitenant_upgrade.exs | 2 ++ test/cadet/courses/courses_test.exs | 7 +++++++ test/cadet_web/controllers/courses_controller_test.exs | 4 ++++ test/factories/courses/course_factory.ex | 1 + 10 files changed, 24 insertions(+), 1 deletion(-) diff --git a/lib/cadet/courses/course.ex b/lib/cadet/courses/course.ex index c74d23bd7..e60dcd6b2 100644 --- a/lib/cadet/courses/course.ex +++ b/lib/cadet/courses/course.ex @@ -14,6 +14,7 @@ defmodule Cadet.Courses.Course do enable_achievements: boolean(), enable_sourcecast: boolean(), enable_stories: boolean(), + enable_exam_mode: boolean(), source_chapter: integer(), source_variant: String.t(), module_help_text: String.t(), @@ -28,6 +29,7 @@ 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(:source_chapter, :integer) field(:source_variant, :string) field(:module_help_text, :string) @@ -41,7 +43,7 @@ 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 + enable_exam_mode enable_achievements enable_sourcecast enable_stories source_chapter source_variant)a @optional_fields ~w(course_short_name module_help_text)a def changeset(course, params) do diff --git a/lib/cadet_web/admin_controllers/admin_courses_controller.ex b/lib/cadet_web/admin_controllers/admin_courses_controller.ex index 7220a4d80..963721f92 100644 --- a/lib/cadet_web/admin_controllers/admin_courses_controller.ex +++ b/lib/cadet_web/admin_controllers/admin_courses_controller.ex @@ -108,6 +108,7 @@ 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") sublanguage(:body, Schema.ref(:AdminSublanguage), "sublanguage object") module_help_text(:body, :string, "Module help text") end diff --git a/lib/cadet_web/controllers/courses_controller.ex b/lib/cadet_web/controllers/courses_controller.ex index e6555bd7a..7962d4f40 100644 --- a/lib/cadet_web/controllers/courses_controller.ex +++ b/lib/cadet_web/controllers/courses_controller.ex @@ -56,6 +56,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 +98,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..ad383d1c1 100644 --- a/lib/cadet_web/controllers/user_controller.ex +++ b/lib/cadet_web/controllers/user_controller.ex @@ -317,6 +317,7 @@ 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) 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 +333,7 @@ defmodule CadetWeb.UserController do enable_achievements: true, enable_sourcecast: true, enable_stories: false, + enable_exam_mode: false, source_chapter: 1, source_variant: "default", module_help_text: "Help text", diff --git a/lib/cadet_web/views/courses_view.ex b/lib/cadet_web/views/courses_view.ex index a6ae9c4fa..2c6d49f76 100644 --- a/lib/cadet_web/views/courses_view.ex +++ b/lib/cadet_web/views/courses_view.ex @@ -12,6 +12,7 @@ defmodule CadetWeb.CoursesView do enableAchievements: :enable_achievements, enableSourcecast: :enable_sourcecast, enableStories: :enable_stories, + enableExamMode: :enable_exam_mode, 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 b547e2440..71ef2bcc0 100644 --- a/lib/cadet_web/views/user_view.ex +++ b/lib/cadet_web/views/user_view.ex @@ -105,6 +105,7 @@ defmodule CadetWeb.UserView do enableAchievements: :enable_achievements, enableSourcecast: :enable_sourcecast, enableStories: :enable_stories, + enableExamMode: :enable_exam_mode, sourceChapter: :source_chapter, sourceVariant: :source_variant, moduleHelpText: :module_help_text, diff --git a/priv/repo/migrations/20210531155751_multitenant_upgrade.exs b/priv/repo/migrations/20210531155751_multitenant_upgrade.exs index 9181bf4c2..2fbd9aedb 100644 --- a/priv/repo/migrations/20210531155751_multitenant_upgrade.exs +++ b/priv/repo/migrations/20210531155751_multitenant_upgrade.exs @@ -11,6 +11,7 @@ defmodule Cadet.Repo.Migrations.MultitenantUpgrade do 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(:enable_exam_mode, :boolean, null: false, default: false) add(:source_chapter, :integer, null: false) add(:source_variant, :string, null: false) add(:module_help_text, :string) @@ -144,6 +145,7 @@ defmodule Cadet.Repo.Migrations.MultitenantUpgrade do enable_game: true, enable_achievements: true, enable_sourcecast: true, + enable_exam_mode: false, source_chapter: 1, source_variant: "default", inserted_at: Timex.now(), diff --git a/test/cadet/courses/courses_test.exs b/test/cadet/courses/courses_test.exs index fc8880f79..8f12a8bd8 100644 --- a/test/cadet/courses/courses_test.exs +++ b/test/cadet/courses/courses_test.exs @@ -21,6 +21,7 @@ defmodule Cadet.CoursesTest do enable_achievements: true, enable_sourcecast: true, enable_stories: false, + enable_exam_mode: false, source_chapter: 1, source_variant: "default", module_help_text: "Help Text" @@ -57,6 +58,7 @@ 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.source_chapter == 1 assert course.source_variant == "default" assert course.module_help_text == "Help Text" @@ -84,6 +86,7 @@ defmodule Cadet.CoursesTest do enable_achievements: false, enable_sourcecast: false, enable_stories: true, + enable_exam_mode: true, module_help_text: "" }) @@ -94,6 +97,7 @@ 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.source_chapter == 1 assert updated_course.source_variant == "default" assert updated_course.module_help_text == nil @@ -112,6 +116,7 @@ defmodule Cadet.CoursesTest do enable_achievements: false, enable_sourcecast: false, enable_stories: true, + enable_exam_mode: false, source_chapter: new_chapter, source_variant: "default", module_help_text: "help" @@ -124,6 +129,7 @@ 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.source_chapter == new_chapter assert updated_course.source_variant == "default" assert updated_course.module_help_text == "help" @@ -142,6 +148,7 @@ defmodule Cadet.CoursesTest do enable_achievements: false, enable_sourcecast: false, enable_stories: false, + enable_exam_mode: false, module_help_text: "help" }) diff --git a/test/cadet_web/controllers/courses_controller_test.exs b/test/cadet_web/controllers/courses_controller_test.exs index 1a24caebf..d0ea612f2 100644 --- a/test/cadet_web/controllers/courses_controller_test.exs +++ b/test/cadet_web/controllers/courses_controller_test.exs @@ -28,6 +28,7 @@ defmodule CadetWeb.CoursesControllerTest do "enable_achievements" => "true", "enable_sourcecast" => "true", "enable_stories" => "true", + "enable_exam_mode" => "false", "source_chapter" => "1", "source_variant" => "default", "module_help_text" => "Help Text" @@ -73,6 +74,7 @@ defmodule CadetWeb.CoursesControllerTest do "enable_achievements" => "true", "enable_sourcecast" => "true", "enable_stories" => "true", + "enable_exam_mode" => "false", "source_chapter" => "1", "source_variant" => "default", "module_help_text" => "Help Text" @@ -96,6 +98,7 @@ defmodule CadetWeb.CoursesControllerTest do "enable_achievements" => "true", "enable_sourcecast" => "true", "enable_stories" => "true", + "enable_exam_mode" => "false", "source_chapter" => "1", "source_variant" => "default", "module_help_text" => "Help Text" @@ -120,6 +123,7 @@ defmodule CadetWeb.CoursesControllerTest do "enable_achievements" => "true", "enable_sourcecast" => "true", "enable_stories" => "true", + "enable_exam_mode" => "false", "source_chapter" => "1", "source_variant" => "default", "module_help_text" => "Help Text" diff --git a/test/factories/courses/course_factory.ex b/test/factories/courses/course_factory.ex index 0ca2c3ec6..5f52856bc 100644 --- a/test/factories/courses/course_factory.ex +++ b/test/factories/courses/course_factory.ex @@ -16,6 +16,7 @@ defmodule Cadet.Courses.CourseFactory do enable_achievements: true, enable_sourcecast: true, enable_stories: false, + enable_exam_mode: false, source_chapter: 1, source_variant: "default", module_help_text: "Help Text" From 2fc38ca9f5778d66633b35620a2eeba352565450 Mon Sep 17 00:00:00 2001 From: Izumi Kyouka Date: Sat, 15 Feb 2025 21:49:57 +0800 Subject: [PATCH 02/38] force student whose courses has exam mode to only access the particular course under exam mode --- lib/cadet/accounts/course_registrations.ex | 16 ++++ lib/cadet_web/controllers/user_controller.ex | 77 +++++++++++++------- 2 files changed, 67 insertions(+), 26 deletions(-) diff --git a/lib/cadet/accounts/course_registrations.ex b/lib/cadet/accounts/course_registrations.ex index c45549199..ae9271140 100644 --- a/lib/cadet/accounts/course_registrations.ex +++ b/lib/cadet/accounts/course_registrations.ex @@ -2,6 +2,8 @@ defmodule Cadet.Accounts.CourseRegistrations do @moduledoc """ Provides functions fetch, add, update course_registration """ +alias Cadet.Accounts.CourseRegistrations +alias Cadet.Courses use Cadet, [:context, :display] import Ecto.Query @@ -39,6 +41,7 @@ defmodule Cadet.Accounts.CourseRegistrations do |> Repo.one() end + @spec get_courses(Cadet.Accounts.User.t()) :: any() def get_courses(%User{id: id}) do CourseRegistration |> where([cr], cr.user_id == ^id) @@ -47,6 +50,19 @@ defmodule Cadet.Accounts.CourseRegistrations do |> Repo.all() end + @spec get_exam_mode_course(Cadet.Accounts.User.t()) :: any() + def get_exam_mode_course(%User{id: id}) do + CourseRegistration + |> where([cr], cr.user_id == ^id) + |> join(:inner, [cr], c in assoc(cr, :course), on: c.enable_exam_mode == 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_web/controllers/user_controller.ex b/lib/cadet_web/controllers/user_controller.ex index ad383d1c1..6d8bb209a 100644 --- a/lib/cadet_web/controllers/user_controller.ex +++ b/lib/cadet_web/controllers/user_controller.ex @@ -5,6 +5,7 @@ defmodule CadetWeb.UserController do use CadetWeb, :controller use PhoenixSwagger + alias Cadet.Courses.Course alias Cadet.Accounts.CourseRegistrations alias Cadet.{Accounts, Assessments} @@ -12,39 +13,63 @@ 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 -> + IO.puts("Course #{exam_mode_course.course_id} is under exam mode.") + 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: courses |> Enum.filter(fn c -> c.course_id == exam_mode_course.course_id end), + 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 exam_mode_course do + _ -> CourseRegistrations.get_user_course(user.id, exam_mode_course.course_id) + end case user.latest_viewed_course_id do nil -> nil _ -> CourseRegistrations.get_user_course(user.id, user.latest_viewed_course_id) From 9953b9c1446ade9f30306184e29d715e5f3ce097 Mon Sep 17 00:00:00 2001 From: Izumi Kyouka Date: Sat, 15 Feb 2025 22:08:56 +0800 Subject: [PATCH 03/38] fixed failure to switch course even when no exam mode is enabled for any of a user's courses --- lib/cadet_web/controllers/user_controller.ex | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/lib/cadet_web/controllers/user_controller.ex b/lib/cadet_web/controllers/user_controller.ex index 6d8bb209a..eeb7b4a3d 100644 --- a/lib/cadet_web/controllers/user_controller.ex +++ b/lib/cadet_web/controllers/user_controller.ex @@ -65,17 +65,19 @@ defmodule CadetWeb.UserController do 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 exam_mode_course do - _ -> CourseRegistrations.get_user_course(user.id, exam_mode_course.course_id) - end - case user.latest_viewed_course_id do - nil -> nil - _ -> CourseRegistrations.get_user_course(user.id, user.latest_viewed_course_id) + if exam_mode_course do + latest = CourseRegistrations.get_user_course(user.id, exam_mode_course.course_id) + get_course_reg_config(conn, latest) + else + latest = + case user.latest_viewed_course_id do + nil -> nil + _ -> CourseRegistrations.get_user_course(user.id, user.latest_viewed_course_id) end - get_course_reg_config(conn, latest) + get_course_reg_config(conn, latest) + end + end defp get_course_reg_config(conn, course_reg) when is_nil(course_reg) do From f450e8553ffc36183ce1114e9076b3daca4eb754 Mon Sep 17 00:00:00 2001 From: Izumi Kyouka Date: Mon, 3 Mar 2025 23:35:47 +0800 Subject: [PATCH 04/38] fixed unintended changes to previous migration; added migration file for addition of enable_exam_mode and is_official_course to courses table --- .../migrations/20210531155751_multitenant_upgrade.exs | 1 - ...03232500_alter_courses_table_add_enable_exam_mode.exs | 9 +++++++++ ...232500_alter_courses_table_add_is_official_course.exs | 9 +++++++++ 3 files changed, 18 insertions(+), 1 deletion(-) create mode 100644 priv/repo/migrations/20250303232500_alter_courses_table_add_enable_exam_mode.exs create mode 100644 priv/repo/migrations/20250303232500_alter_courses_table_add_is_official_course.exs diff --git a/priv/repo/migrations/20210531155751_multitenant_upgrade.exs b/priv/repo/migrations/20210531155751_multitenant_upgrade.exs index 2fbd9aedb..301e8f227 100644 --- a/priv/repo/migrations/20210531155751_multitenant_upgrade.exs +++ b/priv/repo/migrations/20210531155751_multitenant_upgrade.exs @@ -11,7 +11,6 @@ defmodule Cadet.Repo.Migrations.MultitenantUpgrade do 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(:enable_exam_mode, :boolean, null: false, default: false) add(:source_chapter, :integer, null: false) add(:source_variant, :string, null: false) add(:module_help_text, :string) diff --git a/priv/repo/migrations/20250303232500_alter_courses_table_add_enable_exam_mode.exs b/priv/repo/migrations/20250303232500_alter_courses_table_add_enable_exam_mode.exs new file mode 100644 index 000000000..e86124843 --- /dev/null +++ b/priv/repo/migrations/20250303232500_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/20250303232500_alter_courses_table_add_is_official_course.exs b/priv/repo/migrations/20250303232500_alter_courses_table_add_is_official_course.exs new file mode 100644 index 000000000..c3e92849d --- /dev/null +++ b/priv/repo/migrations/20250303232500_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 From 0bb9857ba6f188014554ecbfd23a162a1c71e633 Mon Sep 17 00:00:00 2001 From: Izumi Kyouka Date: Tue, 4 Mar 2025 00:12:26 +0800 Subject: [PATCH 05/38] added is_official_course field to models; added logic to prevent students circumventing force redirection to official course under exam mode --- lib/cadet/accounts/course_registrations.ex | 2 +- lib/cadet/courses/course.ex | 2 ++ lib/cadet_web/controllers/user_controller.ex | 2 ++ lib/cadet_web/views/courses_view.ex | 1 + lib/cadet_web/views/user_view.ex | 1 + .../repo/migrations/20210531155751_multitenant_upgrade.exs | 1 - ...303232000_alter_courses_table_add_enable_exam_mode.exs} | 0 test/cadet/courses/courses_test.exs | 7 +++++++ test/cadet_web/controllers/courses_controller_test.exs | 4 ++++ test/factories/courses/course_factory.ex | 1 + 10 files changed, 19 insertions(+), 2 deletions(-) rename priv/repo/migrations/{20250303232500_alter_courses_table_add_enable_exam_mode.exs => 20250303232000_alter_courses_table_add_enable_exam_mode.exs} (100%) diff --git a/lib/cadet/accounts/course_registrations.ex b/lib/cadet/accounts/course_registrations.ex index ae9271140..4c4a0505d 100644 --- a/lib/cadet/accounts/course_registrations.ex +++ b/lib/cadet/accounts/course_registrations.ex @@ -54,7 +54,7 @@ alias Cadet.Courses def get_exam_mode_course(%User{id: id}) do CourseRegistration |> where([cr], cr.user_id == ^id) - |> join(:inner, [cr], c in assoc(cr, :course), on: c.enable_exam_mode == true) + |> 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])} diff --git a/lib/cadet/courses/course.ex b/lib/cadet/courses/course.ex index e60dcd6b2..e115199a2 100644 --- a/lib/cadet/courses/course.ex +++ b/lib/cadet/courses/course.ex @@ -15,6 +15,7 @@ defmodule Cadet.Courses.Course do enable_sourcecast: boolean(), enable_stories: boolean(), enable_exam_mode: boolean(), + is_official_course: boolean(), source_chapter: integer(), source_variant: String.t(), module_help_text: String.t(), @@ -30,6 +31,7 @@ defmodule Cadet.Courses.Course do field(:enable_sourcecast, :boolean, default: true) field(:enable_stories, :boolean, default: false) field(:enable_exam_mode, :boolean, default: false) + field(:is_official_course, :boolean, default: false) field(:source_chapter, :integer) field(:source_variant, :string) field(:module_help_text, :string) diff --git a/lib/cadet_web/controllers/user_controller.ex b/lib/cadet_web/controllers/user_controller.ex index eeb7b4a3d..73a49d8a2 100644 --- a/lib/cadet_web/controllers/user_controller.ex +++ b/lib/cadet_web/controllers/user_controller.ex @@ -345,6 +345,7 @@ defmodule CadetWeb.UserController do 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) @@ -361,6 +362,7 @@ defmodule CadetWeb.UserController do 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/views/courses_view.ex b/lib/cadet_web/views/courses_view.ex index 2c6d49f76..75f075a08 100644 --- a/lib/cadet_web/views/courses_view.ex +++ b/lib/cadet_web/views/courses_view.ex @@ -13,6 +13,7 @@ defmodule CadetWeb.CoursesView do 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/lib/cadet_web/views/user_view.ex b/lib/cadet_web/views/user_view.ex index 71ef2bcc0..5977da7ed 100644 --- a/lib/cadet_web/views/user_view.ex +++ b/lib/cadet_web/views/user_view.ex @@ -106,6 +106,7 @@ defmodule CadetWeb.UserView do 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/20210531155751_multitenant_upgrade.exs b/priv/repo/migrations/20210531155751_multitenant_upgrade.exs index 301e8f227..9181bf4c2 100644 --- a/priv/repo/migrations/20210531155751_multitenant_upgrade.exs +++ b/priv/repo/migrations/20210531155751_multitenant_upgrade.exs @@ -144,7 +144,6 @@ defmodule Cadet.Repo.Migrations.MultitenantUpgrade do enable_game: true, enable_achievements: true, enable_sourcecast: true, - enable_exam_mode: false, source_chapter: 1, source_variant: "default", inserted_at: Timex.now(), diff --git a/priv/repo/migrations/20250303232500_alter_courses_table_add_enable_exam_mode.exs b/priv/repo/migrations/20250303232000_alter_courses_table_add_enable_exam_mode.exs similarity index 100% rename from priv/repo/migrations/20250303232500_alter_courses_table_add_enable_exam_mode.exs rename to priv/repo/migrations/20250303232000_alter_courses_table_add_enable_exam_mode.exs diff --git a/test/cadet/courses/courses_test.exs b/test/cadet/courses/courses_test.exs index 8f12a8bd8..50bf1b8c4 100644 --- a/test/cadet/courses/courses_test.exs +++ b/test/cadet/courses/courses_test.exs @@ -22,6 +22,7 @@ defmodule Cadet.CoursesTest do 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" @@ -59,6 +60,7 @@ defmodule Cadet.CoursesTest do assert course.enable_sourcecast == true assert course.enable_stories == false assert course.enable_exam_mode == false + assert course.is_official_course == true assert course.source_chapter == 1 assert course.source_variant == "default" assert course.module_help_text == "Help Text" @@ -87,6 +89,7 @@ defmodule Cadet.CoursesTest do enable_sourcecast: false, enable_stories: true, enable_exam_mode: true, + is_official_course: true, module_help_text: "" }) @@ -98,6 +101,7 @@ defmodule Cadet.CoursesTest do 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.source_chapter == 1 assert updated_course.source_variant == "default" assert updated_course.module_help_text == nil @@ -117,6 +121,7 @@ defmodule Cadet.CoursesTest do enable_sourcecast: false, enable_stories: true, enable_exam_mode: false, + is_official_course: true, source_chapter: new_chapter, source_variant: "default", module_help_text: "help" @@ -130,6 +135,7 @@ defmodule Cadet.CoursesTest do 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.source_chapter == new_chapter assert updated_course.source_variant == "default" assert updated_course.module_help_text == "help" @@ -149,6 +155,7 @@ defmodule Cadet.CoursesTest do enable_sourcecast: false, enable_stories: false, enable_exam_mode: false, + is_official_course: true, module_help_text: "help" }) diff --git a/test/cadet_web/controllers/courses_controller_test.exs b/test/cadet_web/controllers/courses_controller_test.exs index d0ea612f2..5687ff5df 100644 --- a/test/cadet_web/controllers/courses_controller_test.exs +++ b/test/cadet_web/controllers/courses_controller_test.exs @@ -29,6 +29,7 @@ defmodule CadetWeb.CoursesControllerTest do "enable_sourcecast" => "true", "enable_stories" => "true", "enable_exam_mode" => "false", + "is_official_course" => "true", "source_chapter" => "1", "source_variant" => "default", "module_help_text" => "Help Text" @@ -75,6 +76,7 @@ defmodule CadetWeb.CoursesControllerTest do "enable_sourcecast" => "true", "enable_stories" => "true", "enable_exam_mode" => "false", + "is_official_course" => "true", "source_chapter" => "1", "source_variant" => "default", "module_help_text" => "Help Text" @@ -99,6 +101,7 @@ defmodule CadetWeb.CoursesControllerTest do "enable_sourcecast" => "true", "enable_stories" => "true", "enable_exam_mode" => "false", + "is_official_course" => "true", "source_chapter" => "1", "source_variant" => "default", "module_help_text" => "Help Text" @@ -124,6 +127,7 @@ defmodule CadetWeb.CoursesControllerTest do "enable_sourcecast" => "true", "enable_stories" => "true", "enable_exam_mode" => "false", + "is_official_course" => "true", "source_chapter" => "1", "source_variant" => "default", "module_help_text" => "Help Text" diff --git a/test/factories/courses/course_factory.ex b/test/factories/courses/course_factory.ex index 5f52856bc..1ef2b443f 100644 --- a/test/factories/courses/course_factory.ex +++ b/test/factories/courses/course_factory.ex @@ -17,6 +17,7 @@ defmodule Cadet.Courses.CourseFactory do 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" From d7ad8b31a3523ee2975f0edf2f7c322db7d458b5 Mon Sep 17 00:00:00 2001 From: Izumi Kyouka Date: Tue, 4 Mar 2025 23:07:26 +0800 Subject: [PATCH 06/38] removed unnecessary changes in course_registrations.ex --- lib/cadet/accounts/course_registrations.ex | 4 ---- 1 file changed, 4 deletions(-) diff --git a/lib/cadet/accounts/course_registrations.ex b/lib/cadet/accounts/course_registrations.ex index 4c4a0505d..070e0c466 100644 --- a/lib/cadet/accounts/course_registrations.ex +++ b/lib/cadet/accounts/course_registrations.ex @@ -2,8 +2,6 @@ defmodule Cadet.Accounts.CourseRegistrations do @moduledoc """ Provides functions fetch, add, update course_registration """ -alias Cadet.Accounts.CourseRegistrations -alias Cadet.Courses use Cadet, [:context, :display] import Ecto.Query @@ -41,7 +39,6 @@ alias Cadet.Courses |> Repo.one() end - @spec get_courses(Cadet.Accounts.User.t()) :: any() def get_courses(%User{id: id}) do CourseRegistration |> where([cr], cr.user_id == ^id) @@ -50,7 +47,6 @@ alias Cadet.Courses |> Repo.all() end - @spec get_exam_mode_course(Cadet.Accounts.User.t()) :: any() def get_exam_mode_course(%User{id: id}) do CourseRegistration |> where([cr], cr.user_id == ^id) From f216d1b2b8cc5aef2f7314c05b60a23bc3045dc1 Mon Sep 17 00:00:00 2001 From: Izumi Kyouka Date: Tue, 4 Mar 2025 23:09:59 +0800 Subject: [PATCH 07/38] removed unnecessary changes from user_controller.ex --- lib/cadet_web/controllers/user_controller.ex | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/cadet_web/controllers/user_controller.ex b/lib/cadet_web/controllers/user_controller.ex index 73a49d8a2..15b41b5bb 100644 --- a/lib/cadet_web/controllers/user_controller.ex +++ b/lib/cadet_web/controllers/user_controller.ex @@ -5,7 +5,6 @@ defmodule CadetWeb.UserController do use CadetWeb, :controller use PhoenixSwagger - alias Cadet.Courses.Course alias Cadet.Accounts.CourseRegistrations alias Cadet.{Accounts, Assessments} From bc4c48c298f4762764b3246feae6d296fcf80938 Mon Sep 17 00:00:00 2001 From: Izumi Kyouka Date: Wed, 12 Mar 2025 03:14:32 +0800 Subject: [PATCH 08/38] added resume code functionality --- lib/cadet/accounts/course_registrations.ex | 3 +- lib/cadet/courses/course.ex | 4 +- lib/cadet/courses/courses.ex | 2 +- .../admin_courses_controller.ex | 1 + .../admin_user_controller.ex | 3 +- .../controllers/courses_controller.ex | 6 +- lib/cadet_web/views/courses_view.ex | 25 +++ lib/cadet_web/views/user_view.ex | 100 +++++++-- lib/tree | 199 ++++++++++++++++++ test/cadet/courses/courses_test.exs | 7 + .../controllers/courses_controller_test.exs | 4 + test/factories/courses/course_factory.ex | 1 + 12 files changed, 331 insertions(+), 24 deletions(-) create mode 100644 lib/tree diff --git a/lib/cadet/accounts/course_registrations.ex b/lib/cadet/accounts/course_registrations.ex index 070e0c466..181af5948 100644 --- a/lib/cadet/accounts/course_registrations.ex +++ b/lib/cadet/accounts/course_registrations.ex @@ -53,8 +53,7 @@ defmodule Cadet.Accounts.CourseRegistrations do |> 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])} - ) + course: {c, assessment_config: ^from(ac in AssessmentConfig, order_by: [asc: ac.order])}) |> preload(:group) |> Repo.one() end diff --git a/lib/cadet/courses/course.ex b/lib/cadet/courses/course.ex index e115199a2..4682e4014 100644 --- a/lib/cadet/courses/course.ex +++ b/lib/cadet/courses/course.ex @@ -15,6 +15,7 @@ defmodule Cadet.Courses.Course do enable_sourcecast: boolean(), enable_stories: boolean(), enable_exam_mode: boolean(), + resume_code: string(), is_official_course: boolean(), source_chapter: integer(), source_variant: String.t(), @@ -31,6 +32,7 @@ defmodule Cadet.Courses.Course do 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) @@ -46,7 +48,7 @@ defmodule Cadet.Courses.Course do @required_fields ~w(course_name viewable enable_game enable_exam_mode enable_achievements enable_sourcecast enable_stories source_chapter source_variant)a - @optional_fields ~w(course_short_name module_help_text)a + @optional_fields ~w(course_short_name module_help_text resume_code)a def changeset(course, params) do course diff --git a/lib/cadet/courses/courses.ex b/lib/cadet/courses/courses.ex index 5c0464fae..d27268644 100644 --- a/lib/cadet/courses/courses.ex +++ b/lib/cadet/courses/courses.ex @@ -44,7 +44,7 @@ defmodule Cadet.Courses do """ @spec get_course_config(integer) :: {:ok, Course.t()} | {:error, {:bad_request, String.t()}} - def get_course_config(course_id) when is_ecto_id(course_id) do + def get_course_config(course_id, is_admin \\ false) when is_ecto_id(course_id) do case retrieve_course(course_id) do nil -> {:error, {:bad_request, "Invalid 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 963721f92..d2fd4047f 100644 --- a/lib/cadet_web/admin_controllers/admin_courses_controller.ex +++ b/lib/cadet_web/admin_controllers/admin_courses_controller.ex @@ -109,6 +109,7 @@ defmodule CadetWeb.AdminCoursesController do 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 when attempt to open DevTool is detected") sublanguage(:body, Schema.ref(:AdminSublanguage), "sublanguage object") module_help_text(:body, :string, "Module help text") end diff --git a/lib/cadet_web/admin_controllers/admin_user_controller.ex b/lib/cadet_web/admin_controllers/admin_user_controller.ex index 53add9133..f9a4965a2 100644 --- a/lib/cadet_web/admin_controllers/admin_user_controller.ex +++ b/lib/cadet_web/admin_controllers/admin_user_controller.ex @@ -3,7 +3,6 @@ defmodule CadetWeb.AdminUserController do use PhoenixSwagger import Ecto.Query - alias Cadet.Repo alias Cadet.{Accounts, Assessments, Courses} alias Cadet.Accounts.{CourseRegistrations, CourseRegistration, Role} @@ -14,7 +13,7 @@ defmodule CadetWeb.AdminUserController do users = filter |> try_keywordise_string_keys() |> Accounts.get_users_by(conn.assigns.course_reg) - render(conn, "users.json", users: users) + render(conn, "users.json", users: users, is_admin: false) end def combined_total_xp(conn, %{"course_reg_id" => course_reg_id}) do diff --git a/lib/cadet_web/controllers/courses_controller.ex b/lib/cadet_web/controllers/courses_controller.ex index 7962d4f40..a72e10c9c 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 || 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 diff --git a/lib/cadet_web/views/courses_view.ex b/lib/cadet_web/views/courses_view.ex index 75f075a08..8388ba9fc 100644 --- a/lib/cadet_web/views/courses_view.ex +++ b/lib/cadet_web/views/courses_view.ex @@ -2,6 +2,7 @@ defmodule CadetWeb.CoursesView do use CadetWeb, :view def render("config.json", %{config: config}) do + IO.puts("resume code not sent") %{ config: transform_map_for_view(config, %{ @@ -22,4 +23,28 @@ defmodule CadetWeb.CoursesView do }) } end + + def render("config_admin.json", %{config: config}) do + IO.puts("resume code sent") + %{ + 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, + assessmentTypes: :assessment_configs, + assetsPrefix: :assets_prefix + }) + } + end end diff --git a/lib/cadet_web/views/user_view.ex b/lib/cadet_web/views/user_view.ex index 5977da7ed..7181c1091 100644 --- a/lib/cadet_web/views/user_view.ex +++ b/lib/cadet_web/views/user_view.ex @@ -29,12 +29,38 @@ defmodule CadetWeb.UserView do } end + def render("index.json", %{ + user: user, + courses: courses, + latest: latest, + max_xp: max_xp, + xp: xp, + story: story + }, is_admin) do + %{ + user: %{ + userId: user.id, + name: user.name, + courses: render_many(courses, CadetWeb.UserView, "courses.json", as: :cr) + }, + courseRegistration: + render_latest(%{ + latest: latest, + max_xp: max_xp, + xp: xp, + story: story + }), + courseConfiguration: render_config(latest, is_admin), + assessmentConfigurations: render_assessment_configs(latest) + } + end + def render("course.json", %{ latest: latest, max_xp: max_xp, xp: xp, story: story - }) do + }) do %{ courseRegistration: render_latest(%{ @@ -48,6 +74,25 @@ defmodule CadetWeb.UserView do } end + def render("course.json", %{ + latest: latest, + max_xp: max_xp, + xp: xp, + story: story + }, is_admin) do + %{ + courseRegistration: + render_latest(%{ + latest: latest, + max_xp: max_xp, + xp: xp, + story: story + }), + courseConfiguration: render_config(latest, is_admin), + assessmentConfigurations: render_assessment_configs(latest) + } + end + def render("courses.json", %{cr: cr}) do %{ courseId: cr.course_id, @@ -95,23 +140,44 @@ defmodule CadetWeb.UserView do case latest do nil -> nil + _ -> transform_map_for_view(latest.course, %{ + 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, + sourceChapter: :source_chapter, + sourceVariant: :source_variant, + moduleHelpText: :module_help_text, + assetsPrefix: &Courses.assets_prefix/1 + }) + end + end - _ -> - transform_map_for_view(latest.course, %{ - 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, - sourceChapter: :source_chapter, - sourceVariant: :source_variant, - moduleHelpText: :module_help_text, - assetsPrefix: &Courses.assets_prefix/1 - }) + defp render_config(latest, is_admin) do + case latest do + nil -> + nil + _ -> transform_map_for_view(latest.course, %{ + 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, + resume_code: :resume_code, + isOfficialCourse: :is_official_course, + sourceChapter: :source_chapter, + sourceVariant: :source_variant, + moduleHelpText: :module_help_text, + assetsPrefix: &Courses.assets_prefix/1 + }) end end diff --git a/lib/tree b/lib/tree new file mode 100644 index 000000000..d902ac770 --- /dev/null +++ b/lib/tree @@ -0,0 +1,199 @@ +. +├── cadet +│   ├── accounts +│   │   ├── accounts.ex +│   │   ├── course_registration.ex +│   │   ├── course_registrations.ex +│   │   ├── notification.ex +│   │   ├── notification_type.ex +│   │   ├── notifications.ex +│   │   ├── query.ex +│   │   ├── role.ex +│   │   ├── team.ex +│   │   ├── team_member.ex +│   │   ├── teams.ex +│   │   └── user.ex +│   ├── application.ex +│   ├── assessments +│   │   ├── answer.ex +│   │   ├── answer_types +│   │   │   ├── mcq_answer.ex +│   │   │   ├── programming_answer.ex +│   │   │   └── voting_answer.ex +│   │   ├── assessment.ex +│   │   ├── assessment_access.ex +│   │   ├── assessments.ex +│   │   ├── autograding_status.ex +│   │   ├── external_library.ex +│   │   ├── library.ex +│   │   ├── query.ex +│   │   ├── question.ex +│   │   ├── question_type.ex +│   │   ├── question_types +│   │   │   ├── mcq_choice.ex +│   │   │   ├── mcq_question.ex +│   │   │   ├── programming_question.ex +│   │   │   ├── programming_question_testcases.ex +│   │   │   └── voting_question.ex +│   │   ├── submission.ex +│   │   ├── submission_status.ex +│   │   ├── submission_votes.ex +│   │   └── upload.ex +│   ├── assets +│   │   └── assets.ex +│   ├── auth +│   │   ├── empty_guardian.ex +│   │   ├── error_handler.ex +│   │   ├── guardian.ex +│   │   ├── pipeline.ex +│   │   ├── provider.ex +│   │   └── providers +│   │   ├── adfs.ex +│   │   ├── config.ex +│   │   ├── github.ex +│   │   ├── openid +│   │   │   ├── auth0_claim_extractor.ex +│   │   │   ├── cognito_claim_extractor.ex +│   │   │   ├── google_claim_extractor.ex +│   │   │   ├── mit_claim_extractor.ex +│   │   │   ├── mit_csail_claim_extractor.ex +│   │   │   └── openid.ex +│   │   └── saml +│   │   ├── nusstf_assertion_extractor.ex +│   │   ├── nusstu_assertion_extractor.ex +│   │   └── saml.ex +│   ├── chatbot +│   │   ├── conversation.ex +│   │   ├── llm_conversations.ex +│   │   ├── prompt_builder.ex +│   │   └── sicp_notes.ex +│   ├── courses +│   │   ├── assessment_config.ex +│   │   ├── course.ex +│   │   ├── courses.ex +│   │   ├── group.ex +│   │   ├── sourcecast.ex +│   │   └── sourcecast_upload.ex +│   ├── devices +│   │   ├── device.ex +│   │   ├── device_registration.ex +│   │   └── devices.ex +│   ├── email.ex +│   ├── env.ex +│   ├── helpers +│   │   ├── aws_helper.ex +│   │   ├── context_helper.ex +│   │   ├── display_helper.ex +│   │   ├── model_helper.ex +│   │   └── shared_helper.ex +│   ├── incentives +│   │   ├── achievement.ex +│   │   ├── achievement_prerequisite.ex +│   │   ├── achievement_to_goal.ex +│   │   ├── achievements.ex +│   │   ├── goal.ex +│   │   ├── goal_progress.ex +│   │   └── goals.ex +│   ├── jobs +│   │   ├── autograder +│   │   │   ├── grading_job.ex +│   │   │   ├── lambda_worker.ex +│   │   │   ├── result_store_worker.ex +│   │   │   └── utilities.ex +│   │   ├── log.ex +│   │   ├── scheduler.ex +│   │   └── xml_parser.ex +│   ├── mailer.ex +│   ├── notifications +│   │   ├── notification_config.ex +│   │   ├── notification_preference.ex +│   │   ├── notification_type.ex +│   │   ├── sent_notification.ex +│   │   └── time_option.ex +│   ├── notifications.ex +│   ├── program_analysis +│   │   └── lexer.ex +│   ├── release.ex +│   ├── repo.ex +│   ├── stories +│   │   ├── stories.ex +│   │   └── story.ex +│   └── workers +│   └── NotificationWorker.ex +├── cadet.ex +├── cadet_web +│   ├── admin_controllers +│   │   ├── admin_achievements_controller.ex +│   │   ├── admin_assessments_controller.ex +│   │   ├── admin_assets_controller.ex +│   │   ├── admin_courses_controller.ex +│   │   ├── admin_goals_controller.ex +│   │   ├── admin_grading_controller.ex +│   │   ├── admin_sourcecast_controller.ex +│   │   ├── admin_stories_controller.ex +│   │   ├── admin_teams_controller.ex +│   │   └── admin_user_controller.ex +│   ├── admin_views +│   │   ├── admin_assessments_view.ex +│   │   ├── admin_assets_view.ex +│   │   ├── admin_courses_view.ex +│   │   ├── admin_goals_view.ex +│   │   ├── admin_grading_view.ex +│   │   ├── admin_teams_view.ex +│   │   └── admin_user_view.ex +│   ├── controllers +│   │   ├── answer_controller.ex +│   │   ├── assessments_controller.ex +│   │   ├── auth_controller.ex +│   │   ├── chat_controller.ex +│   │   ├── courses_controller.ex +│   │   ├── default_controller.ex +│   │   ├── devices_controller.ex +│   │   ├── incentives_controller.ex +│   │   ├── jwks_controller.ex +│   │   ├── notifications_controller.ex +│   │   ├── sourcecast_controller.ex +│   │   ├── stories_controller.ex +│   │   ├── team_controller.ex +│   │   └── user_controller.ex +│   ├── endpoint.ex +│   ├── gettext.ex +│   ├── helpers +│   │   ├── assessments_helpers.ex +│   │   ├── controller_helper.ex +│   │   └── view_helper.ex +│   ├── plug +│   │   ├── assign_current_user.ex +│   │   └── rate_limiter.ex +│   ├── router.ex +│   ├── templates +│   │   ├── email +│   │   │   ├── assessment_submission.html.eex +│   │   │   └── avenger_backlog.html.eex +│   │   └── layout +│   │   └── email.html.eex +│   └── views +│   ├── answer_view.ex +│   ├── assessments_view.ex +│   ├── auth_view.ex +│   ├── chat_view.ex +│   ├── courses_view.ex +│   ├── devices_view.ex +│   ├── email_view.ex +│   ├── error_view.ex +│   ├── incentives_view.ex +│   ├── layout_view.ex +│   ├── notifications_view.ex +│   ├── sourcecast_view.ex +│   ├── stories_view.ex +│   ├── team_view.ex +│   └── user_view.ex +├── cadet_web.ex +├── context_manager.ex +├── mix +│   └── tasks +│   ├── server.ex +│   └── token.ex +└── tree + +34 directories, 163 files diff --git a/test/cadet/courses/courses_test.exs b/test/cadet/courses/courses_test.exs index 50bf1b8c4..7b72c75cf 100644 --- a/test/cadet/courses/courses_test.exs +++ b/test/cadet/courses/courses_test.exs @@ -22,6 +22,7 @@ defmodule Cadet.CoursesTest do enable_sourcecast: true, enable_stories: false, enable_exam_mode: false, + resume_code: "resume_code", is_official_course: true, source_chapter: 1, source_variant: "default", @@ -61,6 +62,7 @@ defmodule Cadet.CoursesTest do 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" @@ -90,6 +92,7 @@ defmodule Cadet.CoursesTest do enable_stories: true, enable_exam_mode: true, is_official_course: true, + resume_code: "resume_code", module_help_text: "" }) @@ -102,6 +105,7 @@ defmodule Cadet.CoursesTest do assert updated_course.enable_stories == true assert updated_course.enable_exam_mode == true assert updated_course.is_official_course == true + assert updated_course.resumce_code == "resume_code" assert updated_course.source_chapter == 1 assert updated_course.source_variant == "default" assert updated_course.module_help_text == nil @@ -122,6 +126,7 @@ defmodule Cadet.CoursesTest do 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" @@ -136,6 +141,7 @@ defmodule Cadet.CoursesTest do 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" @@ -156,6 +162,7 @@ defmodule Cadet.CoursesTest do 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/controllers/courses_controller_test.exs b/test/cadet_web/controllers/courses_controller_test.exs index 5687ff5df..d7f1931b7 100644 --- a/test/cadet_web/controllers/courses_controller_test.exs +++ b/test/cadet_web/controllers/courses_controller_test.exs @@ -30,6 +30,7 @@ defmodule CadetWeb.CoursesControllerTest do "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" @@ -77,6 +78,7 @@ defmodule CadetWeb.CoursesControllerTest do "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" @@ -102,6 +104,7 @@ defmodule CadetWeb.CoursesControllerTest do "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" @@ -128,6 +131,7 @@ defmodule CadetWeb.CoursesControllerTest do "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/factories/courses/course_factory.ex b/test/factories/courses/course_factory.ex index 1ef2b443f..f2322f76a 100644 --- a/test/factories/courses/course_factory.ex +++ b/test/factories/courses/course_factory.ex @@ -18,6 +18,7 @@ defmodule Cadet.Courses.CourseFactory do 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" From fe80662fadf849aacd146a312dfab46b96cbfc71 Mon Sep 17 00:00:00 2001 From: Izumi Kyouka Date: Wed, 12 Mar 2025 23:10:30 +0800 Subject: [PATCH 09/38] added resume code checking endpoint and its handler --- .../admin_courses_controller.ex | 22 +------------------ .../controllers/courses_controller.ex | 18 +++++++++++++++ lib/cadet_web/router.ex | 1 + 3 files changed, 20 insertions(+), 21 deletions(-) diff --git a/lib/cadet_web/admin_controllers/admin_courses_controller.ex b/lib/cadet_web/admin_controllers/admin_courses_controller.ex index d2fd4047f..0bec3812a 100644 --- a/lib/cadet_web/admin_controllers/admin_courses_controller.ex +++ b/lib/cadet_web/admin_controllers/admin_courses_controller.ex @@ -5,27 +5,7 @@ defmodule CadetWeb.AdminCoursesController do alias Cadet.Courses - def update_course_config(conn, params = %{"course_id" => course_id}) - when is_ecto_id(course_id) do - params = params |> to_snake_case_atom_keys() - - case Courses.update_course_config(course_id, params) do - {:ok, _} -> - text(conn, "OK") - - # coveralls-ignore-start - # case of invalid course_id will not happen here - {:error, {status, message}} -> - send_resp(conn, status, message) - - # coveralls-ignore-stop - - {:error, _} -> - conn - |> put_status(:bad_request) - |> text("Invalid parameter(s)") - end - end +So def get_assessment_configs(conn, %{"course_id" => course_id}) when is_ecto_id(course_id) do assessment_configs = Courses.get_assessment_configs(course_id) diff --git a/lib/cadet_web/controllers/courses_controller.ex b/lib/cadet_web/controllers/courses_controller.ex index a72e10c9c..fee7e97fb 100644 --- a/lib/cadet_web/controllers/courses_controller.ex +++ b/lib/cadet_web/controllers/courses_controller.ex @@ -44,6 +44,24 @@ defmodule CadetWeb.CoursesController do end end + def resume_code(conn, params = %{"course_id" => course_id}) when is_ecto_id(course_id) do + params = params |> to_snake_case_atom_keys() + + case Courses.get_course_config(course_id) do + {:ok, config} -> + if config.resume_code == params["resume_code"] do + send_resp(conn, 200, "") + else + send_resp(conn, 403, "") + end + # coveralls-ignore-start + # no course error will not happen here + {:error, {status, message}} -> + send_resp(conn, status, message) + # coveralls-ignore-stop + end + end + swagger_path :create do post("/config/create") diff --git a/lib/cadet_web/router.ex b/lib/cadet_web/router.ex index 1ee304c01..827c647a0 100644 --- a/lib/cadet_web/router.ex +++ b/lib/cadet_web/router.ex @@ -119,6 +119,7 @@ defmodule CadetWeb.Router do put("/user/research_agreement", UserController, :update_research_agreement) get("/config", CoursesController, :index) + post("/resume_code", CoursesController, :check_resume_code) get("/team/:assessmentid", TeamController, :index) end From f3c9ea96637be0b382a286a88b0940db94fca615 Mon Sep 17 00:00:00 2001 From: Izumi Kyouka Date: Wed, 12 Mar 2025 23:15:55 +0800 Subject: [PATCH 10/38] minor change to resume_code handler --- lib/cadet_web/controllers/courses_controller.ex | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/lib/cadet_web/controllers/courses_controller.ex b/lib/cadet_web/controllers/courses_controller.ex index fee7e97fb..0313ce49e 100644 --- a/lib/cadet_web/controllers/courses_controller.ex +++ b/lib/cadet_web/controllers/courses_controller.ex @@ -44,12 +44,13 @@ defmodule CadetWeb.CoursesController do end end - def resume_code(conn, params = %{"course_id" => course_id}) when is_ecto_id(course_id) do - params = params |> to_snake_case_atom_keys() + def resume_code(conn, %{"course_id" => course_id}) when is_ecto_id(course_id) do + params = conn.body_params + resume_code = Map.get(params, "resume_code", nil) case Courses.get_course_config(course_id) do {:ok, config} -> - if config.resume_code == params["resume_code"] do + if config.resume_code == resume_code do send_resp(conn, 200, "") else send_resp(conn, 403, "") From 665ad2aa2e699121aeefbc620bcadd96f5fb6862 Mon Sep 17 00:00:00 2001 From: Izumi Kyouka Date: Thu, 13 Mar 2025 20:16:18 +0800 Subject: [PATCH 11/38] restore accidental deletion of function --- .../admin_courses_controller.ex | 22 ++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/lib/cadet_web/admin_controllers/admin_courses_controller.ex b/lib/cadet_web/admin_controllers/admin_courses_controller.ex index 0bec3812a..d2fd4047f 100644 --- a/lib/cadet_web/admin_controllers/admin_courses_controller.ex +++ b/lib/cadet_web/admin_controllers/admin_courses_controller.ex @@ -5,7 +5,27 @@ defmodule CadetWeb.AdminCoursesController do alias Cadet.Courses -So + def update_course_config(conn, params = %{"course_id" => course_id}) + when is_ecto_id(course_id) do + params = params |> to_snake_case_atom_keys() + + case Courses.update_course_config(course_id, params) do + {:ok, _} -> + text(conn, "OK") + + # coveralls-ignore-start + # case of invalid course_id will not happen here + {:error, {status, message}} -> + send_resp(conn, status, message) + + # coveralls-ignore-stop + + {:error, _} -> + conn + |> put_status(:bad_request) + |> text("Invalid parameter(s)") + 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) From abb1014acacd317c0077c12d76df270be7366773 Mon Sep 17 00:00:00 2001 From: Izumi Kyouka Date: Thu, 13 Mar 2025 23:27:34 +0800 Subject: [PATCH 12/38] renamed resume_code to check_resume_code --- lib/cadet_web/controllers/courses_controller.ex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/cadet_web/controllers/courses_controller.ex b/lib/cadet_web/controllers/courses_controller.ex index 0313ce49e..a4b1ff3f6 100644 --- a/lib/cadet_web/controllers/courses_controller.ex +++ b/lib/cadet_web/controllers/courses_controller.ex @@ -44,7 +44,7 @@ defmodule CadetWeb.CoursesController do end end - def resume_code(conn, %{"course_id" => course_id}) when is_ecto_id(course_id) do + def check_resume_code(conn, %{"course_id" => course_id}) when is_ecto_id(course_id) do params = conn.body_params resume_code = Map.get(params, "resume_code", nil) From 82afebf39ab97a19cfadda2f01a8ad7cee2a8917 Mon Sep 17 00:00:00 2001 From: Izumi Kyouka Date: Fri, 14 Mar 2025 00:13:09 +0800 Subject: [PATCH 13/38] added validation for enabling exam mode and setting resume code; deleted unused IO.puts --- lib/cadet/courses/course.ex | 24 +++++++++++++++++++ .../controllers/courses_controller.ex | 4 ++-- lib/cadet_web/views/courses_view.ex | 2 -- 3 files changed, 26 insertions(+), 4 deletions(-) diff --git a/lib/cadet/courses/course.ex b/lib/cadet/courses/course.ex index 4682e4014..04689025e 100644 --- a/lib/cadet/courses/course.ex +++ b/lib/cadet/courses/course.ex @@ -55,6 +55,30 @@ defmodule Cadet.Courses.Course do |> 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 + resume_code = Map.get(params, :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, resume_code} do + {false, _, _} -> changeset + {true, false, _} -> + add_error( + changeset, + :enable_exam_mode, + "Exam mode is only available for official institution course.") + {true, true, ""} -> + add_error( + changeset, + :resume_code, + "Resume code must be set to non-empty value upon enabling of exam mode." + ) + {_, _, _} -> changeset + end end # Validates combination of Source chapter and variant diff --git a/lib/cadet_web/controllers/courses_controller.ex b/lib/cadet_web/controllers/courses_controller.ex index a4b1ff3f6..5c103db17 100644 --- a/lib/cadet_web/controllers/courses_controller.ex +++ b/lib/cadet_web/controllers/courses_controller.ex @@ -51,9 +51,9 @@ defmodule CadetWeb.CoursesController do case Courses.get_course_config(course_id) do {:ok, config} -> if config.resume_code == resume_code do - send_resp(conn, 200, "") + send_resp(conn, 200, "Resume code is correct.") else - send_resp(conn, 403, "") + send_resp(conn, 403, "Resume code is wrong.") end # coveralls-ignore-start # no course error will not happen here diff --git a/lib/cadet_web/views/courses_view.ex b/lib/cadet_web/views/courses_view.ex index 8388ba9fc..ddc79674c 100644 --- a/lib/cadet_web/views/courses_view.ex +++ b/lib/cadet_web/views/courses_view.ex @@ -2,7 +2,6 @@ defmodule CadetWeb.CoursesView do use CadetWeb, :view def render("config.json", %{config: config}) do - IO.puts("resume code not sent") %{ config: transform_map_for_view(config, %{ @@ -25,7 +24,6 @@ defmodule CadetWeb.CoursesView do end def render("config_admin.json", %{config: config}) do - IO.puts("resume code sent") %{ config: transform_map_for_view(config, %{ From 1db44204c0def4a7fd9ed5591a2a692ecbbd2c83 Mon Sep 17 00:00:00 2001 From: Izumi Kyouka Date: Wed, 19 Mar 2025 01:10:37 +0800 Subject: [PATCH 14/38] added is_paused column and setting functionality to mitigate students bypassing overlay by refreshing --- lib/cadet/accounts/user.ex | 3 ++- .../controllers/courses_controller.ex | 19 +++++++++++++++++-- lib/cadet_web/controllers/user_controller.ex | 17 +++++++++++++++++ lib/cadet_web/router.ex | 1 + lib/cadet_web/views/user_view.ex | 2 ++ test/factories/accounts/user_factory.ex | 2 +- 6 files changed, 40 insertions(+), 4 deletions(-) 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_web/controllers/courses_controller.ex b/lib/cadet_web/controllers/courses_controller.ex index 5c103db17..ca9b80dfd 100644 --- a/lib/cadet_web/controllers/courses_controller.ex +++ b/lib/cadet_web/controllers/courses_controller.ex @@ -46,14 +46,29 @@ defmodule CadetWeb.CoursesController do def check_resume_code(conn, %{"course_id" => course_id}) when is_ecto_id(course_id) do params = conn.body_params + user = conn.assigns.current_user resume_code = Map.get(params, "resume_code", nil) case Courses.get_course_config(course_id) do {:ok, config} -> if config.resume_code == resume_code do - send_resp(conn, 200, "Resume code is correct.") + user + |> Cadet.Accounts.User.changeset(%{is_paused: false}) + |> Cadet.Repo.update() + |> case do + result = {:ok, _} -> + conn + |> put_status(:ok) + |> text("Resume code validated.") + {:error, _} -> + conn + |> put_status(500) + |> text(:error) + end else - send_resp(conn, 403, "Resume code is wrong.") + conn + |> put_status(403) + |> text("Resume code wrong.") end # coveralls-ignore-start # no course error will not happen here diff --git a/lib/cadet_web/controllers/user_controller.ex b/lib/cadet_web/controllers/user_controller.ex index 15b41b5bb..bb6ad91ff 100644 --- a/lib/cadet_web/controllers/user_controller.ex +++ b/lib/cadet_web/controllers/user_controller.ex @@ -124,6 +124,23 @@ 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 update_research_agreement(conn, %{"agreedToResearch" => agreed_to_research}) do course_reg = conn.assigns[:course_reg] diff --git a/lib/cadet_web/router.ex b/lib/cadet_web/router.ex index 827c647a0..ff5752f0a 100644 --- a/lib/cadet_web/router.ex +++ b/lib/cadet_web/router.ex @@ -117,6 +117,7 @@ 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) get("/config", CoursesController, :index) post("/resume_code", CoursesController, :check_resume_code) diff --git a/lib/cadet_web/views/user_view.ex b/lib/cadet_web/views/user_view.ex index 7181c1091..0746c8eb9 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: @@ -41,6 +42,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: diff --git a/test/factories/accounts/user_factory.ex b/test/factories/accounts/user_factory.ex index 60df091be..a364ce81d 100644 --- a/test/factories/accounts/user_factory.ex +++ b/test/factories/accounts/user_factory.ex @@ -18,7 +18,7 @@ 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, } end From d88eebaf553eb97e26e5b87c3d03c41ae456dec1 Mon Sep 17 00:00:00 2001 From: Izumi Kyouka Date: Wed, 19 Mar 2025 01:11:01 +0800 Subject: [PATCH 15/38] added migrations for is_paused_column --- ..._alter_courses_table_add_is_official_course copy.exs} | 0 .../20250316232500_alter_users_table_add_is_paused.exs | 9 +++++++++ 2 files changed, 9 insertions(+) rename priv/repo/migrations/{20250303232500_alter_courses_table_add_is_official_course.exs => 20250303232500_alter_courses_table_add_is_official_course copy.exs} (100%) create mode 100644 priv/repo/migrations/20250316232500_alter_users_table_add_is_paused.exs diff --git a/priv/repo/migrations/20250303232500_alter_courses_table_add_is_official_course.exs b/priv/repo/migrations/20250303232500_alter_courses_table_add_is_official_course copy.exs similarity index 100% rename from priv/repo/migrations/20250303232500_alter_courses_table_add_is_official_course.exs rename to priv/repo/migrations/20250303232500_alter_courses_table_add_is_official_course copy.exs 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..73c6e5f6b --- /dev/null +++ b/priv/repo/migrations/20250316232500_alter_users_table_add_is_paused.exs @@ -0,0 +1,9 @@ +defmodule Cadet.Repo.Migrations.AlterCoursesTableAddIsOfficialCourse do + use Ecto.Migration + + def change do + alter table(:users) do + add(:is_paused, :boolean, null: false, default: false) + end + end +end From 40ea38662034b4befc7eabe04c55c7011c39be41 Mon Sep 17 00:00:00 2001 From: Richard Dominick <34370238+RichDom2185@users.noreply.github.com> Date: Mon, 31 Mar 2025 17:06:29 +0800 Subject: [PATCH 16/38] Remove unused tree --- lib/tree | 199 ------------------------------------------------------- 1 file changed, 199 deletions(-) delete mode 100644 lib/tree diff --git a/lib/tree b/lib/tree deleted file mode 100644 index d902ac770..000000000 --- a/lib/tree +++ /dev/null @@ -1,199 +0,0 @@ -. -├── cadet -│   ├── accounts -│   │   ├── accounts.ex -│   │   ├── course_registration.ex -│   │   ├── course_registrations.ex -│   │   ├── notification.ex -│   │   ├── notification_type.ex -│   │   ├── notifications.ex -│   │   ├── query.ex -│   │   ├── role.ex -│   │   ├── team.ex -│   │   ├── team_member.ex -│   │   ├── teams.ex -│   │   └── user.ex -│   ├── application.ex -│   ├── assessments -│   │   ├── answer.ex -│   │   ├── answer_types -│   │   │   ├── mcq_answer.ex -│   │   │   ├── programming_answer.ex -│   │   │   └── voting_answer.ex -│   │   ├── assessment.ex -│   │   ├── assessment_access.ex -│   │   ├── assessments.ex -│   │   ├── autograding_status.ex -│   │   ├── external_library.ex -│   │   ├── library.ex -│   │   ├── query.ex -│   │   ├── question.ex -│   │   ├── question_type.ex -│   │   ├── question_types -│   │   │   ├── mcq_choice.ex -│   │   │   ├── mcq_question.ex -│   │   │   ├── programming_question.ex -│   │   │   ├── programming_question_testcases.ex -│   │   │   └── voting_question.ex -│   │   ├── submission.ex -│   │   ├── submission_status.ex -│   │   ├── submission_votes.ex -│   │   └── upload.ex -│   ├── assets -│   │   └── assets.ex -│   ├── auth -│   │   ├── empty_guardian.ex -│   │   ├── error_handler.ex -│   │   ├── guardian.ex -│   │   ├── pipeline.ex -│   │   ├── provider.ex -│   │   └── providers -│   │   ├── adfs.ex -│   │   ├── config.ex -│   │   ├── github.ex -│   │   ├── openid -│   │   │   ├── auth0_claim_extractor.ex -│   │   │   ├── cognito_claim_extractor.ex -│   │   │   ├── google_claim_extractor.ex -│   │   │   ├── mit_claim_extractor.ex -│   │   │   ├── mit_csail_claim_extractor.ex -│   │   │   └── openid.ex -│   │   └── saml -│   │   ├── nusstf_assertion_extractor.ex -│   │   ├── nusstu_assertion_extractor.ex -│   │   └── saml.ex -│   ├── chatbot -│   │   ├── conversation.ex -│   │   ├── llm_conversations.ex -│   │   ├── prompt_builder.ex -│   │   └── sicp_notes.ex -│   ├── courses -│   │   ├── assessment_config.ex -│   │   ├── course.ex -│   │   ├── courses.ex -│   │   ├── group.ex -│   │   ├── sourcecast.ex -│   │   └── sourcecast_upload.ex -│   ├── devices -│   │   ├── device.ex -│   │   ├── device_registration.ex -│   │   └── devices.ex -│   ├── email.ex -│   ├── env.ex -│   ├── helpers -│   │   ├── aws_helper.ex -│   │   ├── context_helper.ex -│   │   ├── display_helper.ex -│   │   ├── model_helper.ex -│   │   └── shared_helper.ex -│   ├── incentives -│   │   ├── achievement.ex -│   │   ├── achievement_prerequisite.ex -│   │   ├── achievement_to_goal.ex -│   │   ├── achievements.ex -│   │   ├── goal.ex -│   │   ├── goal_progress.ex -│   │   └── goals.ex -│   ├── jobs -│   │   ├── autograder -│   │   │   ├── grading_job.ex -│   │   │   ├── lambda_worker.ex -│   │   │   ├── result_store_worker.ex -│   │   │   └── utilities.ex -│   │   ├── log.ex -│   │   ├── scheduler.ex -│   │   └── xml_parser.ex -│   ├── mailer.ex -│   ├── notifications -│   │   ├── notification_config.ex -│   │   ├── notification_preference.ex -│   │   ├── notification_type.ex -│   │   ├── sent_notification.ex -│   │   └── time_option.ex -│   ├── notifications.ex -│   ├── program_analysis -│   │   └── lexer.ex -│   ├── release.ex -│   ├── repo.ex -│   ├── stories -│   │   ├── stories.ex -│   │   └── story.ex -│   └── workers -│   └── NotificationWorker.ex -├── cadet.ex -├── cadet_web -│   ├── admin_controllers -│   │   ├── admin_achievements_controller.ex -│   │   ├── admin_assessments_controller.ex -│   │   ├── admin_assets_controller.ex -│   │   ├── admin_courses_controller.ex -│   │   ├── admin_goals_controller.ex -│   │   ├── admin_grading_controller.ex -│   │   ├── admin_sourcecast_controller.ex -│   │   ├── admin_stories_controller.ex -│   │   ├── admin_teams_controller.ex -│   │   └── admin_user_controller.ex -│   ├── admin_views -│   │   ├── admin_assessments_view.ex -│   │   ├── admin_assets_view.ex -│   │   ├── admin_courses_view.ex -│   │   ├── admin_goals_view.ex -│   │   ├── admin_grading_view.ex -│   │   ├── admin_teams_view.ex -│   │   └── admin_user_view.ex -│   ├── controllers -│   │   ├── answer_controller.ex -│   │   ├── assessments_controller.ex -│   │   ├── auth_controller.ex -│   │   ├── chat_controller.ex -│   │   ├── courses_controller.ex -│   │   ├── default_controller.ex -│   │   ├── devices_controller.ex -│   │   ├── incentives_controller.ex -│   │   ├── jwks_controller.ex -│   │   ├── notifications_controller.ex -│   │   ├── sourcecast_controller.ex -│   │   ├── stories_controller.ex -│   │   ├── team_controller.ex -│   │   └── user_controller.ex -│   ├── endpoint.ex -│   ├── gettext.ex -│   ├── helpers -│   │   ├── assessments_helpers.ex -│   │   ├── controller_helper.ex -│   │   └── view_helper.ex -│   ├── plug -│   │   ├── assign_current_user.ex -│   │   └── rate_limiter.ex -│   ├── router.ex -│   ├── templates -│   │   ├── email -│   │   │   ├── assessment_submission.html.eex -│   │   │   └── avenger_backlog.html.eex -│   │   └── layout -│   │   └── email.html.eex -│   └── views -│   ├── answer_view.ex -│   ├── assessments_view.ex -│   ├── auth_view.ex -│   ├── chat_view.ex -│   ├── courses_view.ex -│   ├── devices_view.ex -│   ├── email_view.ex -│   ├── error_view.ex -│   ├── incentives_view.ex -│   ├── layout_view.ex -│   ├── notifications_view.ex -│   ├── sourcecast_view.ex -│   ├── stories_view.ex -│   ├── team_view.ex -│   └── user_view.ex -├── cadet_web.ex -├── context_manager.ex -├── mix -│   └── tasks -│   ├── server.ex -│   └── token.ex -└── tree - -34 directories, 163 files From 7ae6f141dc6be4c155219f38786ef69a8fd51693 Mon Sep 17 00:00:00 2001 From: Richard Dominick <34370238+RichDom2185@users.noreply.github.com> Date: Mon, 31 Mar 2025 17:06:47 +0800 Subject: [PATCH 17/38] Fix format --- lib/cadet/accounts/course_registrations.ex | 7 +- lib/cadet/courses/course.ex | 12 +- .../controllers/courses_controller.ex | 5 +- lib/cadet_web/controllers/user_controller.ex | 4 +- lib/cadet_web/views/courses_view.ex | 2 +- lib/cadet_web/views/user_view.ex | 104 ++++++++++-------- ...ter_courses_table_add_enable_exam_mode.exs | 6 +- ...r_courses_table_add_is_official_course.exs | 6 +- 8 files changed, 84 insertions(+), 62 deletions(-) diff --git a/lib/cadet/accounts/course_registrations.ex b/lib/cadet/accounts/course_registrations.ex index 181af5948..7944d4ec4 100644 --- a/lib/cadet/accounts/course_registrations.ex +++ b/lib/cadet/accounts/course_registrations.ex @@ -50,10 +50,13 @@ defmodule Cadet.Accounts.CourseRegistrations do def get_exam_mode_course(%User{id: id}) do CourseRegistration |> where([cr], cr.user_id == ^id) - |> join(:inner, [cr], c in assoc(cr, :course), on: c.enable_exam_mode == true and c.is_official_course == true) + |> 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])}) + course: {c, assessment_config: ^from(ac in AssessmentConfig, order_by: [asc: ac.order])} + ) |> preload(:group) |> Repo.one() end diff --git a/lib/cadet/courses/course.ex b/lib/cadet/courses/course.ex index 04689025e..961e3070b 100644 --- a/lib/cadet/courses/course.ex +++ b/lib/cadet/courses/course.ex @@ -65,19 +65,25 @@ defmodule Cadet.Courses.Course do is_official_course = get_field(changeset, :is_official_course, false) case {enable_exam_mode, is_official_course, resume_code} do - {false, _, _} -> changeset + {false, _, _} -> + changeset + {true, false, _} -> add_error( changeset, :enable_exam_mode, - "Exam mode is only available for official institution course.") + "Exam mode is only available for official institution course." + ) + {true, true, ""} -> add_error( changeset, :resume_code, "Resume code must be set to non-empty value upon enabling of exam mode." ) - {_, _, _} -> changeset + + {_, _, _} -> + changeset end end diff --git a/lib/cadet_web/controllers/courses_controller.ex b/lib/cadet_web/controllers/courses_controller.ex index 5c103db17..aea108dc6 100644 --- a/lib/cadet_web/controllers/courses_controller.ex +++ b/lib/cadet_web/controllers/courses_controller.ex @@ -9,7 +9,7 @@ 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} -> - if (conn.assigns.course_reg.role == :admin || conn.assigns.course_reg.role == "admin") do + if conn.assigns.course_reg.role == :admin || conn.assigns.course_reg.role == "admin" do render(conn, "config_admin.json", config: config) else render(conn, "config.json", config: config) @@ -55,7 +55,8 @@ defmodule CadetWeb.CoursesController do else send_resp(conn, 403, "Resume code is wrong.") end - # coveralls-ignore-start + + # coveralls-ignore-start # no course error will not happen here {:error, {status, message}} -> send_resp(conn, status, message) diff --git a/lib/cadet_web/controllers/user_controller.ex b/lib/cadet_web/controllers/user_controller.ex index 15b41b5bb..a2487e206 100644 --- a/lib/cadet_web/controllers/user_controller.ex +++ b/lib/cadet_web/controllers/user_controller.ex @@ -64,6 +64,7 @@ defmodule CadetWeb.UserController do def get_latest_viewed(conn, _) do user = conn.assigns.current_user exam_mode_course = CourseRegistrations.get_exam_mode_course(conn.assigns.current_user) + if exam_mode_course do latest = CourseRegistrations.get_user_course(user.id, exam_mode_course.course_id) get_course_reg_config(conn, latest) @@ -72,11 +73,10 @@ defmodule CadetWeb.UserController do case user.latest_viewed_course_id do nil -> nil _ -> CourseRegistrations.get_user_course(user.id, user.latest_viewed_course_id) - end + end get_course_reg_config(conn, latest) end - end defp get_course_reg_config(conn, course_reg) when is_nil(course_reg) do diff --git a/lib/cadet_web/views/courses_view.ex b/lib/cadet_web/views/courses_view.ex index ddc79674c..d528048b5 100644 --- a/lib/cadet_web/views/courses_view.ex +++ b/lib/cadet_web/views/courses_view.ex @@ -23,7 +23,7 @@ defmodule CadetWeb.CoursesView do } end - def render("config_admin.json", %{config: config}) do + def render("config_admin.json", %{config: config}) do %{ config: transform_map_for_view(config, %{ diff --git a/lib/cadet_web/views/user_view.ex b/lib/cadet_web/views/user_view.ex index 7181c1091..5d28ca91b 100644 --- a/lib/cadet_web/views/user_view.ex +++ b/lib/cadet_web/views/user_view.ex @@ -29,14 +29,18 @@ defmodule CadetWeb.UserView do } end - def render("index.json", %{ - user: user, - courses: courses, - latest: latest, - max_xp: max_xp, - xp: xp, - story: story - }, is_admin) do + def render( + "index.json", + %{ + user: user, + courses: courses, + latest: latest, + max_xp: max_xp, + xp: xp, + story: story + }, + is_admin + ) do %{ user: %{ userId: user.id, @@ -60,7 +64,7 @@ defmodule CadetWeb.UserView do max_xp: max_xp, xp: xp, story: story - }) do + }) do %{ courseRegistration: render_latest(%{ @@ -74,12 +78,16 @@ defmodule CadetWeb.UserView do } end - def render("course.json", %{ - latest: latest, - max_xp: max_xp, - xp: xp, - story: story - }, is_admin) do + def render( + "course.json", + %{ + latest: latest, + max_xp: max_xp, + xp: xp, + story: story + }, + is_admin + ) do %{ courseRegistration: render_latest(%{ @@ -140,21 +148,23 @@ defmodule CadetWeb.UserView do case latest do nil -> nil - _ -> transform_map_for_view(latest.course, %{ - 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, - sourceChapter: :source_chapter, - sourceVariant: :source_variant, - moduleHelpText: :module_help_text, - assetsPrefix: &Courses.assets_prefix/1 - }) + + _ -> + transform_map_for_view(latest.course, %{ + 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, + sourceChapter: :source_chapter, + sourceVariant: :source_variant, + moduleHelpText: :module_help_text, + assetsPrefix: &Courses.assets_prefix/1 + }) end end @@ -162,22 +172,24 @@ defmodule CadetWeb.UserView do case latest do nil -> nil - _ -> transform_map_for_view(latest.course, %{ - 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, - resume_code: :resume_code, - isOfficialCourse: :is_official_course, - sourceChapter: :source_chapter, - sourceVariant: :source_variant, - moduleHelpText: :module_help_text, - assetsPrefix: &Courses.assets_prefix/1 - }) + + _ -> + transform_map_for_view(latest.course, %{ + 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, + resume_code: :resume_code, + isOfficialCourse: :is_official_course, + sourceChapter: :source_chapter, + sourceVariant: :source_variant, + moduleHelpText: :module_help_text, + assetsPrefix: &Courses.assets_prefix/1 + }) end end diff --git a/priv/repo/migrations/20250303232000_alter_courses_table_add_enable_exam_mode.exs b/priv/repo/migrations/20250303232000_alter_courses_table_add_enable_exam_mode.exs index e86124843..d8f66e6bb 100644 --- a/priv/repo/migrations/20250303232000_alter_courses_table_add_enable_exam_mode.exs +++ b/priv/repo/migrations/20250303232000_alter_courses_table_add_enable_exam_mode.exs @@ -2,8 +2,8 @@ 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 + alter table(:courses) do + add(:enable_exam_mode, :boolean, null: false, default: false) + end end end diff --git a/priv/repo/migrations/20250303232500_alter_courses_table_add_is_official_course.exs b/priv/repo/migrations/20250303232500_alter_courses_table_add_is_official_course.exs index c3e92849d..eb6a0f0db 100644 --- a/priv/repo/migrations/20250303232500_alter_courses_table_add_is_official_course.exs +++ b/priv/repo/migrations/20250303232500_alter_courses_table_add_is_official_course.exs @@ -2,8 +2,8 @@ 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 + alter table(:courses) do + add(:is_official_course, :boolean, null: false, default: false) + end end end From e0330f2cf38b2d8af12bffd20f4cac2158d607fc Mon Sep 17 00:00:00 2001 From: Richard Dominick <34370238+RichDom2185@users.noreply.github.com> Date: Mon, 31 Mar 2025 17:12:20 +0800 Subject: [PATCH 18/38] Redate migrations to maintain total ordering --- ...> 20250331171140_alter_courses_table_add_enable_exam_mode.exs} | 0 ...20250331232502_alter_courses_table_add_is_official_course.exs} | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename priv/repo/migrations/{20250303232000_alter_courses_table_add_enable_exam_mode.exs => 20250331171140_alter_courses_table_add_enable_exam_mode.exs} (100%) rename priv/repo/migrations/{20250303232500_alter_courses_table_add_is_official_course.exs => 20250331232502_alter_courses_table_add_is_official_course.exs} (100%) diff --git a/priv/repo/migrations/20250303232000_alter_courses_table_add_enable_exam_mode.exs b/priv/repo/migrations/20250331171140_alter_courses_table_add_enable_exam_mode.exs similarity index 100% rename from priv/repo/migrations/20250303232000_alter_courses_table_add_enable_exam_mode.exs rename to priv/repo/migrations/20250331171140_alter_courses_table_add_enable_exam_mode.exs diff --git a/priv/repo/migrations/20250303232500_alter_courses_table_add_is_official_course.exs b/priv/repo/migrations/20250331232502_alter_courses_table_add_is_official_course.exs similarity index 100% rename from priv/repo/migrations/20250303232500_alter_courses_table_add_is_official_course.exs rename to priv/repo/migrations/20250331232502_alter_courses_table_add_is_official_course.exs From d971fcdfbc21001b677ae25f51e0111d08bd7d7a Mon Sep 17 00:00:00 2001 From: Izumi Kyouka Date: Tue, 1 Apr 2025 22:30:48 +0800 Subject: [PATCH 19/38] fixed failing tests due to missing fields in factory method, and expected object. fixed migrations --- lib/cadet_web/controllers/user_controller.ex | 1 - ...500_alter_courses_table_add_is_official_course.exs} | 6 +++--- .../20250316232500_alter_users_table_add_is_paused.exs | 8 ++++---- ...50401214500_alter_courses_table_add_resume_code.exs | 9 +++++++++ test/cadet/courses/courses_test.exs | 2 +- test/cadet_web/controllers/user_controller_test.exs | 10 ++++++++-- test/factories/accounts/user_factory.ex | 4 +++- 7 files changed, 28 insertions(+), 12 deletions(-) rename priv/repo/migrations/{20250303232500_alter_courses_table_add_is_official_course copy.exs => 20250303232500_alter_courses_table_add_is_official_course.exs} (51%) create mode 100644 priv/repo/migrations/20250401214500_alter_courses_table_add_resume_code.exs diff --git a/lib/cadet_web/controllers/user_controller.ex b/lib/cadet_web/controllers/user_controller.ex index bb6ad91ff..37b6678a2 100644 --- a/lib/cadet_web/controllers/user_controller.ex +++ b/lib/cadet_web/controllers/user_controller.ex @@ -16,7 +16,6 @@ defmodule CadetWeb.UserController do cond do exam_mode_course -> - IO.puts("Course #{exam_mode_course.course_id} is under exam mode.") 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) diff --git a/priv/repo/migrations/20250303232500_alter_courses_table_add_is_official_course copy.exs b/priv/repo/migrations/20250303232500_alter_courses_table_add_is_official_course.exs similarity index 51% rename from priv/repo/migrations/20250303232500_alter_courses_table_add_is_official_course copy.exs rename to priv/repo/migrations/20250303232500_alter_courses_table_add_is_official_course.exs index c3e92849d..eb6a0f0db 100644 --- a/priv/repo/migrations/20250303232500_alter_courses_table_add_is_official_course copy.exs +++ b/priv/repo/migrations/20250303232500_alter_courses_table_add_is_official_course.exs @@ -2,8 +2,8 @@ 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 + alter table(:courses) do + add(:is_official_course, :boolean, null: false, default: false) + end end end 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 index 73c6e5f6b..74b1ad2b5 100644 --- a/priv/repo/migrations/20250316232500_alter_users_table_add_is_paused.exs +++ b/priv/repo/migrations/20250316232500_alter_users_table_add_is_paused.exs @@ -1,9 +1,9 @@ -defmodule Cadet.Repo.Migrations.AlterCoursesTableAddIsOfficialCourse do +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 + alter table(:users) do + add(:is_paused, :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..3205da665 --- /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: "") + end + end +end diff --git a/test/cadet/courses/courses_test.exs b/test/cadet/courses/courses_test.exs index 7b72c75cf..e8090ffc2 100644 --- a/test/cadet/courses/courses_test.exs +++ b/test/cadet/courses/courses_test.exs @@ -105,7 +105,7 @@ defmodule Cadet.CoursesTest do assert updated_course.enable_stories == true assert updated_course.enable_exam_mode == true assert updated_course.is_official_course == true - assert updated_course.resumce_code == "resume_code" + 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 diff --git a/test/cadet_web/controllers/user_controller_test.exs b/test/cadet_web/controllers/user_controller_test.exs index 18264bd1b..f8272fd46 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" => [ @@ -169,7 +172,8 @@ defmodule CadetWeb.UserControllerTest do "user" => %{ "userId" => user.id, "name" => user.name, - "courses" => [] + "courses" => [], + "isPaused" => false }, "courseRegistration" => nil, "courseConfiguration" => nil, @@ -325,6 +329,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 a364ce81d..7fcaafc8d 100644 --- a/test/factories/accounts/user_factory.ex +++ b/test/factories/accounts/user_factory.ex @@ -19,6 +19,7 @@ defmodule Cadet.Accounts.UserFactory do ), latest_viewed_course: build(:course), 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 From 0302764bcd022d181dc043dbcde0092a56f8f3af Mon Sep 17 00:00:00 2001 From: Izumi Kyouka Date: Tue, 1 Apr 2025 22:52:01 +0800 Subject: [PATCH 20/38] makes latest course retrieval logic more concise --- lib/cadet_web/controllers/user_controller.ex | 17 ++++++----------- 1 file changed, 6 insertions(+), 11 deletions(-) diff --git a/lib/cadet_web/controllers/user_controller.ex b/lib/cadet_web/controllers/user_controller.ex index 64b85403d..496ceecfd 100644 --- a/lib/cadet_web/controllers/user_controller.ex +++ b/lib/cadet_web/controllers/user_controller.ex @@ -64,18 +64,13 @@ defmodule CadetWeb.UserController do user = conn.assigns.current_user exam_mode_course = CourseRegistrations.get_exam_mode_course(conn.assigns.current_user) - if exam_mode_course do - latest = CourseRegistrations.get_user_course(user.id, exam_mode_course.course_id) - get_course_reg_config(conn, latest) - else - latest = - case user.latest_viewed_course_id do - nil -> nil - _ -> CourseRegistrations.get_user_course(user.id, user.latest_viewed_course_id) - end - - get_course_reg_config(conn, latest) + latest = 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) end defp get_course_reg_config(conn, course_reg) when is_nil(course_reg) do From 7fcbc89961030de19d073c230c50c06566cb94f3 Mon Sep 17 00:00:00 2001 From: Izumi Kyouka Date: Tue, 1 Apr 2025 22:55:07 +0800 Subject: [PATCH 21/38] ran mix format on courses and user controller --- .../controllers/courses_controller.ex | 1 + lib/cadet_web/controllers/user_controller.ex | 18 +++++++++++++----- 2 files changed, 14 insertions(+), 5 deletions(-) diff --git a/lib/cadet_web/controllers/courses_controller.ex b/lib/cadet_web/controllers/courses_controller.ex index c776b5e69..55fed0181 100644 --- a/lib/cadet_web/controllers/courses_controller.ex +++ b/lib/cadet_web/controllers/courses_controller.ex @@ -60,6 +60,7 @@ defmodule CadetWeb.CoursesController do conn |> put_status(:ok) |> text("Resume code validated.") + {:error, _} -> conn |> put_status(500) diff --git a/lib/cadet_web/controllers/user_controller.ex b/lib/cadet_web/controllers/user_controller.ex index 496ceecfd..18673e456 100644 --- a/lib/cadet_web/controllers/user_controller.ex +++ b/lib/cadet_web/controllers/user_controller.ex @@ -64,11 +64,17 @@ defmodule CadetWeb.UserController do user = conn.assigns.current_user exam_mode_course = CourseRegistrations.get_exam_mode_course(conn.assigns.current_user) - latest = 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 + latest = + 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) end @@ -120,6 +126,7 @@ defmodule CadetWeb.UserController do def pause_user(conn, params) do user = conn.assigns.current_user + user |> Cadet.Accounts.User.changeset(%{is_paused: true}) |> Cadet.Repo.update() @@ -128,6 +135,7 @@ defmodule CadetWeb.UserController do conn |> put_status(:ok) |> text("User is paused.") + {:error, _} -> conn |> put_status(500) From cad39c8a23009de0668be7ec6afd750f4f84c389 Mon Sep 17 00:00:00 2001 From: Izumi Kyouka Date: Wed, 2 Apr 2025 13:29:38 +0800 Subject: [PATCH 22/38] added new endpoint to report lost/regain of user focus --- lib/cadet_web/controllers/user_controller.ex | 19 +++++++++++++++++++ lib/cadet_web/router.ex | 1 + 2 files changed, 20 insertions(+) diff --git a/lib/cadet_web/controllers/user_controller.ex b/lib/cadet_web/controllers/user_controller.ex index 18673e456..6ce18ce64 100644 --- a/lib/cadet_web/controllers/user_controller.ex +++ b/lib/cadet_web/controllers/user_controller.ex @@ -143,6 +143,25 @@ defmodule CadetWeb.UserController do end end + def log_user_focus_change(conn, %{"state" => state}) do + user = conn.assigns.current_user + focus_state = case state do + "0" -> "OUT of focus" + "1" -> "IN focus" + _ -> nil + end + + if focus_state do + IO.puts("[#{Calendar.strftime(DateTime.utc_now(), "%Y-%m-%d %H:%M:%S")}] User #{user.id} (#{user.name}) is #{focus_state}.") + conn + |> put_status(200) + end + + conn + |> put_status(403) + |> text("invalid user focus state") + end + def update_research_agreement(conn, %{"agreedToResearch" => agreed_to_research}) do course_reg = conn.assigns[:course_reg] diff --git a/lib/cadet_web/router.ex b/lib/cadet_web/router.ex index ff5752f0a..438b8cd0b 100644 --- a/lib/cadet_web/router.ex +++ b/lib/cadet_web/router.ex @@ -118,6 +118,7 @@ defmodule CadetWeb.Router do 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, :check_resume_code) From 46af8d70eeaa7bb1482e9b6f2d682e4d5d83f838 Mon Sep 17 00:00:00 2001 From: Izumi Kyouka Date: Thu, 3 Apr 2025 16:23:42 +0800 Subject: [PATCH 23/38] removed filters for supplying exam_mode_course to renderer function --- lib/cadet_web/controllers/user_controller.ex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/cadet_web/controllers/user_controller.ex b/lib/cadet_web/controllers/user_controller.ex index 18673e456..6d86396ed 100644 --- a/lib/cadet_web/controllers/user_controller.ex +++ b/lib/cadet_web/controllers/user_controller.ex @@ -24,7 +24,7 @@ defmodule CadetWeb.UserController do conn, "index.json", user: user, - courses: courses |> Enum.filter(fn c -> c.course_id == exam_mode_course.course_id end), + courses: [exam_mode_course], latest: exam_mode_course, max_xp: max_xp, story: story, From 6531376c582e519f99db344f6916a6cff0c12c5f Mon Sep 17 00:00:00 2001 From: Izumi Kyouka Date: Thu, 3 Apr 2025 18:58:16 +0800 Subject: [PATCH 24/38] renamed check_resume_code to try_unpause_user; split up the logic into separate private functions for readability. --- .../controllers/courses_controller.ex | 56 +++++++++---------- lib/cadet_web/router.ex | 2 +- 2 files changed, 26 insertions(+), 32 deletions(-) diff --git a/lib/cadet_web/controllers/courses_controller.ex b/lib/cadet_web/controllers/courses_controller.ex index 55fed0181..c57c78024 100644 --- a/lib/cadet_web/controllers/courses_controller.ex +++ b/lib/cadet_web/controllers/courses_controller.ex @@ -44,39 +44,33 @@ defmodule CadetWeb.CoursesController do end end - def check_resume_code(conn, %{"course_id" => course_id}) when is_ecto_id(course_id) do - params = conn.body_params - user = conn.assigns.current_user - resume_code = Map.get(params, "resume_code", nil) - + defp check_resume_code(course_id, resume_code) do case Courses.get_course_config(course_id) do - {:ok, config} -> - if config.resume_code == resume_code do - user - |> Cadet.Accounts.User.changeset(%{is_paused: false}) - |> Cadet.Repo.update() - |> case do - result = {:ok, _} -> - conn - |> put_status(:ok) - |> text("Resume code validated.") - - {:error, _} -> - conn - |> put_status(500) - |> text(:error) - end - else - conn - |> put_status(403) - |> text("Resume code wrong.") - end + {:ok, config} -> {:ok, config.resume_code == resume_code} + {:error, {status_code, message}} -> {:error, {status_code, message}} + end + end - # coveralls-ignore-start - # no course error will not happen here - {:error, {status, message}} -> - send_resp(conn, status, message) - # coveralls-ignore-stop + 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, :error) + 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 diff --git a/lib/cadet_web/router.ex b/lib/cadet_web/router.ex index ff5752f0a..ba14b0ca6 100644 --- a/lib/cadet_web/router.ex +++ b/lib/cadet_web/router.ex @@ -120,7 +120,7 @@ defmodule CadetWeb.Router do put("/user/pause", UserController, :pause_user) get("/config", CoursesController, :index) - post("/resume_code", CoursesController, :check_resume_code) + post("/resume_code", CoursesController, :try_unpause_user) get("/team/:assessmentid", TeamController, :index) end From 3dcc288b23995d4ff5d550f08d7f85c2a1339e09 Mon Sep 17 00:00:00 2001 From: Izumi Kyouka Date: Thu, 3 Apr 2025 22:49:15 +0800 Subject: [PATCH 25/38] removed unnecessary admin config renderer in user_view and admin_user_controller; fixed a typing error in send_resp --- lib/cadet/courses/courses.ex | 2 +- .../admin_user_controller.ex | 2 +- .../controllers/courses_controller.ex | 4 +- lib/cadet_web/views/user_view.ex | 79 ------------------- 4 files changed, 4 insertions(+), 83 deletions(-) diff --git a/lib/cadet/courses/courses.ex b/lib/cadet/courses/courses.ex index d27268644..5c0464fae 100644 --- a/lib/cadet/courses/courses.ex +++ b/lib/cadet/courses/courses.ex @@ -44,7 +44,7 @@ defmodule Cadet.Courses do """ @spec get_course_config(integer) :: {:ok, Course.t()} | {:error, {:bad_request, String.t()}} - def get_course_config(course_id, is_admin \\ false) when is_ecto_id(course_id) do + 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"}} diff --git a/lib/cadet_web/admin_controllers/admin_user_controller.ex b/lib/cadet_web/admin_controllers/admin_user_controller.ex index f9a4965a2..67e9b6986 100644 --- a/lib/cadet_web/admin_controllers/admin_user_controller.ex +++ b/lib/cadet_web/admin_controllers/admin_user_controller.ex @@ -13,7 +13,7 @@ defmodule CadetWeb.AdminUserController do users = filter |> try_keywordise_string_keys() |> Accounts.get_users_by(conn.assigns.course_reg) - render(conn, "users.json", users: users, is_admin: false) + render(conn, "users.json", users: users) end def combined_total_xp(conn, %{"course_reg_id" => course_reg_id}) do diff --git a/lib/cadet_web/controllers/courses_controller.ex b/lib/cadet_web/controllers/courses_controller.ex index c57c78024..2a20ee75d 100644 --- a/lib/cadet_web/controllers/courses_controller.ex +++ b/lib/cadet_web/controllers/courses_controller.ex @@ -9,7 +9,7 @@ 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} -> - if conn.assigns.course_reg.role == :admin || conn.assigns.course_reg.role == "admin" do + if conn.assigns.course_reg.role == :admin do render(conn, "config_admin.json", config: config) else render(conn, "config.json", config: config) @@ -59,7 +59,7 @@ defmodule CadetWeb.CoursesController do case update_result do {:ok, _} -> conn |> send_resp(:ok, "") - {:error, _} -> conn |> send_resp(500, :error) + {:error, _} -> conn |> send_resp(500, "") end end diff --git a/lib/cadet_web/views/user_view.ex b/lib/cadet_web/views/user_view.ex index 560e71d27..15e07148f 100644 --- a/lib/cadet_web/views/user_view.ex +++ b/lib/cadet_web/views/user_view.ex @@ -30,37 +30,6 @@ defmodule CadetWeb.UserView do } end - def render( - "index.json", - %{ - user: user, - courses: courses, - latest: latest, - max_xp: max_xp, - xp: xp, - story: story - }, - is_admin - ) do - %{ - user: %{ - userId: user.id, - name: user.name, - isPaused: user.is_paused, - courses: render_many(courses, CadetWeb.UserView, "courses.json", as: :cr) - }, - courseRegistration: - render_latest(%{ - latest: latest, - max_xp: max_xp, - xp: xp, - story: story - }), - courseConfiguration: render_config(latest, is_admin), - assessmentConfigurations: render_assessment_configs(latest) - } - end - def render("course.json", %{ latest: latest, max_xp: max_xp, @@ -80,29 +49,6 @@ defmodule CadetWeb.UserView do } end - def render( - "course.json", - %{ - latest: latest, - max_xp: max_xp, - xp: xp, - story: story - }, - is_admin - ) do - %{ - courseRegistration: - render_latest(%{ - latest: latest, - max_xp: max_xp, - xp: xp, - story: story - }), - courseConfiguration: render_config(latest, is_admin), - assessmentConfigurations: render_assessment_configs(latest) - } - end - def render("courses.json", %{cr: cr}) do %{ courseId: cr.course_id, @@ -170,31 +116,6 @@ defmodule CadetWeb.UserView do end end - defp render_config(latest, is_admin) do - case latest do - nil -> - nil - - _ -> - transform_map_for_view(latest.course, %{ - 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, - resume_code: :resume_code, - isOfficialCourse: :is_official_course, - sourceChapter: :source_chapter, - sourceVariant: :source_variant, - moduleHelpText: :module_help_text, - assetsPrefix: &Courses.assets_prefix/1 - }) - end - end - defp render_assessment_configs(latest) do case latest do nil -> From 31bbf5a1fefeab1d432cf1b39168d3fee77113e6 Mon Sep 17 00:00:00 2001 From: Izumi Kyouka Date: Thu, 3 Apr 2025 23:22:51 +0800 Subject: [PATCH 26/38] excludes staff and admins from exam_mode restriction on courses returned; improved readibility for pause_user --- lib/cadet_web/controllers/user_controller.ex | 22 ++++++++------------ 1 file changed, 9 insertions(+), 13 deletions(-) diff --git a/lib/cadet_web/controllers/user_controller.ex b/lib/cadet_web/controllers/user_controller.ex index 6d86396ed..aa390dbe6 100644 --- a/lib/cadet_web/controllers/user_controller.ex +++ b/lib/cadet_web/controllers/user_controller.ex @@ -11,11 +11,12 @@ defmodule CadetWeb.UserController do def index(conn, _) do user = conn.assigns.current_user + role = conn.assigns.course_reg.role courses = CourseRegistrations.get_courses(conn.assigns.current_user) exam_mode_course = CourseRegistrations.get_exam_mode_course(conn.assigns.current_user) cond do - exam_mode_course -> + exam_mode_course && role == :student -> 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) @@ -127,19 +128,14 @@ defmodule CadetWeb.UserController do 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.") + update_result = + user + |> Cadet.Accounts.User.changeset(%{is_paused: true}) + |> Cadet.Repo.update() - {:error, _} -> - conn - |> put_status(500) - |> text(:error) + case update_result do + {:ok, _} -> conn |> send_resp(:ok, "") + {:error, _} -> conn |> send_resp(500, "") end end From 48c1b10253b80dafe948f34624d3f099abf26e1e Mon Sep 17 00:00:00 2001 From: Izumi Kyouka Date: Thu, 3 Apr 2025 23:22:51 +0800 Subject: [PATCH 27/38] excludes staff and admins from exam_mode restriction on courses returned; improved readibility for pause_user --- lib/cadet/accounts/course_registrations.ex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/cadet/accounts/course_registrations.ex b/lib/cadet/accounts/course_registrations.ex index 7944d4ec4..79728c0dc 100644 --- a/lib/cadet/accounts/course_registrations.ex +++ b/lib/cadet/accounts/course_registrations.ex @@ -49,7 +49,7 @@ defmodule Cadet.Accounts.CourseRegistrations do def get_exam_mode_course(%User{id: id}) do CourseRegistration - |> where([cr], cr.user_id == ^id) + |> 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 ) From e0758fb45cc63d757c17ffb90be2aef048b0185b Mon Sep 17 00:00:00 2001 From: Izumi Kyouka Date: Fri, 4 Apr 2025 00:25:38 +0800 Subject: [PATCH 28/38] Revert "excludes staff and admins from exam_mode restriction on courses returned; improved readibility for pause_user" This reverts commit 31bbf5a1fefeab1d432cf1b39168d3fee77113e6. --- lib/cadet_web/controllers/user_controller.ex | 22 ++++++++++++-------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/lib/cadet_web/controllers/user_controller.ex b/lib/cadet_web/controllers/user_controller.ex index aa390dbe6..6d86396ed 100644 --- a/lib/cadet_web/controllers/user_controller.ex +++ b/lib/cadet_web/controllers/user_controller.ex @@ -11,12 +11,11 @@ defmodule CadetWeb.UserController do def index(conn, _) do user = conn.assigns.current_user - role = conn.assigns.course_reg.role courses = CourseRegistrations.get_courses(conn.assigns.current_user) exam_mode_course = CourseRegistrations.get_exam_mode_course(conn.assigns.current_user) cond do - exam_mode_course && role == :student -> + 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) @@ -128,14 +127,19 @@ defmodule CadetWeb.UserController do def pause_user(conn, params) do user = conn.assigns.current_user - update_result = - user - |> Cadet.Accounts.User.changeset(%{is_paused: true}) - |> Cadet.Repo.update() + user + |> Cadet.Accounts.User.changeset(%{is_paused: true}) + |> Cadet.Repo.update() + |> case do + result = {:ok, _} -> + conn + |> put_status(:ok) + |> text("User is paused.") - case update_result do - {:ok, _} -> conn |> send_resp(:ok, "") - {:error, _} -> conn |> send_resp(500, "") + {:error, _} -> + conn + |> put_status(500) + |> text(:error) end end From 7bd79fdad3238aba2c0a968e75d2be633fe710ba Mon Sep 17 00:00:00 2001 From: Izumi Kyouka Date: Sat, 5 Apr 2025 00:07:00 +0800 Subject: [PATCH 29/38] created new user_browser_focus_log; and created its type definition, changeset, and applied it in the focus logging controlelr --- lib/cadet/focus_logs/focus_log.ex | 31 +++++++++++++++++++ lib/cadet/focus_logs/focus_logs.ex | 22 +++++++++++++ lib/cadet_web/controllers/user_controller.ex | 18 ++++++----- ...00_create_table_user_browser_focus_log.exs | 12 +++++++ 4 files changed, 76 insertions(+), 7 deletions(-) create mode 100644 lib/cadet/focus_logs/focus_log.ex create mode 100644 lib/cadet/focus_logs/focus_logs.ex create mode 100644 priv/repo/migrations/20250404231500_create_table_user_browser_focus_log.exs diff --git a/lib/cadet/focus_logs/focus_log.ex b/lib/cadet/focus_logs/focus_log.ex new file mode 100644 index 000000000..c75da00eb --- /dev/null +++ b/lib/cadet/focus_logs/focus_log.ex @@ -0,0 +1,31 @@ +defmodule Cadet.FocusLogs.FocusLog do + @moduledoc """ + The Conversation entity stores the messages exchanged between the user and the chatbot. + """ + 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..adbcf9ee9 --- /dev/null +++ b/lib/cadet/focus_logs/focus_logs.ex @@ -0,0 +1,22 @@ +defmodule Cadet.FocusLogs do + alias Cadet.FocusLogs.FocusLog + + use Cadet, [:context, :display] + + def insert_log(user_id, course_id, focus_type) do + datetime = DateTime.utc_now() |> DateTime.to_naive() + IO.puts(datetime) + insert_result = %FocusLog{} + |> FocusLog.changeset(%{ + user_id: user_id, + course_id: course_id, + time: datetime, + 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/controllers/user_controller.ex b/lib/cadet_web/controllers/user_controller.ex index 67b26f179..a243aecb7 100644 --- a/lib/cadet_web/controllers/user_controller.ex +++ b/lib/cadet_web/controllers/user_controller.ex @@ -143,7 +143,7 @@ defmodule CadetWeb.UserController do end end - def log_user_focus_change(conn, %{"state" => state}) do + def log_user_focus_change(conn, %{"course_id" => course_id, "state" => state}) do user = conn.assigns.current_user focus_state = case state do "0" -> "OUT of focus" @@ -152,14 +152,18 @@ defmodule CadetWeb.UserController do end if focus_state do - IO.puts("[#{Calendar.strftime(DateTime.utc_now(), "%Y-%m-%d %H:%M:%S")}] User #{user.id} (#{user.name}) is #{focus_state}.") + # IO.puts("[#{Calendar.strftime(DateTime.utc_now(), "%Y-%m-%d %H:%M:%S")}] User #{user.id} (#{user.name}) is #{focus_state}.") + # conn + # |> put_status(200) + 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(200) + |> put_status(403) + |> text("invalid user focus state") end - - conn - |> put_status(403) - |> text("invalid user focus state") end def update_research_agreement(conn, %{"agreedToResearch" => agreed_to_research}) do 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..30b650875 --- /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 From ed16cb4c417c987ef7eb3f574f13f9892a48e1f9 Mon Sep 17 00:00:00 2001 From: Izumi Kyouka Date: Sat, 5 Apr 2025 00:08:24 +0800 Subject: [PATCH 30/38] removed redundant logic from focus logging controller --- lib/cadet_web/controllers/user_controller.ex | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/lib/cadet_web/controllers/user_controller.ex b/lib/cadet_web/controllers/user_controller.ex index a243aecb7..9f74040a9 100644 --- a/lib/cadet_web/controllers/user_controller.ex +++ b/lib/cadet_web/controllers/user_controller.ex @@ -146,15 +146,12 @@ defmodule CadetWeb.UserController do def log_user_focus_change(conn, %{"course_id" => course_id, "state" => state}) do user = conn.assigns.current_user focus_state = case state do - "0" -> "OUT of focus" - "1" -> "IN focus" + "0" -> 0 + "1" -> 1 _ -> nil end if focus_state do - # IO.puts("[#{Calendar.strftime(DateTime.utc_now(), "%Y-%m-%d %H:%M:%S")}] User #{user.id} (#{user.name}) is #{focus_state}.") - # conn - # |> put_status(200) case Cadet.FocusLogs.insert_log(user.id, course_id, state) do {:ok, _} -> conn |> send_resp(:ok, "") {:error, message} -> conn |> send_resp(500, message) From 22a5704d8b4003637e79106ff79764a2fb780eec Mon Sep 17 00:00:00 2001 From: Izumi Kyouka Date: Sun, 6 Apr 2025 00:56:48 +0800 Subject: [PATCH 31/38] sets a default resume_code in migration; add random resume code generation on creation of new course; rejects all course config update with empty resume code regardless of exam mode state --- lib/cadet/courses/course.ex | 18 +++++++----------- lib/cadet/courses/courses.ex | 10 ++++++++-- ...500_alter_courses_table_add_resume_code.exs | 2 +- 3 files changed, 16 insertions(+), 14 deletions(-) diff --git a/lib/cadet/courses/course.ex b/lib/cadet/courses/course.ex index 961e3070b..38ced96ee 100644 --- a/lib/cadet/courses/course.ex +++ b/lib/cadet/courses/course.ex @@ -65,25 +65,21 @@ defmodule Cadet.Courses.Course do is_official_course = get_field(changeset, :is_official_course, false) case {enable_exam_mode, is_official_course, resume_code} do - {false, _, _} -> - changeset - - {true, false, _} -> + {_, _, ""} -> add_error( changeset, - :enable_exam_mode, - "Exam mode is only available for official institution course." + :resume_code, + "Resume code must not be empty." ) - {true, true, ""} -> + {true, false, _} -> add_error( changeset, - :resume_code, - "Resume code must be set to non-empty value upon enabling of exam mode." + :enable_exam_mode, + "Exam mode is only available for official institution course." ) - {_, _, _} -> - changeset + _ -> changeset end end 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/priv/repo/migrations/20250401214500_alter_courses_table_add_resume_code.exs b/priv/repo/migrations/20250401214500_alter_courses_table_add_resume_code.exs index 3205da665..1ad90dceb 100644 --- a/priv/repo/migrations/20250401214500_alter_courses_table_add_resume_code.exs +++ b/priv/repo/migrations/20250401214500_alter_courses_table_add_resume_code.exs @@ -3,7 +3,7 @@ defmodule Cadet.Repo.Migrations.AlterCoursesTableAddResumeCode do def change do alter table(:courses) do - add(:resume_code, :string, null: false, default: "") + add(:resume_code, :string, null: false, default: "resume_code") end end end From 6cba5f56a61288b443594d0864d84a0465f87119 Mon Sep 17 00:00:00 2001 From: Izumi Kyouka Date: Sun, 6 Apr 2025 01:04:09 +0800 Subject: [PATCH 32/38] fixed resume code validation to consider whitespace; formatting --- lib/cadet/courses/course.ex | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/lib/cadet/courses/course.ex b/lib/cadet/courses/course.ex index 38ced96ee..b9af6f826 100644 --- a/lib/cadet/courses/course.ex +++ b/lib/cadet/courses/course.ex @@ -60,7 +60,11 @@ defmodule Cadet.Courses.Course do # Validates combination of exam mode, resume code, and official course state defp validate_exam_mode(changeset, params) do - resume_code = Map.get(params, :resume_code, "") + resume_code = + params + |> Map.get(:resume_code, "") + |> String.trim() + enable_exam_mode = Map.get(params, :enable_exam_mode, false) is_official_course = get_field(changeset, :is_official_course, false) @@ -79,7 +83,8 @@ defmodule Cadet.Courses.Course do "Exam mode is only available for official institution course." ) - _ -> changeset + _ -> + changeset end end From f6ab3586d55277506ece8b98fb4015d79eff4a41 Mon Sep 17 00:00:00 2001 From: Izumi Kyouka Date: Wed, 9 Apr 2025 00:27:20 +0800 Subject: [PATCH 33/38] added default resume_code value to schema definition in course.ex; made resume_code a required field --- lib/cadet/courses/course.ex | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/cadet/courses/course.ex b/lib/cadet/courses/course.ex index b9af6f826..c5076ba73 100644 --- a/lib/cadet/courses/course.ex +++ b/lib/cadet/courses/course.ex @@ -32,7 +32,7 @@ defmodule Cadet.Courses.Course do field(:enable_sourcecast, :boolean, default: true) field(:enable_stories, :boolean, default: false) field(:enable_exam_mode, :boolean, default: false) - field(:resume_code, :string) + field(:resume_code, :string, default: "resume_code") field(:is_official_course, :boolean, default: false) field(:source_chapter, :integer) field(:source_variant, :string) @@ -47,8 +47,8 @@ defmodule Cadet.Courses.Course do end @required_fields ~w(course_name viewable enable_game - 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 + enable_exam_mode enable_achievements enable_sourcecast enable_stories source_chapter source_variant resume_code)a + @optional_fields ~w(course_short_name module_help_text)a def changeset(course, params) do course From b99a82d778e6f864aab91a62842a40fa98939a36 Mon Sep 17 00:00:00 2001 From: Izumi Kyouka Date: Wed, 9 Apr 2025 00:35:33 +0800 Subject: [PATCH 34/38] improved swagger help text for resume_code field; fix formatting --- lib/cadet_web/admin_controllers/admin_courses_controller.ex | 2 +- lib/cadet_web/admin_controllers/admin_user_controller.ex | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/cadet_web/admin_controllers/admin_courses_controller.ex b/lib/cadet_web/admin_controllers/admin_courses_controller.ex index d2fd4047f..234455a99 100644 --- a/lib/cadet_web/admin_controllers/admin_courses_controller.ex +++ b/lib/cadet_web/admin_controllers/admin_courses_controller.ex @@ -109,7 +109,7 @@ defmodule CadetWeb.AdminCoursesController do 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 when attempt to open DevTool is detected") + 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 diff --git a/lib/cadet_web/admin_controllers/admin_user_controller.ex b/lib/cadet_web/admin_controllers/admin_user_controller.ex index 67e9b6986..53add9133 100644 --- a/lib/cadet_web/admin_controllers/admin_user_controller.ex +++ b/lib/cadet_web/admin_controllers/admin_user_controller.ex @@ -3,6 +3,7 @@ defmodule CadetWeb.AdminUserController do use PhoenixSwagger import Ecto.Query + alias Cadet.Repo alias Cadet.{Accounts, Assessments, Courses} alias Cadet.Accounts.{CourseRegistrations, CourseRegistration, Role} From 604b61b25a9832f9d5e114958a3eef83c7bd3c3c Mon Sep 17 00:00:00 2001 From: Izumi Kyouka Date: Wed, 9 Apr 2025 03:10:58 +0800 Subject: [PATCH 35/38] fixed bug in resume_code validation; added tests for exam_mode, is_official_course, and resume_code --- lib/cadet/courses/course.ex | 20 +++++++++------ .../admin_courses_controller.ex | 14 +++++++++-- test/cadet/courses/course_test.exs | 8 ++++++ .../admin_courses_controller_test.exs | 25 +++++++++++++++++++ 4 files changed, 58 insertions(+), 9 deletions(-) diff --git a/lib/cadet/courses/course.ex b/lib/cadet/courses/course.ex index c5076ba73..ce977dedb 100644 --- a/lib/cadet/courses/course.ex +++ b/lib/cadet/courses/course.ex @@ -32,7 +32,7 @@ defmodule Cadet.Courses.Course do field(:enable_sourcecast, :boolean, default: true) field(:enable_stories, :boolean, default: false) field(:enable_exam_mode, :boolean, default: false) - field(:resume_code, :string, default: "resume_code") + field(:resume_code, :string) field(:is_official_course, :boolean, default: false) field(:source_chapter, :integer) field(:source_variant, :string) @@ -47,8 +47,8 @@ defmodule Cadet.Courses.Course do end @required_fields ~w(course_name viewable enable_game - enable_exam_mode enable_achievements enable_sourcecast enable_stories source_chapter source_variant resume_code)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 @@ -60,23 +60,29 @@ defmodule Cadet.Courses.Course do # Validates combination of exam mode, resume code, and official course state defp validate_exam_mode(changeset, params) do - resume_code = + 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, resume_code} do - {_, _, ""} -> + 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, _} -> + {true, false, _, _} -> add_error( changeset, :enable_exam_mode, diff --git a/lib/cadet_web/admin_controllers/admin_courses_controller.ex b/lib/cadet_web/admin_controllers/admin_courses_controller.ex index 234455a99..d73d4f525 100644 --- a/lib/cadet_web/admin_controllers/admin_courses_controller.ex +++ b/lib/cadet_web/admin_controllers/admin_courses_controller.ex @@ -109,7 +109,13 @@ defmodule CadetWeb.AdminCoursesController do 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") + + 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 @@ -145,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/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_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] From ca082c9aa1cd827e4657af5f3451c9514ffd8fb6 Mon Sep 17 00:00:00 2001 From: Izumi Kyouka Date: Wed, 9 Apr 2025 23:08:06 +0800 Subject: [PATCH 36/38] formatting --- lib/cadet/focus_logs/focus_logs.ex | 18 +++++++++--------- lib/cadet_web/controllers/user_controller.ex | 14 ++++++++------ 2 files changed, 17 insertions(+), 15 deletions(-) diff --git a/lib/cadet/focus_logs/focus_logs.ex b/lib/cadet/focus_logs/focus_logs.ex index adbcf9ee9..33ebd8afc 100644 --- a/lib/cadet/focus_logs/focus_logs.ex +++ b/lib/cadet/focus_logs/focus_logs.ex @@ -4,15 +4,15 @@ defmodule Cadet.FocusLogs do use Cadet, [:context, :display] def insert_log(user_id, course_id, focus_type) do - datetime = DateTime.utc_now() |> DateTime.to_naive() - IO.puts(datetime) - insert_result = %FocusLog{} - |> FocusLog.changeset(%{ - user_id: user_id, - course_id: course_id, - time: datetime, - focus_type: focus_type}) - |> Repo.insert() + 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} diff --git a/lib/cadet_web/controllers/user_controller.ex b/lib/cadet_web/controllers/user_controller.ex index 9f74040a9..e6e440122 100644 --- a/lib/cadet_web/controllers/user_controller.ex +++ b/lib/cadet_web/controllers/user_controller.ex @@ -145,11 +145,13 @@ defmodule CadetWeb.UserController do 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 + + 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 @@ -159,7 +161,7 @@ defmodule CadetWeb.UserController do else conn |> put_status(403) - |> text("invalid user focus state") + |> text("Invalid user focus state") end end From 398bee0fa09ceec6c4e9cda2bfb621d6ed034499 Mon Sep 17 00:00:00 2001 From: Izumi Kyouka Date: Wed, 9 Apr 2025 23:10:09 +0800 Subject: [PATCH 37/38] formatting --- ...20250404231500_create_table_user_browser_focus_log.exs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) 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 index 30b650875..237d0a102 100644 --- a/priv/repo/migrations/20250404231500_create_table_user_browser_focus_log.exs +++ b/priv/repo/migrations/20250404231500_create_table_user_browser_focus_log.exs @@ -3,10 +3,10 @@ defmodule Cadet.Repo.Migrations.CreateUserBrowserFocusLogTable do 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 + 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 From 1c3d78726fcfd57e8fc7e69d534f36a07b73e714 Mon Sep 17 00:00:00 2001 From: Izumi Kyouka Date: Wed, 9 Apr 2025 23:16:07 +0800 Subject: [PATCH 38/38] add moduledoc for focus log --- lib/cadet/focus_logs/focus_log.ex | 3 ++- lib/cadet/focus_logs/focus_logs.ex | 4 ++++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/lib/cadet/focus_logs/focus_log.ex b/lib/cadet/focus_logs/focus_log.ex index c75da00eb..47cac0bbc 100644 --- a/lib/cadet/focus_logs/focus_log.ex +++ b/lib/cadet/focus_logs/focus_log.ex @@ -1,6 +1,7 @@ defmodule Cadet.FocusLogs.FocusLog do @moduledoc """ - The Conversation entity stores the messages exchanged between the user and the chatbot. + The FocusLog entity represents a log of user's browser focus + while using Source Academy under exam mode. """ use Cadet, :model diff --git a/lib/cadet/focus_logs/focus_logs.ex b/lib/cadet/focus_logs/focus_logs.ex index 33ebd8afc..3eca5344c 100644 --- a/lib/cadet/focus_logs/focus_logs.ex +++ b/lib/cadet/focus_logs/focus_logs.ex @@ -1,4 +1,8 @@ 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]