Skip to content

Commit

Permalink
Merge pull request #203 from camino-school/192-formative-assessment-v…
Browse files Browse the repository at this point in the history
…iz-prior-to-report-cards

strands visualization prior to report cards
  • Loading branch information
endoooo authored Sep 3, 2024
2 parents 06b820c + 5188cfc commit e2ae565
Show file tree
Hide file tree
Showing 44 changed files with 2,176 additions and 1,214 deletions.
29 changes: 9 additions & 20 deletions lib/lanttern/assessments.ex
Original file line number Diff line number Diff line change
Expand Up @@ -988,27 +988,24 @@ defmodule Lanttern.Assessments do
@doc """
Returns the list of strand goals, goal entries, and related moment entries for the given student and strand.
Assessment points without entries are ignored.
Assessment points without entries will return `nil`.
Moments without entries will return `nil`.
Moments without entries are ignored.
Ordered by `AssessmentPoint` positions.
Assessment point fields and preloads:
- `:has_diff_rubric_for_student` calculated based on student id
- curriculum item with curriculum component
Assessment point entry preload:
- `ordinal_value` and `student_ordinal_value`
"""

@spec list_strand_goals_student_entries(student_id :: pos_integer(), strand_id :: pos_integer()) ::
@spec list_strand_goals_for_student(student_id :: pos_integer(), strand_id :: pos_integer()) ::
[
{AssessmentPoint.t(), AssessmentPointEntry.t(), [AssessmentPointEntry.t() | nil]}
{AssessmentPoint.t(), AssessmentPointEntry.t() | nil, [AssessmentPointEntry.t()]}
]

def list_strand_goals_student_entries(student_id, strand_id) do
def list_strand_goals_for_student(student_id, strand_id) do
goals_and_entries =
from(
ap in AssessmentPoint,
Expand All @@ -1018,7 +1015,7 @@ defmodule Lanttern.Assessments do
on: diff_r_s.student_id == ^student_id and diff_r_s.rubric_id == diff_r.id,
join: ci in assoc(ap, :curriculum_item),
join: cc in assoc(ci, :curriculum_component),
join: e in AssessmentPointEntry,
left_join: e in AssessmentPointEntry,
on: e.assessment_point_id == ap.id and e.student_id == ^student_id,
left_join: ov in assoc(e, :ordinal_value),
left_join: s_ov in assoc(e, :student_ordinal_value),
Expand All @@ -1038,29 +1035,21 @@ defmodule Lanttern.Assessments do
|> Enum.map(fn {ap, e, ov, s_ov} ->
{
ap,
%{e | ordinal_value: ov, student_ordinal_value: s_ov}
e && %{e | ordinal_value: ov, student_ordinal_value: s_ov}
}
end)

goals_and_moments_entries_map =
from(
ap in AssessmentPoint,
join: m in assoc(ap, :moment),
left_join: e in AssessmentPointEntry,
join: e in AssessmentPointEntry,
on: e.assessment_point_id == ap.id and e.student_id == ^student_id,
left_join: ov in assoc(e, :ordinal_value),
left_join: s_ov in assoc(e, :student_ordinal_value),
where: m.strand_id == ^strand_id,
order_by: [asc: m.position, asc: ap.position],
select: {ap.curriculum_item_id, e, ov, s_ov}
select: {ap.curriculum_item_id, e}
)
|> Repo.all()
|> Enum.map(fn {ci_id, e, ov, s_ov} ->
{
ci_id,
e && %{e | ordinal_value: ov, student_ordinal_value: s_ov}
}
end)
|> Enum.group_by(
fn {ci_id, _e} -> ci_id end,
fn {_ci_id, e} -> e end
Expand Down
33 changes: 19 additions & 14 deletions lib/lanttern/grading.ex
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,7 @@ defmodule Lanttern.Grading do
`:preloads` – preloads associated data
`:scale_id` – filter ordinal values by scale and order results by `normalized_value`
`:ids` – filter ordinal values by given ids
## Examples
Expand All @@ -151,26 +152,30 @@ defmodule Lanttern.Grading do
"""
def list_ordinal_values(opts \\ []) do
OrdinalValue
|> maybe_filter_ordinal_values_by_scale(opts)
from(
ov in OrdinalValue,
order_by: ov.normalized_value
)
|> apply_list_ordinal_values_opts(opts)
|> Repo.all()
|> maybe_preload(opts)
end

defp maybe_filter_ordinal_values_by_scale(ordinal_value_query, opts) do
case Keyword.get(opts, :scale_id) do
nil ->
ordinal_value_query

scale_id ->
from(
ov in ordinal_value_query,
where: ov.scale_id == ^scale_id,
order_by: ov.normalized_value
)
end
defp apply_list_ordinal_values_opts(queryable, []), do: queryable

defp apply_list_ordinal_values_opts(queryable, [{:scale_id, scale_id} | opts]) do
from(ov in queryable, where: ov.scale_id == ^scale_id)
|> apply_list_ordinal_values_opts(opts)
end

defp apply_list_ordinal_values_opts(queryable, [{:ids, ids} | opts]) do
from(ov in queryable, where: ov.id in ^ids)
|> apply_list_ordinal_values_opts(opts)
end

defp apply_list_ordinal_values_opts(queryable, [_ | opts]),
do: apply_list_ordinal_values_opts(queryable, opts)

@doc """
Gets a single ordinal_value.
Optionally preloads associated data.
Expand Down
88 changes: 88 additions & 0 deletions lib/lanttern/learning_context.ex
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ defmodule Lanttern.LearningContext do
import LantternWeb.Gettext
alias Lanttern.Repo

alias Lanttern.Assessments.AssessmentPointEntry
alias Lanttern.LearningContext.Strand
alias Lanttern.LearningContext.StarredStrand
alias Lanttern.LearningContext.Moment
Expand Down Expand Up @@ -50,6 +51,93 @@ defmodule Lanttern.LearningContext do
{results |> maybe_preload(opts), meta}
end

@doc """
Returns the list of strands linked to the student with
linked moments entries.
Strands are "linked to the student" through report cards:
strand -> strand report -> report card -> student report card
Strands results are ordered by cycle (desc), then by strand report position.
Entries are ordered by moment position, then by assessment point position.
Preloads subjects, years, and report cycle.
The same strand can appear more than once, because it can
be linked to more than one report card at the same time.
The `report_card_id` and `report_cycle` can help differentiate them.
## Options:
- `:cycles_ids` - filter results by given cycles, using the report card cycle
## Examples
iex> list_student_strands(1)
[{%Strand{}, [%AssessmentPointEntry{}, ...]}, ...]
"""
@spec list_student_strands(student_id :: pos_integer(), opts :: Keyword.t()) :: [
{Strand.t(), [AssessmentPointEntry.t()]}
]
def list_student_strands(student_id, opts \\ []) do
student_strands =
from(
s in Strand,
left_join: sub in assoc(s, :subjects),
left_join: y in assoc(s, :years),
join: sr in assoc(s, :strand_reports),
join: rc in assoc(sr, :report_card),
join: c in assoc(rc, :school_cycle),
as: :cycles,
join: src in assoc(rc, :students_report_cards),
order_by: [desc: c.end_at, asc: c.start_at, asc: sr.position],
where: src.student_id == ^student_id,
preload: [subjects: sub, years: y],
select: %{s | strand_report_id: sr.id, report_cycle: c}
)
|> apply_list_student_strands_opts(opts)
|> Repo.all()

student_strands_ids =
student_strands
|> Enum.map(& &1.id)

student_strands_entries_map =
from(
e in AssessmentPointEntry,
join: ap in assoc(e, :assessment_point),
join: m in assoc(ap, :moment),
where: m.strand_id in ^student_strands_ids,
where: e.student_id == ^student_id,
order_by: [asc: m.position, asc: ap.position],
select: {e, m.strand_id}
)
|> Repo.all()
|> Enum.group_by(
fn {_entry, strand_id} -> strand_id end,
fn {entry, _strand_id} -> entry end
)

student_strands
|> Enum.map(&{&1, Map.get(student_strands_entries_map, &1.id, [])})
end

defp apply_list_student_strands_opts(queryable, []), do: queryable

defp apply_list_student_strands_opts(queryable, [{:cycles_ids, cycles_ids} | opts])
when cycles_ids != [] do
from(
[_s, cycles: c] in queryable,
where: c.id in ^cycles_ids
)
|> apply_list_student_strands_opts(opts)
end

defp apply_list_student_strands_opts(queryable, [_opt | opts]),
do: apply_list_student_strands_opts(queryable, opts)

@doc """
Search strands by name.
Expand Down
5 changes: 5 additions & 0 deletions lib/lanttern/learning_context/strand.ex
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ defmodule Lanttern.LearningContext.Strand do
alias Lanttern.Notes.Note
alias Lanttern.Notes.StrandNoteRelationship
alias Lanttern.Reporting.StrandReport
alias Lanttern.Schools.Cycle
alias Lanttern.Taxonomy.Subject
alias Lanttern.Taxonomy.Year

Expand All @@ -28,6 +29,8 @@ defmodule Lanttern.LearningContext.Strand do
year_id: pos_integer(),
years_ids: [pos_integer()],
is_starred: boolean(),
strand_report_id: pos_integer(),
report_cycle: Cycle.t(),
moments: [Moment.t()],
assessment_points: [AssessmentPoint.t()],
strand_reports: [StrandReport.t()],
Expand All @@ -49,6 +52,8 @@ defmodule Lanttern.LearningContext.Strand do
field :year_id, :id, virtual: true
field :years_ids, {:array, :id}, virtual: true
field :is_starred, :boolean, virtual: true
field :strand_report_id, :id, virtual: true
field :report_cycle, :map, virtual: true

has_many :moments, Moment
has_many :assessment_points, AssessmentPoint
Expand Down
68 changes: 0 additions & 68 deletions lib/lanttern/notes.ex
Original file line number Diff line number Diff line change
Expand Up @@ -65,74 +65,6 @@ defmodule Lanttern.Notes do
|> Repo.all()
end

@doc """
Returns the list of student strand notes based on their report cards.
The function lists all strands linked to the student report cards, ordering the results
by (report card) cycle descending and strand reports position ascending.
The list is comprised of tuples of note (or `nil`) and strand.
### Options:
`:cycles_ids` – filter results by given cycles
## Examples
iex> list_student_strands_notes(user, opts)
[{%Note{}, %Strand{}}, ...]
"""
@spec list_student_strands_notes(user :: User.t(), opts :: Keyword.t()) :: [
{Note.t() | nil, Strand.t()}
]
def list_student_strands_notes(%{current_profile: profile} = _user, opts \\ []) do
strand_student_notes_map =
from(
n in Note,
join: snr in assoc(n, :strand_note_relationship),
where: n.author_id == ^profile.id,
select: {n, snr}
)
|> Repo.all()
|> Enum.map(fn {n, snr} ->
{snr.strand_id, n}
end)
|> Enum.into(%{})

from(
s in Strand,
join: sr in assoc(s, :strand_reports),
join: rc in assoc(sr, :report_card),
join: c in assoc(rc, :school_cycle),
as: :cycles,
join: src in assoc(rc, :students_report_cards),
order_by: [desc: c.end_at, asc: c.start_at, asc: sr.position],
where: src.student_id == ^profile.student_id
)
|> apply_list_student_strands_notes_opts(opts)
|> Repo.all()
|> Enum.uniq()
|> maybe_preload(preloads: [:subjects, :years])
|> Enum.map(fn strand ->
{Map.get(strand_student_notes_map, strand.id), strand}
end)
end

defp apply_list_student_strands_notes_opts(queryable, []), do: queryable

defp apply_list_student_strands_notes_opts(queryable, [{:cycles_ids, cycles_ids} | opts])
when cycles_ids != [] do
from(
[_s, cycles: c] in queryable,
where: c.id in ^cycles_ids
)
|> apply_list_student_strands_notes_opts(opts)
end

defp apply_list_student_strands_notes_opts(queryable, [_opt | opts]),
do: apply_list_student_strands_notes_opts(queryable, opts)

@doc """
Returns the list of students of the given classes with their strand notes.
Expand Down
39 changes: 39 additions & 0 deletions lib/lanttern/reporting.ex
Original file line number Diff line number Diff line change
Expand Up @@ -615,6 +615,45 @@ defmodule Lanttern.Reporting do
|> maybe_preload(opts)
end

@doc """
Gets a single student report card based on given student and children strand report.
Returns `nil` if the Student report card does not exist.
## Options
- `:preloads` – preloads associated data
## Examples
iex> get_student_report_card_by_student_and_strand_report(123, 123)
%StudentReportCard{}
iex> get_student_report_card_by_student_and_strand_report(456, 456)
nil
"""
@spec get_student_report_card_by_student_and_strand_report(
student_id :: pos_integer(),
strand_report_id :: pos_integer(),
opts :: Keyword.t()
) :: StudentReportCard.t() | nil
def get_student_report_card_by_student_and_strand_report(
student_id,
strand_report_id,
opts \\ []
) do
from(
src in StudentReportCard,
join: rc in assoc(src, :report_card),
join: sr in assoc(rc, :strand_reports),
where: sr.id == ^strand_report_id,
where: src.student_id == ^student_id
)
|> Repo.one()
|> maybe_preload(opts)
end

@doc """
Gets a single student report card by student and report card id.
Expand Down
Loading

0 comments on commit e2ae565

Please sign in to comment.