Skip to content

Commit

Permalink
feat: manage strand goals rubrics
Browse files Browse the repository at this point in the history
- fixed `Assessments.list_assessment_points/1` `strand_id` filter opt

- created `Assessments.create_assessment_point_rubric/3` to create rubric and link to assessment point in a single transaction

- created `LantternWeb.RubricsComponents` with `<.rubric_descriptors>` component

- adjusted rubrics live to use `<.rubric_descriptors>`

- created `LantternWeb.StrandLive.StrandRubricsComponent`

- adjusted `LantternWeb.Rubrics.RubricFormComponent`, removing the need of `action` assign (which can be inferred from the existence of the rubric id), adding support to `link_to_assessment_point_id` assign, and removing `maybe_push_patch/2` private function in favor of `handle_navigation/1` helper

- added migration to fix `assessment_points` `rubric_id` constraint (it was on delete `CASCADE`, it should be `SET NULL`)
  • Loading branch information
endoooo committed Jan 19, 2024
1 parent c8c8672 commit e30aeb1
Show file tree
Hide file tree
Showing 15 changed files with 407 additions and 54 deletions.
56 changes: 54 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 Down Expand Up @@ -57,8 +58,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 +799,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
33 changes: 33 additions & 0 deletions lib/lanttern_web/components/rubrics_components.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
defmodule LantternWeb.RubricsComponents do
use Phoenix.Component

import LantternWeb.CoreComponents

alias Lanttern.Rubrics.Rubric

@doc """
Renders rubric descriptors.
"""
attr :rubric, Rubric, required: true, doc: "Requires descriptors + ordinal_value preloads"
attr :class, :any, default: nil

def rubric_descriptors(assigns) do
~H"""
<div class={["flex items-stretch gap-2", @class]}>
<div :for={descriptor <- @rubric.descriptors} class="flex-[1_0] flex flex-col items-start gap-2">
<%= if descriptor.scale_type == "numeric" do %>
<.badge theme="dark"><%= descriptor.score %></.badge>
<% else %>
<.badge style_from_ordinal_value={descriptor.ordinal_value}>
<%= descriptor.ordinal_value.name %>
</.badge>
<% end %>
<.markdown
text={descriptor.descriptor}
class="prose-sm flex-1 w-full p-2 border border-ltrn-lighter rounded-sm bg-ltrn-lightest"
/>
</div>
</div>
"""
end
end
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,6 @@ defmodule LantternWeb.AssessmentPointLive.DifferentiationRubricComponent do
<.live_component
module={RubricFormComponent}
id={"entry-#{@entry.id}"}
action={:new}
rubric={
%Rubric{
scale_id: @entry.scale_id,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,13 +41,13 @@ defmodule LantternWeb.AssessmentPointLive.RubricsOverlayComponent do
<.live_component
module={RubricFormComponent}
id={:new}
action={:new}
rubric={
%Rubric{
scale_id: @assessment_point.scale_id,
is_differentiation: false
}
}
link_to_assessment_point_id={@assessment_point.id}
hide_diff_and_scale
show_buttons
on_cancel={JS.push("cancel_create_new", target: @myself)}
Expand Down Expand Up @@ -143,9 +143,11 @@ defmodule LantternWeb.AssessmentPointLive.RubricsOverlayComponent do
do: {:ok, link_rubric_to_assessment_and_notify_parent(socket, rubric_id)}

def update(%{action: {RubricFormComponent, {:created, rubric}}}, socket) do
notify_parent({:rubric_linked, rubric.id})

{:ok,
socket
|> link_rubric_to_assessment_and_notify_parent(rubric.id)
|> assign(:rubric, Rubrics.get_full_rubric!(rubric.id))
|> assign(:is_creating_rubric, false)}
end

Expand Down
2 changes: 2 additions & 0 deletions lib/lanttern_web/live/pages/rubrics/rubrics_live.ex
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ defmodule LantternWeb.RubricsLive do
alias Lanttern.Rubrics
alias Lanttern.Rubrics.Rubric

import LantternWeb.RubricsComponents

# lifecycle

def mount(_params, _session, socket) do
Expand Down
19 changes: 1 addition & 18 deletions lib/lanttern_web/live/pages/rubrics/rubrics_live.html.heex
Original file line number Diff line number Diff line change
Expand Up @@ -33,24 +33,7 @@
</.link>
</div>
</div>
<div class="flex items-stretch gap-2">
<div
:for={descriptor <- rubric.descriptors}
class="flex-[1_0] flex flex-col items-start gap-2"
>
<%= if descriptor.scale_type == "numeric" do %>
<.badge theme="dark"><%= descriptor.score %></.badge>
<% else %>
<.badge style_from_ordinal_value={descriptor.ordinal_value}>
<%= descriptor.ordinal_value.name %>
</.badge>
<% end %>
<.markdown
text={descriptor.descriptor}
class="prose-sm flex-1 w-full p-2 border border-ltrn-lighter rounded-sm bg-ltrn-lightest"
/>
</div>
</div>
<.rubric_descriptors rubric={rubric} />
</div>
</div>
</div>
Expand Down
2 changes: 1 addition & 1 deletion lib/lanttern_web/live/pages/strands/id/about_component.ex
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ defmodule LantternWeb.StrandLive.AboutComponent do
Under the hood, goals in Lanttern are defined by assessment points linked directly to the strand — when adding goals, we are adding assessment points which, in turn, hold the curriculum items we'll want to assess along the strand course.
</p>
<div :for={{curriculum_item, i} <- @curriculum_items} class="mt-6">
<div class="flex-1 flex items-stretch gap-6 p-6 rounded bg-white shadow-lg">
<div class="flex items-stretch gap-6 p-6 rounded bg-white shadow-lg">
<div class="flex-1">
<div class="flex items-center gap-4">
<p class="font-display font-bold text-sm">
Expand Down
51 changes: 31 additions & 20 deletions lib/lanttern_web/live/pages/strands/id/assessment_component.ex
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ defmodule LantternWeb.StrandLive.AssessmentComponent do
alias Lanttern.Schools

# shared components
alias LantternWeb.StrandLive.StrandRubricsComponent
alias LantternWeb.Schools.ClassFilterFormComponent

@impl true
Expand Down Expand Up @@ -90,6 +91,12 @@ defmodule LantternWeb.StrandLive.AssessmentComponent do
/>
</div>
</div>
<.live_component
module={StrandRubricsComponent}
id={:strand_rubrics}
strand={@strand}
live_action={@live_action}
/>
<.slide_over id="classes-filter-overlay">
<:title>Classes</:title>
<.live_component
Expand Down Expand Up @@ -211,30 +218,34 @@ defmodule LantternWeb.StrandLive.AssessmentComponent do

@impl true
def mount(socket) do
{:ok,
socket
|> stream_configure(
:assessment_points,
dom_id: fn
{ap, _index} -> "assessment-point-#{ap.id}"
_ -> ""
end
)
|> stream_configure(
:students_entries,
dom_id: fn {student, _entries} -> "student-#{student.id}" end
)
|> assign(:classes, nil)
|> assign(:classes_ids, [])}
socket =
socket
|> stream_configure(
:assessment_points,
dom_id: fn
{ap, _index} -> "assessment-point-#{ap.id}"
_ -> ""
end
)
|> stream_configure(
:students_entries,
dom_id: fn {student, _entries} -> "student-#{student.id}" end
)
|> assign(:classes, nil)
|> assign(:classes_ids, [])

{:ok, socket}
end

@impl true
def update(%{strand: strand} = assigns, socket) do
{:ok,
socket
|> assign(assigns)
|> assign_classes(assigns.params)
|> core_assigns(strand.id)}
socket =
socket
|> assign(assigns)
|> assign_classes(assigns.params)
|> core_assigns(strand.id)

{:ok, socket}
end

def update(_assigns, socket), do: {:ok, socket}
Expand Down
21 changes: 19 additions & 2 deletions lib/lanttern_web/live/pages/strands/id/strand_live.ex
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,23 @@ defmodule LantternWeb.StrandLive do
# lifecycle

@impl true
def mount(_params, _session, socket) do
{:ok, assign(socket, :strand, nil), layout: {LantternWeb.Layouts, :app_logged_in_blank}}
def mount(params, _session, socket) do
socket =
socket
|> assign(:strand, nil)
|> maybe_redirect(params)

{:ok, socket, layout: {LantternWeb.Layouts, :app_logged_in_blank}}
end

# prevent user from navigating directly to nested views

defp maybe_redirect(%{assigns: %{live_action: live_action}} = socket, params)
when live_action == :manage_rubric,
do: redirect(socket, to: ~p"/strands/#{params["id"]}?tab=assessment")

defp maybe_redirect(socket, _params), do: socket

@impl true
def handle_params(%{"tab" => "assessment"} = params, _url, socket) do
# when in assessment tab, sync classes_ids filter with profile
Expand Down Expand Up @@ -55,6 +68,10 @@ defmodule LantternWeb.StrandLive do
defp set_current_tab(socket, %{"tab" => tab}, _live_action),
do: assign(socket, :current_tab, Map.get(@tabs, tab, :about))

defp set_current_tab(socket, _params, live_action)
when live_action in [:manage_rubric],
do: assign(socket, :current_tab, :assessment)

defp set_current_tab(socket, _params, _live_action),
do: assign(socket, :current_tab, :about)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@
strand={@strand}
params={@params}
current_user={@current_user}
live_action={@live_action}
/>
<.live_component
:if={@current_tab == :notes}
Expand Down
Loading

0 comments on commit e30aeb1

Please sign in to comment.