diff --git a/.tool-versions b/.tool-versions new file mode 100644 index 000000000..bbf0778cb --- /dev/null +++ b/.tool-versions @@ -0,0 +1,2 @@ +elixir 1.13.4 +erlang 25.3.2 diff --git a/lib/cadet/notebooks/cell.ex b/lib/cadet/notebooks/cell.ex new file mode 100644 index 000000000..a6685e09a --- /dev/null +++ b/lib/cadet/notebooks/cell.ex @@ -0,0 +1,29 @@ +defmodule Cadet.Notebooks.Cell do + @moduledoc """ + The Cell entity stores content of a Notebook cell + """ + use Cadet, :model + + alias Cadet.Notebooks.{Notebook, Environment} + + schema "cell" do + field(:iscode, :boolean) + field(:content, :string) + field(:output, :string) + field(:index, :integer) + + belongs_to(:notebook, Notebook) + belongs_to(:environment, Environment) + + timestamps() + end + + @required_fields ~w(iscode content output index notebook environment)a + + def changeset(cell, attrs \\ %{}) do + cell + |> cast(attrs, @required_fields) + |> validate_required(@required_fields) + |> add_belongs_to_id_from_model([:notebook, :environment], attrs) + end +end diff --git a/lib/cadet/notebooks/environment.ex b/lib/cadet/notebooks/environment.ex new file mode 100644 index 000000000..d823ab90a --- /dev/null +++ b/lib/cadet/notebooks/environment.ex @@ -0,0 +1,20 @@ +defmodule Cadet.Notebooks.Environment do + @moduledoc """ + The Environment entity stores environment names of Notebook cells + """ + use Cadet, :model + + schema "environment" do + field(:name, :string) + + timestamps() + end + + @required_fields ~w(name)a + + def changeset(environment, attrs \\ %{}) do + environment + |> cast(attrs, @required_fields) + |> validate_required(@required_fields) + end +end diff --git a/lib/cadet/notebooks/notebook.ex b/lib/cadet/notebooks/notebook.ex new file mode 100644 index 000000000..a0eeafafc --- /dev/null +++ b/lib/cadet/notebooks/notebook.ex @@ -0,0 +1,34 @@ +defmodule Cadet.Notebooks.Notebook do + @moduledoc """ + The Notebook entity stores metadata of a notebook + """ + use Cadet, :model + + alias Cadet.Courses.Course + alias Cadet.Accounts.{User, CourseRegistration} + + schema "notebooks" do + field(:title, :string) + field(:config, :string) + field(:is_published, :boolean, default: false) + field(:pin_order, :integer) + + belongs_to(:course, Course) + # author + belongs_to(:user, User) + # to get role + belongs_to(:course_registration, CourseRegistration) + + timestamps() + end + + @required_fields ~w(title config course user course_registration pin_order)a + @optional_fields ~w(is_published)a + + def changeset(notebooks, attrs \\ %{}) do + notebooks + |> cast(attrs, @required_fields ++ @optional_fields) + |> validate_required(@required_fields) + |> add_belongs_to_id_from_model([:user, :course_registration, :course], attrs) + end +end diff --git a/lib/cadet/notebooks/notebooks.ex b/lib/cadet/notebooks/notebooks.ex new file mode 100644 index 000000000..e9502f97e --- /dev/null +++ b/lib/cadet/notebooks/notebooks.ex @@ -0,0 +1,146 @@ +defmodule Cadet.Notebooks.Notebooks do + @moduledoc """ + Manages notebooks for Source Academy + """ + use Cadet, [:context, :display] + + import Ecto.Query + + alias Cadet.Repo + alias Cadet.Notebooks.{Notebook, Cell, Environment} + alias Cadet.Accounts.CourseRegistration + + def list_notebook_for_user(user_id, course_id) do + role = + Repo.one( + from(cr in CourseRegistration, + where: cr.user == ^user_id and cr.course == ^course_id, + select: cr.role + ) + ) + + if role == :admin do + Notebook + |> join(:inner, [n], cr in CourseRegistration, on: cr.id == n.course_registration) + |> where([n, cr], cr.role == :admin) + |> where([n], n.is_published == false) + |> Repo.all() + else + Notebook + |> where(course: ^course_id) + |> where(user: ^user_id) + |> Repo.all() + end + end + + def list_published_notebooks(course_id) do + Notebook + |> where(course: ^course_id) + |> where(is_published: true) + |> Repo.all() + end + + def list_notebook_cells(notebook_id) do + Cell + |> where(notebook: ^notebook_id) + |> preload(:environment) + |> Repo.all() + end + + def create_notebook(attrs = %{}, course_id, user_id, course_registration_id) do + case %Notebook{} + |> Notebook.changeset( + attrs + |> Map.put(:course_id, course_id) + |> Map.put(:user_id, user_id) + |> Map.put(:course_registration_id, course_registration_id) + ) + |> Repo.insert() do + {:ok, _} = result -> + result + + {:error, changeset} -> + {:error, {:bad_request, full_error_messages(changeset)}} + end + end + + def create_cell(attrs = %{}, notebook_id, environment_id) do + case %Cell{} + |> Cell.changeset( + attrs + |> Map.put(:notebook, notebook_id) + |> Map.put(:environment, environment_id) + ) + |> Repo.insert() do + {:ok, _} = result -> + result + + {:error, changeset} -> + {:error, {:bad_request, full_error_messages(changeset)}} + end + end + + def create_environment(attrs = %{}) do + case %Environment{} + |> Environment.changeset(attrs) + |> Repo.insert() do + {:ok, _} = result -> + result + + {:error, changeset} -> + {:error, {:bad_request, full_error_messages(changeset)}} + end + end + + def update_notebook(attrs = %{}, id) do + case Repo.get(Notebook, id) do + nil -> + {:error, {:not_found, "Notebook not found"}} + + notebook -> + notebook + |> Notebook.changeset(attrs) + |> Repo.update() + end + end + + def update_cell(attrs = %{}, cell_id, notebook_id) do + case Repo.get(Cell, cell_id) do + nil -> + {:error, {:not_found, "Cell not found"}} + + cell -> + if cell.notebook == notebook_id do + cell + |> Cell.changeset(attrs) + |> Repo.update() + else + {:error, {:forbidden, "Cell is not found in that notebook"}} + end + end + end + + def delete_notebook(id) do + case Repo.get(Notebook, id) do + nil -> + {:error, {:not_found, "Notebook not found"}} + + notebook -> + Repo.delete(notebook) + end + end + + def delete_cell(cell_id, notebook_id) do + case Repo.get(Cell, cell_id) do + nil -> + {:error, {:not_found, "Cell not found"}} + + cell -> + if cell.notebook == notebook_id do + Repo.delete(cell) + else + {:error, {:forbidden, "Cell is not found in that notebook"}} + end + end + end +end diff --git a/priv/repo/migrations/20250402083623_create_notebooks.exs b/priv/repo/migrations/20250402083623_create_notebooks.exs new file mode 100644 index 000000000..7d593cebe --- /dev/null +++ b/priv/repo/migrations/20250402083623_create_notebooks.exs @@ -0,0 +1,24 @@ +defmodule Cadet.Repo.Migrations.CreateNotebooks do + use Ecto.Migration + + def change do + create table(:notebooks) do + add(:title, :string, null: false) + add(:config, :string) + add(:is_published, :boolean, default: false) + add(:pin_order, :integer) + + # Foreign keys + add(:course, references(:courses), null: false) + add(:user, references(:users), null: false) + add(:course_registration, references(:course_registrations), null: false) + + timestamps() + end + + create(index(:notebooks, [:course_registration])) + create(index(:notebooks, [:user])) + create(index(:notebooks, [:course])) + create(index(:notebooks, [:is_published])) + end +end diff --git a/priv/repo/migrations/20250402085122_create_environments.exs b/priv/repo/migrations/20250402085122_create_environments.exs new file mode 100644 index 000000000..339782f34 --- /dev/null +++ b/priv/repo/migrations/20250402085122_create_environments.exs @@ -0,0 +1,11 @@ +defmodule Cadet.Repo.Migrations.CreateEnvironments do + use Ecto.Migration + + def change do + create table(:environments) do + add(:name, :string, null: false) + + timestamps() + end + end +end diff --git a/priv/repo/migrations/20250402085427_create_cells.exs b/priv/repo/migrations/20250402085427_create_cells.exs new file mode 100644 index 000000000..95e7083cc --- /dev/null +++ b/priv/repo/migrations/20250402085427_create_cells.exs @@ -0,0 +1,20 @@ +defmodule Cadet.Repo.Migrations.CreateCells do + use Ecto.Migration + + def change do + create table(:cells) do + add(:iscode, :boolean, null: false) + add(:content, :string, null: false) + add(:output, :string, null: false) + add(:index, :integer, null: false) + + add(:notebook, references(:notebooks), null: false) + add(:environment, references(:environments), null: false) + + timestamps() + end + + create(index(:cells, [:notebook])) + create(index(:cells, [:environment])) + end +end