diff --git a/lib/lanttern/schools.ex b/lib/lanttern/schools.ex
index 238223b8..b26c9f34 100644
--- a/lib/lanttern/schools.ex
+++ b/lib/lanttern/schools.ex
@@ -7,6 +7,7 @@ defmodule Lanttern.Schools do
import Lanttern.RepoHelpers
alias Lanttern.Repo
alias Lanttern.Schools.School
+ alias Lanttern.Schools.Cycle
alias Lanttern.Schools.Class
alias Lanttern.Schools.Student
alias Lanttern.Schools.Teacher
@@ -108,6 +109,106 @@ defmodule Lanttern.Schools do
School.changeset(school, attrs)
end
+ @doc """
+ Returns the list of school cycles.
+
+ ### Options:
+
+ `:schools_ids` – filter classes by schools
+
+ ## Examples
+
+ iex> list_cycles()
+ [%Cycle{}, ...]
+
+ """
+ def list_cycles(opts \\ []) do
+ Cycle
+ |> maybe_filter_by_schools(opts)
+ |> Repo.all()
+ end
+
+ @doc """
+ Gets a single cycle.
+
+ Raises `Ecto.NoResultsError` if the Cycle does not exist.
+
+ ## Examples
+
+ iex> get_cycle!(123)
+ %Cycle{}
+
+ iex> get_cycle!(456)
+ ** (Ecto.NoResultsError)
+
+ """
+ def get_cycle!(id), do: Repo.get!(Cycle, id)
+
+ @doc """
+ Creates a cycle.
+
+ ## Examples
+
+ iex> create_cycle(%{field: value})
+ {:ok, %Cycle{}}
+
+ iex> create_cycle(%{field: bad_value})
+ {:error, %Ecto.Changeset{}}
+
+ """
+ def create_cycle(attrs \\ %{}) do
+ %Cycle{}
+ |> Cycle.changeset(attrs)
+ |> Repo.insert()
+ end
+
+ @doc """
+ Updates a cycle.
+
+ ## Examples
+
+ iex> update_cycle(cycle, %{field: new_value})
+ {:ok, %Cycle{}}
+
+ iex> update_cycle(cycle, %{field: bad_value})
+ {:error, %Ecto.Changeset{}}
+
+ """
+ def update_cycle(%Cycle{} = cycle, attrs) do
+ cycle
+ |> Cycle.changeset(attrs)
+ |> Repo.update()
+ end
+
+ @doc """
+ Deletes a cycle.
+
+ ## Examples
+
+ iex> delete_cycle(cycle)
+ {:ok, %Cycle{}}
+
+ iex> delete_cycle(cycle)
+ {:error, %Ecto.Changeset{}}
+
+ """
+ def delete_cycle(%Cycle{} = cycle) do
+ Repo.delete(cycle)
+ end
+
+ @doc """
+ Returns an `%Ecto.Changeset{}` for tracking cycle changes.
+
+ ## Examples
+
+ iex> change_cycle(cycle)
+ %Ecto.Changeset{data: %Cycle{}}
+
+ """
+ def change_cycle(%Cycle{} = cycle, attrs \\ %{}) do
+ Cycle.changeset(cycle, attrs)
+ end
+
@doc """
Returns the list of classes.
@@ -124,28 +225,43 @@ defmodule Lanttern.Schools do
"""
def list_classes(opts \\ []) do
Class
- |> maybe_filter_classes_by_schools(opts)
+ |> maybe_filter_by_schools(opts)
|> Repo.all()
|> maybe_preload(opts)
end
- defp maybe_filter_classes_by_schools(classes_query, opts) do
- case Keyword.get(opts, :schools_ids) do
- nil ->
- classes_query
+ @doc """
+ Returns the list of user's school classes.
- schools_ids ->
- from(
- c in classes_query,
- where: c.school_id in ^schools_ids
- )
- end
+ The list is sorted by cycle end date (desc), class year (asc), and class name (asc).
+
+ ## Examples
+
+ iex> list_user_classes()
+ [%Class{}, ...]
+
+ """
+ def list_user_classes(%{current_profile: %{teacher: %{school_id: school_id}}} = _current_user) do
+ from(
+ cl in Class,
+ join: cy in assoc(cl, :cycle),
+ left_join: s in assoc(cl, :students),
+ left_join: y in assoc(cl, :years),
+ group_by: [cl.id, cy.end_at],
+ order_by: [desc: cy.end_at, asc: min(y.id), asc: cl.name],
+ where: cl.school_id == ^school_id,
+ preload: [:cycle, :students, :years]
+ )
+ |> Repo.all()
end
+ def list_user_classes(_current_user),
+ do: {:error, "User not allowed to list classes"}
+
@doc """
Gets a single class.
- Raises `Ecto.NoResultsError` if the Class does not exist.
+ Returns nil if the Class does not exist.
### Options:
@@ -157,8 +273,18 @@ defmodule Lanttern.Schools do
%Class{}
iex> get_class!(456)
- ** (Ecto.NoResultsError)
+ nil
+ """
+ def get_class(id, opts \\ []) do
+ Repo.get(Class, id)
+ |> maybe_preload(opts)
+ end
+
+ @doc """
+ Gets a single class.
+
+ Same as `get_class/2`, but raises `Ecto.NoResultsError` if the Class does not exist.
"""
def get_class!(id, opts \\ []) do
Repo.get!(Class, id)
@@ -197,7 +323,7 @@ defmodule Lanttern.Schools do
"""
def update_class(%Class{} = class, attrs) do
class
- |> Repo.preload(:students)
+ |> Repo.preload([:students, :years])
|> Class.changeset(attrs)
|> Repo.update()
end
@@ -269,7 +395,7 @@ defmodule Lanttern.Schools do
@doc """
Gets a single student.
- Raises `Ecto.NoResultsError` if the Student does not exist.
+ Returns `nil` if the Student does not exist.
### Options:
@@ -277,12 +403,22 @@ defmodule Lanttern.Schools do
## Examples
- iex> get_student!(123)
+ iex> get_student(123)
%Student{}
- iex> get_student!(456)
- ** (Ecto.NoResultsError)
+ iex> get_student(456)
+ nil
+
+ """
+ def get_student(id, opts \\ []) do
+ Repo.get(Student, id)
+ |> maybe_preload(opts)
+ end
+ @doc """
+ Gets a single student.
+
+ Same as `get_student/2`, but raises `Ecto.NoResultsError` if the Student does not exist.
"""
def get_student!(id, opts \\ []) do
Repo.get!(Student, id)
@@ -482,10 +618,10 @@ defmodule Lanttern.Schools do
[{csv_student, {:ok, %Student{}}}, ...]
"""
- def create_students_from_csv(csv_rows, class_name_id_map, school_id) do
+ def create_students_from_csv(csv_rows, class_name_id_map, school_id, cycle_id) do
Ecto.Multi.new()
|> Ecto.Multi.run(:classes, fn _repo, _changes ->
- insert_csv_classes(class_name_id_map, school_id)
+ insert_csv_classes(class_name_id_map, school_id, cycle_id)
end)
|> Ecto.Multi.run(:students, fn _repo, changes ->
insert_csv_students(changes, csv_rows, school_id)
@@ -506,26 +642,27 @@ defmodule Lanttern.Schools do
end
end
- defp insert_csv_classes(class_name_id_map, school_id) do
+ defp insert_csv_classes(class_name_id_map, school_id, cycle_id) do
name_class_map =
class_name_id_map
- |> Enum.map(&get_or_insert_csv_class(&1, school_id))
+ |> Enum.map(&get_or_insert_csv_class(&1, school_id, cycle_id))
|> Enum.into(%{})
{:ok, name_class_map}
end
- defp get_or_insert_csv_class({csv_class_name, ""}, school_id) do
+ defp get_or_insert_csv_class({csv_class_name, ""}, school_id, cycle_id) do
{:ok, class} =
create_class(%{
name: csv_class_name,
- school_id: school_id
+ school_id: school_id,
+ cycle_id: cycle_id
})
{csv_class_name, class}
end
- defp get_or_insert_csv_class({csv_class_name, class_id}, _school_id),
+ defp get_or_insert_csv_class({csv_class_name, class_id}, _school_id, _cycle_id),
do: {csv_class_name, get_class!(class_id)}
defp insert_csv_students(%{classes: name_class_map} = _changes, csv_rows, school_id) do
@@ -715,4 +852,19 @@ defmodule Lanttern.Schools do
{:ok, response}
end
+
+ # Helpers
+
+ defp maybe_filter_by_schools(query, opts) do
+ case Keyword.get(opts, :schools_ids) do
+ nil ->
+ query
+
+ schools_ids ->
+ from(
+ q in query,
+ where: q.school_id in ^schools_ids
+ )
+ end
+ end
end
diff --git a/lib/lanttern/schools/class.ex b/lib/lanttern/schools/class.ex
index d8c0327c..703f09d4 100644
--- a/lib/lanttern/schools/class.ex
+++ b/lib/lanttern/schools/class.ex
@@ -8,12 +8,20 @@ defmodule Lanttern.Schools.Class do
schema "classes" do
field :name, :string
field :students_ids, {:array, :id}, virtual: true
+ field :years_ids, {:array, :id}, virtual: true
belongs_to :school, Lanttern.Schools.School
+ belongs_to :cycle, Lanttern.Schools.Cycle
many_to_many :students, Lanttern.Schools.Student,
join_through: "classes_students",
- on_replace: :delete
+ on_replace: :delete,
+ preload_order: [asc: :name]
+
+ many_to_many :years, Lanttern.Taxonomy.Year,
+ join_through: "classes_years",
+ on_replace: :delete,
+ preload_order: [asc: :id]
timestamps()
end
@@ -21,9 +29,15 @@ defmodule Lanttern.Schools.Class do
@doc false
def changeset(class, attrs) do
class
- |> cast(attrs, [:name, :school_id, :students_ids])
- |> validate_required([:name, :school_id])
+ |> cast(attrs, [:name, :school_id, :students_ids, :years_ids, :cycle_id])
+ |> validate_required([:name, :school_id, :cycle_id])
+ |> foreign_key_constraint(
+ :cycle_id,
+ name: :classes_cycle_id_fkey,
+ message: "Check if the cycle exists and belongs to the same school"
+ )
|> put_students()
+ |> put_years()
end
defp put_students(changeset) do
@@ -43,4 +57,22 @@ defmodule Lanttern.Schools.Class do
changeset
|> put_assoc(:students, students)
end
+
+ defp put_years(changeset) do
+ put_years(
+ changeset,
+ get_change(changeset, :years_ids)
+ )
+ end
+
+ defp put_years(changeset, nil), do: changeset
+
+ defp put_years(changeset, years_ids) do
+ years =
+ from(y in Lanttern.Taxonomy.Year, where: y.id in ^years_ids)
+ |> Repo.all()
+
+ changeset
+ |> put_assoc(:years, years)
+ end
end
diff --git a/lib/lanttern/schools/cycle.ex b/lib/lanttern/schools/cycle.ex
new file mode 100644
index 00000000..630988be
--- /dev/null
+++ b/lib/lanttern/schools/cycle.ex
@@ -0,0 +1,25 @@
+defmodule Lanttern.Schools.Cycle do
+ use Ecto.Schema
+ import Ecto.Changeset
+
+ schema "school_cycles" do
+ field :name, :string
+ field :start_at, :date
+ field :end_at, :date
+ belongs_to :school, Lanttern.Schools.School
+
+ timestamps()
+ end
+
+ @doc false
+ def changeset(cycle, attrs) do
+ cycle
+ |> cast(attrs, [:name, :start_at, :end_at, :school_id])
+ |> validate_required([:name, :start_at, :end_at, :school_id])
+ |> check_constraint(
+ :end_at,
+ name: :cycle_end_date_is_greater_than_start_date,
+ message: "End date should be greater than start date"
+ )
+ end
+end
diff --git a/lib/lanttern/schools/student.ex b/lib/lanttern/schools/student.ex
index 0a70996c..43ba51e3 100644
--- a/lib/lanttern/schools/student.ex
+++ b/lib/lanttern/schools/student.ex
@@ -13,7 +13,8 @@ defmodule Lanttern.Schools.Student do
many_to_many :classes, Lanttern.Schools.Class,
join_through: "classes_students",
- on_replace: :delete
+ on_replace: :delete,
+ preload_order: [asc: :name]
timestamps()
end
diff --git a/lib/lanttern_web/components/core_components.ex b/lib/lanttern_web/components/core_components.ex
index 11035f8d..b990f784 100644
--- a/lib/lanttern_web/components/core_components.ex
+++ b/lib/lanttern_web/components/core_components.ex
@@ -368,6 +368,62 @@ defmodule LantternWeb.CoreComponents do
"""
end
+ @doc ~S"""
+ Renders a table with generic styling.
+
+ ## Examples
+
+ <.stream_table id="users" rows={@users}>
+ <:col :let={user} label="id"><%= user.id %>
+ <:col :let={user} label="username"><%= user.username %>
+
+ """
+ attr :id, :string, required: true
+ attr :stream, Phoenix.LiveView.LiveStream, required: true
+ attr :row_click, :any, default: nil, doc: "the function for handling phx-click on each row"
+
+ slot :col, required: true do
+ attr :label, :string
+ end
+
+ slot :action, doc: "the slot for showing user actions in the last table column"
+
+ def stream_table(assigns) do
+ ~H"""
+
+
+
+ <%= col[:label] %>
+
+ <%= gettext("Actions") %>
+
+
+
+
+
+
+ <%= render_slot(col, row) %>
+
+
+
+
+ <%= render_slot(action, row) %>
+
+
+
+
+
+
+ """
+ end
+
@doc """
Renders a data list.
@@ -535,6 +591,40 @@ defmodule LantternWeb.CoreComponents do
|> Map.get(theme, "text-ltrn-subtle")
end
+ @doc """
+ Renders a student or teacher badge.
+
+ ## Examples
+
+ <.person_badge person={student} />
+
+ """
+ attr :id, :string, default: nil
+ attr :person, :map, required: true
+ attr :theme, :string, default: "subtle", doc: "subtle | cyan"
+ attr :rest, :global
+
+ def person_badge(assigns) do
+ ~H"""
+
+ <.profile_icon profile_name={@person.name} size="xs" theme={@theme} />
+
+ <%= @person.name %>
+
+
+ """
+ end
+
+ defp person_badge_theme_style("cyan"), do: "text-ltrn-dark bg-ltrn-mesh-cyan"
+ defp person_badge_theme_style(_subtle), do: "text-ltrn-subtle bg-ltrn-lighter"
+
@doc """
Creates a style attr based on ordinal values `bg_color` and `text_color`
"""
@@ -581,7 +671,7 @@ defmodule LantternWeb.CoreComponents do
~H"""
+
+ """
+ attr :class, :any, default: nil
+
+ slot :item, required: true do
+ attr :link, :string
+ end
+
+ def breadcrumbs(assigns) do
+ ~H"""
+
+
+
+ 0}>/
+ <%= if Map.get(item, :link) do %>
+ <.link navigate={item.link} class="underline"><%= render_slot(item) %>
+ <% else %>
+ <%= render_slot(item) %>
+ <% end %>
+
+
+
+ """
+ end
end
diff --git a/lib/lanttern_web/controllers/admin_html/home.html.heex b/lib/lanttern_web/controllers/admin_html/home.html.heex
index 49f19507..663bd7eb 100644
--- a/lib/lanttern_web/controllers/admin_html/home.html.heex
+++ b/lib/lanttern_web/controllers/admin_html/home.html.heex
@@ -27,6 +27,7 @@
<.link_list title="Schools">
<:item link={~p"/admin/schools"}>Schools
+ <:item link={~p"/admin/school_cycles"}>Cycles
<:item link={~p"/admin/classes"}>Classes
<:item link={~p"/admin/students"}>Students
<:item link={~p"/admin/import_students"}>Import students
diff --git a/lib/lanttern_web/controllers/class_controller.ex b/lib/lanttern_web/controllers/class_controller.ex
index 4588de4a..b77f0afe 100644
--- a/lib/lanttern_web/controllers/class_controller.ex
+++ b/lib/lanttern_web/controllers/class_controller.ex
@@ -2,11 +2,12 @@ defmodule LantternWeb.ClassController do
use LantternWeb, :controller
import LantternWeb.SchoolsHelpers
+ import LantternWeb.TaxonomyHelpers
alias Lanttern.Schools
alias Lanttern.Schools.Class
def index(conn, _params) do
- classes = Schools.list_classes(preloads: [:school, :students])
+ classes = Schools.list_classes(preloads: [:school, :cycle, :years, :students])
render(conn, :index, classes: classes)
end
@@ -14,9 +15,13 @@ defmodule LantternWeb.ClassController do
changeset = Schools.change_class(%Class{})
student_options = generate_student_options()
school_options = generate_school_options()
+ cycle_options = generate_cycle_options()
+ year_options = generate_year_options()
render(conn, :new,
school_options: school_options,
+ cycle_options: cycle_options,
+ year_options: year_options,
student_options: student_options,
changeset: changeset
)
@@ -32,9 +37,13 @@ defmodule LantternWeb.ClassController do
{:error, %Ecto.Changeset{} = changeset} ->
student_options = generate_student_options()
school_options = generate_school_options()
+ cycle_options = generate_cycle_options()
+ year_options = generate_year_options()
render(conn, :new,
school_options: school_options,
+ cycle_options: cycle_options,
+ year_options: year_options,
student_options: student_options,
changeset: changeset
)
@@ -42,25 +51,34 @@ defmodule LantternWeb.ClassController do
end
def show(conn, %{"id" => id}) do
- class = Schools.get_class!(id, preloads: [:school, :students])
+ class = Schools.get_class!(id, preloads: [:school, :cycle, :years, :students])
render(conn, :show, class: class)
end
def edit(conn, %{"id" => id}) do
school_options = generate_school_options()
student_options = generate_student_options()
+ cycle_options = generate_cycle_options()
+ year_options = generate_year_options()
- class = Schools.get_class!(id, preloads: :students)
+ class = Schools.get_class!(id, preloads: [:students, :years])
- # insert existing students_ids
+ # insert existing students_ids and years
students_ids = Enum.map(class.students, & &1.id)
- class = class |> Map.put(:students_ids, students_ids)
+ years_ids = Enum.map(class.years, & &1.id)
+
+ class =
+ class
+ |> Map.put(:students_ids, students_ids)
+ |> Map.put(:years_ids, years_ids)
changeset = Schools.change_class(class)
render(conn, :edit,
class: class,
school_options: school_options,
+ cycle_options: cycle_options,
+ year_options: year_options,
student_options: student_options,
changeset: changeset
)
@@ -77,11 +95,15 @@ defmodule LantternWeb.ClassController do
{:error, %Ecto.Changeset{} = changeset} ->
school_options = generate_school_options()
+ cycle_options = generate_cycle_options()
+ year_options = generate_year_options()
student_options = generate_student_options()
render(conn, :edit,
class: class,
school_options: school_options,
+ cycle_options: cycle_options,
+ year_options: year_options,
student_options: student_options,
changeset: changeset
)
diff --git a/lib/lanttern_web/controllers/class_html.ex b/lib/lanttern_web/controllers/class_html.ex
index 44a0b3b5..2d6526ff 100644
--- a/lib/lanttern_web/controllers/class_html.ex
+++ b/lib/lanttern_web/controllers/class_html.ex
@@ -9,6 +9,8 @@ defmodule LantternWeb.ClassHTML do
attr :changeset, Ecto.Changeset, required: true
attr :action, :string, required: true
attr :school_options, :list, required: true
+ attr :year_options, :list, required: true
+ attr :cycle_options, :list, required: true
attr :student_options, :list, required: true
def class_form(assigns)
diff --git a/lib/lanttern_web/controllers/class_html/class_form.html.heex b/lib/lanttern_web/controllers/class_html/class_form.html.heex
index 7aebad91..3f499e2b 100644
--- a/lib/lanttern_web/controllers/class_html/class_form.html.heex
+++ b/lib/lanttern_web/controllers/class_html/class_form.html.heex
@@ -9,6 +9,21 @@
options={@school_options}
prompt="Select school"
/>
+ <.input
+ field={f[:cycle_id]}
+ type="select"
+ label="Cycle"
+ options={@cycle_options}
+ prompt="Select cycle"
+ />
+ <.input
+ field={f[:years_ids]}
+ type="select"
+ label="Years"
+ options={@year_options}
+ prompt="Select years"
+ multiple
+ />
<.input field={f[:name]} type="text" label="Name" />
<.input
field={f[:students_ids]}
diff --git a/lib/lanttern_web/controllers/class_html/edit.html.heex b/lib/lanttern_web/controllers/class_html/edit.html.heex
index 29632a54..a099aec3 100644
--- a/lib/lanttern_web/controllers/class_html/edit.html.heex
+++ b/lib/lanttern_web/controllers/class_html/edit.html.heex
@@ -5,6 +5,8 @@
<.class_form
school_options={@school_options}
+ cycle_options={@cycle_options}
+ year_options={@year_options}
student_options={@student_options}
changeset={@changeset}
action={~p"/admin/classes/#{@class}"}
diff --git a/lib/lanttern_web/controllers/class_html/index.html.heex b/lib/lanttern_web/controllers/class_html/index.html.heex
index f6595964..334a427b 100644
--- a/lib/lanttern_web/controllers/class_html/index.html.heex
+++ b/lib/lanttern_web/controllers/class_html/index.html.heex
@@ -10,6 +10,10 @@
<.table id="classes" rows={@classes} row_click={&JS.navigate(~p"/admin/classes/#{&1}")}>
<:col :let={class} label="Name"><%= class.name %>
<:col :let={class} label="School"><%= class.school.name %>
+ <:col :let={class} label="Cycle"><%= class.cycle.name %>
+ <:col :let={class} label="Years">
+ <%= class.years |> Enum.map(& &1.name) |> Enum.join(", ") %>
+
<:col :let={class} label="Students"><%= length(class.students) %>
<:action :let={class}>
diff --git a/lib/lanttern_web/controllers/class_html/new.html.heex b/lib/lanttern_web/controllers/class_html/new.html.heex
index 277f8334..184474e8 100644
--- a/lib/lanttern_web/controllers/class_html/new.html.heex
+++ b/lib/lanttern_web/controllers/class_html/new.html.heex
@@ -5,6 +5,8 @@
<.class_form
school_options={@school_options}
+ cycle_options={@cycle_options}
+ year_options={@year_options}
student_options={@student_options}
changeset={@changeset}
action={~p"/admin/classes"}
diff --git a/lib/lanttern_web/controllers/class_html/show.html.heex b/lib/lanttern_web/controllers/class_html/show.html.heex
index 0aad0a21..b2216bd7 100644
--- a/lib/lanttern_web/controllers/class_html/show.html.heex
+++ b/lib/lanttern_web/controllers/class_html/show.html.heex
@@ -11,6 +11,12 @@
<.list>
<:item title="Name"><%= @class.name %>
<:item title="School"><%= @class.school.name %>
+ <:item title="Cycle"><%= @class.cycle.name %>
+ <:item title="Years">
+ <%= @class.years
+ |> Enum.map(& &1.name)
+ |> Enum.join(", ") %>
+
<:item title="Students">
<%= @class.students
|> Enum.map(& &1.name)
diff --git a/lib/lanttern_web/helpers/schools_helpers.ex b/lib/lanttern_web/helpers/schools_helpers.ex
index a542794c..a705ae10 100644
--- a/lib/lanttern_web/helpers/schools_helpers.ex
+++ b/lib/lanttern_web/helpers/schools_helpers.ex
@@ -14,6 +14,21 @@ defmodule LantternWeb.SchoolsHelpers do
|> Enum.map(fn s -> {s.name, s.id} end)
end
+ @doc """
+ Generate list of cycles to use as `Phoenix.HTML.Form.options_for_select/2` arg
+
+ Accepts `list_opts` arg, which will be forwarded to `Schools.list_cycles/1`.
+
+ ## Examples
+
+ iex> generate_cycle_options()
+ [{"cycle name", 1}, ...]
+ """
+ def generate_cycle_options(list_opts \\ []) do
+ Schools.list_cycles(list_opts)
+ |> Enum.map(fn c -> {c.name, c.id} end)
+ end
+
@doc """
Generate list of classes to use as `Phoenix.HTML.Form.options_for_select/2` arg
diff --git a/lib/lanttern_web/live/admin/cycle_live/form_component.ex b/lib/lanttern_web/live/admin/cycle_live/form_component.ex
new file mode 100644
index 00000000..fc603e49
--- /dev/null
+++ b/lib/lanttern_web/live/admin/cycle_live/form_component.ex
@@ -0,0 +1,101 @@
+defmodule LantternWeb.Admin.CycleLive.FormComponent do
+ use LantternWeb, :live_component
+
+ alias Lanttern.Schools
+ import LantternWeb.SchoolsHelpers
+
+ @impl true
+ def render(assigns) do
+ ~H"""
+
+ <.header>
+ <%= @title %>
+ <:subtitle>Use this form to manage cycle records in your database.
+
+
+ <.simple_form
+ for={@form}
+ id="cycle-form"
+ phx-target={@myself}
+ phx-change="validate"
+ phx-submit="save"
+ >
+ <.input
+ field={@form[:school_id]}
+ type="select"
+ label="School"
+ prompt="Select school"
+ options={@school_options}
+ />
+ <.input field={@form[:name]} type="text" label="Name" />
+ <.input field={@form[:start_at]} type="date" label="Start at" />
+ <.input field={@form[:end_at]} type="date" label="End at" />
+ <:actions>
+ <.button phx-disable-with="Saving...">Save Cycle
+
+
+
+ """
+ end
+
+ @impl true
+ def update(%{cycle: cycle} = assigns, socket) do
+ changeset = Schools.change_cycle(cycle)
+
+ {:ok,
+ socket
+ |> assign(assigns)
+ |> assign(:school_options, generate_school_options())
+ |> assign_form(changeset)}
+ end
+
+ @impl true
+ def handle_event("validate", %{"cycle" => cycle_params}, socket) do
+ changeset =
+ socket.assigns.cycle
+ |> Schools.change_cycle(cycle_params)
+ |> Map.put(:action, :validate)
+
+ {:noreply, assign_form(socket, changeset)}
+ end
+
+ def handle_event("save", %{"cycle" => cycle_params}, socket) do
+ save_cycle(socket, socket.assigns.action, cycle_params)
+ end
+
+ defp save_cycle(socket, :edit, cycle_params) do
+ case Schools.update_cycle(socket.assigns.cycle, cycle_params) do
+ {:ok, cycle} ->
+ notify_parent({:saved, cycle})
+
+ {:noreply,
+ socket
+ |> put_flash(:info, "Cycle updated successfully")
+ |> push_patch(to: socket.assigns.patch)}
+
+ {:error, %Ecto.Changeset{} = changeset} ->
+ {:noreply, assign_form(socket, changeset)}
+ end
+ end
+
+ defp save_cycle(socket, :new, cycle_params) do
+ case Schools.create_cycle(cycle_params) do
+ {:ok, cycle} ->
+ notify_parent({:saved, cycle})
+
+ {:noreply,
+ socket
+ |> put_flash(:info, "Cycle created successfully")
+ |> push_patch(to: socket.assigns.patch)}
+
+ {:error, %Ecto.Changeset{} = changeset} ->
+ {:noreply, assign_form(socket, changeset)}
+ end
+ end
+
+ defp assign_form(socket, %Ecto.Changeset{} = changeset) do
+ assign(socket, :form, to_form(changeset))
+ end
+
+ defp notify_parent(msg), do: send(self(), {__MODULE__, msg})
+end
diff --git a/lib/lanttern_web/live/admin/cycle_live/index.ex b/lib/lanttern_web/live/admin/cycle_live/index.ex
new file mode 100644
index 00000000..1902104b
--- /dev/null
+++ b/lib/lanttern_web/live/admin/cycle_live/index.ex
@@ -0,0 +1,47 @@
+defmodule LantternWeb.Admin.CycleLive.Index do
+ use LantternWeb, {:live_view, layout: :admin}
+
+ alias Lanttern.Schools
+ alias Lanttern.Schools.Cycle
+
+ @impl true
+ def mount(_params, _session, socket) do
+ {:ok, stream(socket, :school_cycles, Schools.list_cycles())}
+ end
+
+ @impl true
+ def handle_params(params, _url, socket) do
+ {:noreply, apply_action(socket, socket.assigns.live_action, params)}
+ end
+
+ defp apply_action(socket, :edit, %{"id" => id}) do
+ socket
+ |> assign(:page_title, "Edit Cycle")
+ |> assign(:cycle, Schools.get_cycle!(id))
+ end
+
+ defp apply_action(socket, :new, _params) do
+ socket
+ |> assign(:page_title, "New Cycle")
+ |> assign(:cycle, %Cycle{})
+ end
+
+ defp apply_action(socket, :index, _params) do
+ socket
+ |> assign(:page_title, "Listing School cycles")
+ |> assign(:cycle, nil)
+ end
+
+ @impl true
+ def handle_info({LantternWeb.Admin.CycleLive.FormComponent, {:saved, cycle}}, socket) do
+ {:noreply, stream_insert(socket, :school_cycles, cycle)}
+ end
+
+ @impl true
+ def handle_event("delete", %{"id" => id}, socket) do
+ cycle = Schools.get_cycle!(id)
+ {:ok, _} = Schools.delete_cycle(cycle)
+
+ {:noreply, stream_delete(socket, :school_cycles, cycle)}
+ end
+end
diff --git a/lib/lanttern_web/live/admin/cycle_live/index.html.heex b/lib/lanttern_web/live/admin/cycle_live/index.html.heex
new file mode 100644
index 00000000..1086f96c
--- /dev/null
+++ b/lib/lanttern_web/live/admin/cycle_live/index.html.heex
@@ -0,0 +1,48 @@
+<.header>
+ Listing School cycles
+ <:actions>
+ <.link patch={~p"/admin/school_cycles/new"}>
+ <.button>New Cycle
+
+
+
+
+<.table
+ id="school_cycles"
+ rows={@streams.school_cycles}
+ row_click={fn {_id, cycle} -> JS.navigate(~p"/admin/school_cycles/#{cycle}") end}
+>
+ <:col :let={{_id, cycle}} label="Name"><%= cycle.name %>
+ <:col :let={{_id, cycle}} label="Start at"><%= cycle.start_at %>
+ <:col :let={{_id, cycle}} label="End at"><%= cycle.end_at %>
+ <:action :let={{_id, cycle}}>
+
+ <.link navigate={~p"/admin/school_cycles/#{cycle}"}>Show
+
+ <.link patch={~p"/admin/school_cycles/#{cycle}/edit"}>Edit
+
+ <:action :let={{id, cycle}}>
+ <.link
+ phx-click={JS.push("delete", value: %{id: cycle.id}) |> hide("##{id}")}
+ data-confirm="Are you sure?"
+ >
+ Delete
+
+
+
+
+<.modal
+ :if={@live_action in [:new, :edit]}
+ id="cycle-modal"
+ show
+ on_cancel={JS.patch(~p"/admin/school_cycles")}
+>
+ <.live_component
+ module={LantternWeb.Admin.CycleLive.FormComponent}
+ id={@cycle.id || :new}
+ title={@page_title}
+ action={@live_action}
+ cycle={@cycle}
+ patch={~p"/admin/school_cycles"}
+ />
+
diff --git a/lib/lanttern_web/live/admin/cycle_live/show.ex b/lib/lanttern_web/live/admin/cycle_live/show.ex
new file mode 100644
index 00000000..92a71606
--- /dev/null
+++ b/lib/lanttern_web/live/admin/cycle_live/show.ex
@@ -0,0 +1,21 @@
+defmodule LantternWeb.Admin.CycleLive.Show do
+ use LantternWeb, {:live_view, layout: :admin}
+
+ alias Lanttern.Schools
+
+ @impl true
+ def mount(_params, _session, socket) do
+ {:ok, socket}
+ end
+
+ @impl true
+ def handle_params(%{"id" => id}, _, socket) do
+ {:noreply,
+ socket
+ |> assign(:page_title, page_title(socket.assigns.live_action))
+ |> assign(:cycle, Schools.get_cycle!(id))}
+ end
+
+ defp page_title(:show), do: "Show Cycle"
+ defp page_title(:edit), do: "Edit Cycle"
+end
diff --git a/lib/lanttern_web/live/admin/cycle_live/show.html.heex b/lib/lanttern_web/live/admin/cycle_live/show.html.heex
new file mode 100644
index 00000000..0cafe2ef
--- /dev/null
+++ b/lib/lanttern_web/live/admin/cycle_live/show.html.heex
@@ -0,0 +1,33 @@
+<.header>
+ Cycle <%= @cycle.id %>
+ <:subtitle>This is a cycle record from your database.
+ <:actions>
+ <.link patch={~p"/admin/school_cycles/#{@cycle}/show/edit"} phx-click={JS.push_focus()}>
+ <.button>Edit cycle
+
+
+
+
+<.list>
+ <:item title="Name"><%= @cycle.name %>
+ <:item title="Start at"><%= @cycle.start_at %>
+ <:item title="End at"><%= @cycle.end_at %>
+
+
+<.back navigate={~p"/admin/school_cycles"}>Back to school_cycles
+
+<.modal
+ :if={@live_action == :edit}
+ id="cycle-modal"
+ show
+ on_cancel={JS.patch(~p"/admin/school_cycles/#{@cycle}")}
+>
+ <.live_component
+ module={LantternWeb.Admin.CycleLive.FormComponent}
+ id={@cycle.id}
+ title={@page_title}
+ action={@live_action}
+ cycle={@cycle}
+ patch={~p"/admin/school_cycles/#{@cycle}"}
+ />
+
diff --git a/lib/lanttern_web/live/admin/school_live/import_students.ex b/lib/lanttern_web/live/admin/school_live/import_students.ex
index 4b87eb87..e05daa1a 100644
--- a/lib/lanttern_web/live/admin/school_live/import_students.ex
+++ b/lib/lanttern_web/live/admin/school_live/import_students.ex
@@ -18,15 +18,23 @@ defmodule LantternWeb.Admin.SchoolLive.ImportStudents do
phx-change="validate"
class="flex items-start gap-10"
>
- <.input
- field={@form[:school_id]}
- type="select"
- label="Select school"
- options={@school_options}
- prompt="No school selected"
- class="flex-1"
- />
-
+
+ <.input
+ field={@form[:school_id]}
+ type="select"
+ label="Select school"
+ options={@school_options}
+ prompt="No school selected"
+ class="mb-4"
+ />
+ <.input
+ field={@form[:cycle_id]}
+ type="select"
+ label="Select cycle"
+ options={@cycle_options}
+ prompt="No cycle selected"
+ />
+
assign(:state, "uploading")
- |> assign(:form, to_form(%{"school_id" => "", "csv" => ""}))
+ |> assign(:form, to_form(%{"school_id" => "", "cycle_id" => "", "csv" => ""}))
|> assign(:csv_error, nil)
|> assign(:school_options, SchoolsHelpers.generate_school_options())
+ |> assign(:cycle_options, [])
|> allow_upload(:csv, accept: ~w(.csv), max_entries: 1)
{:ok, socket}
@@ -299,8 +308,32 @@ defmodule LantternWeb.Admin.SchoolLive.ImportStudents do
# event handlers
@impl true
+ def handle_event(
+ "validate",
+ %{"_target" => ["school_id"], "school_id" => ""} = params,
+ socket
+ ) do
+ {:noreply,
+ socket
+ |> assign(:cycle_options, [])
+ # without this assign the input fields are reset
+ |> assign(:form, to_form(params |> Map.put("cycle_id", "")))}
+ end
+
+ def handle_event(
+ "validate",
+ %{"_target" => ["school_id"], "school_id" => school_id} = params,
+ socket
+ ) do
+ {:noreply,
+ socket
+ |> assign(:cycle_options, SchoolsHelpers.generate_cycle_options(schools_ids: [school_id]))
+ # without this assign the input fields are reset
+ |> assign(:form, to_form(params))}
+ end
+
def handle_event("validate", params, socket) do
- # without this assign the school_id field is reset
+ # without this assign the input fields are reset
{:noreply, assign(socket, :form, to_form(params))}
end
@@ -320,6 +353,16 @@ defmodule LantternWeb.Admin.SchoolLive.ImportStudents do
{:noreply, socket}
end
+ def handle_event("upload", %{"cycle_id" => ""} = params, socket) do
+ errors = [cycle_id: {"Can't be blank", []}]
+
+ socket =
+ socket
+ |> assign(:form, to_form(params, errors: errors))
+
+ {:noreply, socket}
+ end
+
def handle_event("upload", params, %{assigns: %{uploads: %{csv: %{entries: []}}}} = socket) do
errors = [csv: "Can't be blank"]
@@ -330,7 +373,7 @@ defmodule LantternWeb.Admin.SchoolLive.ImportStudents do
{:noreply, socket}
end
- def handle_event("upload", %{"school_id" => school_id}, socket) do
+ def handle_event("upload", %{"school_id" => school_id, "cycle_id" => cycle_id}, socket) do
case parse_upload_entry(socket, hd(socket.assigns.uploads.csv.entries)) do
{:ok, csv_rows} ->
school_classes = Schools.list_classes(schools_ids: [school_id])
@@ -340,6 +383,7 @@ defmodule LantternWeb.Admin.SchoolLive.ImportStudents do
socket =
socket
|> assign(:school_id, school_id)
+ |> assign(:cycle_id, cycle_id)
|> assign(:school_classes, school_classes)
|> assign(:class_options, class_options)
|> assign(:csv_class_name_id_map, csv_class_name_id_map)
@@ -366,7 +410,8 @@ defmodule LantternWeb.Admin.SchoolLive.ImportStudents do
case Schools.create_students_from_csv(
socket.assigns.csv_rows,
socket.assigns.csv_class_name_id_map,
- socket.assigns.school_id
+ socket.assigns.school_id,
+ socket.assigns.cycle_id
) do
{:ok, import_result} ->
socket =
diff --git a/lib/lanttern_web/live/assessment_point_live/details.html.heex b/lib/lanttern_web/live/assessment_point_live/details.html.heex
index d55670ee..501149e6 100644
--- a/lib/lanttern_web/live/assessment_point_live/details.html.heex
+++ b/lib/lanttern_web/live/assessment_point_live/details.html.heex
@@ -1,10 +1,9 @@
<.page_title_with_menu>Assessment point details
-
- <.link navigate={~p"/assessment_points"} class="underline">Assessment points explorer
- /
- Details
-
+ <.breadcrumbs class="mt-2">
+ <:item link={~p"/assessment_points"}>Assessment points explorer
+ <:item>Details
+
<.link navigate={~p"/assessment_points"} class="flex items-center text-sm text-ltrn-subtle">
diff --git a/lib/lanttern_web/live/menu_component.ex b/lib/lanttern_web/live/menu_component.ex
index 6774eaf8..48da1e44 100644
--- a/lib/lanttern_web/live/menu_component.ex
+++ b/lib/lanttern_web/live/menu_component.ex
@@ -16,6 +16,9 @@ defmodule LantternWeb.MenuComponent do
<.nav_item active={@active_nav == :dashboard} path={~p"/dashboard"}>
Dashboard
+ <.nav_item active={@active_nav == :school} path={~p"/school"}>
+ School
+
<.nav_item active={@active_nav == :assessment_points} path={~p"/assessment_points"}>
Assessment points
@@ -47,10 +50,9 @@ defmodule LantternWeb.MenuComponent do
<.link
href={~p"/admin"}
- target="_blank"
class="flex items-center gap-2 underline hover:text-ltrn-dark"
>
- Admin <.icon name="hero-arrow-top-right-on-square" />
+ Admin
@@ -100,7 +102,7 @@ defmodule LantternWeb.MenuComponent do
~H"""
<.link
- navigate={@path}
+ patch={@path}
class={[
"group relative block p-10 font-display font-black text-lg",
if(@active, do: "text-ltrn-dark", else: "text-ltrn-subtle underline hover:text-ltrn-dark")
@@ -198,9 +200,15 @@ defmodule LantternWeb.MenuComponent do
:dashboard
socket.view in [
- LantternWeb.AssessmentPointsLive,
- LantternWeb.AssessmentPointsExplorerLive,
- LantternWeb.AssessmentPointLive
+ LantternWeb.SchoolLive.Show,
+ LantternWeb.SchoolLive.Class,
+ LantternWeb.SchoolLive.Student
+ ] ->
+ :school
+
+ socket.view in [
+ LantternWeb.AssessmentPointLive.Explorer,
+ LantternWeb.AssessmentPointLive.Details
] ->
:assessment_points
diff --git a/lib/lanttern_web/live/school_live/class.ex b/lib/lanttern_web/live/school_live/class.ex
new file mode 100644
index 00000000..5e1038f5
--- /dev/null
+++ b/lib/lanttern_web/live/school_live/class.ex
@@ -0,0 +1,42 @@
+defmodule LantternWeb.SchoolLive.Class do
+ use LantternWeb, :live_view
+
+ alias Lanttern.Schools
+
+ # lifecycle
+
+ def mount(_params, _session, socket) do
+ user_school =
+ case socket.assigns.current_user.current_profile.type do
+ "student" ->
+ socket.assigns.current_user.current_profile.student.school
+
+ "teacher" ->
+ socket.assigns.current_user.current_profile.teacher.school
+ end
+
+ {:ok,
+ socket
+ |> assign(:user_school, user_school)}
+ end
+
+ def handle_params(params, _url, socket) do
+ {:noreply, apply_action(socket, socket.assigns.live_action, params)}
+ end
+
+ defp apply_action(socket, :show, %{"id" => id}) do
+ case Schools.get_class(id, preloads: :students) do
+ class when is_nil(class) or class.school_id != socket.assigns.user_school.id ->
+ socket
+ |> put_flash(:error, "Couldn't find class")
+ |> redirect(to: ~p"/school")
+
+ class ->
+ socket
+ |> assign(:class_name, class.name)
+ |> stream(:students, class.students)
+ end
+ end
+
+ defp apply_action(socket, _live_action, _params), do: socket
+end
diff --git a/lib/lanttern_web/live/school_live/class.html.heex b/lib/lanttern_web/live/school_live/class.html.heex
new file mode 100644
index 00000000..32284154
--- /dev/null
+++ b/lib/lanttern_web/live/school_live/class.html.heex
@@ -0,0 +1,26 @@
+
+ <.page_title_with_menu><%= @class_name %>
+ <.breadcrumbs class="mt-2">
+ <:item link={~p"/school"}>School
+ <:item>Class
+
+
+
+ Students
+
+
+
+ <.link
+ patch={~p"/school/student/#{student.id}"}
+ class="flex-1 font-display font-black text-2xl underline hover:text-ltrn-subtle"
+ >
+ <%= student.name %>
+
+ <.profile_icon profile_name={student.name} class="-mt-2 -mr-2" />
+
+
+
diff --git a/lib/lanttern_web/live/school_live/show.ex b/lib/lanttern_web/live/school_live/show.ex
new file mode 100644
index 00000000..c74f6cad
--- /dev/null
+++ b/lib/lanttern_web/live/school_live/show.ex
@@ -0,0 +1,63 @@
+defmodule LantternWeb.SchoolLive.Show do
+ use LantternWeb, :live_view
+
+ alias Lanttern.Schools
+
+ # function components
+
+ attr :students, :list, required: true
+
+ def class_students(%{students: students} = assigns) when length(students) > 5 do
+ assigns =
+ assigns
+ |> assign(:len, "+ #{length(students) - 3} students")
+ |> assign(:students, students |> Enum.take(3))
+
+ ~H"""
+
+ <.person_badge :for={std <- @students} person={std} theme="cyan" />
+ <%= @len %>
+
+ """
+ end
+
+ def class_students(%{students: []} = assigns) do
+ ~H"""
+ No students in this class
+ """
+ end
+
+ def class_students(assigns) do
+ ~H"""
+
+ <.person_badge :for={std <- @students} person={std} theme="cyan" />
+
+ """
+ end
+
+ # lifecycle
+
+ def mount(_params, _session, socket) do
+ school =
+ case socket.assigns.current_user.current_profile.type do
+ "student" ->
+ socket.assigns.current_user.current_profile.student.school
+
+ "teacher" ->
+ socket.assigns.current_user.current_profile.teacher.school
+ end
+
+ classes = Schools.list_user_classes(socket.assigns.current_user)
+
+ {:ok,
+ socket
+ |> assign(:school, school)
+ |> stream(:classes, classes)}
+ end
+
+ def handle_params(params, _url, socket) do
+ {:noreply, apply_action(socket, socket.assigns.live_action, params)}
+ end
+
+ defp apply_action(socket, _live_action, _params), do: socket
+end
diff --git a/lib/lanttern_web/live/school_live/show.html.heex b/lib/lanttern_web/live/school_live/show.html.heex
new file mode 100644
index 00000000..b3f62d1e
--- /dev/null
+++ b/lib/lanttern_web/live/school_live/show.html.heex
@@ -0,0 +1,32 @@
+
+ <.page_title_with_menu><%= @school.name %>
+
+
+ All classes
+
+ <%!-- <.link
+ type="button"
+ class="shrink-0 flex items-center gap-2 font-display text-base underline hover:text-ltrn-primary"
+ patch={~p"/school/new_class"}
+ >
+ Create new class <.icon name="hero-plus-circle" class="w-6 h-6 text-ltrn-primary" />
+ --%>
+
+
+ <.stream_table
+ id="classes"
+ stream={@streams.classes}
+ row_click={&JS.navigate(~p"/school/class/#{&1}")}
+ >
+ <:col :let={class} label="Class"><%= class.name %>
+ <:col :let={class} label="Students">
+ <.class_students students={class.students} />
+
+ <:col :let={class} label="Years">
+ <%= class.years
+ |> Enum.map(& &1.name)
+ |> Enum.join(", ") %>
+
+ <:col :let={class} label="Cycle"><%= class.cycle.name %>
+
+
diff --git a/lib/lanttern_web/live/school_live/student.ex b/lib/lanttern_web/live/school_live/student.ex
new file mode 100644
index 00000000..201db883
--- /dev/null
+++ b/lib/lanttern_web/live/school_live/student.ex
@@ -0,0 +1,42 @@
+defmodule LantternWeb.SchoolLive.Student do
+ use LantternWeb, :live_view
+
+ alias Lanttern.Schools
+
+ # lifecycle
+
+ def mount(_params, _session, socket) do
+ user_school =
+ case socket.assigns.current_user.current_profile.type do
+ "student" ->
+ socket.assigns.current_user.current_profile.student.school
+
+ "teacher" ->
+ socket.assigns.current_user.current_profile.teacher.school
+ end
+
+ {:ok,
+ socket
+ |> assign(:user_school, user_school)}
+ end
+
+ def handle_params(params, _url, socket) do
+ {:noreply, apply_action(socket, socket.assigns.live_action, params)}
+ end
+
+ defp apply_action(socket, :show, %{"id" => id}) do
+ case Schools.get_student(id, preloads: [classes: [:cycle, :years]]) do
+ student when is_nil(student) or student.school_id != socket.assigns.user_school.id ->
+ socket
+ |> put_flash(:error, "Couldn't find student")
+ |> redirect(to: ~p"/school")
+
+ student ->
+ socket
+ |> assign(:student_name, student.name)
+ |> stream(:classes, student.classes)
+ end
+ end
+
+ defp apply_action(socket, _live_action, _params), do: socket
+end
diff --git a/lib/lanttern_web/live/school_live/student.html.heex b/lib/lanttern_web/live/school_live/student.html.heex
new file mode 100644
index 00000000..a969fbf2
--- /dev/null
+++ b/lib/lanttern_web/live/school_live/student.html.heex
@@ -0,0 +1,29 @@
+
+ <.page_title_with_menu><%= @student_name %>
+ <.breadcrumbs class="mt-2">
+ <:item link={~p"/school"}>School
+ <:item>Student
+
+
+
+ Classes
+
+
+
+ <.link
+ patch={~p"/school/class/#{class}"}
+ class="font-display font-black text-2xl underline hover:text-ltrn-subtle"
+ >
+ <%= class.name %>
+
+
+ <.badge><%= class.cycle.name %>
+ <.badge :for={year <- class.years}><%= year.name %>
+
+
+
+
diff --git a/lib/lanttern_web/router.ex b/lib/lanttern_web/router.ex
index dcfbcfc3..1a0e89a6 100644
--- a/lib/lanttern_web/router.ex
+++ b/lib/lanttern_web/router.ex
@@ -51,6 +51,10 @@ defmodule LantternWeb.Router do
DashboardLive.Index,
:edit_filter_view
+ live "/school", SchoolLive.Show, :show
+ live "/school/class/:id", SchoolLive.Class, :show
+ live "/school/student/:id", SchoolLive.Student, :show
+
live "/assessment_points", AssessmentPointLive.Explorer, :index
live "/assessment_points/new", AssessmentPointLive.Explorer, :new
@@ -104,9 +108,17 @@ defmodule LantternWeb.Router do
resources "/classes", ClassController
resources "/students", StudentController
resources "/teachers", TeacherController
+
live "/import_students", Admin.SchoolLive.ImportStudents
live "/import_teachers", Admin.SchoolLive.ImportTeachers
+ live "/school_cycles", Admin.CycleLive.Index, :index
+ live "/school_cycles/new", Admin.CycleLive.Index, :new
+
+ live "/school_cycles/:id/edit", Admin.CycleLive.Index, :edit
+ live "/school_cycles/:id", Admin.CycleLive.Show, :show
+ live "/school_cycles/:id/show/edit", Admin.CycleLive.Show, :edit
+
# Taxonomy context
resources "/subjects", SubjectController
resources "/years", YearController
diff --git a/priv/repo/migrations/20231110160629_create_school_cycles.exs b/priv/repo/migrations/20231110160629_create_school_cycles.exs
new file mode 100644
index 00000000..2b57043d
--- /dev/null
+++ b/priv/repo/migrations/20231110160629_create_school_cycles.exs
@@ -0,0 +1,22 @@
+defmodule Lanttern.Repo.Migrations.CreateSchoolCycles do
+ use Ecto.Migration
+
+ def change do
+ create table(:school_cycles) do
+ add :name, :text, null: false
+ add :start_at, :date, null: false
+ add :end_at, :date, null: false
+ add :school_id, references(:schools, on_delete: :delete_all), null: false
+
+ timestamps()
+ end
+
+ create index(:school_cycles, [:school_id])
+
+ create constraint(
+ :school_cycles,
+ :cycle_end_date_is_greater_than_start_date,
+ check: "start_at < end_at"
+ )
+ end
+end
diff --git a/priv/repo/migrations/20231113115932_add_cycle_to_classes.exs b/priv/repo/migrations/20231113115932_add_cycle_to_classes.exs
new file mode 100644
index 00000000..0fee3357
--- /dev/null
+++ b/priv/repo/migrations/20231113115932_add_cycle_to_classes.exs
@@ -0,0 +1,52 @@
+defmodule Lanttern.Repo.Migrations.AddCycleToClasses do
+ use Ecto.Migration
+
+ def change do
+ # creating unique constraints to allow composite foreign keys.
+ # this guarantees, in the database level, that the selected cycle
+ # belongs to the same school of the class.
+
+ # removing existing "school_cycles_school_id_index" to prevent unnecessary index
+ drop index(:school_cycles, [:school_id])
+ create unique_index(:school_cycles, [:school_id, :id])
+
+ alter table(:classes) do
+ # `cycle_id` is `null: false`.
+ # we'll add this in the execute blocks below
+ # after we add a scale to all classes
+
+ add :cycle_id,
+ references(:school_cycles,
+ with: [school_id: :school_id],
+ on_delete: :nothing
+ )
+ end
+
+ create index(:classes, [:cycle_id])
+
+ # creating one temp cycle to each school in the database
+ execute """
+ INSERT INTO school_cycles (name, start_at, end_at, school_id, inserted_at, updated_at)
+ SELECT
+ 'TEMP ' || date_part('year', CURRENT_DATE)::text,
+ make_date(date_part('year', CURRENT_DATE)::int, 1, 1),
+ make_date(date_part('year', CURRENT_DATE)::int, 12, 31),
+ schools.id,
+ now() AT time zone 'utc',
+ now() AT time zone 'utc'
+ FROM schools
+ """,
+ ""
+
+ # link temp cycles to existing classes
+ execute """
+ UPDATE classes SET cycle_id = school_cycles.id
+ FROM school_cycles
+ WHERE school_cycles.school_id = classes.school_id
+ """,
+ ""
+
+ # add not null constraints to classes' cycle_id
+ execute "ALTER TABLE classes ALTER COLUMN cycle_id SET NOT NULL", ""
+ end
+end
diff --git a/priv/repo/migrations/20231114122320_create_classes_years.exs b/priv/repo/migrations/20231114122320_create_classes_years.exs
new file mode 100644
index 00000000..7b4e743c
--- /dev/null
+++ b/priv/repo/migrations/20231114122320_create_classes_years.exs
@@ -0,0 +1,13 @@
+defmodule Lanttern.Repo.Migrations.CreateClassesYears do
+ use Ecto.Migration
+
+ def change do
+ create table(:classes_years, primary_key: false) do
+ add :class_id, references(:classes)
+ add :year_id, references(:years)
+ end
+
+ create index(:classes_years, [:class_id])
+ create unique_index(:classes_years, [:year_id, :class_id])
+ end
+end
diff --git a/test/lanttern/schools_test.exs b/test/lanttern/schools_test.exs
index c80ff5ed..d3121848 100644
--- a/test/lanttern/schools_test.exs
+++ b/test/lanttern/schools_test.exs
@@ -57,6 +57,87 @@ defmodule Lanttern.SchoolsTest do
end
end
+ describe "school_cycles" do
+ alias Lanttern.Schools.Cycle
+
+ import Lanttern.SchoolsFixtures
+
+ @invalid_attrs %{name: nil, start_at: nil, end_at: nil}
+
+ test "list_cycles/1 returns all school_cycles" do
+ cycle = cycle_fixture()
+ assert Schools.list_cycles() == [cycle]
+ end
+
+ test "list_cycles/1 with school filter returns all cycles as expected" do
+ school = school_fixture()
+ cycle = cycle_fixture(%{school_id: school.id})
+
+ # extra cycles for school filter validation
+ class_fixture()
+ class_fixture()
+
+ assert [cycle] == Schools.list_cycles(schools_ids: [school.id])
+ end
+
+ test "get_cycle!/1 returns the cycle with given id" do
+ cycle = cycle_fixture()
+ assert Schools.get_cycle!(cycle.id) == cycle
+ end
+
+ test "create_cycle/1 with valid data creates a cycle" do
+ school = school_fixture()
+
+ valid_attrs = %{
+ name: "some name",
+ start_at: ~D[2023-11-09],
+ end_at: ~D[2023-12-09],
+ school_id: school.id
+ }
+
+ assert {:ok, %Cycle{} = cycle} = Schools.create_cycle(valid_attrs)
+ assert cycle.name == "some name"
+ assert cycle.start_at == ~D[2023-11-09]
+ assert cycle.end_at == ~D[2023-12-09]
+ end
+
+ test "create_cycle/1 with invalid data returns error changeset" do
+ assert {:error, %Ecto.Changeset{}} = Schools.create_cycle(@invalid_attrs)
+ end
+
+ test "update_cycle/2 with valid data updates the cycle" do
+ cycle = cycle_fixture()
+
+ update_attrs = %{
+ name: "some updated name",
+ start_at: ~D[2023-11-10],
+ end_at: ~D[2023-12-10]
+ }
+
+ assert {:ok, %Cycle{} = cycle} = Schools.update_cycle(cycle, update_attrs)
+ assert cycle.name == "some updated name"
+ assert cycle.start_at == ~D[2023-11-10]
+ assert cycle.end_at == ~D[2023-12-10]
+ end
+
+ test "update_cycle/2 with invalid data returns error changeset" do
+ cycle = cycle_fixture()
+ assert {:error, %Ecto.Changeset{}} = Schools.update_cycle(cycle, @invalid_attrs)
+ assert cycle == Schools.get_cycle!(cycle.id)
+ end
+
+ test "delete_cycle/1 deletes the cycle" do
+ cycle = cycle_fixture()
+ assert {:ok, %Cycle{}} = Schools.delete_cycle(cycle)
+ assert_raise Ecto.NoResultsError, fn -> Schools.get_cycle!(cycle.id) end
+ end
+
+ test "change_cycle/1 returns a cycle changeset" do
+ cycle = cycle_fixture()
+ assert %Ecto.Changeset{} = Schools.change_cycle(cycle)
+ end
+ end
+
describe "classes" do
alias Lanttern.Schools.Class
@@ -70,18 +151,63 @@ defmodule Lanttern.SchoolsTest do
test "list_classes/1 with preloads and school filter returns all classes as expected" do
school = school_fixture()
student = student_fixture()
- class = class_fixture(%{school_id: school.id, students_ids: [student.id]})
+ year = Lanttern.TaxonomyFixtures.year_fixture()
+
+ class =
+ class_fixture(%{school_id: school.id, students_ids: [student.id], years_ids: [year.id]})
# extra classes for school filter validation
class_fixture()
class_fixture()
[expected_class] =
- Schools.list_classes(preloads: [:school, :students], schools_ids: [school.id])
+ Schools.list_classes(preloads: [:school, :students, :years], schools_ids: [school.id])
assert expected_class.id == class.id
assert expected_class.school == school
assert expected_class.students == [student]
+ assert expected_class.years == [year]
+ end
+
+ test "list_user_classes/1 returns all classes from user's school with preloaded data and correct order" do
+ school = school_fixture()
+ class_b = class_fixture(%{school_id: school.id, name: "BBB"})
+ class_a = class_fixture(%{school_id: school.id, name: "AAA"})
+ teacher = teacher_fixture(%{school_id: school.id})
+ profile = Lanttern.IdentityFixtures.teacher_profile_fixture(%{teacher_id: teacher.id})
+ user = %{current_profile: Lanttern.Identity.get_profile!(profile.id, preloads: :teacher)}
+
+ # extra classes for school filter validation
+ class_fixture()
+ class_fixture()
+
+ [expected_a, expected_b] = Schools.list_user_classes(user)
+
+ assert expected_a.id == class_a.id
+ assert expected_b.id == class_b.id
+ end
+
+ test "list_user_classes/1 returns error tuple when user is student" do
+ school = school_fixture()
+ student = student_fixture(%{school_id: school.id})
+ profile = Lanttern.IdentityFixtures.student_profile_fixture(%{student_id: student.id})
+
+ user = %{
+ current_profile:
+ Lanttern.Identity.get_profile!(profile.id, preloads: [:teacher, :student])
+ }
+
+ assert {:error, "User not allowed to list classes"} == Schools.list_user_classes(user)
+ end
+
+ test "get_class/2 returns the class with given id" do
+ class = class_fixture()
+ assert Schools.get_class(class.id) == class
+ end
+
+ test "get_class/2 returns nil if class with given id does not exist" do
+ class_fixture()
+ assert Schools.get_class(99999) == nil
end
test "get_class!/2 returns the class with given id" do
@@ -102,7 +228,8 @@ defmodule Lanttern.SchoolsTest do
test "create_class/1 with valid data creates a class" do
school = school_fixture()
- valid_attrs = %{school_id: school.id, name: "some name"}
+ cycle = cycle_fixture(%{school_id: school.id})
+ valid_attrs = %{school_id: school.id, cycle_id: cycle.id, name: "some name"}
assert {:ok, %Class{} = class} = Schools.create_class(valid_attrs)
assert class.name == "some name"
@@ -111,6 +238,7 @@ defmodule Lanttern.SchoolsTest do
test "create_class/1 with valid data containing students creates a class with students" do
school = school_fixture()
+ cycle = cycle_fixture(%{school_id: school.id})
student_1 = student_fixture()
student_2 = student_fixture()
student_3 = student_fixture()
@@ -118,6 +246,7 @@ defmodule Lanttern.SchoolsTest do
valid_attrs = %{
name: "some name",
school_id: school.id,
+ cycle_id: cycle.id,
students_ids: [
student_1.id,
student_2.id,
@@ -224,6 +353,16 @@ defmodule Lanttern.SchoolsTest do
end
end
+ test "get_student/2 returns the student with given id" do
+ student = student_fixture()
+ assert Schools.get_student(student.id) == student
+ end
+
+ test "get_student/2 returns nil if student with given id does not exist" do
+ student_fixture()
+ assert Schools.get_student(99999) == nil
+ end
+
test "get_student!/2 returns the student with given id" do
student = student_fixture()
assert Schools.get_student!(student.id) == student
@@ -418,7 +557,8 @@ defmodule Lanttern.SchoolsTest do
describe "csv parsing" do
test "create_students_from_csv/3 creates classes and students, and returns a list with the registration status for each row" do
school = school_fixture()
- class = class_fixture(name: "existing class", school_id: school.id)
+ cycle = cycle_fixture(%{school_id: school.id})
+ class = class_fixture(name: "existing class", school_id: school.id, cycle_id: cycle.id)
user = Lanttern.IdentityFixtures.user_fixture(email: "existing-user@school.com")
csv_std_1 = %{
@@ -466,7 +606,7 @@ defmodule Lanttern.SchoolsTest do
}
{:ok, expected} =
- Schools.create_students_from_csv(csv_students, class_name_id_map, school.id)
+ Schools.create_students_from_csv(csv_students, class_name_id_map, school.id, cycle.id)
[
{returned_csv_std_1, {:ok, std_1}},
diff --git a/test/lanttern_web/controllers/class_controller_test.exs b/test/lanttern_web/controllers/class_controller_test.exs
index 63ea9bfb..0d8688da 100644
--- a/test/lanttern_web/controllers/class_controller_test.exs
+++ b/test/lanttern_web/controllers/class_controller_test.exs
@@ -3,7 +3,6 @@ defmodule LantternWeb.ClassControllerTest do
import Lanttern.SchoolsFixtures
- @create_attrs %{name: "some name"}
@update_attrs %{name: "some updated name"}
@invalid_attrs %{name: nil}
@@ -26,7 +25,8 @@ defmodule LantternWeb.ClassControllerTest do
describe "create class" do
test "redirects to show when data is valid", %{conn: conn} do
school = school_fixture()
- create_attrs = @create_attrs |> Map.put_new(:school_id, school.id)
+ cycle = cycle_fixture(%{school_id: school.id})
+ create_attrs = %{name: "some name", school_id: school.id, cycle_id: cycle.id}
conn = post(conn, ~p"/admin/classes", class: create_attrs)
assert %{id: id} = redirected_params(conn)
diff --git a/test/lanttern_web/live/admin/cycle_live_test.exs b/test/lanttern_web/live/admin/cycle_live_test.exs
new file mode 100644
index 00000000..3cf65ae5
--- /dev/null
+++ b/test/lanttern_web/live/admin/cycle_live_test.exs
@@ -0,0 +1,140 @@
+defmodule LantternWeb.Admin.CycleLiveTest do
+ use LantternWeb.ConnCase
+
+ import Phoenix.LiveViewTest
+ import Lanttern.SchoolsFixtures
+
+ @invalid_attrs %{name: nil, start_at: nil, end_at: nil}
+
+ defp create_cycle(_) do
+ cycle = cycle_fixture()
+ %{cycle: cycle}
+ end
+
+ setup :register_and_log_in_root_admin
+
+ describe "Index" do
+ setup [:create_cycle]
+
+ test "lists all school_cycles", %{conn: conn, cycle: cycle} do
+ {:ok, _index_live, html} = live(conn, ~p"/admin/school_cycles")
+
+ assert html =~ "Listing School cycles"
+ assert html =~ cycle.name
+ end
+
+ test "saves new cycle", %{conn: conn} do
+ school = school_fixture()
+
+ create_attrs = %{
+ name: "some name",
+ start_at: "2023-11-09",
+ end_at: "2023-12-09",
+ school_id: school.id
+ }
+
+ {:ok, index_live, _html} = live(conn, ~p"/admin/school_cycles")
+
+ assert index_live |> element("a", "New Cycle") |> render_click() =~
+ "New Cycle"
+
+ assert_patch(index_live, ~p"/admin/school_cycles/new")
+
+ assert index_live
+ |> form("#cycle-form", cycle: @invalid_attrs)
+ |> render_change() =~ "can't be blank"
+
+ assert index_live
+ |> form("#cycle-form", cycle: create_attrs)
+ |> render_submit()
+
+ assert_patch(index_live, ~p"/admin/school_cycles")
+
+ html = render(index_live)
+ assert html =~ "Cycle created successfully"
+ assert html =~ "some name"
+ end
+
+ test "updates cycle in listing", %{conn: conn, cycle: cycle} do
+ school = school_fixture()
+
+ update_attrs = %{
+ name: "some updated name",
+ start_at: "2023-11-10",
+ end_at: "2023-12-10",
+ school_id: school.id
+ }
+
+ {:ok, index_live, _html} = live(conn, ~p"/admin/school_cycles")
+
+ assert index_live |> element("#school_cycles-#{cycle.id} a", "Edit") |> render_click() =~
+ "Edit Cycle"
+
+ assert_patch(index_live, ~p"/admin/school_cycles/#{cycle}/edit")
+
+ assert index_live
+ |> form("#cycle-form", cycle: @invalid_attrs)
+ |> render_change() =~ "can't be blank"
+
+ assert index_live
+ |> form("#cycle-form", cycle: update_attrs)
+ |> render_submit()
+
+ assert_patch(index_live, ~p"/admin/school_cycles")
+
+ html = render(index_live)
+ assert html =~ "Cycle updated successfully"
+ assert html =~ "some updated name"
+ end
+
+ test "deletes cycle in listing", %{conn: conn, cycle: cycle} do
+ {:ok, index_live, _html} = live(conn, ~p"/admin/school_cycles")
+
+ assert index_live |> element("#school_cycles-#{cycle.id} a", "Delete") |> render_click()
+ refute has_element?(index_live, "#school_cycles-#{cycle.id}")
+ end
+ end
+
+ describe "Show" do
+ setup [:create_cycle]
+
+ test "displays cycle", %{conn: conn, cycle: cycle} do
+ {:ok, _show_live, html} = live(conn, ~p"/admin/school_cycles/#{cycle}")
+
+ assert html =~ "Show Cycle"
+ assert html =~ cycle.name
+ end
+
+ test "updates cycle within modal", %{conn: conn, cycle: cycle} do
+ school = school_fixture()
+
+ update_attrs = %{
+ name: "some updated name",
+ start_at: "2023-11-10",
+ end_at: "2023-12-10",
+ school_id: school.id
+ }
+
+ {:ok, show_live, _html} = live(conn, ~p"/admin/school_cycles/#{cycle}")
+
+ assert show_live |> element("a", "Edit") |> render_click() =~
+ "Edit Cycle"
+
+ assert_patch(show_live, ~p"/admin/school_cycles/#{cycle}/show/edit")
+
+ assert show_live
+ |> form("#cycle-form", cycle: @invalid_attrs)
+ |> render_change() =~ "can't be blank"
+
+ assert show_live
+ |> form("#cycle-form", cycle: update_attrs)
+ |> render_submit()
+
+ assert_patch(show_live, ~p"/admin/school_cycles/#{cycle}")
+
+ html = render(show_live)
+ assert html =~ "Cycle updated successfully"
+ assert html =~ "some updated name"
+ end
+ end
+end
diff --git a/test/lanttern_web/live/school_live/class_test.exs b/test/lanttern_web/live/school_live/class_test.exs
new file mode 100644
index 00000000..92cb5f0f
--- /dev/null
+++ b/test/lanttern_web/live/school_live/class_test.exs
@@ -0,0 +1,54 @@
+defmodule LantternWeb.SchoolLive.ClassTest do
+ use LantternWeb.ConnCase
+
+ alias Lanttern.SchoolsFixtures
+
+ @live_view_base_path "/school/class"
+
+ setup [:register_and_log_in_user]
+
+ describe "Class live view basic navigation" do
+ test "disconnected and connected mount", %{conn: conn, user: user} do
+ school = user.current_profile.teacher.school
+ class = SchoolsFixtures.class_fixture(%{school_id: school.id, name: "some class abc xyz"})
+ conn = get(conn, "#{@live_view_base_path}/#{class.id}")
+
+ assert html_response(conn, 200) =~ ~r/\s*some class abc xyz\s*<\/h1>/
+
+ {:ok, _view, _html} = live(conn)
+ end
+
+ test "list students", %{conn: conn, user: user} do
+ school = user.current_profile.teacher.school
+ std_b = SchoolsFixtures.student_fixture(%{school_id: school.id, name: "bbb"})
+ std_a = SchoolsFixtures.student_fixture(%{school_id: school.id, name: "aaa"})
+
+ class =
+ SchoolsFixtures.class_fixture(%{school_id: school.id, students_ids: [std_a.id, std_b.id]})
+
+ {:ok, view, _html} = live(conn, "#{@live_view_base_path}/#{class.id}")
+
+ assert view |> has_element?("a", std_a.name)
+ assert view |> has_element?("a", std_b.name)
+ end
+
+ test "navigate to student", %{conn: conn, user: user} do
+ school = user.current_profile.teacher.school
+ student = SchoolsFixtures.student_fixture(%{school_id: school.id})
+
+ class =
+ SchoolsFixtures.class_fixture(%{
+ school_id: school.id,
+ students_ids: [student.id]
+ })
+
+ {:ok, view, _html} = live(conn, "#{@live_view_base_path}/#{class.id}")
+
+ view
+ |> element("a", student.name)
+ |> render_click()
+
+ assert_patch(view, "/school/student/#{student.id}")
+ end
+ end
+end
diff --git a/test/lanttern_web/live/school_live/show_test.exs b/test/lanttern_web/live/school_live/show_test.exs
new file mode 100644
index 00000000..4006bfa1
--- /dev/null
+++ b/test/lanttern_web/live/school_live/show_test.exs
@@ -0,0 +1,31 @@
+defmodule LantternWeb.SchoolLive.ShowTest do
+ use LantternWeb.ConnCase
+
+ alias Lanttern.SchoolsFixtures
+
+ @live_view_path "/school"
+
+ setup [:register_and_log_in_user]
+
+ describe "School live view basic navigation" do
+ test "disconnected and connected mount", %{conn: conn} do
+ conn = get(conn, @live_view_path)
+
+ school = conn.assigns.current_user.current_profile.teacher.school
+ {:ok, regex} = Regex.compile("\\s*#{school.name}\\s*<\/h1>")
+
+ assert html_response(conn, 200) =~ regex
+
+ {:ok, _view, _html} = live(conn)
+ end
+
+ test "list classes", %{conn: conn, user: user} do
+ school = user.current_profile.teacher.school
+ class = SchoolsFixtures.class_fixture(%{school_id: school.id, name: "school abc"})
+
+ {:ok, view, _html} = live(conn, @live_view_path)
+
+ assert view |> has_element?("td", class.name)
+ end
+ end
+end
diff --git a/test/lanttern_web/live/school_live/student_test.exs b/test/lanttern_web/live/school_live/student_test.exs
new file mode 100644
index 00000000..95ccf4a4
--- /dev/null
+++ b/test/lanttern_web/live/school_live/student_test.exs
@@ -0,0 +1,60 @@
+defmodule LantternWeb.SchoolLive.StudentTest do
+ use LantternWeb.ConnCase
+
+ alias Lanttern.SchoolsFixtures
+
+ @live_view_base_path "/school/student"
+
+ setup [:register_and_log_in_user]
+
+ describe "Student live view basic navigation" do
+ test "disconnected and connected mount", %{conn: conn, user: user} do
+ school = user.current_profile.teacher.school
+
+ student =
+ SchoolsFixtures.student_fixture(%{school_id: school.id, name: "some student abc xyz"})
+
+ conn = get(conn, "#{@live_view_base_path}/#{student.id}")
+
+ assert html_response(conn, 200) =~ ~r/\s*some student abc xyz\s*<\/h1>/
+
+ {:ok, _view, _html} = live(conn)
+ end
+
+ test "list classes", %{conn: conn, user: user} do
+ school = user.current_profile.teacher.school
+ class_b = SchoolsFixtures.class_fixture(%{school_id: school.id, name: "bbb"})
+ class_a = SchoolsFixtures.class_fixture(%{school_id: school.id, name: "aaa"})
+
+ student =
+ SchoolsFixtures.student_fixture(%{
+ school_id: school.id,
+ classes_ids: [class_a.id, class_b.id]
+ })
+
+ {:ok, view, _html} = live(conn, "#{@live_view_base_path}/#{student.id}")
+
+ assert view |> has_element?("a", class_a.name)
+ assert view |> has_element?("a", class_b.name)
+ end
+
+ test "navigate to class", %{conn: conn, user: user} do
+ school = user.current_profile.teacher.school
+ class = SchoolsFixtures.class_fixture(%{school_id: school.id})
+
+ student =
+ SchoolsFixtures.student_fixture(%{
+ school_id: school.id,
+ classes_ids: [class.id]
+ })
+
+ {:ok, view, _html} = live(conn, "#{@live_view_base_path}/#{student.id}")
+
+ view
+ |> element("a", class.name)
+ |> render_click()
+
+ assert_patch(view, "/school/class/#{class.id}")
+ end
+ end
+end
diff --git a/test/support/fixtures/schools_fixtures.ex b/test/support/fixtures/schools_fixtures.ex
index b6550d69..9496ac52 100644
--- a/test/support/fixtures/schools_fixtures.ex
+++ b/test/support/fixtures/schools_fixtures.ex
@@ -18,15 +18,34 @@ defmodule Lanttern.SchoolsFixtures do
school
end
+ @doc """
+ Generate a cycle.
+ """
+ def cycle_fixture(attrs \\ %{}) do
+ {:ok, cycle} =
+ attrs
+ |> Enum.into(%{
+ school_id: maybe_gen_school_id(attrs),
+ name: "some name",
+ start_at: ~D[2023-11-09],
+ end_at: ~D[2024-11-09]
+ })
+ |> Lanttern.Schools.create_cycle()
+
+ cycle
+ end
+
@doc """
Generate a class.
"""
def class_fixture(attrs \\ %{})
- def class_fixture(%{school_id: _school_id} = attrs) do
+ def class_fixture(%{cycle_id: cycle_id} = attrs) do
{:ok, class} =
attrs
|> Enum.into(%{
+ school_id: maybe_gen_school_id(attrs),
+ cycle_id: cycle_id,
name: "some class name #{Ecto.UUID.generate()}"
})
|> Lanttern.Schools.create_class()
@@ -35,13 +54,15 @@ defmodule Lanttern.SchoolsFixtures do
end
def class_fixture(attrs) do
- school = school_fixture()
+ school_id = maybe_gen_school_id(attrs)
+ cycle = cycle_fixture(%{school_id: school_id})
{:ok, class} =
attrs
|> Enum.into(%{
- school_id: school.id,
- name: "some class name"
+ school_id: school_id,
+ cycle_id: cycle.id,
+ name: "some class name #{Ecto.UUID.generate()}"
})
|> Lanttern.Schools.create_class()
@@ -51,12 +72,11 @@ defmodule Lanttern.SchoolsFixtures do
@doc """
Generate a student.
"""
- def student_fixture(attrs \\ %{})
-
- def student_fixture(%{school_id: _school_id} = attrs) do
+ def student_fixture(attrs \\ %{}) do
{:ok, student} =
attrs
|> Enum.into(%{
+ school_id: maybe_gen_school_id(attrs),
name: "some full name #{Ecto.UUID.generate()}"
})
|> Lanttern.Schools.create_student()
@@ -64,29 +84,14 @@ defmodule Lanttern.SchoolsFixtures do
student
end
- def student_fixture(attrs) do
- school = school_fixture()
-
- {:ok, student} =
- attrs
- |> Enum.into(%{
- name: "some full name #{Ecto.UUID.generate()}",
- school_id: school.id
- })
- |> Lanttern.Schools.create_student()
-
- student
- end
-
@doc """
Generate a teacher.
"""
- def teacher_fixture(attrs \\ %{})
-
- def teacher_fixture(%{school_id: _school_id} = attrs) do
+ def teacher_fixture(attrs \\ %{}) do
{:ok, teacher} =
attrs
|> Enum.into(%{
+ school_id: maybe_gen_school_id(attrs),
name: "some full name #{Ecto.UUID.generate()}"
})
|> Lanttern.Schools.create_teacher()
@@ -94,17 +99,11 @@ defmodule Lanttern.SchoolsFixtures do
teacher
end
- def teacher_fixture(attrs) do
- school = school_fixture()
+ # helpers
- {:ok, teacher} =
- attrs
- |> Enum.into(%{
- name: "some full name #{Ecto.UUID.generate()}",
- school_id: school.id
- })
- |> Lanttern.Schools.create_teacher()
+ defp maybe_gen_school_id(%{school_id: school_id} = _attrs),
+ do: school_id
- teacher
- end
+ defp maybe_gen_school_id(_attrs),
+ do: school_fixture().id
end