diff --git a/assets/static/images/gradients/assignments-bg.png b/assets/static/images/gradients/assignments-bg.png new file mode 100644 index 00000000000..85b5a8eee01 Binary files /dev/null and b/assets/static/images/gradients/assignments-bg.png differ diff --git a/lib/oli/delivery/paywall.ex b/lib/oli/delivery/paywall.ex index 51525b12ce7..0306519f4a6 100644 --- a/lib/oli/delivery/paywall.ex +++ b/lib/oli/delivery/paywall.ex @@ -791,12 +791,16 @@ defmodule Oli.Delivery.Paywall do fragment( """ CASE - WHEN ? = 'deferred' THEN 1 - WHEN ? = 'direct' THEN 2 - ELSE 3 + WHEN ? = 'bypass' THEN 1 + WHEN ? = 'deferred' THEN 2 + WHEN ? = 'direct' THEN 3 + WHEN ? = 'invalidated' THEN 4 + ELSE 5 END """, p.type, + p.type, + p.type, p.type )}, {:desc, p.generation_date} diff --git a/lib/oli/delivery/sections/section_resource_depot.ex b/lib/oli/delivery/sections/section_resource_depot.ex index 3f4c9f784c8..ffeaf765962 100644 --- a/lib/oli/delivery/sections/section_resource_depot.ex +++ b/lib/oli/delivery/sections/section_resource_depot.ex @@ -69,13 +69,25 @@ defmodule Oli.Delivery.Sections.SectionResourceDepot do @doc """ Returns a list of SectionResource records for all graded pages for a given section. + + An optional keyword list can be passed to extend the filtering conditions. + + Example: + SectionResourceDepot.graded_pages(some_section_id, [hidden: false]) """ - def graded_pages(section_id) do + def graded_pages(section_id, additional_query_conditions \\ []) do init_if_necessary(section_id) page = Oli.Resources.ResourceType.id_for_page() - Depot.query(@depot_desc, section_id, graded: true, resource_type_id: page) + query_conditions = + Keyword.merge([graded: true, resource_type_id: page], additional_query_conditions) + + Depot.query( + @depot_desc, + section_id, + query_conditions + ) |> Enum.sort_by(& &1.numbering_index) end diff --git a/lib/oli_web/components/delivery/layouts.ex b/lib/oli_web/components/delivery/layouts.ex index 8045a33b514..4edf85071ef 100644 --- a/lib/oli_web/components/delivery/layouts.ex +++ b/lib/oli_web/components/delivery/layouts.ex @@ -520,6 +520,16 @@ defmodule OliWeb.Components.Delivery.Layouts do <:text>Notes + <.nav_link + id="assignments_nav_link" + href={path_for(:assignments, @section, @preview_mode, @sidebar_expanded)} + is_active={@active_tab == :assignments} + sidebar_expanded={@sidebar_expanded} + > + <:icon> + <:text>Assignments + + <.nav_link :if={@section.contains_explorations} id="explorations_nav_link" @@ -611,7 +621,7 @@ defmodule OliWeb.Components.Delivery.Layouts do "#" end - defp path_for(:schedule, %Section{slug: section_slug}, preview_mode, sidebar_expanded) do + defp path_for(:assignments, %Section{slug: section_slug}, preview_mode, sidebar_expanded) do if preview_mode do ~p"/sections/#{section_slug}/preview/assignments" else @@ -619,6 +629,18 @@ defmodule OliWeb.Components.Delivery.Layouts do end end + defp path_for(:assignments, _section, _preview_mode, _sidebar_expanded) do + "#" + end + + defp path_for(:schedule, %Section{slug: section_slug}, preview_mode, sidebar_expanded) do + if preview_mode do + ~p"/sections/#{section_slug}/preview/student_schedule" + else + ~p"/sections/#{section_slug}/student_schedule?#{%{sidebar_expanded: sidebar_expanded}}" + end + end + defp path_for(:schedule, _section, _preview_mode, _sidebar_expanded) do "#" end diff --git a/lib/oli_web/components/delivery/utils.ex b/lib/oli_web/components/delivery/utils.ex index ce4d657af3a..c00da42982a 100644 --- a/lib/oli_web/components/delivery/utils.ex +++ b/lib/oli_web/components/delivery/utils.ex @@ -11,6 +11,8 @@ defmodule OliWeb.Components.Delivery.Utils do alias Oli.Delivery.Sections.Section alias Oli.Accounts alias Oli.Accounts.{User, Author, SystemRole} + alias OliWeb.Icons + alias Phoenix.LiveView.JS alias Lti_1p3.Tool.ContextRoles alias Lti_1p3.Tool.PlatformRoles @@ -299,6 +301,41 @@ defmodule OliWeb.Components.Delivery.Utils do """ end + attr :target_selector, :string, required: true, doc: "CSS Selector of the elements to hide/show" + + def toggle_visibility_button(assigns) do + ~H""" + + + """ + end + + def hide_completed(target_selector) do + JS.hide() + |> JS.hide(to: target_selector) + |> JS.show(to: "#show_completed_button", display: "flex") + end + + def show_completed(target_selector) do + JS.hide() + |> JS.show(to: target_selector, display: "flex") + |> JS.show(to: "#hide_completed_button", display: "flex") + end + @doc """ Returns the course week number of a resource based on the section start date. It considers that weeks start on Sunday, regardless of the section start date that could be any day of the week. diff --git a/lib/oli_web/icons.ex b/lib/oli_web/icons.ex index 8e3c9300c75..9b8324a0054 100644 --- a/lib/oli_web/icons.ex +++ b/lib/oli_web/icons.ex @@ -20,6 +20,7 @@ defmodule OliWeb.Icons do def flag(assigns) do ~H""" + """ @@ -464,6 +474,7 @@ defmodule OliWeb.Icons do def world(assigns) do ~H""" + + + + + + + + + + """ + end + + def assignments(%{is_active: false} = assigns) do + ~H""" + + + + + + + + + + + """ + end + + attr :is_active, :boolean, default: false + def practice(%{is_active: true} = assigns) do ~H""" - + @@ -1186,12 +1254,12 @@ defmodule OliWeb.Icons do def hidden(assigns) do ~H""" @@ -1216,12 +1284,7 @@ defmodule OliWeb.Icons do - + diff --git a/lib/oli_web/live/delivery/student/assignments_live.ex b/lib/oli_web/live/delivery/student/assignments_live.ex new file mode 100644 index 00000000000..e5137e9fa49 --- /dev/null +++ b/lib/oli_web/live/delivery/student/assignments_live.ex @@ -0,0 +1,245 @@ +defmodule OliWeb.Delivery.Student.AssignmentsLive do + use OliWeb, :live_view + + alias Oli.Accounts.User + alias Oli.Delivery.Sections.Section + alias Oli.Delivery.Sections.SectionResourceDepot + alias Oli.Delivery.{Metrics, Settings} + alias OliWeb.Common.{FormatDateTime, SessionContext} + alias OliWeb.Components.Delivery.Utils, as: DeliveryUtils + alias OliWeb.Delivery.Student.Utils + alias OliWeb.Icons + + # this is an optimization to reduce the memory footprint of the liveview process + @required_keys_per_assign %{ + section: + {[ + :id, + :slug, + :customizations, + :title, + :brand, + :contains_discussions, + :contains_explorations, + :contains_deliberate_practice + ], %Section{}}, + current_user: {[:id, :name, :email], %User{}} + } + + def mount(_params, _session, socket) do + %{section: section, current_user: %{id: current_user_id}} = socket.assigns + + send(self(), :gc) + + {:ok, + assign(socket, + active_tab: :assignments, + assignments: get_assignments(section, current_user_id) + ) + |> slim_assigns(), temporary_assigns: [assignments: []]} + end + + defp slim_assigns(socket) do + Enum.reduce(@required_keys_per_assign, socket, fn {assign_name, {required_keys, struct}}, + socket -> + assign( + socket, + assign_name, + Map.merge( + struct, + Map.filter(socket.assigns[assign_name], fn {k, _v} -> k in required_keys end) + ) + ) + end) + end + + def handle_info(:gc, socket) do + # manually garbage collect to reduce memory usage after mount/3 + :erlang.garbage_collect(socket.transport_pid) + :erlang.garbage_collect(self()) + {:noreply, socket} + end + + def render(assigns) do + ~H""" + <.top_hero_banner /> +
+ <.assignments_agenda assignments={@assignments} ctx={@ctx} section_slug={@section.slug} /> +
+ """ + end + + def top_hero_banner(assigns) do + ~H""" +
+
+

+ Assignments +

+
+
+ """ + end + + attr :assignments, :list, required: true + attr :ctx, SessionContext, required: true + attr :section_slug, :string, required: true + + def assignments_agenda(assigns) do + ~H""" +
+
+
+
+ + <%= Enum.count(@assignments, &(!is_nil(&1.raw_avg_score))) %> of <%= Enum.count( + @assignments + ) %> Assignments + +
+ +
+
+ <.assignment + :for={assignment <- @assignments} + :if={@assignments != []} + assignment={assignment} + ctx={@ctx} + target={ + Utils.lesson_live_path(@section_slug, assignment.slug, + request_path: ~p"/sections/#{@section_slug}/assignments" + ) + } + /> + There are no assignments +
+
+ """ + end + + attr :assignment, :map, required: true + attr :ctx, SessionContext, required: true + attr :target, :string, required: true, doc: "The target URL for the assignment" + + def assignment(assigns) do + ~H""" +
+
+ <.page_icon purpose={@assignment.purpose} completed={!is_nil(@assignment.raw_avg_score)} /> +
+
+ <%= @assignment.numbering_index %> +
+
+ <.link + navigate={@target} + class="h-6 mt-0.5 text-[#353740] dark:text-[#eeebf5] text-base font-semibold leading-normal whitespace-nowrap truncate" + > + <%= @assignment.title %> + + + <%= Utils.label_for_scheduling_type(@assignment.scheduling_type) %> <%= FormatDateTime.to_formatted_datetime( + @assignment.end_date, + @ctx, + "{WDshort} {Mshort} {D}, {YYYY}" + ) %> + +
+
+ + Attempt <%= @assignment.attempts %> of <%= max_attempts(@assignment.max_attempts) %> + +
+
+ + <%= Utils.parse_score(@assignment.raw_avg_score.score) %> / <%= Utils.parse_score( + @assignment.raw_avg_score.out_of + ) %> + +
+
+
+ + -- + +
+
+ """ + end + + _docp = """ + Returns a list of assignments by querying the section resources form the SectionResourceDepot + and merging the results with the combined settings and metrics for the current user. + + Only required fields needed for render/1 are returned (to reduce memory usage). + """ + + defp get_assignments(section, current_user_id) do + raw_assignments = SectionResourceDepot.graded_pages(section.id, hidden: false) + resource_ids = Enum.map(raw_assignments, & &1.resource_id) + + combined_settings = + Settings.get_combined_settings_for_all_resources(section.id, current_user_id, resource_ids) + + progress_per_page_id = + Metrics.progress_across_for_pages(section.id, resource_ids, [current_user_id]) + + raw_avg_score_per_page_id = + Metrics.raw_avg_score_across_for_pages(section, resource_ids, [current_user_id]) + + user_resource_attempt_counts = + Metrics.get_all_user_resource_attempt_counts(section, current_user_id) + + Enum.map(raw_assignments, fn assignment -> + effective_settings = Map.get(combined_settings, assignment.resource_id, %{}) + + %{ + id: assignment.resource_id, + title: assignment.title, + numbering_index: assignment.numbering_index, + scheduling_type: effective_settings.scheduling_type, + end_date: effective_settings.end_date, + purpose: assignment.purpose, + progress: progress_per_page_id[assignment.resource_id], + raw_avg_score: raw_avg_score_per_page_id[assignment.resource_id], + max_attempts: effective_settings.max_attempts, + attempts: user_resource_attempt_counts[assignment.resource_id] || 0, + slug: assignment.revision_slug + } + end) + end + + defp max_attempts(0), do: "∞" + defp max_attempts(max_attempts), do: max_attempts + + attr :completed, :boolean, required: true + attr :purpose, :atom, required: true + + defp page_icon(assigns) do + ~H""" + <%= cond do %> + <% @purpose == :application -> %> + + <% @completed -> %> + + <% true -> %> + + <% end %> + """ + end +end diff --git a/lib/oli_web/live/delivery/student/learn_live.ex b/lib/oli_web/live/delivery/student/learn_live.ex index 7c4b29e355d..14876024572 100644 --- a/lib/oli_web/live/delivery/student/learn_live.ex +++ b/lib/oli_web/live/delivery/student/learn_live.ex @@ -1376,7 +1376,7 @@ defmodule OliWeb.Delivery.Student.LearnLive do phx-click="toggle_completed_pages" phx-value-module_resource_id={@module["resource_id"]} > -
+
diff --git a/lib/oli_web/live/delivery/student/schedule_live.ex b/lib/oli_web/live/delivery/student/schedule_live.ex index a93a8c902d4..cea61996d5d 100644 --- a/lib/oli_web/live/delivery/student/schedule_live.ex +++ b/lib/oli_web/live/delivery/student/schedule_live.ex @@ -98,7 +98,7 @@ defmodule OliWeb.Delivery.Student.ScheduleLive do non_scheduled_container_groups={@schedule} section_slug={@section_slug} historical_graded_attempt_summary={@historical_graded_attempt_summary} - request_path={~p"/sections/#{@section_slug}/assignments"} + request_path={~p"/sections/#{@section_slug}/student_schedule"} />
@@ -146,7 +146,7 @@ defmodule OliWeb.Delivery.Student.ScheduleLive do schedule_ranges={schedule_ranges} section_slug={@section_slug} historical_graded_attempt_summary={@historical_graded_attempt_summary} - request_path={~p"/sections/#{@section_slug}/assignments"} + request_path={~p"/sections/#{@section_slug}/student_schedule"} /> <% end %> diff --git a/lib/oli_web/live/delivery/student/utils.ex b/lib/oli_web/live/delivery/student/utils.ex index 3dff6b02548..4362e932e36 100644 --- a/lib/oli_web/live/delivery/student/utils.ex +++ b/lib/oli_web/live/delivery/student/utils.ex @@ -389,16 +389,16 @@ defmodule OliWeb.Delivery.Student.Utils do - `params`: (Optional) Additional query parameters in a list or map format. If omitted, a URL is generated without additional parameters. ## Examples - - `schedule_live_path("math")` returns `"/sections/math/assignments"`. - - `schedule_live_path("math", request_path: "some/previous/url")` returns `"/sections/math/assignments?request_path=some/previous/url"`. + - `schedule_live_path("math")` returns `"/sections/math/student_schedule"`. + - `schedule_live_path("math", request_path: "some/previous/url")` returns `"/sections/math/student_schedule?request_path=some/previous/url"`. """ def schedule_live_path(section_slug, params \\ []) def schedule_live_path(section_slug, []), - do: ~p"/sections/#{section_slug}/assignments" + do: ~p"/sections/#{section_slug}/student_schedule" def schedule_live_path(section_slug, params), - do: ~p"/sections/#{section_slug}/assignments?#{params}" + do: ~p"/sections/#{section_slug}/student_schedule?#{params}" # nil case arises for linked loose pages not in in hierarchy index def get_container_label(nil, section), do: section.title diff --git a/lib/oli_web/plugs/redirect_by_attempt_state.ex b/lib/oli_web/plugs/redirect_by_attempt_state.ex index fa355825a5e..d9a96e73129 100644 --- a/lib/oli_web/plugs/redirect_by_attempt_state.ex +++ b/lib/oli_web/plugs/redirect_by_attempt_state.ex @@ -227,7 +227,9 @@ defmodule OliWeb.Plugs.RedirectByAttemptState do conn |> halt() |> assign(:already_been_redirected?, true) - |> Phoenix.Controller.redirect(to: "/sections/#{section_slug}/prologue/#{revision_slug}") + |> Phoenix.Controller.redirect( + to: "/sections/#{section_slug}/prologue/#{revision_slug}?#{conn.query_string}" + ) end end diff --git a/lib/oli_web/router.ex b/lib/oli_web/router.ex index 45c31a54aff..caec0c2eb48 100644 --- a/lib/oli_web/router.ex +++ b/lib/oli_web/router.ex @@ -998,7 +998,8 @@ defmodule OliWeb.Router do live("/", Delivery.Student.IndexLive) live("/learn", Delivery.Student.LearnLive) live("/discussions", Delivery.Student.DiscussionsLive) - live("/assignments", Delivery.Student.ScheduleLive) + live("/assignments", Delivery.Student.AssignmentsLive) + live("/student_schedule", Delivery.Student.ScheduleLive) live("/explorations", Delivery.Student.ExplorationsLive) live("/practice", Delivery.Student.PracticeLive) end @@ -1021,7 +1022,8 @@ defmodule OliWeb.Router do live("/", Delivery.Student.IndexLive, :preview) live("/learn", Delivery.Student.LearnLive, :preview) live("/discussions", Delivery.Student.DiscussionsLive, :preview) - live("/assignments", Delivery.Student.ScheduleLive, :preview) + live("/assignments", Delivery.Student.AssignmentsLive, :preview) + live("/student_schedule", Delivery.Student.ScheduleLive, :preview) live("/explorations", Delivery.Student.ExplorationsLive, :preview) live("/practice", Delivery.Student.PracticeLive, :preview) end diff --git a/test/oli/delivery/paywall_test.exs b/test/oli/delivery/paywall_test.exs index 630e26110ff..8c0b149da37 100644 --- a/test/oli/delivery/paywall_test.exs +++ b/test/oli/delivery/paywall_test.exs @@ -894,8 +894,8 @@ defmodule Oli.Delivery.PaywallTest do end test "browse_payments/4 applies paging", %{product: product} do - payment_1_id = insert(:payment, section: product, code: 123_456_789).id - _payment_2_id = insert(:payment, section: product, code: 987_654_321).id + payment_1_id = insert(:payment, section: product, code: 123_456_789, type: :bypass).id + _payment_2_id = insert(:payment, section: product, code: 987_654_321, type: :direct).id [%{payment: %Payment{id: ^payment_1_id}}] = Paywall.browse_payments(product.slug, %Paging{limit: 1, offset: 0}, %Sorting{ diff --git a/test/oli_web/controllers/page_delivery_controller_test.exs b/test/oli_web/controllers/page_delivery_controller_test.exs index 7d115ff8c8f..2f31095f166 100644 --- a/test/oli_web/controllers/page_delivery_controller_test.exs +++ b/test/oli_web/controllers/page_delivery_controller_test.exs @@ -2426,7 +2426,7 @@ defmodule OliWeb.PageDeliveryControllerTest do conn = recycle(conn) |> Pow.Plug.assign_current_user(user, OliWeb.Pow.PowHelpers.get_pow_config(:user)) - |> get(~p"/sections/#{section.slug}/assignments") + |> get(~p"/sections/#{section.slug}/student_schedule") assert html_response(conn, 200) =~ section.title @@ -2459,7 +2459,7 @@ defmodule OliWeb.PageDeliveryControllerTest do conn = recycle(conn) |> Pow.Plug.assign_current_user(user, OliWeb.Pow.PowHelpers.get_pow_config(:user)) - |> get(~p"/sections/#{section.slug}/preview/assignments") + |> get(~p"/sections/#{section.slug}/preview/schedule") assert html_response(conn, 200) =~ section.title assert html_response(conn, 200) =~ "Course content" diff --git a/test/oli_web/live/delivery/student/assignments_live_test.exs b/test/oli_web/live/delivery/student/assignments_live_test.exs new file mode 100644 index 00000000000..22799c07844 --- /dev/null +++ b/test/oli_web/live/delivery/student/assignments_live_test.exs @@ -0,0 +1,434 @@ +defmodule OliWeb.Delivery.Student.AssignmentsLiveTest do + use ExUnit.Case, async: true + use OliWeb.ConnCase + + import Phoenix.LiveViewTest + import Oli.Factory + import Ecto.Query, warn: false + + alias Lti_1p3.Tool.ContextRoles + alias Oli.Delivery.Attempts.Core.ResourceAccess + alias Oli.Delivery.Sections + alias Oli.Resources.ResourceType + + defp live_view_assignments_live_route(section_slug) do + ~p"/sections/#{section_slug}/assignments" + end + + defp create_elixir_project(_) do + author = insert(:author) + project = insert(:project, authors: [author]) + + # revisions... + ## pages... + page_1_revision = + insert(:revision, + resource_type_id: ResourceType.get_id_by_type("page"), + title: "Start here", + graded: true + ) + + page_2_revision = + insert(:revision, + resource_type_id: ResourceType.get_id_by_type("page"), + title: "Page 2", + graded: true + ) + + page_3_revision = + insert(:revision, + resource_type_id: ResourceType.get_id_by_type("page"), + title: "Page 3", + graded: true + ) + + page_4_revision = + insert(:revision, + resource_type_id: ResourceType.get_id_by_type("page"), + title: "Page 4", + graded: true, + purpose: :application + ) + + ## modules... + module_1_revision = + insert(:revision, %{ + resource_type_id: Oli.Resources.ResourceType.get_id_by_type("container"), + children: [page_1_revision.resource_id, page_2_revision.resource_id], + title: "How to use this course" + }) + + module_2_revision = + insert(:revision, %{ + resource_type_id: Oli.Resources.ResourceType.get_id_by_type("container"), + children: [page_3_revision.resource_id], + title: "Configure your setup" + }) + + ## units... + unit_1_revision = + insert(:revision, %{ + resource_type_id: Oli.Resources.ResourceType.get_id_by_type("container"), + children: [module_1_revision.resource_id], + title: "Introduction" + }) + + unit_2_revision = + insert(:revision, %{ + resource_type_id: Oli.Resources.ResourceType.get_id_by_type("container"), + children: [module_2_revision.resource_id], + title: "Building a Phoenix app" + }) + + ## root container... + container_revision = + insert(:revision, %{ + resource_type_id: Oli.Resources.ResourceType.get_id_by_type("container"), + children: [ + unit_1_revision.resource_id, + unit_2_revision.resource_id, + page_4_revision.resource_id + ], + title: "Root Container" + }) + + all_revisions = + [ + page_1_revision, + page_2_revision, + page_3_revision, + page_4_revision, + module_1_revision, + module_2_revision, + unit_1_revision, + unit_2_revision, + container_revision + ] + + # asociate resources to project + Enum.each(all_revisions, fn revision -> + insert(:project_resource, %{ + project_id: project.id, + resource_id: revision.resource_id + }) + end) + + # publish project + publication = + insert(:publication, %{project: project, root_resource_id: container_revision.resource_id}) + + # publish resources + Enum.each(all_revisions, fn revision -> + insert(:published_resource, %{ + publication: publication, + resource: revision.resource, + revision: revision, + author: author + }) + end) + + # create section... + section = + insert(:section, + base_project: project, + title: "The best course ever!", + analytics_version: :v2 + ) + + {:ok, section} = Sections.create_section_resources(section, publication) + {:ok, _} = Sections.rebuild_contained_pages(section) + + %{ + section: section, + page_1: page_1_revision, + page_2: page_2_revision, + page_3: page_3_revision, + page_4: page_4_revision, + module_1: module_1_revision, + module_2: module_2_revision, + unit_1: unit_1_revision, + unit_2: unit_2_revision, + root_container: container_revision + } + end + + defp create_another_elixir_project(_) do + author = insert(:author) + project = insert(:project, authors: [author]) + + # revisions... + ## pages... + page_1_revision = + insert(:revision, + resource_type_id: ResourceType.get_id_by_type("page"), + title: "Start here", + graded: false + ) + + page_2_revision = + insert(:revision, + resource_type_id: ResourceType.get_id_by_type("page"), + title: "Page 2", + graded: false + ) + + ## modules... + module_1_revision = + insert(:revision, %{ + resource_type_id: Oli.Resources.ResourceType.get_id_by_type("container"), + children: [page_1_revision.resource_id, page_2_revision.resource_id], + title: "How to use this course" + }) + + ## units... + unit_1_revision = + insert(:revision, %{ + resource_type_id: Oli.Resources.ResourceType.get_id_by_type("container"), + children: [module_1_revision.resource_id], + title: "Introduction" + }) + + ## root container... + container_revision = + insert(:revision, %{ + resource_type_id: Oli.Resources.ResourceType.get_id_by_type("container"), + children: [unit_1_revision.resource_id], + title: "Root Container" + }) + + all_revisions = + [ + page_1_revision, + page_2_revision, + module_1_revision, + unit_1_revision, + container_revision + ] + + # asociate resources to project + Enum.each(all_revisions, fn revision -> + insert(:project_resource, %{ + project_id: project.id, + resource_id: revision.resource_id + }) + end) + + # publish project + publication = + insert(:publication, %{project: project, root_resource_id: container_revision.resource_id}) + + # publish resources + Enum.each(all_revisions, fn revision -> + insert(:published_resource, %{ + publication: publication, + resource: revision.resource, + revision: revision, + author: author + }) + end) + + # create section... + section = + insert(:section, + base_project: project, + title: "Another course!", + analytics_version: :v2 + ) + + {:ok, section} = Sections.create_section_resources(section, publication) + {:ok, _} = Sections.rebuild_contained_pages(section) + + %{ + section: section, + page_1: page_1_revision, + page_2: page_2_revision, + module_1: module_1_revision, + unit_1: unit_1_revision, + root_container: container_revision + } + end + + defp create_attempt(student, section, revision) do + resource_access = get_or_insert_resource_access(student, section, revision) + + resource_attempt = + insert(:resource_attempt, %{ + resource_access: resource_access, + revision: revision, + date_submitted: ~U[2023-11-14 20:00:00Z], + date_evaluated: ~U[2023-11-14 20:30:00Z], + score: 5, + out_of: 10, + lifecycle_state: :evaluated, + content: %{model: []} + }) + + resource_attempt + end + + defp get_or_insert_resource_access(student, section, revision) do + Oli.Repo.get_by( + ResourceAccess, + resource_id: revision.resource_id, + section_id: section.id, + user_id: student.id + ) + |> case do + nil -> + insert(:resource_access, %{ + user: student, + section: section, + resource: revision.resource, + score: 5, + out_of: 10 + }) + + resource_access -> + resource_access + end + end + + def enroll_and_visit_section(%{user: user, section: section} = _attrs) do + Sections.enroll(user.id, section.id, [ContextRoles.get_role(:context_learner)]) + Sections.mark_section_visited_for_student(section, user) + end + + describe "user" do + test "can not access page when it is not logged in", %{conn: conn} do + section = insert(:section) + student = insert(:user) + + Sections.enroll(student.id, section.id, [ContextRoles.get_role(:context_learner)]) + + {:error, {:redirect, %{to: redirect_path}}} = + live(conn, live_view_assignments_live_route(section.slug)) + + assert redirect_path == + "/?request_path=%2Fsections%2F#{section.slug}%2Fassignments§ion=#{section.slug}" + end + end + + describe "not enrolled student" do + setup [:user_conn, :create_elixir_project] + + test "can not access when not enrolled to course", %{conn: conn, section: section} do + {:error, {:redirect, %{to: redirect_path, flash: _flash_msg}}} = + live(conn, live_view_assignments_live_route(section.slug)) + + assert redirect_path == "/unauthorized" + end + end + + describe "student" do + setup [:user_conn, :create_elixir_project, :enroll_and_visit_section] + + test "can access when enrolled to course", %{conn: conn, section: section} do + {:ok, view, _html} = live(conn, live_view_assignments_live_route(section.slug)) + + assert has_element?(view, "h1", "Assignments") + end + + test "can navigate to an assignment", %{ + conn: conn, + section: section, + page_1: page_1 + } do + {:ok, view, _html} = live(conn, live_view_assignments_live_route(section.slug)) + + {:error, {:live_redirect, %{kind: :push, to: path}}} = + view + |> element( + "div[role='assignment detail'][id='assignment_#{page_1.resource_id}'] a", + "Start here" + ) + |> render_click() + + assert path == + "/sections/#{section.slug}/lesson/#{page_1.slug}?request_path=%2Fsections%2F#{section.slug}%2Fassignments" + end + + test "page icons correspond to the resource purpose and completed state", %{ + conn: conn, + section: section, + page_1: page_1, + page_2: page_2, + page_3: page_3, + page_4: page_4, + user: user + } do + _completed_page = + create_attempt(user, section, page_2) + + {:ok, view, _html} = live(conn, live_view_assignments_live_route(section.slug)) + + assert element( + view, + "div[role='assignment detail'][id='assignment_#{page_1.resource_id}'] div[role='page icon'] svg" + ) + |> render() =~ "flag icon" + + # page 2 is completed, so we see it's checked icon + assert element( + view, + "div[role='assignment detail'][id='assignment_#{page_2.resource_id}'] div[role='page icon'] svg" + ) + |> render() =~ "square checked icon" + + # and can see it's attempt summary + assert has_element?( + view, + "div[role='assignment detail'][id='assignment_#{page_2.resource_id}'] span", + "Attempt 1 of ∞" + ) + + assert has_element?( + view, + "div[role='assignment detail'][id='assignment_#{page_2.resource_id}'] span", + "5 / 10" + ) + + assert element( + view, + "div[role='assignment detail'][id='assignment_#{page_3.resource_id}'] div[role='page icon'] svg" + ) + |> render() =~ "flag icon" + + assert element( + view, + "div[role='assignment detail'][id='assignment_#{page_4.resource_id}'] div[role='page icon'] svg" + ) + |> render() =~ "world icon" + end + + test "can see completed pages summary", %{ + conn: conn, + section: section, + page_1: page_1, + page_2: page_2, + user: user + } do + _completed_page = + create_attempt(user, section, page_1) + + _completed_page = + create_attempt(user, section, page_2) + + {:ok, view, _html} = live(conn, live_view_assignments_live_route(section.slug)) + + assert has_element?(view, "span", "2 of 4 Assignments") + end + + test "gets a `no assignments` message when there are not assignments to show", %{ + conn: conn, + user: user + } do + %{section: section} = create_another_elixir_project(%{}) + + enroll_and_visit_section(%{user: user, section: section}) + + {:ok, view, _html} = live(conn, live_view_assignments_live_route(section.slug)) + + assert has_element?(view, "span", "There are no assignments") + end + end +end diff --git a/test/oli_web/live/delivery/student/learn_live_test.exs b/test/oli_web/live/delivery/student/learn_live_test.exs index 6ed0ee03d10..16078ce5d70 100644 --- a/test/oli_web/live/delivery/student/learn_live_test.exs +++ b/test/oli_web/live/delivery/student/learn_live_test.exs @@ -2694,7 +2694,7 @@ defmodule OliWeb.Delivery.Student.ContentLiveTest do |> element(~s{nav[id=desktop-nav-menu] a}, "Schedule") |> render_click() - assert_redirect(view, "/sections/#{section.slug}/assignments?sidebar_expanded=true") + assert_redirect(view, "/sections/#{section.slug}/student_schedule?sidebar_expanded=true") {:ok, view, _html} = live(conn, Utils.learn_live_path(section.slug, sidebar_expanded: false)) @@ -2705,7 +2705,7 @@ defmodule OliWeb.Delivery.Student.ContentLiveTest do |> element(~s{nav[id="desktop-nav-menu"] a[id="schedule_nav_link"])}) |> render_click() - assert_redirect(view, "/sections/#{section.slug}/assignments?sidebar_expanded=false") + assert_redirect(view, "/sections/#{section.slug}/student_schedule?sidebar_expanded=false") end test "exit course button redirects to the student workspace", %{ diff --git a/test/oli_web/live/delivery/student/lesson_live_test.exs b/test/oli_web/live/delivery/student/lesson_live_test.exs index e63697d8237..931f39da9f6 100644 --- a/test/oli_web/live/delivery/student/lesson_live_test.exs +++ b/test/oli_web/live/delivery/student/lesson_live_test.exs @@ -1230,7 +1230,7 @@ defmodule OliWeb.Delivery.Student.LessonLiveTest do Sections.mark_section_visited_for_student(section, user) # when the request path is not the learn view, it keeps it when navigating between pages - request_path = ~p"/sections/#{section.slug}/assignments" + request_path = ~p"/sections/#{section.slug}/student_schedule" {:ok, view, _html} = live( diff --git a/test/oli_web/live/delivery/student/review_live_test.exs b/test/oli_web/live/delivery/student/review_live_test.exs index 755c740a6f1..f6252b27ccd 100644 --- a/test/oli_web/live/delivery/student/review_live_test.exs +++ b/test/oli_web/live/delivery/student/review_live_test.exs @@ -493,7 +493,7 @@ defmodule OliWeb.Delivery.Student.ReviewLiveTest do attempt = create_attempt(user, section, page_1) # when the request path is not the learn view, it keeps it when navigating between pages - request_path = ~p"/sections/#{section.slug}/assignments" + request_path = ~p"/sections/#{section.slug}/student_schedule" {:ok, view, _html} = live( diff --git a/test/oli_web/live/delivery/student/schedule_live_test.exs b/test/oli_web/live/delivery/student/schedule_live_test.exs index f5b9b147fff..fa546440381 100644 --- a/test/oli_web/live/delivery/student/schedule_live_test.exs +++ b/test/oli_web/live/delivery/student/schedule_live_test.exs @@ -567,10 +567,10 @@ defmodule OliWeb.Delivery.Student.ScheduleLiveTest do Sections.enroll(student.id, section.id, [ContextRoles.get_role(:context_learner)]) {:error, {:redirect, %{to: redirect_path}}} = - live(conn, ~p"/sections/#{section.slug}/assignments") + live(conn, ~p"/sections/#{section.slug}/student_schedule") assert redirect_path == - "/?request_path=%2Fsections%2F#{section.slug}%2Fassignments§ion=#{section.slug}" + "/?request_path=%2Fsections%2F#{section.slug}%2Fstudent_schedule§ion=#{section.slug}" end end @@ -579,7 +579,7 @@ defmodule OliWeb.Delivery.Student.ScheduleLiveTest do test "can not access when not enrolled to course", %{conn: conn, section: section} do {:error, {:redirect, %{to: redirect_path, flash: _flash_msg}}} = - live(conn, ~p"/sections/#{section.slug}/assignments") + live(conn, ~p"/sections/#{section.slug}/student_schedule") assert redirect_path == "/unauthorized" end @@ -588,7 +588,7 @@ defmodule OliWeb.Delivery.Student.ScheduleLiveTest do Sections.enroll(user.id, section.id, [ContextRoles.get_role(:context_learner)]) Sections.mark_section_visited_for_student(section, user) - {:ok, view, _html} = live(conn, ~p"/sections/#{section.slug}/assignments") + {:ok, view, _html} = live(conn, ~p"/sections/#{section.slug}/student_schedule") assert has_element?(view, "h1", "Course Schedule") end @@ -620,7 +620,7 @@ defmodule OliWeb.Delivery.Student.ScheduleLiveTest do false ) - {:ok, view, _html} = live(conn, ~p"/sections/#{section.slug}/assignments") + {:ok, view, _html} = live(conn, ~p"/sections/#{section.slug}/student_schedule") # open attempts summary for page 3 view @@ -709,7 +709,7 @@ defmodule OliWeb.Delivery.Student.ScheduleLiveTest do false ) - {:ok, view, _html} = live(conn, ~p"/sections/#{section.slug}/assignments") + {:ok, view, _html} = live(conn, ~p"/sections/#{section.slug}/student_schedule") # open attempts summary for page 4 view @@ -780,7 +780,7 @@ defmodule OliWeb.Delivery.Student.ScheduleLiveTest do Sections.enroll(user.id, section.id, [ContextRoles.get_role(:context_learner)]) Sections.mark_section_visited_for_student(section, user) - {:ok, view, _html} = live(conn, ~p"/sections/#{section.slug}/assignments") + {:ok, view, _html} = live(conn, ~p"/sections/#{section.slug}/student_schedule") assert has_element?(view, "h1", "Course Schedule")