diff --git a/.tool-versions b/.tool-versions new file mode 100644 index 000000000..54c50512d --- /dev/null +++ b/.tool-versions @@ -0,0 +1,2 @@ +elixer 1.13.4 +erlang 25.3.2 diff --git a/lib/cadet/accounts/page.ex b/lib/cadet/accounts/page.ex new file mode 100644 index 000000000..f7af25521 --- /dev/null +++ b/lib/cadet/accounts/page.ex @@ -0,0 +1,33 @@ +defmodule Cadet.Accounts.Page do + @moduledoc """ + The Page entity represents data about a specific page, + as of now it just contains time spent at the page. + """ + + use Cadet, :model + alias Cadet.Accounts.{CourseRegistration, User} + alias Cadet.Courses.Course + + schema "pages" do + field(:path, :string) + field(:time_spent, :integer) + + belongs_to(:user, User) + belongs_to(:course_registration, CourseRegistration, type: :integer) + belongs_to(:course, Course) + + timestamps() + end + + @required_fields ~w(user_id path time_spent)a + @optional_fields ~w(course_registration_id course_id)a + + def changeset(page, params \\ %{}) do + page + |> cast(params, @required_fields ++ @optional_fields) + |> validate_required(@required_fields) + |> foreign_key_constraint(:user_id) + |> foreign_key_constraint(:course_registration_id) + |> foreign_key_constraint(:course_id) + end +end diff --git a/lib/cadet/accounts/pages.ex b/lib/cadet/accounts/pages.ex new file mode 100644 index 000000000..01bf65738 --- /dev/null +++ b/lib/cadet/accounts/pages.ex @@ -0,0 +1,110 @@ +defmodule Cadet.Accounts.Pages do + @moduledoc """ + Provides functions to manage page data + """ + use Cadet, [:context, :display] + + import Ecto.Query + + alias Cadet.Repo + alias Cadet.Accounts.{CourseRegistration, Page} + + # Get time spent by user at a specific path + def get_user_time_spent_at_path(user_id, path) do + Page + |> where([p], p.user_id == ^user_id and p.path == ^path) + |> select([p], p.time_spent) + |> Repo.one() + end + + # Get all time spent entries for a specific user + def get_user_all_time_spent(user_id) do + Page + |> where([p], p.user_id == ^user_id) + |> select([p], {p.path, p.time_spent}) + |> order_by([p], desc: p.time_spent) + |> Repo.all() + |> Enum.into(%{}) + end + + # Get total time spent by a specific user + def get_user_total_time_spent(user_id) do + Page + |> where([p], p.user_id == ^user_id) + |> select([p], sum(p.time_spent)) + |> Repo.one() + end + + # Get total time spent by a specific user on a specific course + def get_user_total_time_spent_on_course(course_registration_id) do + Page + |> where([p], p.course_registration_id == ^course_registration_id) + |> select([p], sum(p.time_spent)) + |> Repo.one() + end + + # Get aggregate time spent for a specific course and path + def get_aggregate_time_spent_at_path(course_id, path) do + Page + |> where([p], p.course_id == ^course_id and p.path == ^path) + |> select([p], sum(p.time_spent)) + |> Repo.one() + end + + # Get aggregate time spent for a specific course (all paths) + def get_aggregate_time_spent_on_course(course_id) do + Page + |> where([p], p.course_id == ^course_id) + |> select([p], sum(p.time_spent)) + |> Repo.one() + end + + # Upsert time spent for a user on a specific path + def upsert_time_spent_by_user(user_id, path, time_spent) do + Page + |> where([p], p.user_id == ^user_id and p.path == ^path) + |> Repo.one() + |> case do + # If no entry found, create a new one + nil -> + %Page{user_id: user_id, path: path, time_spent: time_spent} + |> Repo.insert() + + # If entry exists, update the time_spent + page -> + page + |> Page.changeset(%{time_spent: page.time_spent + time_spent}) + |> Repo.update() + end + end + + # Upsert time spent for a user on a specific path + def upsert_time_spent_by_course_registration(course_registration_id, path, time_spent) do + Page + |> where([p], p.course_registration_id == ^course_registration_id and p.path == ^path) + |> Repo.one() + |> case do + nil -> + {user_id, course_id} = + CourseRegistration + |> where([c], c.id == ^course_registration_id) + |> select([c], {c.user_id, c.course_id}) + |> Repo.one() + + %Page{ + user_id: user_id, + course_registration_id: course_registration_id, + course_id: course_id, + path: path, + time_spent: time_spent + } + |> Repo.insert() + + page -> + page + # Properly pass the map to the changeset + |> Page.changeset(%{time_spent: page.time_spent + time_spent}) + |> Repo.update() + end + end +end diff --git a/lib/cadet_web/controllers/user_controller.ex b/lib/cadet_web/controllers/user_controller.ex index cfc162652..885ca4a19 100644 --- a/lib/cadet_web/controllers/user_controller.ex +++ b/lib/cadet_web/controllers/user_controller.ex @@ -5,7 +5,7 @@ defmodule CadetWeb.UserController do use CadetWeb, :controller use PhoenixSwagger - alias Cadet.Accounts.CourseRegistrations + alias Cadet.Accounts.{CourseRegistrations, Pages} alias Cadet.{Accounts, Assessments} @@ -121,6 +121,24 @@ defmodule CadetWeb.UserController do json(conn, %{totalXp: total_xp}) end + def update_time_spent(conn, %{"path" => site_path, "time" => time_spent}) do + course_registration_id = conn.assigns.course_reg.id + + case Pages.upsert_time_spent_by_course_registration( + course_registration_id, + site_path, + time_spent + ) do + {:ok, %{}} -> + text(conn, "OK") + + {:error, {status, message}} -> + conn + |> put_status(status) + |> text(message) + end + end + swagger_path :index do get("/user") diff --git a/lib/cadet_web/router.ex b/lib/cadet_web/router.ex index 1ee304c01..cd55cfe32 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) + post("/user/update_time_spent", UserController, :update_time_spent) get("/config", CoursesController, :index) diff --git a/priv/repo/migrations/20250402022830_create_pages.exs b/priv/repo/migrations/20250402022830_create_pages.exs new file mode 100644 index 000000000..d5b89ba4d --- /dev/null +++ b/priv/repo/migrations/20250402022830_create_pages.exs @@ -0,0 +1,19 @@ +defmodule Cadet.Repo.Migrations.CreatePages do + use Ecto.Migration + + def change do + create table(:pages) do + add(:user_id, references(:users, on_delete: :nothing), null: false) + + add(:course_registration_id, references(:course_registrations, on_delete: :nothing), + null: true + ) + + add(:course_id, references(:courses, on_delete: :nothing), null: true) + add(:path, :string, null: false) + add(:time_spent, :integer, null: false) + + timestamps() + end + end +end