Skip to content

Commit

Permalink
chore: added support to current cycle in student about tab
Browse files Browse the repository at this point in the history
- created `StudentsCycleInfo.list_cycles_and_classes_for_student/1`
- added support to `student_info_cycle_id` profile filter (including support in `assign_user_filters/2` and `save_profile_filters/2` filter helpers)
- moved student about tab content from parent live view to its own component `AboutComponent`
- adjusted "register and login" test helpers to inject `current_school_cycle` in `user`'s `current_profile` (some adjustments in tests affected by current cycle were needed)
  • Loading branch information
endoooo committed Dec 23, 2024
1 parent 572d58f commit fd5961e
Show file tree
Hide file tree
Showing 17 changed files with 475 additions and 109 deletions.
7 changes: 5 additions & 2 deletions lib/lanttern/personalization/profile_settings.ex
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,8 @@ defmodule Lanttern.Personalization.ProfileSettings do
students_ids: [pos_integer()],
student_record_types_ids: [pos_integer()],
student_record_statuses_ids: [pos_integer()],
only_starred_strands: boolean()
only_starred_strands: boolean(),
student_info_cycle_id: pos_integer()
}

schema "profile_settings" do
Expand All @@ -53,6 +54,7 @@ defmodule Lanttern.Personalization.ProfileSettings do
field :student_record_types_ids, {:array, :id}
field :student_record_statuses_ids, {:array, :id}
field :only_starred_strands, :boolean, default: false
field :student_info_cycle_id, :id
end

timestamps()
Expand Down Expand Up @@ -85,7 +87,8 @@ defmodule Lanttern.Personalization.ProfileSettings do
:students_ids,
:student_record_types_ids,
:student_record_statuses_ids,
:only_starred_strands
:only_starred_strands,
:student_info_cycle_id
])
|> validate_change(:assessment_view, fn :assessment_view, view ->
if view in ["teacher", "student", "compare"],
Expand Down
3 changes: 3 additions & 0 deletions lib/lanttern/schools/cycle.ex
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ defmodule Lanttern.Schools.Cycle do
import Ecto.Changeset

import LantternWeb.Gettext
alias Lanttern.Schools.Class
alias Lanttern.Schools.School

@type t :: %__MODULE__{
Expand All @@ -16,6 +17,7 @@ defmodule Lanttern.Schools.Cycle do
end_at: Date.t(),
school: School.t(),
school_id: pos_integer(),
classes: [Class.t()],
inserted_at: DateTime.t(),
updated_at: DateTime.t()
}
Expand All @@ -29,6 +31,7 @@ defmodule Lanttern.Schools.Cycle do
belongs_to :parent_cycle, __MODULE__

has_many :subcycles, __MODULE__, foreign_key: :parent_cycle_id
has_many :classes, Class

timestamps()
end
Expand Down
42 changes: 42 additions & 0 deletions lib/lanttern/students_cycle_info.ex
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,12 @@ defmodule Lanttern.StudentsCycleInfo do
"""

import Ecto.Query, warn: false
alias Lanttern.Schools.Student
alias Lanttern.Repo

alias Lanttern.StudentsCycleInfo.StudentCycleInfo
alias Lanttern.Schools.Class
alias Lanttern.Schools.Cycle

@doc """
Returns the list of students_cycle_info.
Expand Down Expand Up @@ -101,4 +104,43 @@ defmodule Lanttern.StudentsCycleInfo do
def change_student_cycle_info(%StudentCycleInfo{} = student_cycle_info, attrs \\ %{}) do
StudentCycleInfo.changeset(student_cycle_info, attrs)
end

@doc """
List parent cycles with a list of classes related to the given student.
Results are ordered by cycle end_at desc and cycle start_at asc.
Classes in tuple are ordered alphabetically.
## Examples
iex> list_cycles_and_classes_for_student(student)
[{%Cycle{}, [%Class{}, ...]}, ...]
"""
@spec list_cycles_and_classes_for_student(Student.t()) :: [
{Cycle.t(), [Class.t()]}
]
def list_cycles_and_classes_for_student(%Student{} = student) do
student_classes_map =
from(
c in Class,
join: s in assoc(c, :students),
where: s.id == ^student.id,
order_by: [asc: c.name]
)
|> Repo.all()
|> Enum.group_by(& &1.cycle_id)

from(
cy in Cycle,
where: cy.school_id == ^student.school_id,
where: is_nil(cy.parent_cycle_id),
order_by: [desc: cy.end_at, asc: cy.start_at]
)
|> Repo.all()
|> Enum.map(fn cycle ->
{cycle, student_classes_map[cycle.id] || []}
end)
end
end
34 changes: 31 additions & 3 deletions lib/lanttern_web/helpers/filters_helpers.ex
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,10 @@ defmodule LantternWeb.FiltersHelpers do
- `:only_starred_strands`
### `:student_info`
- `:student_info_selected_cycle_id`
## Examples
iex> assign_user_filters(socket, [:subjects], user)
Expand Down Expand Up @@ -220,6 +224,27 @@ defmodule LantternWeb.FiltersHelpers do
|> assign_filter_type(current_user, current_filters, filter_types)
end

defp assign_filter_type(
socket,
current_user,
current_filters,
[:student_info | filter_types]
) do
student_info_selected_cycle_id =
case Map.get(current_filters, :student_info_cycle_id) do
nil ->
# use profile current cycle as default
Map.get(current_user.current_profile.current_school_cycle || %{}, :id)

cycle_id ->
cycle_id
end

socket
|> assign(:student_info_selected_cycle_id, student_info_selected_cycle_id)
|> assign_filter_type(current_user, current_filters, filter_types)
end

defp assign_filter_type(socket, current_user, current_filters, [_ | filter_types]),
do: assign_filter_type(socket, current_user, current_filters, filter_types)

Expand All @@ -232,7 +257,8 @@ defmodule LantternWeb.FiltersHelpers do
students: :students_ids,
student_record_types: :student_record_types_ids,
student_record_statuses: :student_record_statuses_ids,
starred_strands: :only_starred_strands
starred_strands: :only_starred_strands,
student_info: :student_info_cycle_id
}

@type_to_current_value_key_map %{
Expand All @@ -244,7 +270,8 @@ defmodule LantternWeb.FiltersHelpers do
students: :selected_students_ids,
student_record_types: :selected_student_record_types_ids,
student_record_statuses: :selected_student_record_statuses_ids,
starred_strands: :only_starred_strands
starred_strands: :only_starred_strands,
student_info: :student_info_selected_cycle_id
}

@doc """
Expand Down Expand Up @@ -599,10 +626,11 @@ defmodule LantternWeb.FiltersHelpers do
- `:students`
- `:student_record_types`
- `:student_record_status`
- `:student_info`
## Examples
iex> save_profile_filters(socket, user, [:subjects])
iex> save_profile_filters(socket, [:subjects])
%Phoenix.LiveView.Socket{}
"""

Expand Down
166 changes: 166 additions & 0 deletions lib/lanttern_web/live/pages/school/students/id/about_component.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
defmodule LantternWeb.StudentLive.AboutComponent do
use LantternWeb, :live_component

alias Lanttern.StudentsCycleInfo

# shared components
# import LantternWeb.ReportingComponents
import LantternWeb.FiltersHelpers, only: [assign_user_filters: 2, save_profile_filters: 2]

@impl true
def render(assigns) do
~H"""
<div class="p-10">
<div class="flex items-center gap-6">
<div class="w-32 h-32 rounded-full bg-ltrn-subtle shadow-lg"></div>
<div>
<h2 class="font-display font-black text-2xl">
<%= @student.name %>
</h2>
<div class="flex items-center gap-4 mt-2">
<div class="relative">
<.action
type="button"
id="current-cycle-dropdown-button"
icon_name="hero-chevron-down-mini"
>
<%= @current_cycle.name %>
</.action>
<.dropdown_menu
id="current-cycle-dropdown"
button_id="current-cycle-dropdown-button"
z_index="10"
>
<:item
:for={{cycle, classes} <- @cycles_and_classes}
text={"#{cycle.name} (#{cycle_classes_opt(classes)})"}
on_click={
JS.push("change_cycle", value: %{"cycle_id" => cycle.id}, target: @myself)
}
/>
</.dropdown_menu>
</div>
<%= if @current_classes == [] do %>
<.badge>
<%= gettext("No classes linked to student in cycle") %>
</.badge>
<% else %>
<.badge :for={class <- @current_classes} id={"current-student-class-#{class.id}"}>
<%= class.name %>
</.badge>
<% end %>
</div>
</div>
</div>
<div class="flex items-start gap-10 mt-12">
<div class="flex-1">
<div class="pb-6 border-b-2 border-ltrn-light">
<h4 class="font-display font-black text-lg"><%= gettext("School area") %></h4>
<p class="flex items-center gap-2 mt-2">
<.icon name="hero-information-circle-mini" class="text-ltrn-subtle" />
<%= gettext("Access to information in this area is restricted to school staff") %>
</p>
</div>
<div class="mt-10">
<.empty_state_simple>
<%= gettext("No information about student in school area") %>
</.empty_state_simple>
<.action type="button" icon_name="hero-pencil-mini" class="mt-10">
<%= gettext("Edit information") %>
</.action>
</div>
</div>
<div class="flex-1">
<div class="pb-6 border-b-2 border-ltrn-student-lighter">
<h4 class="font-display font-black text-lg text-ltrn-student-dark">
<%= gettext("Family area") %>
</h4>
<p class="flex items-center gap-2 mt-2">
<.icon name="hero-information-circle-mini" class="text-ltrn-subtle" />
<%= gettext("Information shared with student and family") %>
</p>
</div>
<div class="mt-10">
<.empty_state_simple>
<%= gettext("No information about student in family area") %>
</.empty_state_simple>
<.action type="button" icon_name="hero-pencil-mini" class="mt-10">
<%= gettext("Edit information") %>
</.action>
</div>
</div>
</div>
</div>
"""
end

# lifecycle

@impl true
def mount(socket),
do: {:ok, assign(socket, :initialized, false)}

@impl true
def update(assigns, socket) do
socket =
socket
|> assign(assigns)
|> initialize()

{:ok, socket}
end

defp initialize(%{assigns: %{initialized: false}} = socket) do
socket
|> assign_user_filters([:student_info])
|> assign_cycles_and_classes()
|> assign_current_cycle_and_classes()
|> assign(:initialized, true)
end

defp initialize(socket), do: socket

defp assign_cycles_and_classes(socket) do
cycles_and_classes =
StudentsCycleInfo.list_cycles_and_classes_for_student(socket.assigns.student)

socket
|> assign(:cycles_and_classes, cycles_and_classes)
end

defp assign_current_cycle_and_classes(socket) do
{current_cycle, current_classes} =
socket.assigns.cycles_and_classes
|> Enum.find(fn {cycle, _classes} ->
cycle.id == socket.assigns.student_info_selected_cycle_id
end)
|> case do
# if for some reason we can't find cycle and classes,
# use the first item of the list
nil -> List.first(socket.assigns.cycles_and_classes)
cycle_and_classes -> cycle_and_classes
end

socket
|> assign(:current_cycle, current_cycle)
|> assign(:current_classes, current_classes)
end

# event handlers

@impl true
def handle_event("change_cycle", %{"cycle_id" => id}, socket) do
socket =
socket
|> assign(:student_info_selected_cycle_id, id)
|> save_profile_filters([:student_info])
|> assign_current_cycle_and_classes()

{:noreply, socket}
end

# helpers

defp cycle_classes_opt([]), do: gettext("No classes")
defp cycle_classes_opt(classes), do: Enum.map_join(classes, ", ", & &1.name)
end
Loading

0 comments on commit fd5961e

Please sign in to comment.