-
Notifications
You must be signed in to change notification settings - Fork 56
Added Exam mode #1236
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Added Exam mode #1236
Changes from all commits
fc72d29
2fc38ca
9953b9c
f450e85
0bb9857
d7ad8b3
f216d1b
48fff28
b5f2722
bc4c48c
46c2ff0
fe80662
f3c9ea9
665ad2a
abb1014
82afebf
1db4420
d88eeba
d9a97e3
40ea386
7ae6f14
e62fcef
e0330f2
d971fcd
40ce982
0302764
7fcbc89
cad39c8
46af8d7
6531376
3dcc288
31bbf5a
48c1b10
e0758fb
9154177
7bd79fd
ed16cb4
22a5704
6cba5f5
f6ab358
b99a82d
604b61b
ca082c9
398bee0
1c3d787
434d21c
58906d7
7d1a400
f1ed0bc
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -18,6 +18,9 @@ defmodule Cadet.Courses.Course do | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| top_contest_leaderboard_display: integer(), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| enable_sourcecast: boolean(), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| enable_stories: boolean(), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| enable_exam_mode: boolean(), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| resume_code: string(), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| is_official_course: boolean(), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| source_chapter: integer(), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| source_variant: String.t(), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| module_help_text: String.t(), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -36,6 +39,9 @@ defmodule Cadet.Courses.Course do | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| field(:top_contest_leaderboard_display, :integer, default: 10) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| 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) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
GabrielCWT marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| field(:source_chapter, :integer) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| field(:source_variant, :string) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| field(:module_help_text, :string) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -49,14 +55,51 @@ defmodule Cadet.Courses.Course do | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| end | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| @required_fields ~w(course_name viewable enable_game | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| enable_achievements enable_overall_leaderboard enable_contest_leaderboard top_leaderboard_display top_contest_leaderboard_display enable_sourcecast enable_stories source_chapter source_variant)a | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| @optional_fields ~w(course_short_name module_help_text)a | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| enable_exam_mode enable_achievements enable_overall_leaderboard enable_contest_leaderboard top_leaderboard_display top_contest_leaderboard_display enable_sourcecast enable_stories source_chapter source_variant)a | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| @optional_fields ~w(course_short_name module_help_text resume_code)a | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| def changeset(course, params) do | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| course | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| |> cast(params, @required_fields ++ @optional_fields) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| |> validate_required(@required_fields) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| |> validate_sublanguage_combination(params) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| |> validate_exam_mode(params) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| end | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # Validates combination of exam mode, resume code, and official course state | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| defp validate_exam_mode(changeset, params) do | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| has_resume_code = params |> Map.has_key?(:resume_code) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| resume_code_params = | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| params | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| |> Map.get(:resume_code, "") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| |> String.trim() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| resume_code = | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| changeset | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| |> get_field(:resume_code) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| enable_exam_mode = Map.get(params, :enable_exam_mode, false) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| is_official_course = get_field(changeset, :is_official_course, false) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| case {enable_exam_mode, is_official_course, has_resume_code, resume_code_params} do | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| {_, _, true, ""} -> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| add_error( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| changeset, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| :resume_code, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "Resume code must not be empty." | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| {true, false, _, _} -> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+71
to
+93
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| has_resume_code = params |> Map.has_key?(:resume_code) | |
| resume_code_params = | |
| params | |
| |> Map.get(:resume_code, "") | |
| |> String.trim() | |
| resume_code = | |
| changeset | |
| |> get_field(:resume_code) | |
| enable_exam_mode = Map.get(params, :enable_exam_mode, false) | |
| is_official_course = get_field(changeset, :is_official_course, false) | |
| case {enable_exam_mode, is_official_course, has_resume_code, resume_code_params} do | |
| {_, _, true, ""} -> | |
| add_error( | |
| changeset, | |
| :resume_code, | |
| "Resume code must not be empty." | |
| ) | |
| {true, false, _, _} -> | |
| resume_code_params = | |
| 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) | |
| case {enable_exam_mode, is_official_course, resume_code_params} do | |
| {_, _, ""} -> | |
| add_error( | |
| changeset, | |
| :resume_code, | |
| "Resume code must not be empty." | |
| ) | |
| {true, false, _} -> |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -25,14 +25,15 @@ 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 | ||
| Logger.info("Creating new course configuration for user #{user.id}") | ||
|
|
||
| result = | ||
| 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, | ||
|
|
@@ -118,6 +119,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) | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,32 @@ | ||
| defmodule Cadet.FocusLogs.FocusLog do | ||
| @moduledoc """ | ||
| The FocusLog entity represents a log of user's browser focus | ||
| while using Source Academy under exam mode. | ||
| """ | ||
| use Cadet, :model | ||
|
|
||
| alias Cadet.Accounts.User | ||
| alias Cadet.Courses.Course | ||
|
|
||
| @type t :: %__MODULE__{ | ||
| user: User.t(), | ||
| course: Course.t(), | ||
| focus_type: integer() | ||
| } | ||
|
|
||
| schema "user_browser_focus_log" do | ||
| belongs_to(:user, User) | ||
| belongs_to(:course, Course) | ||
| field(:time, :naive_datetime) | ||
| field(:focus_type, :integer) | ||
| end | ||
|
|
||
| @required_fields ~w(user_id course_id time focus_type)a | ||
|
|
||
| def changeset(focus_log, params) do | ||
| focus_log | ||
| |> cast(params, @required_fields) | ||
| |> add_belongs_to_id_from_model([:user, :course], params) | ||
| |> validate_required(@required_fields) | ||
| end | ||
| end |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,26 @@ | ||
| defmodule Cadet.FocusLogs do | ||
| @moduledoc """ | ||
| Contains logic to manage user's browser focus log | ||
| such as insertion | ||
| """ | ||
| alias Cadet.FocusLogs.FocusLog | ||
|
|
||
| use Cadet, [:context, :display] | ||
|
|
||
| def insert_log(user_id, course_id, focus_type) do | ||
| insert_result = | ||
| %FocusLog{} | ||
| |> FocusLog.changeset(%{ | ||
| user_id: user_id, | ||
| course_id: course_id, | ||
| time: DateTime.utc_now(), | ||
| focus_type: focus_type | ||
| }) | ||
| |> Repo.insert() | ||
|
|
||
| case insert_result do | ||
| {:ok, log} -> {:ok, log} | ||
| {:error, changeset} -> {:error, full_error_messages(changeset)} | ||
| end | ||
| end | ||
| end |
| Original file line number | Diff line number | Diff line change | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -13,11 +13,11 @@ defmodule CadetWeb.CoursesController do | |||||||||
|
|
||||||||||
| case Courses.get_course_config(course_id) do | ||||||||||
| {:ok, config} -> | ||||||||||
| Logger.info( | ||||||||||
| "Successfully retrieved course configuration for user #{user.id} and course #{course_id}." | ||||||||||
| ) | ||||||||||
|
|
||||||||||
| render(conn, "config.json", config: config) | ||||||||||
| if conn.assigns.course_reg.role == :admin do | ||||||||||
| render(conn, "config_admin.json", config: config) | ||||||||||
|
||||||||||
| render(conn, "config_admin.json", config: config) | |
| # Remove resume_code before rendering for admins | |
| config_admin = Map.delete(config, :resume_code) | |
| render(conn, "config_admin.json", config: config_admin) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is there a reason why this is linked to the user and not their course_registration? Isn't exam mode related to their course.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Initially I thought linking this to the user would allow for the enforcing of the pause beyond the course so that if a user is paused due to opening other app / using dev tool (which is our plan), the user will have to settle the problem with the course admin / coordinator. But, now that you pointed out, maybe this should not affect the user through all their courses. Should I move this course_registration?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@RichDom2185 thoughts?