diff --git a/lib/oli/analytics/by_activity.ex b/lib/oli/analytics/by_activity.ex index 28d0557c82c..217f09ad328 100644 --- a/lib/oli/analytics/by_activity.ex +++ b/lib/oli/analytics/by_activity.ex @@ -18,7 +18,7 @@ defmodule Oli.Analytics.ByActivity do defp get_base_query(project_slug, filtered_sections) do subquery = if filtered_sections != [] do - DeliveryResolver.revisions_filter_by_section_ids( + DeliveryResolver.revisions_by_section_ids( filtered_sections, ResourceType.id_for_activity() ) @@ -39,7 +39,6 @@ defmodule Oli.Analytics.ByActivity do number_of_attempts: analytics.number_of_attempts, relative_difficulty: analytics.relative_difficulty }, - preload: [:resource_type], - distinct: [activity] + preload: [:resource_type] end end diff --git a/lib/oli/analytics/by_objective.ex b/lib/oli/analytics/by_objective.ex index 256ac807445..1492b375fb7 100644 --- a/lib/oli/analytics/by_objective.ex +++ b/lib/oli/analytics/by_objective.ex @@ -20,7 +20,7 @@ defmodule Oli.Analytics.ByObjective do defp get_base_query(project_slug, activity_objectives, filtered_sections) do subquery = if filtered_sections != [] do - DeliveryResolver.revisions_filter_by_section_ids( + DeliveryResolver.revisions_by_section_ids( filtered_sections, ResourceType.id_for_objective() ) diff --git a/lib/oli/analytics/by_page.ex b/lib/oli/analytics/by_page.ex index 18ffcace7cf..e8a424bcc99 100644 --- a/lib/oli/analytics/by_page.ex +++ b/lib/oli/analytics/by_page.ex @@ -22,7 +22,7 @@ defmodule Oli.Analytics.ByPage do defp get_base_query(project_slug, activity_pages, filtered_sections) do subquery = if filtered_sections != [] do - DeliveryResolver.revisions_filter_by_section_ids( + DeliveryResolver.revisions_by_section_ids( filtered_sections, ResourceType.id_for_page() ) @@ -35,9 +35,9 @@ defmodule Oli.Analytics.ByPage do subquery_activity = if filtered_sections != [] do - DeliveryResolver.revisions_filter_by_section_ids( + DeliveryResolver.revisions_by_section_ids( filtered_sections, - ResourceType.id_for_page() + ResourceType.id_for_activity() ) else Publishing.query_unpublished_revisions_by_type( @@ -62,8 +62,7 @@ defmodule Oli.Analytics.ByPage do number_of_attempts: analytics.number_of_attempts, relative_difficulty: analytics.relative_difficulty }, - preload: [:resource_type], - distinct: [activity] + preload: [:resource_type] ) end diff --git a/lib/oli/delivery/metrics.ex b/lib/oli/delivery/metrics.ex index 1901e70a7bb..6b42c8855d0 100644 --- a/lib/oli/delivery/metrics.ex +++ b/lib/oli/delivery/metrics.ex @@ -1158,6 +1158,77 @@ defmodule Oli.Delivery.Metrics do end) end + @doc """ + Calculates the learning proficiency ("High", "Medium", "Low", "Not enough data") + for a given student in a given section. + + It returns a map: + + %{objective_id_1 => "High", + ... + objective_id_n => "Low" + } + """ + @spec proficiency_for_student_per_learning_objective( + section :: %Oli.Delivery.Sections.Section{}, + student_id :: integer + ) :: map + def proficiency_for_student_per_learning_objective( + %Section{slug: section_slug, analytics_version: :v1}, + student_id + ) do + query = + from(sn in Snapshot, + join: s in Section, + on: sn.section_id == s.id, + where: + sn.attempt_number == 1 and sn.part_attempt_number == 1 and s.slug == ^section_slug and + sn.user_id == ^student_id, + group_by: sn.objective_id, + select: + {sn.objective_id, + fragment( + "CAST(COUNT(CASE WHEN ? THEN 1 END) as float) / CAST(COUNT(*) as float)", + sn.correct + )} + ) + + Repo.all(query) + |> Enum.into(%{}, fn {objective_id, proficiency} -> + {objective_id, proficiency_range(proficiency)} + end) + end + + def proficiency_for_student_per_learning_objective( + %Section{id: section_id, analytics_version: :v2}, + student_id + ) do + objective_type_id = Oli.Resources.ResourceType.id_for_objective() + + query = + from(summary in Oli.Analytics.Summary.ResourceSummary, + where: + summary.section_id == ^section_id and + summary.project_id == -1 and + summary.publication_id == -1 and + summary.user_id == ^student_id and + summary.resource_type_id == ^objective_type_id, + group_by: summary.resource_id, + select: + {summary.resource_id, + fragment( + "CAST(SUM(?) as float) / CAST(SUM(?) as float)", + summary.num_first_attempts_correct, + summary.num_first_attempts + )} + ) + + Repo.all(query) + |> Enum.into(%{}, fn {objective_id, proficiency} -> + {objective_id, proficiency_range(proficiency)} + end) + end + def proficiency_range(nil), do: "Not enough data" def proficiency_range(proficiency) when proficiency <= 0.5, do: "Low" def proficiency_range(proficiency) when proficiency <= 0.8, do: "Medium" diff --git a/lib/oli/delivery/sections.ex b/lib/oli/delivery/sections.ex index fee608f0f52..76ab3c52b59 100644 --- a/lib/oli/delivery/sections.ex +++ b/lib/oli/delivery/sections.ex @@ -2335,7 +2335,8 @@ defmodule Oli.Delivery.Sections do # create any remaining section resources which are not in the hierarchy create_nonstructural_section_resources(section.id, [publication_id], skip_resource_ids: processed_ids, - required_survey_resource_id: survey_id + required_survey_resource_id: survey_id, + reference_activity_ids: [] ) root_section_resource_id = section_resources_by_resource_id[root_resource_id].id @@ -2823,10 +2824,15 @@ defmodule Oli.Delivery.Sections do |> select([p], p.required_survey_resource_id) |> Repo.one() - create_nonstructural_section_resources(section_id, publication_ids, - skip_resource_ids: processed_resource_ids, - required_survey_resource_id: survey_id - ) + reference_activity_ids = build_reference_activity_ids(hierarchy.children) + + if length(processed_resource_ids) != 1 do + create_nonstructural_section_resources(section_id, publication_ids, + skip_resource_ids: processed_resource_ids, + required_survey_resource_id: survey_id, + reference_activity_ids: reference_activity_ids + ) + end # Rebuild section previous next index PreviousNextIndex.rebuild(section, hierarchy) @@ -2838,6 +2844,62 @@ defmodule Oli.Delivery.Sections do end) end + defp build_reference_activity_ids([]), do: [] + + defp build_reference_activity_ids(children_activities) do + tuple_of_children_activities = + process_activity_ids(children_activities) + + tuple_of_children_activities + |> Tuple.to_list() + |> Enum.concat() + end + + defp process_activity_ids(children_activities) do + children_activities + |> Enum.reduce({[], []}, fn r, {children_unit_activities_ids, children_activities_ids} -> + new_children_unit_activities_ids = build_child_activity_ids_for_units(r.children) + + if Map.has_key?(r.revision, :content) do + new_children_activities_ids = build_child_activity_ids(r.revision.content) + + {children_unit_activities_ids ++ new_children_unit_activities_ids, + children_activities_ids ++ new_children_activities_ids} + else + {children_unit_activities_ids ++ new_children_unit_activities_ids, + children_activities_ids ++ []} + end + end) + end + + defp build_child_activity_ids_for_units([]), do: [] + + defp build_child_activity_ids_for_units(children) do + children + |> Enum.map(fn c -> + if Map.has_key?(c.revision, :content) do + build_child_activity_ids(c.revision.content) + else + [] + end + end) + |> List.flatten() + end + + defp build_child_activity_ids(%{"model" => nil}), do: [] + + defp build_child_activity_ids(%{"model" => model}) do + model + |> Enum.reduce([], fn item, activity_ids -> + case item["activity_id"] do + nil -> activity_ids + activity_id -> [activity_id | activity_ids] + end + end) + end + + defp build_child_activity_ids(_), do: [] + def get_contained_pages(%Section{id: section_id}) do from(cp in ContainedPage, where: cp.section_id == ^section_id @@ -3630,10 +3692,11 @@ defmodule Oli.Delivery.Sections do # any that belong to the resource ids in skip_resource_ids defp create_nonstructural_section_resources(section_id, publication_ids, skip_resource_ids: skip_resource_ids, - required_survey_resource_id: required_survey_resource_id + required_survey_resource_id: required_survey_resource_id, + reference_activity_ids: reference_activity_ids ) do published_resources_by_resource_id = - MinimalHierarchy.published_resources_map(publication_ids) + build_published_resources_by_resource_id(publication_ids, reference_activity_ids) now = DateTime.utc_now() |> DateTime.truncate(:second) @@ -3677,6 +3740,14 @@ defmodule Oli.Delivery.Sections do Database.batch_insert_all(SectionResource, section_resource_rows) end + defp build_published_resources_by_resource_id(publication_ids, []), + do: MinimalHierarchy.published_resources_map(publication_ids) + + defp build_published_resources_by_resource_id(publication_ids, list_children_section_ids), + do: + MinimalHierarchy.published_resources_map(publication_ids) + |> Map.take(list_children_section_ids) + def is_structural?(%Revision{resource_type_id: resource_type_id}) do container = ResourceType.id_for_container() diff --git a/lib/oli/publishing/delivery_resolver.ex b/lib/oli/publishing/delivery_resolver.ex index c2b575626c9..aa2b97189a7 100644 --- a/lib/oli/publishing/delivery_resolver.ex +++ b/lib/oli/publishing/delivery_resolver.ex @@ -263,7 +263,7 @@ defmodule Oli.Publishing.DeliveryResolver do |> emit([:oli, :resolvers, :delivery], :duration) end - def revisions_filter_by_section_ids(section_ids, resource_type_id) do + def revisions_by_section_ids(section_ids, resource_type_id) do from(sr in SectionResource, join: s in Section, on: s.id == sr.section_id, @@ -277,7 +277,8 @@ defmodule Oli.Publishing.DeliveryResolver do join: rev in Revision, on: rev.id == pr.revision_id, where: rev.resource_type_id == ^resource_type_id and rev.deleted == false, - select: rev + select: rev, + distinct: [rev] ) end diff --git a/lib/oli/resources.ex b/lib/oli/resources.ex index 1bb3d1db484..9557a652ef0 100644 --- a/lib/oli/resources.ex +++ b/lib/oli/resources.ex @@ -425,4 +425,15 @@ defmodule Oli.Resources do ) |> Repo.one() end + + @doc """ + Returns a list of revisions for the given resource ids. + """ + def get_revisions_by_resource_id(resource_ids) do + from(r in Revision, + where: r.resource_id in ^resource_ids, + select: r + ) + |> Repo.all() + end end diff --git a/lib/oli/utils/db_seeder.ex b/lib/oli/utils/db_seeder.ex index 05acb982a8a..b4803f74cca 100644 --- a/lib/oli/utils/db_seeder.ex +++ b/lib/oli/utils/db_seeder.ex @@ -198,14 +198,34 @@ defmodule Oli.Seeder do create_published_resource(publication, container_resource, container_revision) + %{resource: _activity_resource, revision: activity_revision} = + create_activity( + %{ + activity_type_id: Activities.get_registration_by_slug("oli_short_answer").id, + content: %{} + }, + publication, + project, + author + ) + %{resource: page1, revision: revision1} = create_page("Page one", publication, project, author) %{resource: page2, revision: revision2} = create_page("Page two", publication, project, author, create_sample_content()) + %{resource: page3, revision: revision3} = + create_page( + "Page three", + publication, + project, + author, + create_activity_content(activity_revision.resource_id) + ) + container_revision = - attach_pages_to([page1, page2], container_resource, container_revision, publication) + attach_pages_to([page1, page2, page3], container_resource, container_revision, publication) {:ok, pub1} = Publishing.publish_project(project, "some changes", author.id) @@ -229,8 +249,10 @@ defmodule Oli.Seeder do |> Map.put(:container, %{resource: container_resource, revision: container_revision}) |> Map.put(:page1, page1) |> Map.put(:page2, page2) + |> Map.put(:page3, page3) |> Map.put(:revision1, revision1) |> Map.put(:revision2, revision2) + |> Map.put(:revision3, revision3) |> Map.put(:section, section) end @@ -1290,6 +1312,19 @@ defmodule Oli.Seeder do } end + def create_activity_content(activity_resource_id) do + %{ + "model" => [ + %{ + "type" => "activity-reference", + "activity_id" => activity_resource_id, + "custom" => %{} + } + ], + "advancedDelivery" => false + } + end + def create_sample_content() do %{ "model" => [ diff --git a/lib/oli_web/components/modal.ex b/lib/oli_web/components/modal.ex index b6e50f6e437..b79da063ded 100644 --- a/lib/oli_web/components/modal.ex +++ b/lib/oli_web/components/modal.ex @@ -68,7 +68,7 @@ defmodule OliWeb.Components.Modal do phx-window-keydown={hide_modal(@on_cancel, @id)} phx-key="escape" phx-click-away={hide_modal(@on_cancel, @id)} - class="hidden relative rounded-lg bg-white dark:bg-body-dark shadow-lg shadow-zinc-700/10 ring-1 ring-zinc-700/10 transition" + class="hidden relative bg-white dark:bg-body-dark shadow-lg shadow-zinc-700/10 ring-1 ring-zinc-700/10 transition" >
@@ -148,6 +148,126 @@ defmodule OliWeb.Components.Modal do """ end + attr :id, :string, required: true + attr :class, :string, default: "" + attr :body_class, :string, default: "p-6 space-y-6" + attr :show, :boolean, default: false + attr :on_cancel, JS, default: %JS{} + attr :on_confirm, JS, default: %JS{} + + slot :inner_block, required: true + slot :title + slot :subtitle + slot :confirm + slot :cancel + + def student_delivery_modal(assigns) do + ~H""" +