Skip to content

Commit

Permalink
Merge pull request #44 from camino-school/strands-rubrics
Browse files Browse the repository at this point in the history
Strands rubrics
  • Loading branch information
endoooo authored Jan 24, 2024
2 parents c8c8672 + 7479dea commit e0b1814
Show file tree
Hide file tree
Showing 26 changed files with 1,178 additions and 130 deletions.
67 changes: 65 additions & 2 deletions lib/lanttern/assessments.ex
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ defmodule Lanttern.Assessments do
alias Lanttern.Assessments.AssessmentPointEntry
alias Lanttern.Assessments.Feedback
alias Lanttern.Conversation.Comment
alias Lanttern.Rubrics
alias Lanttern.Schools.Student

@doc """
Expand All @@ -19,6 +20,7 @@ defmodule Lanttern.Assessments do
### Options:
`:preloads` – preloads associated data
`:preload_full_rubrics` – boolean, preloads full associated rubrics using `Rubrics.full_rubric_query/0`
`:assessment_points_ids` – filter result by provided assessment points ids
`:activities_ids` – filter result by provided activities ids
`:activities_from_strand_id` – filter result by activities from provided strand id
Expand All @@ -32,12 +34,22 @@ defmodule Lanttern.Assessments do
"""
def list_assessment_points(opts \\ []) do
AssessmentPoint
|> maybe_preload_full_rubrics(Keyword.get(opts, :preload_full_rubrics))
|> filter_assessment_points(opts)
|> order_assessment_points(opts)
|> Repo.all()
|> maybe_preload(opts)
end

defp maybe_preload_full_rubrics(queryable, true) do
from(
ap in queryable,
preload: [rubric: ^Rubrics.full_rubric_query()]
)
end

defp maybe_preload_full_rubrics(queryable, nil), do: queryable

defp filter_assessment_points(queryable, opts) do
Enum.reduce(opts, queryable, &apply_assessment_points_filter/2)
end
Expand All @@ -57,8 +69,8 @@ defmodule Lanttern.Assessments do
)
end

defp apply_assessment_points_filter({:strands_ids, ids}, queryable),
do: from(ap in queryable, where: ap.strand_id in ^ids)
defp apply_assessment_points_filter({:strand_id, id}, queryable),
do: from(ap in queryable, where: ap.strand_id == ^id)

defp apply_assessment_points_filter(_, queryable), do: queryable

Expand Down Expand Up @@ -798,4 +810,55 @@ defmodule Lanttern.Assessments do
{:error, "Something went wrong"}
end
end

@doc """
Creates a rubric and link it to the given assessment point.
It's a wrapper around `Rubrics.create_rubric/2` with an assessment point update
in the same transaction (avoiding "orphans" rubrics).
If some error happens during rubric creation, it returns a tuple with `:error` and rubric
error changeset. If the error happens elsewhere, it returns a tuple with `:error` and a message.
## Options
- View `Rubrics.create_rubric/2`
## Examples
iex> create_assessment_point_rubric(1, %{field: value})
{:ok, %AssessmentPointEntry{}}
iex> create_assessment_point_rubric(2, %{field: bad_value})
{:error, %Ecto.Changeset{}}
iex> create_assessment_point_rubric(999, %{field: value})
{:ok, "Assessment point not found"}
"""
def create_assessment_point_rubric(assessment_point_id, attrs \\ %{}, opts \\ []) do
Repo.transaction(fn ->
rubric =
case Rubrics.create_rubric(attrs, opts) do
{:ok, rubric} -> rubric
{:error, error_changeset} -> Repo.rollback(error_changeset)
end

assessment_point =
case get_assessment_point(assessment_point_id) do
nil -> Repo.rollback("Assessment point not found")
assessment_point -> assessment_point
end

case update_assessment_point(assessment_point, %{rubric_id: rubric.id}) do
{:ok, _assessment_point} ->
:ok

{:error, _error_changeset} ->
Repo.rollback("Error linking rubric to the assessment point")
end

rubric
end)
end
end
1 change: 1 addition & 0 deletions lib/lanttern/learning_context/strand.ex
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ defmodule Lanttern.LearningContext.Strand do
field :is_starred, :boolean, virtual: true

has_many :activities, Lanttern.LearningContext.Activity
has_many :assessment_points, Lanttern.Assessments.AssessmentPoint

many_to_many :subjects, Lanttern.Taxonomy.Subject,
join_through: "strands_subjects",
Expand Down
180 changes: 157 additions & 23 deletions lib/lanttern/rubrics.ex
Original file line number Diff line number Diff line change
Expand Up @@ -38,18 +38,52 @@ defmodule Lanttern.Rubrics do
View `get_full_rubric!/1` for more details on descriptors sorting.
## Options
- `assessment_points_ids` - filter rubrics by linked assessment points
- `parent_rubrics_ids` - filter differentiation rubrics by parent rubrics
- `students_ids` - filter rubrics by linked students
## Examples
iex> list_full_rubrics()
[%Rubric{}, ...]
"""
def list_full_rubrics() do
def list_full_rubrics(opts \\ []) do
full_rubric_query()
|> filter_rubrics(opts)
|> Repo.all()
|> Enum.map(&sort_rubric_descriptors/1)
end

defp filter_rubrics(queryable, opts) do
Enum.reduce(opts, queryable, &apply_rubrics_filter/2)
end

defp apply_rubrics_filter({:rubrics_ids, ids}, queryable),
do: from(ap in queryable, where: ap.id in ^ids)

defp apply_rubrics_filter({:parent_rubrics_ids, ids}, queryable),
do: from(ap in queryable, where: ap.diff_for_rubric_id in ^ids)

defp apply_rubrics_filter({:assessment_points_ids, ids}, queryable) do
from(
r in queryable,
join: ap in assoc(r, :assessment_points),
where: ap.id in ^ids
)
end

defp apply_rubrics_filter({:students_ids, ids}, queryable) do
from(
r in queryable,
join: s in assoc(r, :students),
where: s.id in ^ids
)
end

defp apply_rubrics_filter(_, queryable), do: queryable

@doc """
Search rubrics by criteria.
Expand Down Expand Up @@ -134,34 +168,31 @@ defmodule Lanttern.Rubrics do
def get_full_rubric!(id) do
full_rubric_query()
|> Repo.get!(id)
|> sort_rubric_descriptors()
end

defp full_rubric_query() do
@doc """
Query used to load rubrics with descriptors
ordered using the following rules:
- when scale type is "ordinal", we use ordinal value's normalized value
- when scale type is "numeric", we use descriptor's score
"""
def full_rubric_query() do
descriptors_query =
from(
d in RubricDescriptor,
left_join: ov in assoc(d, :ordinal_value),
order_by: [d.score, ov.normalized_value],
preload: [ordinal_value: ov]
)

from(r in Rubric,
join: s in assoc(r, :scale),
left_join: d in assoc(r, :descriptors),
left_join: ov in assoc(d, :ordinal_value),
preload: [:scale, descriptors: :ordinal_value],
group_by: r.id,
preload: [scale: s, descriptors: ^descriptors_query],
order_by: r.id
)
end

defp sort_rubric_descriptors(rubric) do
Map.update!(rubric, :descriptors, fn descriptors ->
case rubric.scale.type do
"numeric" ->
descriptors
|> Enum.sort_by(& &1.score)

"ordinal" ->
descriptors
|> Enum.sort_by(& &1.ordinal_value.normalized_value)
end
end)
end

@doc """
Creates a rubric.
Expand Down Expand Up @@ -291,7 +322,9 @@ defmodule Lanttern.Rubrics do
"""
def delete_rubric(%Rubric{} = rubric) do
Repo.delete(rubric)
rubric
|> Rubric.changeset(%{})
|> Repo.delete()
end

@doc """
Expand Down Expand Up @@ -403,6 +436,107 @@ defmodule Lanttern.Rubrics do
RubricDescriptor.changeset(rubric_descriptor, attrs)
end

@doc """
Links a differentiation rubric to a student.
## Examples
iex> link_rubric_to_student(%Rubric{}, 1)
:ok
iex> link_rubric_to_student(%Rubric{}, 1)
{:error, "Error message"}
"""
def link_rubric_to_student(%Rubric{diff_for_rubric_id: nil}, _student_id),
do: {:error, "Only differentiation rubrics can be linked to students"}

def link_rubric_to_student(%Rubric{id: rubric_id}, student_id) do
from("differentiation_rubrics_students",
where: [rubric_id: ^rubric_id, student_id: ^student_id],
select: true
)
|> Repo.one()
|> case do
nil ->
{1, _} =
Repo.insert_all(
"differentiation_rubrics_students",
[[rubric_id: rubric_id, student_id: student_id]]
)

:ok

_ ->
# rubric already linked to student
:ok
end
end

@doc """
Unlinks a rubric from a student.
## Examples
iex> unlink_rubric_from_student(%Rubric{}, 1)
:ok
iex> unlink_rubric_from_student(%Rubric{}, 1)
{:error, "Error message"}
"""
def unlink_rubric_from_student(%Rubric{id: rubric_id}, student_id) do
from("differentiation_rubrics_students",
where: [rubric_id: ^rubric_id, student_id: ^student_id]
)
|> Repo.delete_all()

:ok
end

@doc """
Creates a differentiation rubric and link it to the student.
This function executes `create_rubric/2` and `link_rubric_to_student/2`
inside a single transaction.
## Options
- view `create_rubric/2` for opts
## Examples
iex> create_diff_rubric_for_student(1, %{})
{:ok, %Rubric{}}
iex> create_diff_rubric_for_student(1, %{})
{:error, %Ecto.Changeset{}}
"""
def create_diff_rubric_for_student(student_id, attrs \\ %{}, opts \\ []) do
Repo.transaction(fn ->
rubric =
case create_rubric(attrs, opts) do
{:ok, rubric} -> rubric
{:error, error_changeset} -> Repo.rollback(error_changeset)
end

case link_rubric_to_student(rubric, student_id) do
:ok ->
:ok

{:error, msg} ->
rubric
|> change_rubric(%{})
|> Ecto.Changeset.add_error(:diff_for_rubric_id, msg)
|> Map.put(:action, :insert)
|> Repo.rollback()
end

rubric
end)
end

# helpers

defp apply_filters(rubrics_query, opts) do
Expand Down
15 changes: 14 additions & 1 deletion lib/lanttern/rubrics/rubric.ex
Original file line number Diff line number Diff line change
Expand Up @@ -7,16 +7,29 @@ defmodule Lanttern.Rubrics.Rubric do
field :is_differentiation, :boolean, default: false

belongs_to :scale, Lanttern.Grading.Scale
belongs_to :parent_rubric, __MODULE__, foreign_key: :diff_for_rubric_id

has_many :descriptors, Lanttern.Rubrics.RubricDescriptor, on_replace: :delete
has_many :differentiation_rubrics, __MODULE__, foreign_key: :diff_for_rubric_id
has_many :assessment_points, Lanttern.Assessments.AssessmentPoint

many_to_many :students, Lanttern.Schools.Student,
join_through: "differentiation_rubrics_students"

timestamps()
end

@doc false
def changeset(rubric, attrs) do
rubric
|> cast(attrs, [:criteria, :is_differentiation, :scale_id])
|> cast(attrs, [:criteria, :is_differentiation, :diff_for_rubric_id, :scale_id])
|> validate_required([:criteria, :is_differentiation, :scale_id])
|> cast_assoc(:descriptors)
|> foreign_key_constraint(
:diff_for_rubric_id,
name: :rubrics_diff_for_rubric_id_fkey,
message:
"This rubric has linked differentiation rubrics. Deleting it is not allowed, as it would cause data loss."
)
end
end
Loading

0 comments on commit e0b1814

Please sign in to comment.