From c1b2fbb8aa079b7b8948c50447baf673df86f776 Mon Sep 17 00:00:00 2001
From: Eli Knebel
Date: Thu, 21 Mar 2024 12:32:02 -0400
Subject: [PATCH 01/42] only show pending approval message for public notes
---
.../delivery/student/lesson/annotations.ex | 20 ++++++++++---------
1 file changed, 11 insertions(+), 9 deletions(-)
diff --git a/lib/oli_web/live/delivery/student/lesson/annotations.ex b/lib/oli_web/live/delivery/student/lesson/annotations.ex
index 3e6e0b479a4..371ed7d49ad 100644
--- a/lib/oli_web/live/delivery/student/lesson/annotations.ex
+++ b/lib/oli_web/live/delivery/student/lesson/annotations.ex
@@ -327,16 +327,18 @@ defmodule OliWeb.Delivery.Student.Lesson.Annotations do
attr :post, Oli.Resources.Collaboration.Post, required: true
defp maybe_status(assigns) do
- if assigns.post.status == :submitted do
- ~H"""
-
- Submitted and pending approval
-
- """
- else
- ~H"""
+ case assigns.post do
+ %Oli.Resources.Collaboration.Post{visibility: :public, status: :submitted} ->
+ ~H"""
+
+ Submitted and pending approval
+
+ """
+
+ _ ->
+ ~H"""
- """
+ """
end
end
From 5f3d929d8306dc289a8c0a24e4eb35f2d1a74074 Mon Sep 17 00:00:00 2001
From: Eli Knebel
Date: Thu, 21 Mar 2024 16:11:04 -0400
Subject: [PATCH 02/42] only show notes if collab spaces are enabled refactor
assigns handling
---
.../delivery/student/lesson/annotations.ex | 10 +-
.../live/delivery/student/lesson_live.ex | 221 ++++++++++--------
2 files changed, 126 insertions(+), 105 deletions(-)
diff --git a/lib/oli_web/live/delivery/student/lesson/annotations.ex b/lib/oli_web/live/delivery/student/lesson/annotations.ex
index 371ed7d49ad..df029dceb74 100644
--- a/lib/oli_web/live/delivery/student/lesson/annotations.ex
+++ b/lib/oli_web/live/delivery/student/lesson/annotations.ex
@@ -7,7 +7,7 @@ defmodule OliWeb.Delivery.Student.Lesson.Annotations do
attr :create_new_annotation, :boolean, default: false
attr :annotations, :any, required: true
attr :current_user, Oli.Accounts.User, required: true
- attr :selected_annotations_tab, :atom, default: :my_notes
+ attr :active_tab, :atom, default: :my_notes
def panel(assigns) do
~H"""
@@ -19,10 +19,10 @@ defmodule OliWeb.Delivery.Student.Lesson.Annotations do
<.tab_group class="py-3">
- <.tab name={:my_notes} selected={@selected_annotations_tab == :my_notes}>
+ <.tab name={:my_notes} selected={@active_tab == :my_notes}>
<.user_icon class="mr-2" /> My Notes
- <.tab name={:all_notes} selected={@selected_annotations_tab == :all_notes}>
+ <.tab name={:all_notes} selected={@active_tab == :all_notes}>
<.users_icon class="mr-2" /> Class Notes
@@ -32,9 +32,7 @@ defmodule OliWeb.Delivery.Student.Lesson.Annotations do
<.add_new_annotation_input
class="my-2"
active={@create_new_annotation}
- disable_anonymous_option={
- @selected_annotations_tab == :my_notes || is_guest(@current_user)
- }
+ disable_anonymous_option={@active_tab == :my_notes || is_guest(@current_user)}
/>
<%= case @annotations do %>
diff --git a/lib/oli_web/live/delivery/student/lesson_live.ex b/lib/oli_web/live/delivery/student/lesson_live.ex
index e1fa5e294c8..a49700734ec 100644
--- a/lib/oli_web/live/delivery/student/lesson_live.ex
+++ b/lib/oli_web/live/delivery/student/lesson_live.ex
@@ -11,6 +11,7 @@ defmodule OliWeb.Delivery.Student.LessonLive do
alias Oli.Delivery.Page.PageContext
alias Oli.Delivery.{Sections, Settings}
alias Oli.Resources.Collaboration
+ alias Oli.Resources.Collaboration.CollabSpaceConfig
alias OliWeb.Common.FormatDateTime
alias OliWeb.Components.Delivery.Layouts
alias OliWeb.Components.Modal
@@ -23,14 +24,20 @@ defmodule OliWeb.Delivery.Student.LessonLive do
on_mount {OliWeb.LiveSessionPlugs.InitPage, :previous_next_index}
def mount(_params, _session, %{assigns: %{view: :practice_page}} = socket) do
+ course_collab_space_config =
+ Collaboration.get_course_collab_space_config(
+ socket.assigns.section.root_section_resource_id
+ )
+
# when updating to Liveview 0.20 we should replace this with assign_async/3
# https://hexdocs.pm/phoenix_live_view/Phoenix.LiveView.html#assign_async/3
if connected?(socket) do
async_load_annotations(
self(),
- socket.assigns.section.id,
+ socket.assigns.section,
socket.assigns.page_context.page.resource_id,
- socket.assigns[:current_user],
+ socket.assigns.current_user,
+ course_collab_space_config,
:private,
nil
)
@@ -39,7 +46,8 @@ defmodule OliWeb.Delivery.Student.LessonLive do
{:ok,
socket
|> assign_html_and_scripts()
- |> assign_annotations()}
+ |> annotations_assigns(course_collab_space_config)
+ |> assign(course_collab_space_config: course_collab_space_config)}
end
def mount(
@@ -181,15 +189,15 @@ defmodule OliWeb.Delivery.Student.LessonLive do
def handle_event("update_point_markers", %{"point_markers" => point_markers}, socket) do
markers = Enum.map(point_markers, fn pm -> %{id: pm["id"], top: pm["top"]} end)
- {:noreply, assign(socket, point_markers: markers)}
+ {:noreply, assign_annotations(socket, point_markers: markers)}
end
def handle_event("toggle_sidebar", _params, socket) do
- %{show_sidebar: show_sidebar, selected_point: selected_point} = socket.assigns
+ %{show_sidebar: show_sidebar, selected_point: selected_point} = socket.assigns.annotations
{:noreply,
socket
- |> assign(show_sidebar: !show_sidebar)
+ |> assign_annotations(show_sidebar: !show_sidebar)
|> push_event("request_point_markers", %{})
|> then(fn socket ->
if show_sidebar do
@@ -203,41 +211,43 @@ defmodule OliWeb.Delivery.Student.LessonLive do
def handle_event("select_annotation_point", %{"point-marker-id" => point_marker_id}, socket) do
async_load_annotations(
self(),
- socket.assigns.section.id,
+ socket.assigns.section,
socket.assigns.page_context.page.resource_id,
- socket.assigns[:current_user],
- visibility_for_active_tab(socket.assigns.selected_annotations_tab),
+ socket.assigns.current_user,
+ socket.assigns.course_collab_space_config,
+ visibility_for_active_tab(socket.assigns.annotations.active_tab),
point_marker_id
)
{:noreply,
socket
- |> assign(selected_point: point_marker_id, annotations: nil)
+ |> assign_annotations(selected_point: point_marker_id, posts: nil)
|> push_event("highlight_point_marker", %{id: point_marker_id})}
end
def handle_event("select_annotation_point", _params, socket) do
async_load_annotations(
self(),
- socket.assigns.section.id,
+ socket.assigns.section,
socket.assigns.page_context.page.resource_id,
- socket.assigns[:current_user],
- visibility_for_active_tab(socket.assigns.selected_annotations_tab),
+ socket.assigns.current_user,
+ socket.assigns.course_collab_space_config,
+ visibility_for_active_tab(socket.assigns.annotations.active_tab),
nil
)
{:noreply,
socket
- |> assign(selected_point: nil, annotations: nil)
+ |> assign_annotations(selected_point: nil, posts: nil)
|> push_event("clear_highlighted_point_markers", %{})}
end
def handle_event("begin_create_annotation", _, socket) do
- {:noreply, assign(socket, create_new_annotation: true)}
+ {:noreply, assign_annotations(socket, create_new_annotation: true)}
end
def handle_event("cancel_create_annotation", _, socket) do
- {:noreply, assign(socket, create_new_annotation: false)}
+ {:noreply, assign_annotations(socket, create_new_annotation: false)}
end
def handle_event("create_annotation", %{"content" => ""}, socket) do
@@ -249,13 +259,16 @@ defmodule OliWeb.Delivery.Student.LessonLive do
current_user: current_user,
section: section,
page_context: page_context,
- annotations: annotations,
- selected_point: selected_point,
- selected_annotations_tab: selected_annotations_tab
+ annotations: %{
+ posts: posts,
+ selected_point: selected_point,
+ active_tab: active_tab,
+ auto_approve_annotations: auto_approve_annotations
+ }
} = socket.assigns
attrs = %{
- status: :submitted,
+ status: if(auto_approve_annotations, do: :approved, else: :submitted),
user_id: current_user.id,
section_id: section.id,
resource_id: page_context.page.resource_id,
@@ -263,7 +276,7 @@ defmodule OliWeb.Delivery.Student.LessonLive do
annotated_block_id: selected_point,
annotation_type: :point,
anonymous: params["anonymous"] == "true",
- visibility: visibility_for_active_tab(selected_annotations_tab),
+ visibility: visibility_for_active_tab(active_tab),
content: %Collaboration.PostContent{message: value}
}
@@ -272,7 +285,7 @@ defmodule OliWeb.Delivery.Student.LessonLive do
{:noreply,
socket
|> put_flash(:info, "Note created successfully")
- |> assign(create_new_annotation: false, annotations: [post | annotations])
+ |> assign_annotations(create_new_annotation: false, posts: [post | posts])
|> increment_post_count(selected_point)}
{:error, _} ->
@@ -290,27 +303,29 @@ defmodule OliWeb.Delivery.Student.LessonLive do
async_load_annotations(
self(),
- socket.assigns.section.id,
+ socket.assigns.section,
socket.assigns.page_context.page.resource_id,
- socket.assigns[:current_user],
+ socket.assigns.current_user,
+ socket.assigns.course_collab_space_config,
visibility_for_active_tab(tab),
- socket.assigns.selected_point
+ socket.assigns.annotations.selected_point
)
- {:noreply, assign(socket, selected_annotations_tab: tab, annotations: nil)}
+ {:noreply, assign_annotations(socket, active_tab: tab, posts: nil)}
end
+ # handle assigns directly from other sub-tasks and processes
def handle_info(
- {:assign, key, annotations},
+ {:assign_annotations, annotations},
socket
) do
- {:noreply, assign(socket, [{key, annotations}])}
+ {:noreply, assign_annotations(socket, Enum.into(annotations, socket.assigns.annotations))}
end
- def render(%{view: :practice_page, annotations_enabled: true} = assigns) do
+ def render(%{view: :practice_page, annotations: %{}} = assigns) do
# For practice page the activity scripts and activity_bridge script are needed as soon as the page loads.
~H"""
- <.page_content_with_sidebar_layout show_sidebar={@show_sidebar}>
+ <.page_content_with_sidebar_layout show_sidebar={@annotations.show_sidebar}>
<:header>
<.page_header
page_context={@page_context}
@@ -330,17 +345,17 @@ defmodule OliWeb.Delivery.Student.LessonLive do
<%= raw(@html) %>
- <:point_markers :if={@show_sidebar && @point_markers}>
+ <:point_markers :if={@annotations.show_sidebar && @annotations.point_markers}>
@@ -352,10 +367,10 @@ defmodule OliWeb.Delivery.Student.LessonLive do
<:sidebar>
@@ -833,19 +848,6 @@ defmodule OliWeb.Delivery.Student.LessonLive do
)
end
- defp assign_annotations(socket) do
- assign(socket,
- annotations_enabled: true,
- show_sidebar: false,
- point_markers: nil,
- selected_point: nil,
- create_new_annotation: false,
- annotations: nil,
- post_counts: nil,
- selected_annotations_tab: :my_notes
- )
- end
-
defp get_max_attempts(%{effective_settings: %{max_attempts: 0}} = _page_context),
do: "unlimited"
@@ -876,58 +878,79 @@ defmodule OliWeb.Delivery.Student.LessonLive do
})
end
+ defp annotations_assigns(socket, course_collab_space_config) do
+ case course_collab_space_config do
+ %CollabSpaceConfig{status: :enabled} ->
+ assign(socket,
+ annotations: %{
+ show_sidebar: false,
+ point_markers: nil,
+ selected_point: nil,
+ post_counts: nil,
+ posts: nil,
+ active_tab: :my_notes,
+ create_new_annotation: false,
+ auto_approve_annotations: course_collab_space_config.auto_accept
+ }
+ )
+
+ _ ->
+ socket
+ end
+ end
+
defp async_load_annotations(
- liveview_pid,
- section_id,
+ caller,
+ section,
resource_id,
- %User{id: current_user_id},
+ current_user,
+ course_collab_space_config,
visibility,
point_block_id
) do
- # load annotations
- Task.Supervisor.start_child(Oli.TaskSupervisor, fn ->
- send(
- liveview_pid,
- {:assign, :annotations,
- Collaboration.list_posts_for_user_in_point_block(
- section_id,
- resource_id,
- current_user_id,
- visibility,
- point_block_id
- )}
- )
- end)
-
- # load post counts
- Task.Supervisor.start_child(Oli.TaskSupervisor, fn ->
- send(
- liveview_pid,
- {:assign, :post_counts,
- Collaboration.list_post_counts_for_user_in_section(
- section_id,
- resource_id,
- current_user_id,
- visibility
- )}
- )
- end)
+ if current_user do
+ Task.Supervisor.start_child(Oli.TaskSupervisor, fn ->
+ case course_collab_space_config do
+ %CollabSpaceConfig{status: :enabled} ->
+ # load post counts
+ post_counts =
+ Collaboration.list_post_counts_for_user_in_section(
+ section.id,
+ resource_id,
+ current_user.id,
+ visibility
+ )
+
+ # load posts
+ posts =
+ Collaboration.list_posts_for_user_in_point_block(
+ section.id,
+ resource_id,
+ current_user.id,
+ visibility,
+ point_block_id
+ )
+
+ send(
+ caller,
+ {:assign_annotations,
+ %{
+ post_counts: post_counts,
+ posts: posts,
+ auto_approve_annotations: course_collab_space_config.auto_accept
+ }}
+ )
+
+ _ ->
+ # do nothing
+ nil
+ end
+ end)
+ end
end
- defp async_load_annotations(
- liveview_pid,
- _section_id,
- _resource_id,
- _current_user,
- _visibility,
- _point_block_id
- ) do
- Task.Supervisor.start_child(Oli.TaskSupervisor, fn ->
- send(
- liveview_pid,
- {:assign, :annotations, []}
- )
- end)
+ defp assign_annotations(socket, annotations) do
+ assign(socket, annotations: Enum.into(annotations, socket.assigns.annotations))
end
defp visibility_for_active_tab(:all_notes), do: :public
@@ -935,12 +958,12 @@ defmodule OliWeb.Delivery.Student.LessonLive do
defp visibility_for_active_tab(_), do: :private
defp increment_post_count(socket, selected_point) do
- case socket.assigns.post_counts do
+ case socket.assigns.annotations.post_counts do
nil ->
socket
post_counts ->
- assign(socket,
+ assign_annotations(socket,
post_counts: Map.update(post_counts, selected_point, 1, &(&1 + 1))
)
end
From 2e6e00f82a91b1dfb4d0b93c575625597b1e3e11 Mon Sep 17 00:00:00 2001
From: Eli Knebel
Date: Mon, 25 Mar 2024 10:46:46 -0400
Subject: [PATCH 03/42] WIP
---
.../delivery/student/lesson/annotations.ex | 48 +++++++++++++++++--
1 file changed, 44 insertions(+), 4 deletions(-)
diff --git a/lib/oli_web/live/delivery/student/lesson/annotations.ex b/lib/oli_web/live/delivery/student/lesson/annotations.ex
index df029dceb74..af1b062b5fd 100644
--- a/lib/oli_web/live/delivery/student/lesson/annotations.ex
+++ b/lib/oli_web/live/delivery/student/lesson/annotations.ex
@@ -42,7 +42,7 @@ defmodule OliWeb.Delivery.Student.Lesson.Annotations do
There are no posts yet
<% annotations -> %>
<%= for annotation <- annotations do %>
- <.note post={annotation} current_user={@current_user} />
+ <.post post={annotation} current_user={@current_user} />
<% end %>
<% end %>
@@ -279,7 +279,7 @@ defmodule OliWeb.Delivery.Student.Lesson.Annotations do
attr :post, Oli.Resources.Collaboration.Post, required: true
attr :current_user, Oli.Accounts.User, required: true
- defp note(assigns) do
+ defp post(assigns) do
~H"""
@@ -293,7 +293,19 @@ defmodule OliWeb.Delivery.Student.Lesson.Annotations do
<%= @post.content.message %>
- <.maybe_status post={@post} />
+ <.post_actions post={@post} />
+
+ """
+ end
+
+ attr :replies, :list, required: true
+
+ defp post_replies(assigns) do
+ ~H"""
+
+ <%= for reply <- @post.replies do %>
+ <.post post={reply} current_user={@current_user} />
+ <% end %>
"""
end
@@ -324,7 +336,7 @@ defmodule OliWeb.Delivery.Student.Lesson.Annotations do
attr :post, Oli.Resources.Collaboration.Post, required: true
- defp maybe_status(assigns) do
+ defp post_actions(assigns) do
case assigns.post do
%Oli.Resources.Collaboration.Post{visibility: :public, status: :submitted} ->
~H"""
@@ -333,6 +345,19 @@ defmodule OliWeb.Delivery.Student.Lesson.Annotations do
"""
+ %Oli.Resources.Collaboration.Post{visibility: :public} ->
+ ~H"""
+
+
+ <.replies_bubble_icon /> 3
+
+
+ """
+
_ ->
~H"""
@@ -405,4 +430,19 @@ defmodule OliWeb.Delivery.Student.Lesson.Annotations do
"""
end
+
+ def replies_bubble_icon(assigns) do
+ ~H"""
+
+
+
+
+
+ """
+ end
end
From 6699deb1cf08c463cf94d6105bfa06373b50ff01 Mon Sep 17 00:00:00 2001
From: Eli Knebel
Date: Mon, 25 Mar 2024 12:25:24 -0400
Subject: [PATCH 04/42] change save to post
---
lib/oli_web/live/delivery/student/lesson/annotations.ex | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/lib/oli_web/live/delivery/student/lesson/annotations.ex b/lib/oli_web/live/delivery/student/lesson/annotations.ex
index af1b062b5fd..6350a257515 100644
--- a/lib/oli_web/live/delivery/student/lesson/annotations.ex
+++ b/lib/oli_web/live/delivery/student/lesson/annotations.ex
@@ -249,7 +249,7 @@ defmodule OliWeb.Delivery.Student.Lesson.Annotations do
<% end %>
- Save
+ Post
Cancel
From c72db744136355821b08b4e14abfc1bb92499b7a Mon Sep 17 00:00:00 2001
From: Eli Knebel
Date: Mon, 25 Mar 2024 15:27:40 -0400
Subject: [PATCH 05/42] Update annotation form with dynamic save label and
placeholder
---
.../live/delivery/student/lesson/annotations.ex | 12 +++++++++---
1 file changed, 9 insertions(+), 3 deletions(-)
diff --git a/lib/oli_web/live/delivery/student/lesson/annotations.ex b/lib/oli_web/live/delivery/student/lesson/annotations.ex
index 6350a257515..3941da2ca20 100644
--- a/lib/oli_web/live/delivery/student/lesson/annotations.ex
+++ b/lib/oli_web/live/delivery/student/lesson/annotations.ex
@@ -33,6 +33,10 @@ defmodule OliWeb.Delivery.Student.Lesson.Annotations do
class="my-2"
active={@create_new_annotation}
disable_anonymous_option={@active_tab == :my_notes || is_guest(@current_user)}
+ save_label={if(@active_tab == :my_notes, do: "Save", else: "Post")}
+ placeholder={
+ if(@active_tab == :my_notes, do: "Add a new note...", else: "Post a new note...")
+ }
/>
<%= case @annotations do %>
@@ -222,6 +226,8 @@ defmodule OliWeb.Delivery.Student.Lesson.Annotations do
attr :active, :boolean, default: false
attr :disable_anonymous_option, :boolean, default: false
+ attr :save_label, :string, default: "Save"
+ attr :placeholder, :string, default: "Add a new note..."
attr :rest, :global, include: ~w(class)
defp add_new_annotation_input(%{active: true} = assigns) do
@@ -239,7 +245,7 @@ defmodule OliWeb.Delivery.Student.Lesson.Annotations do
phx-hook="AutoSelect"
rows="4"
class="w-full border border-gray-400 dark:border-gray-700 dark:bg-black rounded-lg p-3"
- placeholder="Add a new note..."
+ placeholder={@placeholder}
/>
<%= unless @disable_anonymous_option do %>
@@ -249,7 +255,7 @@ defmodule OliWeb.Delivery.Student.Lesson.Annotations do
<% end %>
- Post
+ <%= @save_label %>
Cancel
@@ -268,7 +274,7 @@ defmodule OliWeb.Delivery.Student.Lesson.Annotations do
From 9a66fef667719c4dde018a9bfe81244e2c6d4a6b Mon Sep 17 00:00:00 2001
From: Eli Knebel
Date: Tue, 26 Mar 2024 21:12:11 -0400
Subject: [PATCH 06/42] implement reply to post
---
lib/oli/resources/collaboration.ex | 107 +++++++++-----
.../delivery/student/lesson/annotations.ex | 131 +++++++++++++++---
.../live/delivery/student/lesson_live.ex | 83 ++++++++++-
3 files changed, 269 insertions(+), 52 deletions(-)
diff --git a/lib/oli/resources/collaboration.ex b/lib/oli/resources/collaboration.ex
index e00b02a3320..34cfe3ecd4d 100644
--- a/lib/oli/resources/collaboration.ex
+++ b/lib/oli/resources/collaboration.ex
@@ -491,6 +491,40 @@ defmodule Oli.Resources.Collaboration do
)
end
+ # Define a subquery for root thread post replies count
+ defp replies_subquery() do
+ from(p in Post,
+ group_by: p.thread_root_id,
+ select: %{
+ thread_root_id: p.thread_root_id,
+ count: count(p.id),
+ last_reply: max(p.updated_at)
+ }
+ )
+ end
+
+ # Define a subquery for root thread post read replies count
+ # (replies by the user are counted as read)
+ defp read_replies_subquery(user_id) do
+ from(
+ p in Post,
+ left_join: urp in UserReadPost,
+ on: urp.post_id == p.id,
+ where:
+ not is_nil(p.thread_root_id) and
+ (p.user_id == ^user_id or (urp.user_id == ^user_id and not is_nil(urp.post_id))),
+ group_by: p.thread_root_id,
+ select: %{
+ thread_root_id: p.thread_root_id,
+ # Counting both user's posts and read posts
+ count: count(p.id)
+ }
+ )
+ end
+
+ @doc """
+ Returns the list of root posts for a section.
+ """
def list_root_posts_for_section(
user_id,
section_id,
@@ -500,35 +534,6 @@ defmodule Oli.Resources.Collaboration do
sort_by,
sort_order
) do
- # Define a subquery for root thread post replies count
- replies_subquery =
- from(p in Post,
- group_by: p.thread_root_id,
- select: %{
- thread_root_id: p.thread_root_id,
- count: count(p.id),
- last_reply: max(p.updated_at)
- }
- )
-
- # Define a subquery for root thread post read replies count
- # (replies by the user are counted as read)
- read_replies_subquery =
- from(
- p in Post,
- left_join: urp in UserReadPost,
- on: urp.post_id == p.id,
- where:
- not is_nil(p.thread_root_id) and
- (p.user_id == ^user_id or (urp.user_id == ^user_id and not is_nil(urp.post_id))),
- group_by: p.thread_root_id,
- select: %{
- thread_root_id: p.thread_root_id,
- # Counting both user's posts and read posts
- count: count(p.id)
- }
- )
-
order_clause =
case {sort_by, sort_order} do
{"popularity", :desc} ->
@@ -566,9 +571,9 @@ defmodule Oli.Resources.Collaboration do
on: rev.id == pr.revision_id,
join: user in User,
on: post.user_id == user.id,
- left_join: replies in subquery(replies_subquery),
+ left_join: replies in subquery(replies_subquery()),
on: replies.thread_root_id == post.id,
- left_join: read_replies in subquery(read_replies_subquery),
+ left_join: read_replies in subquery(read_replies_subquery(user_id)),
on: read_replies.thread_root_id == post.id,
where:
post.section_id == ^section_id and
@@ -1122,15 +1127,24 @@ defmodule Oli.Resources.Collaboration do
Repo.all(
from(
post in Post,
+ left_join: replies in subquery(replies_subquery()),
+ on: replies.thread_root_id == post.id,
+ left_join: read_replies in subquery(read_replies_subquery(user_id)),
+ on: read_replies.thread_root_id == post.id,
where:
post.section_id == ^section_id and post.resource_id == ^resource_id and
+ is_nil(post.parent_post_id) and is_nil(post.thread_root_id) and
(post.status in [:approved, :archived] or
(post.status == :submitted and post.user_id == ^user_id)),
where: ^filter_by_point_block_id,
where: post.visibility == ^visibility,
- select: post,
order_by: [desc: :inserted_at],
- preload: [:user]
+ preload: [:user],
+ select: %{
+ post
+ | replies_count: coalesce(replies.count, 0),
+ read_replies_count: coalesce(read_replies.count, 0)
+ }
)
)
end
@@ -1153,4 +1167,31 @@ defmodule Oli.Resources.Collaboration do
|> Repo.all()
|> Enum.into(%{})
end
+
+ def list_replies_for_post_in_point_block(user_id, post_id) do
+ Repo.all(
+ from(
+ post in Post,
+ join: sr in SectionResource,
+ on: sr.resource_id == post.resource_id and sr.section_id == post.section_id,
+ join: spp in SectionsProjectsPublications,
+ on: spp.section_id == post.section_id and spp.project_id == sr.project_id,
+ join: pr in PublishedResource,
+ on: pr.publication_id == spp.publication_id and pr.resource_id == post.resource_id,
+ join: rev in Revision,
+ on: rev.id == pr.revision_id,
+ join: user in User,
+ on: post.user_id == user.id,
+ left_join: urp in UserReadPost,
+ on: urp.post_id == post.id and urp.user_id == ^user_id,
+ where:
+ post.parent_post_id == ^post_id and
+ (post.status in [:approved, :archived] or
+ (post.status == :submitted and post.user_id == ^user_id)),
+ select: post,
+ order_by: [asc: :updated_at]
+ )
+ )
+ |> build_metrics_for_reply_posts(user_id)
+ end
end
diff --git a/lib/oli_web/live/delivery/student/lesson/annotations.ex b/lib/oli_web/live/delivery/student/lesson/annotations.ex
index 3941da2ca20..59384013364 100644
--- a/lib/oli_web/live/delivery/student/lesson/annotations.ex
+++ b/lib/oli_web/live/delivery/student/lesson/annotations.ex
@@ -8,6 +8,7 @@ defmodule OliWeb.Delivery.Student.Lesson.Annotations do
attr :annotations, :any, required: true
attr :current_user, Oli.Accounts.User, required: true
attr :active_tab, :atom, default: :my_notes
+ attr :post_replies, :list, default: nil
def panel(assigns) do
~H"""
@@ -43,10 +44,15 @@ defmodule OliWeb.Delivery.Student.Lesson.Annotations do
<% nil -> %>
<% [] -> %>
- There are no posts yet
+ <%= empty_label(@active_tab) %>
<% annotations -> %>
<%= for annotation <- annotations do %>
- <.post post={annotation} current_user={@current_user} />
+ <.post
+ post={annotation}
+ current_user={@current_user}
+ post_replies={@post_replies}
+ disable_anonymous_option={@active_tab == :my_notes || is_guest(@current_user)}
+ />
<% end %>
<% end %>
@@ -58,6 +64,9 @@ defmodule OliWeb.Delivery.Student.Lesson.Annotations do
defp is_guest(%User{guest: guest}), do: guest
defp is_guest(_), do: false
+ defp empty_label(:my_notes), do: "There are no notes yet"
+ defp empty_label(_), do: "There are no posts yet"
+
slot :inner_block, required: true
def toggle_notes_button(assigns) do
@@ -284,6 +293,7 @@ defmodule OliWeb.Delivery.Student.Lesson.Annotations do
attr :post, Oli.Resources.Collaboration.Post, required: true
attr :current_user, Oli.Accounts.User, required: true
+ attr :post_replies, :any, required: true
defp post(assigns) do
~H"""
@@ -300,18 +310,7 @@ defmodule OliWeb.Delivery.Student.Lesson.Annotations do
<%= @post.content.message %>
<.post_actions post={@post} />
-
- """
- end
-
- attr :replies, :list, required: true
-
- defp post_replies(assigns) do
- ~H"""
-
- <%= for reply <- @post.replies do %>
- <.post post={reply} current_user={@current_user} />
- <% end %>
+ <.post_replies post={@post} replies={@post_replies} current_user={@current_user} />
"""
end
@@ -346,20 +345,23 @@ defmodule OliWeb.Delivery.Student.Lesson.Annotations do
case assigns.post do
%Oli.Resources.Collaboration.Post{visibility: :public, status: :submitted} ->
~H"""
-
+
Submitted and pending approval
"""
%Oli.Resources.Collaboration.Post{visibility: :public} ->
~H"""
-
+
- <.replies_bubble_icon /> 3
+ <.replies_bubble_icon /> <%= if(@post.replies_count > 0,
+ do: @post.replies_count,
+ else: "Reply"
+ ) %>
"""
@@ -371,6 +373,101 @@ defmodule OliWeb.Delivery.Student.Lesson.Annotations do
end
end
+ attr :post, Oli.Resources.Collaboration.Post, required: true
+ attr :current_user, Oli.Accounts.User, required: true
+ attr :replies, :any, required: true
+ attr :disable_anonymous_option, :boolean, default: false
+
+ defp post_replies(assigns) do
+ id = assigns.post.id
+
+ ~H"""
+ <%= case @replies do %>
+ <% {^id, :loading} -> %>
+
+ <% {^id, []} -> %>
+ <.add_new_reply_input parent_post_id={@post.id} />
+ <% {^id, replies} -> %>
+
+ <%= for reply <- replies do %>
+ <.reply post={reply} current_user={@current_user} />
+ <% end %>
+
+ <.add_new_reply_input
+ parent_post_id={@post.id}
+ disable_anonymous_option={@disable_anonymous_option}
+ />
+ <% _ -> %>
+ <% end %>
+ """
+ end
+
+ attr :parent_post_id, :integer, required: true
+ attr :disable_anonymous_option, :boolean, default: false
+
+ defp add_new_reply_input(assigns) do
+ ~H"""
+
+ """
+ end
+
+ defp send_icon(assigns) do
+ ~H"""
+
+
+
+
+
+ """
+ end
+
+ attr :post, Oli.Resources.Collaboration.Post, required: true
+ attr :current_user, Oli.Accounts.User, required: true
+
+ defp reply(assigns) do
+ ~H"""
+
+
+
+ <%= post_creator(@post, @current_user) %>
+
+
+ <%= Timex.from_now(@post.inserted_at) %>
+
+
+
+ <%= @post.content.message %>
+
+
+ """
+ end
+
attr :point_marker, :map, required: true
attr :selected, :boolean, default: false
attr :count, :integer, default: nil
diff --git a/lib/oli_web/live/delivery/student/lesson_live.ex b/lib/oli_web/live/delivery/student/lesson_live.ex
index a49700734ec..3e1a4306bb5 100644
--- a/lib/oli_web/live/delivery/student/lesson_live.ex
+++ b/lib/oli_web/live/delivery/student/lesson_live.ex
@@ -274,7 +274,7 @@ defmodule OliWeb.Delivery.Student.LessonLive do
resource_id: page_context.page.resource_id,
annotated_resource_id: page_context.page.resource_id,
annotated_block_id: selected_point,
- annotation_type: :point,
+ annotation_type: if(selected_point, do: :point, else: :none),
anonymous: params["anonymous"] == "true",
visibility: visibility_for_active_tab(active_tab),
content: %Collaboration.PostContent{message: value}
@@ -314,6 +314,72 @@ defmodule OliWeb.Delivery.Student.LessonLive do
{:noreply, assign_annotations(socket, active_tab: tab, posts: nil)}
end
+ def handle_event("toggle_post_replies", %{"post-id" => post_id}, socket) do
+ %{current_user: current_user} = socket.assigns
+ %{post_replies: post_replies} = socket.assigns.annotations
+
+ post_id = String.to_integer(post_id)
+
+ case post_replies do
+ {^post_id, _} ->
+ {:noreply, assign_annotations(socket, post_replies: nil)}
+
+ _ ->
+ async_load_post_replies(self(), current_user.id, post_id)
+
+ {:noreply, assign_annotations(socket, post_replies: {post_id, :loading})}
+ end
+ end
+
+ def handle_event("create_reply", %{"content" => ""}, socket) do
+ {:noreply, put_flash(socket, :error, "Reply cannot be empty")}
+ end
+
+ def handle_event(
+ "create_reply",
+ %{"parent-post-id" => parent_post_id, "content" => value} = params,
+ socket
+ ) do
+ parent_post_id = String.to_integer(parent_post_id)
+
+ %{
+ current_user: current_user,
+ section: section,
+ page_context: page_context,
+ annotations: %{
+ selected_point: selected_point,
+ active_tab: active_tab,
+ auto_approve_annotations: auto_approve_annotations,
+ post_replies: {_, post_replies}
+ }
+ } = socket.assigns
+
+ attrs = %{
+ status: if(auto_approve_annotations, do: :approved, else: :submitted),
+ user_id: current_user.id,
+ section_id: section.id,
+ resource_id: page_context.page.resource_id,
+ annotated_resource_id: page_context.page.resource_id,
+ annotated_block_id: selected_point,
+ annotation_type: if(selected_point, do: :point, else: :none),
+ anonymous: params["anonymous"] == "true",
+ visibility: visibility_for_active_tab(active_tab),
+ content: %Collaboration.PostContent{message: value},
+ parent_post_id: parent_post_id
+ }
+
+ case Collaboration.create_post(attrs) do
+ {:ok, post} ->
+ {:noreply,
+ socket
+ |> put_flash(:info, "Reply successfully created")
+ |> assign_annotations(post_replies: {parent_post_id, post_replies ++ [post]})}
+
+ {:error, _} ->
+ {:noreply, put_flash(socket, :error, "Failed to create reply")}
+ end
+ end
+
# handle assigns directly from other sub-tasks and processes
def handle_info(
{:assign_annotations, annotations},
@@ -371,6 +437,7 @@ defmodule OliWeb.Delivery.Student.LessonLive do
annotations={@annotations.posts}
current_user={@current_user}
active_tab={@annotations.active_tab}
+ post_replies={@annotations.post_replies}
/>
@@ -890,7 +957,8 @@ defmodule OliWeb.Delivery.Student.LessonLive do
posts: nil,
active_tab: :my_notes,
create_new_annotation: false,
- auto_approve_annotations: course_collab_space_config.auto_accept
+ auto_approve_annotations: course_collab_space_config.auto_accept,
+ post_replies: nil
}
)
@@ -949,6 +1017,17 @@ defmodule OliWeb.Delivery.Student.LessonLive do
end
end
+ defp async_load_post_replies(caller, user_id, post_id) do
+ Task.Supervisor.start_child(Oli.TaskSupervisor, fn ->
+ post_replies = Collaboration.list_replies_for_post_in_point_block(user_id, post_id)
+
+ send(
+ caller,
+ {:assign_annotations, %{post_replies: {post_id, post_replies}}}
+ )
+ end)
+ end
+
defp assign_annotations(socket, annotations) do
assign(socket, annotations: Enum.into(annotations, socket.assigns.annotations))
end
From ecd1d5cec965c9a953346955190e3aa883f30c07 Mon Sep 17 00:00:00 2001
From: Devesh Tiwari
Date: Wed, 27 Mar 2024 12:47:08 +0530
Subject: [PATCH 07/42] MER-2487
---
.../src/apps/delivery/layouts/deck/LessonFinishedDialog.tsx | 4 ++--
.../layouts/deck/components/ReviewModeHistoryPanel.tsx | 3 ++-
2 files changed, 4 insertions(+), 3 deletions(-)
diff --git a/assets/src/apps/delivery/layouts/deck/LessonFinishedDialog.tsx b/assets/src/apps/delivery/layouts/deck/LessonFinishedDialog.tsx
index 36be17dc5ae..dc39348dfc4 100644
--- a/assets/src/apps/delivery/layouts/deck/LessonFinishedDialog.tsx
+++ b/assets/src/apps/delivery/layouts/deck/LessonFinishedDialog.tsx
@@ -113,10 +113,10 @@ const LessonFinishedDialog: React.FC = ({
aria-hidden={!isOpen}
style={{
display: isOpen ? 'block' : 'none',
- minHeight: '350px',
+ minHeight: '250px',
height: 'unset',
width: '500px',
- top: '20%',
+ top: '25%',
backgroundImage: imageUrl ? `url('${imageUrl}')` : '',
left: '50%',
}}
diff --git a/assets/src/apps/delivery/layouts/deck/components/ReviewModeHistoryPanel.tsx b/assets/src/apps/delivery/layouts/deck/components/ReviewModeHistoryPanel.tsx
index d6763488e07..5c431c8fc99 100644
--- a/assets/src/apps/delivery/layouts/deck/components/ReviewModeHistoryPanel.tsx
+++ b/assets/src/apps/delivery/layouts/deck/components/ReviewModeHistoryPanel.tsx
@@ -109,6 +109,7 @@ const ReviewModeHistoryPanel: React.FC = ({ items }
Date: Wed, 27 Mar 2024 11:39:13 -0400
Subject: [PATCH 08/42] fix offset issue where chat bubbles are not lined up
correctly with learning objectives
---
assets/src/hooks/point_markers.ts | 13 ++++++++-----
1 file changed, 8 insertions(+), 5 deletions(-)
diff --git a/assets/src/hooks/point_markers.ts b/assets/src/hooks/point_markers.ts
index 62c315360f8..a36e1057488 100644
--- a/assets/src/hooks/point_markers.ts
+++ b/assets/src/hooks/point_markers.ts
@@ -5,9 +5,14 @@ export const PointMarkers = {
mounted() {
const el = this.el as HTMLElement;
+ const pageHeaderOffset =
+ document.getElementById('page_header')?.getBoundingClientRect().height || 0;
+
const UPDATE_DEBOUNCE_INTERVAL = 200;
const updatePointMarkers = debounce(() => {
- this.pushEvent('update_point_markers', { ['point_markers']: queryPointMarkers(el) });
+ this.pushEvent('update_point_markers', {
+ ['point_markers']: queryPointMarkers(el, pageHeaderOffset),
+ });
}, UPDATE_DEBOUNCE_INTERVAL);
// update the marker positions immediately when the page is mounted
@@ -46,14 +51,12 @@ export const PointMarkers = {
},
};
-function queryPointMarkers(el: HTMLElement) {
+function queryPointMarkers(el: HTMLElement, pageHeaderOffset: number) {
const markerElements = el.querySelectorAll('[data-point-marker]');
- const OFFSET_TOP = 110;
-
return Array.from(markerElements).map((markerEl) => ({
id: markerEl.getAttribute('data-point-marker'),
- top: markerEl.getBoundingClientRect().top - el.getBoundingClientRect().top + OFFSET_TOP,
+ top: markerEl.getBoundingClientRect().top - el.getBoundingClientRect().top + pageHeaderOffset,
}));
}
From ffe119c87d7b84e2cf893ebbc264238d6fd08edd Mon Sep 17 00:00:00 2001
From: Eli Knebel
Date: Wed, 27 Mar 2024 13:09:13 -0400
Subject: [PATCH 09/42] optimistically add replies and update counts
---
.../delivery/student/lesson/annotations.ex | 7 +--
.../live/delivery/student/lesson_live.ex | 45 ++++++++++++-------
2 files changed, 31 insertions(+), 21 deletions(-)
diff --git a/lib/oli_web/live/delivery/student/lesson/annotations.ex b/lib/oli_web/live/delivery/student/lesson/annotations.ex
index 59384013364..e6f1db5c225 100644
--- a/lib/oli_web/live/delivery/student/lesson/annotations.ex
+++ b/lib/oli_web/live/delivery/student/lesson/annotations.ex
@@ -47,12 +47,7 @@ defmodule OliWeb.Delivery.Student.Lesson.Annotations do
<%= empty_label(@active_tab) %>
<% annotations -> %>
<%= for annotation <- annotations do %>
- <.post
- post={annotation}
- current_user={@current_user}
- post_replies={@post_replies}
- disable_anonymous_option={@active_tab == :my_notes || is_guest(@current_user)}
- />
+ <.post post={annotation} current_user={@current_user} post_replies={@post_replies} />
<% end %>
<% end %>
diff --git a/lib/oli_web/live/delivery/student/lesson_live.ex b/lib/oli_web/live/delivery/student/lesson_live.ex
index 3e1a4306bb5..8ace1c214a1 100644
--- a/lib/oli_web/live/delivery/student/lesson_live.ex
+++ b/lib/oli_web/live/delivery/student/lesson_live.ex
@@ -285,8 +285,7 @@ defmodule OliWeb.Delivery.Student.LessonLive do
{:noreply,
socket
|> put_flash(:info, "Note created successfully")
- |> assign_annotations(create_new_annotation: false, posts: [post | posts])
- |> increment_post_count(selected_point)}
+ |> optimistically_add_post(selected_point, post)}
{:error, _} ->
{:noreply, put_flash(socket, :error, "Failed to create note")}
@@ -349,8 +348,7 @@ defmodule OliWeb.Delivery.Student.LessonLive do
annotations: %{
selected_point: selected_point,
active_tab: active_tab,
- auto_approve_annotations: auto_approve_annotations,
- post_replies: {_, post_replies}
+ auto_approve_annotations: auto_approve_annotations
}
} = socket.assigns
@@ -365,7 +363,8 @@ defmodule OliWeb.Delivery.Student.LessonLive do
anonymous: params["anonymous"] == "true",
visibility: visibility_for_active_tab(active_tab),
content: %Collaboration.PostContent{message: value},
- parent_post_id: parent_post_id
+ parent_post_id: parent_post_id,
+ thread_root_id: parent_post_id
}
case Collaboration.create_post(attrs) do
@@ -373,7 +372,7 @@ defmodule OliWeb.Delivery.Student.LessonLive do
{:noreply,
socket
|> put_flash(:info, "Reply successfully created")
- |> assign_annotations(post_replies: {parent_post_id, post_replies ++ [post]})}
+ |> optimistically_add_reply_post(post, parent_post_id)}
{:error, _} ->
{:noreply, put_flash(socket, :error, "Failed to create reply")}
@@ -1036,16 +1035,32 @@ defmodule OliWeb.Delivery.Student.LessonLive do
defp visibility_for_active_tab(:my_notes), do: :private
defp visibility_for_active_tab(_), do: :private
- defp increment_post_count(socket, selected_point) do
- case socket.assigns.annotations.post_counts do
- nil ->
- socket
+ defp optimistically_add_post(socket, selected_point, post) do
+ %{posts: posts, post_counts: post_counts} = socket.assigns.annotations
- post_counts ->
- assign_annotations(socket,
- post_counts: Map.update(post_counts, selected_point, 1, &(&1 + 1))
- )
- end
+ socket
+ |> assign_annotations(
+ posts: [%Collaboration.Post{post | replies_count: 0} | posts],
+ post_counts: Map.update(post_counts, selected_point, 1, &(&1 + 1)),
+ create_new_annotation: false
+ )
+ end
+
+ defp optimistically_add_reply_post(socket, reply_post, parent_post_id) do
+ %{posts: posts, post_replies: {_, post_replies}} = socket.assigns.annotations
+
+ socket
+ |> assign_annotations(
+ posts:
+ Enum.map(posts, fn post ->
+ if post.id == parent_post_id do
+ %Collaboration.Post{post | replies_count: post.replies_count + 1}
+ else
+ post
+ end
+ end),
+ post_replies: {parent_post_id, post_replies ++ [reply_post]}
+ )
end
defp check_gating_conditions(section, user, resource_id) do
From 5431df13519993587c513ff11e716c14aeff0442 Mon Sep 17 00:00:00 2001
From: Eli Knebel
Date: Wed, 27 Mar 2024 13:14:13 -0400
Subject: [PATCH 10/42] render correct count for annotation bubbles, do not
include replies
---
lib/oli/resources/collaboration.ex | 1 +
1 file changed, 1 insertion(+)
diff --git a/lib/oli/resources/collaboration.ex b/lib/oli/resources/collaboration.ex
index 34cfe3ecd4d..87d289e0168 100644
--- a/lib/oli/resources/collaboration.ex
+++ b/lib/oli/resources/collaboration.ex
@@ -1158,6 +1158,7 @@ defmodule Oli.Resources.Collaboration do
post in Post,
where:
post.section_id == ^section_id and post.resource_id == ^resource_id and
+ is_nil(post.parent_post_id) and is_nil(post.thread_root_id) and
post.visibility == ^visibility and
(post.status in [:approved, :archived] or
(post.status == :submitted and post.user_id == ^user_id)),
From a2d6bd555d66f7e0160013bb586c56f5daff4b91 Mon Sep 17 00:00:00 2001
From: Eli Knebel
Date: Thu, 28 Mar 2024 11:45:13 -0400
Subject: [PATCH 11/42] cleanup warnings
---
lib/oli_web/live/delivery/student/lesson_live.ex | 2 --
1 file changed, 2 deletions(-)
diff --git a/lib/oli_web/live/delivery/student/lesson_live.ex b/lib/oli_web/live/delivery/student/lesson_live.ex
index 8ace1c214a1..2bd4f53e18c 100644
--- a/lib/oli_web/live/delivery/student/lesson_live.ex
+++ b/lib/oli_web/live/delivery/student/lesson_live.ex
@@ -4,7 +4,6 @@ defmodule OliWeb.Delivery.Student.LessonLive do
import OliWeb.Delivery.Student.Utils,
only: [page_header: 1, star_icon: 1, scripts: 1]
- alias Oli.Accounts.User
alias Oli.Delivery.Attempts.Core.ResourceAttempt
alias Oli.Delivery.Attempts.PageLifecycle
alias Oli.Delivery.Attempts.PageLifecycle.FinalizationSummary
@@ -260,7 +259,6 @@ defmodule OliWeb.Delivery.Student.LessonLive do
section: section,
page_context: page_context,
annotations: %{
- posts: posts,
selected_point: selected_point,
active_tab: active_tab,
auto_approve_annotations: auto_approve_annotations
From b9f2e49fcc9bff3de2afe43fef51c42199286c9f Mon Sep 17 00:00:00 2001
From: Anders Weinstein
Date: Thu, 28 Mar 2024 11:51:42 -0400
Subject: [PATCH 12/42] base multi-input targeted feedback custom score display
on question-wide flag
---
.../components/activities/multi_input/sections/AnswerKeyTab.tsx | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/assets/src/components/activities/multi_input/sections/AnswerKeyTab.tsx b/assets/src/components/activities/multi_input/sections/AnswerKeyTab.tsx
index 27057b33a34..ea006c7c572 100644
--- a/assets/src/components/activities/multi_input/sections/AnswerKeyTab.tsx
+++ b/assets/src/components/activities/multi_input/sections/AnswerKeyTab.tsx
@@ -127,7 +127,7 @@ export const AnswerKeyTab: React.FC = (props) => {
updateScore={(_id, score) =>
dispatch(ResponseActions.editResponseScore(response.id, score))
}
- customScoring={hasCustomScoring(model, props.input.partId)}
+ customScoring={model.customScoring}
removeResponse={(id) => dispatch(ResponseActions.removeResponse(id))}
key={response.id}
>
From 5e9858ab2a5e5b0facfe0e2c0af0abb1aa92e8b9 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Nicol=C3=A1s=20Cirio?=
Date: Thu, 28 Mar 2024 13:54:54 -0300
Subject: [PATCH 13/42] [NG23-107] Authoring input for intro content (#4719)
* refactor RichTextEditor to use it as the intro_content form within a live_component
* fix failing test
* disallow block elements in rich text editor
* add tests
---
assets/css/app.css | 4 +
assets/src/apps/Components.tsx | 2 +
.../src/components/content/RichTextEditor.tsx | 25 ++++
lib/oli_web/common/react.ex | 12 +-
.../curriculum/container/container_live.ex | 15 +-
.../container/container_live.html.heex | 1 +
.../live/curriculum/entries/options_modal.ex | 128 ++++++++++++++++--
.../live/curriculum/container_test.exs | 25 +++-
.../entries/options_modal_content_test.exs | 127 +++++++++++++++++
9 files changed, 323 insertions(+), 16 deletions(-)
diff --git a/assets/css/app.css b/assets/css/app.css
index d2e2c4b9308..0635f7e4138 100644
--- a/assets/css/app.css
+++ b/assets/css/app.css
@@ -127,6 +127,10 @@ https://tailwindcss.com/docs/utility-first
@apply mb-[20px] text-2xl tracking-[0.02px] font-light dark:text-white;
}
+.truncate-form-control p {
+ @apply truncate;
+}
+
.scrollbar-hide::-webkit-scrollbar {
display: none;
}
diff --git a/assets/src/apps/Components.tsx b/assets/src/apps/Components.tsx
index 27d0abdd6ba..973b0cdcc85 100644
--- a/assets/src/apps/Components.tsx
+++ b/assets/src/apps/Components.tsx
@@ -4,6 +4,7 @@ import { Navbar } from 'components/common/Navbar';
import { SelectTimezone } from 'components/common/SelectTimezone';
import { TechSupportButton } from 'components/common/TechSupportButton';
import { UserAccountMenu } from 'components/common/UserAccountMenu';
+import { RichTextEditor } from 'components/content/RichTextEditor';
import { AlternativesPreferenceSelector } from 'components/delivery/AlternativesPreferenceSelector';
import { CourseContentOutline } from 'components/delivery/CourseContentOutline';
import { SurveyControls } from 'components/delivery/SurveyControls';
@@ -35,3 +36,4 @@ registerApplication('SelectTimezone', SelectTimezone, globalStore);
registerApplication('YoutubePlayer', YoutubePlayer, globalStore);
registerApplication('TechSupportButton', TechSupportButton, globalStore);
registerApplication('OfflineDetector', OfflineDetector, globalStore);
+registerApplication('RichTextEditor', RichTextEditor, globalStore);
diff --git a/assets/src/components/content/RichTextEditor.tsx b/assets/src/components/content/RichTextEditor.tsx
index 90bef13a8a1..3926ea3c8c4 100644
--- a/assets/src/components/content/RichTextEditor.tsx
+++ b/assets/src/components/content/RichTextEditor.tsx
@@ -25,6 +25,12 @@ type Props = {
onChangeTextDirection?: (textDirection: 'ltr' | 'rtl') => void;
onEdit: (value: Descendant[], editor: SlateEditor, operations: Operation[]) => void;
onRequestMedia?: (request: MediaItemRequest) => Promise;
+ // the name of the event to be pushed back to the liveview (or live_component) when rendered with React.component
+ onEditEvent?: string;
+ // the name of the event target element (if the target is a live_component, ex: "#my-live-component-id")
+ onEditTarget?: string;
+ pushEvent?: (event: string, payload: any) => void;
+ pushEventTo?: (selectorOrTarget: string, event: string, payload: any) => void;
};
export const RichTextEditor: React.FC = ({
projectSlug,
@@ -42,10 +48,29 @@ export const RichTextEditor: React.FC = ({
children,
textDirection,
onChangeTextDirection,
+ onEditEvent,
+ onEditTarget,
+ pushEvent,
+ pushEventTo,
}) => {
// Support content persisted when RichText had a `model` property.
value = (value as any).model ? (value as any).model : value;
+ // Support for rendering the component within a LiveView or a LiveComponent:
+ // if onEditEvent is not null it means this react component is rendered within a LiveView or a live_component
+ // using the React.component wrapper
+ // If so, events need to be pushed back to the LiveView or the live_component (the optional onEditTarget is used to target the event to a live_component)
+
+ if (onEditEvent && pushEvent && pushEventTo) {
+ onEdit = (values) => {
+ if (onEditTarget) {
+ pushEventTo(onEditTarget, onEditEvent, { values: values });
+ } else {
+ pushEvent(onEditEvent, { values: values });
+ }
+ };
+ }
+
return (
diff --git a/lib/oli_web/common/react.ex b/lib/oli_web/common/react.ex
index 04eeaa65d2f..0201c2f6df2 100644
--- a/lib/oli_web/common/react.ex
+++ b/lib/oli_web/common/react.ex
@@ -1,5 +1,15 @@
defmodule OliWeb.Common.React do
- # use Phoenix.Component
+ @moduledoc """
+ React component wrappers. It wraps the `ReactPhoenix.ClientSide.react_component` function and the
+ `PhoenixLiveReact.live_react_component` function to provide a single `component` function that can be used in non LiveView
+ and LiveViews respectively (that is why the OliWeb.Common.SessionContext (@ctx) is passed as first argument, to distinguish liveview from non-liveview)
+
+ ## Usage in a template
+
+ <%= React.component(@ctx, "Components.MyComponent", %{name: "Bob"}, id: "my-component-1") %>
+
+ Remember to import and register the component in assets/src/apps/Components.tsx
+ """
import PhoenixLiveReact
diff --git a/lib/oli_web/live/curriculum/container/container_live.ex b/lib/oli_web/live/curriculum/container/container_live.ex
index e32a3edc83c..96b9ac1fc6d 100644
--- a/lib/oli_web/live/curriculum/container/container_live.ex
+++ b/lib/oli_web/live/curriculum/container/container_live.ex
@@ -276,8 +276,21 @@ defmodule OliWeb.Curriculum.ContainerLive do
%{"explanation_strategy" => %{"type" => "none"}} ->
Map.put(revision_params, "explanation_strategy", nil)
+ %{"intro_content" => ""} ->
+ Map.put(
+ revision_params,
+ "intro_content",
+ %{}
+ )
+
_ ->
- revision_params
+ intro_content = Jason.decode!(revision_params["intro_content"])
+
+ Map.put(
+ revision_params,
+ "intro_content",
+ intro_content
+ )
end
case ContainerEditor.edit_page(project, revision.slug, revision_params) do
diff --git a/lib/oli_web/live/curriculum/container/container_live.html.heex b/lib/oli_web/live/curriculum/container/container_live.html.heex
index 900469ea2cb..62f7e7a99bb 100644
--- a/lib/oli_web/live/curriculum/container/container_live.html.heex
+++ b/lib/oli_web/live/curriculum/container/container_live.html.heex
@@ -13,6 +13,7 @@
<%= if @options_modal_assigns do %>
<.live_component
module={OptionsModalContent}
+ ctx={@ctx}
id="modal_content"
redirect_url={@options_modal_assigns.redirect_url}
revision={@options_modal_assigns.revision}
diff --git a/lib/oli_web/live/curriculum/entries/options_modal.ex b/lib/oli_web/live/curriculum/entries/options_modal.ex
index f13fff8ba42..16125efb0f4 100644
--- a/lib/oli_web/live/curriculum/entries/options_modal.ex
+++ b/lib/oli_web/live/curriculum/entries/options_modal.ex
@@ -8,6 +8,7 @@ defmodule OliWeb.Curriculum.OptionsModalContent do
alias Oli.Resources.ScoringStrategy
alias Oli.Utils.S3Storage
alias OliWeb.Components.HierarchySelector
+ alias OliWeb.Common.React
@attempt_options [
"1": 1,
@@ -70,6 +71,59 @@ defmodule OliWeb.Curriculum.OptionsModalContent do
attr(:attempt_options, :list, default: @attempt_options)
attr(:selected_resources, :list, default: [])
+ def render(%{step: :intro_content} = assigns) do
+ ~H"""
+
+
+ Introduction content
+
+
+ <%= React.component(
+ @ctx,
+ "Components.RichTextEditor",
+ %{
+ projectSlug: @project.slug,
+ onEdit: "initial_function_that_will_be_overwritten",
+ onEditEvent: "intro_content_change",
+ onEditTarget: "#intro_content_step",
+ editMode: true,
+ value:
+ (fetch_field(@changeset, :intro_content) &&
+ fetch_field(@changeset, :intro_content)["children"]) || [],
+ fixedToolbar: true,
+ allowBlockElements: false
+ },
+ id: "rich_text_editor_react_component"
+ ) %>
+
+
+
+
+ """
+ end
+
def render(%{step: step} = assigns) when step in [:poster_image, :intro_video] do
~H"""
@@ -498,17 +552,50 @@ defmodule OliWeb.Curriculum.OptionsModalContent do
The title is used to identify this <%= resource_type_label(@revision) %>.
- <.poster_image_selection
- target={@myself}
- poster_image={fetch_field(@changeset, :poster_image) || @default_poster_image}
- delete_button_enabled={
- fetch_field(@changeset, :poster_image) not in [nil, @default_poster_image]
- }
- />
- <.intro_video_selection
- target={@myself}
- intro_video={fetch_field(@changeset, :intro_video)}
- />
+
+
+ <.poster_image_selection
+ target={@myself}
+ poster_image={fetch_field(@changeset, :poster_image) || @default_poster_image}
+ delete_button_enabled={
+ fetch_field(@changeset, :poster_image) not in [nil, @default_poster_image]
+ }
+ />
+ <.intro_video_selection
+ target={@myself}
+ intro_video={fetch_field(@changeset, :intro_video)}
+ />
+
<% end %>
@@ -289,6 +294,7 @@ defmodule OliWeb.Delivery.Student.Lesson.Annotations do
attr :post, Oli.Resources.Collaboration.Post, required: true
attr :current_user, Oli.Accounts.User, required: true
attr :post_replies, :any, required: true
+ attr :disable_anonymous_option, :boolean, default: false
defp post(assigns) do
~H"""
@@ -305,7 +311,12 @@ defmodule OliWeb.Delivery.Student.Lesson.Annotations do
<%= @post.content.message %>
<.post_actions post={@post} />
- <.post_replies post={@post} replies={@post_replies} current_user={@current_user} />
+ <.post_replies
+ post={@post}
+ replies={@post_replies}
+ current_user={@current_user}
+ disable_anonymous_option={@disable_anonymous_option}
+ />
"""
end
@@ -347,15 +358,28 @@ defmodule OliWeb.Delivery.Student.Lesson.Annotations do
%Oli.Resources.Collaboration.Post{visibility: :public} ->
~H"""
-
+
+
+ <.like_icon />
+ <%= case Map.get(@post.reaction_counts, :like) do %>
+ <% nil -> %>
+ <% count -> %>
+ <%= if(count > 0, do: count) %>
+ <% end %>
+
- <.replies_bubble_icon /> <%= if(@post.replies_count > 0,
- do: @post.replies_count,
- else: "Reply"
+ <.replies_bubble_icon />
+ <%= if(@post.replies_count > 0,
+ do: @post.replies_count
) %>
@@ -543,4 +567,18 @@ defmodule OliWeb.Delivery.Student.Lesson.Annotations do
"""
end
+
+ defp like_icon(assigns) do
+ ~H"""
+
+
+
+ """
+ end
end
diff --git a/lib/oli_web/live/delivery/student/lesson_live.ex b/lib/oli_web/live/delivery/student/lesson_live.ex
index 2bd4f53e18c..fc28f59820f 100644
--- a/lib/oli_web/live/delivery/student/lesson_live.ex
+++ b/lib/oli_web/live/delivery/student/lesson_live.ex
@@ -328,6 +328,38 @@ defmodule OliWeb.Delivery.Student.LessonLive do
end
end
+ def handle_event("toggle_reaction", %{"post-id" => post_id, "reaction" => reaction}, socket) do
+ %{current_user: current_user, annotations: %{posts: posts}} = socket.assigns
+
+ post_id = String.to_integer(post_id)
+ reaction = String.to_existing_atom(reaction)
+
+ case Collaboration.toggle_reaction(post_id, current_user.id, reaction) do
+ {:ok, change} ->
+ {:noreply,
+ assign_annotations(socket,
+ posts:
+ Enum.map(
+ posts,
+ fn post ->
+ if post.id == post_id do
+ %{
+ post
+ | reaction_counts:
+ Map.update(post.reaction_counts, reaction, 1, &(&1 + change))
+ }
+ else
+ post
+ end
+ end
+ )
+ )}
+
+ {:error, _} ->
+ {:noreply, put_flash(socket, :error, "Failed to update reaction for post")}
+ end
+ end
+
def handle_event("create_reply", %{"content" => ""}, socket) do
{:noreply, put_flash(socket, :error, "Reply cannot be empty")}
end
@@ -1038,7 +1070,7 @@ defmodule OliWeb.Delivery.Student.LessonLive do
socket
|> assign_annotations(
- posts: [%Collaboration.Post{post | replies_count: 0} | posts],
+ posts: [%Collaboration.Post{post | replies_count: 0, reaction_counts: 0} | posts],
post_counts: Map.update(post_counts, selected_point, 1, &(&1 + 1)),
create_new_annotation: false
)
diff --git a/priv/repo/migrations/20240328190716_user_reaction_posts.exs b/priv/repo/migrations/20240328190716_user_reaction_posts.exs
new file mode 100644
index 00000000000..897afebf93c
--- /dev/null
+++ b/priv/repo/migrations/20240328190716_user_reaction_posts.exs
@@ -0,0 +1,15 @@
+defmodule Oli.Repo.Migrations.UserReactionPosts do
+ use Ecto.Migration
+
+ def change do
+ create table(:user_reaction_posts) do
+ add :reaction, :string, default: "like"
+ add :post_id, references(:posts, on_delete: :delete_all)
+ add :user_id, references(:users, on_delete: :delete_all)
+
+ timestamps(type: :timestamptz)
+ end
+
+ create unique_index(:user_reaction_posts, [:reaction, :post_id, :user_id])
+ end
+end
From f25ddcc30f84a96a6c49a35820b080a0d59d63f5 Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Thu, 28 Mar 2024 20:42:12 +0000
Subject: [PATCH 17/42] Bump express from 4.18.2 to 4.19.2 in /assets
Bumps [express](https://github.com/expressjs/express) from 4.18.2 to 4.19.2.
- [Release notes](https://github.com/expressjs/express/releases)
- [Changelog](https://github.com/expressjs/express/blob/master/History.md)
- [Commits](https://github.com/expressjs/express/compare/4.18.2...4.19.2)
---
updated-dependencies:
- dependency-name: express
dependency-type: indirect
...
Signed-off-by: dependabot[bot]
---
assets/yarn.lock | 40 ++++++++++++++++++++--------------------
1 file changed, 20 insertions(+), 20 deletions(-)
diff --git a/assets/yarn.lock b/assets/yarn.lock
index d39fd051c19..96d1b4fb3e0 100644
--- a/assets/yarn.lock
+++ b/assets/yarn.lock
@@ -7054,13 +7054,13 @@ bn.js@^5.0.0, bn.js@^5.1.1:
resolved "https://registry.npmjs.org/bn.js/-/bn.js-5.2.1.tgz"
integrity sha512-eXRvHzWyYPBuB4NBy0cmYQjGitUrtqwbvlzP3G6VFnNRbsZQIxQ10PbKKHt8gZ/HW/D/747aDl+QkDqg3KQLMQ==
-body-parser@1.20.1:
- version "1.20.1"
- resolved "https://registry.npmjs.org/body-parser/-/body-parser-1.20.1.tgz"
- integrity sha512-jWi7abTbYwajOytWCQc37VulmWiRae5RyTpaCyDcS5/lMdtwSz5lOpDE67srw/HYe35f1z3fDQw+3txg7gNtWw==
+body-parser@1.20.2:
+ version "1.20.2"
+ resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.20.2.tgz#6feb0e21c4724d06de7ff38da36dad4f57a747fd"
+ integrity sha512-ml9pReCu3M61kGlqoTm2umSXTlRTuGTx0bfYj+uIUKKYycG5NtSbeetV3faSU6R7ajOPw0g/J1PvK4qNy7s5bA==
dependencies:
bytes "3.1.2"
- content-type "~1.0.4"
+ content-type "~1.0.5"
debug "2.6.9"
depd "2.0.0"
destroy "1.2.0"
@@ -7068,7 +7068,7 @@ body-parser@1.20.1:
iconv-lite "0.4.24"
on-finished "2.4.1"
qs "6.11.0"
- raw-body "2.5.1"
+ raw-body "2.5.2"
type-is "~1.6.18"
unpipe "1.0.0"
@@ -7972,7 +7972,7 @@ content-disposition@0.5.4:
dependencies:
safe-buffer "5.2.1"
-content-type@~1.0.4:
+content-type@~1.0.4, content-type@~1.0.5:
version "1.0.5"
resolved "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz"
integrity sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==
@@ -7989,10 +7989,10 @@ cookie-signature@1.0.6:
resolved "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz"
integrity sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==
-cookie@0.5.0:
- version "0.5.0"
- resolved "https://registry.npmjs.org/cookie/-/cookie-0.5.0.tgz"
- integrity sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==
+cookie@0.6.0:
+ version "0.6.0"
+ resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.6.0.tgz#2798b04b071b0ecbff0dbb62a505a8efa4e19051"
+ integrity sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==
copy-concurrently@^1.0.0:
version "1.0.5"
@@ -9945,16 +9945,16 @@ expect@^26.6.2:
jest-regex-util "^26.0.0"
express@^4.17.1:
- version "4.18.2"
- resolved "https://registry.npmjs.org/express/-/express-4.18.2.tgz"
- integrity sha512-5/PsL6iGPdfQ/lKM1UuielYgv3BUoJfz1aUwU9vHZ+J7gyvwdQXFEBIEIaxeGf0GIcreATNyBExtalisDbuMqQ==
+ version "4.19.2"
+ resolved "https://registry.yarnpkg.com/express/-/express-4.19.2.tgz#e25437827a3aa7f2a827bc8171bbbb664a356465"
+ integrity sha512-5T6nhjsT+EOMzuck8JjBHARTHfMht0POzlA60WV2pMD3gyXw2LZnZ+ueGdNxG+0calOJcWKbpFcuzLZ91YWq9Q==
dependencies:
accepts "~1.3.8"
array-flatten "1.1.1"
- body-parser "1.20.1"
+ body-parser "1.20.2"
content-disposition "0.5.4"
content-type "~1.0.4"
- cookie "0.5.0"
+ cookie "0.6.0"
cookie-signature "1.0.6"
debug "2.6.9"
depd "2.0.0"
@@ -15630,10 +15630,10 @@ range-parser@^1.2.1, range-parser@~1.2.1:
resolved "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz"
integrity sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==
-raw-body@2.5.1:
- version "2.5.1"
- resolved "https://registry.npmjs.org/raw-body/-/raw-body-2.5.1.tgz"
- integrity sha512-qqJBtEyVgS0ZmPGdCFPWJ3FreoqvG4MVQln/kCgF7Olq95IbOp0/BWyMwbdtn4VTvkM8Y7khCQ2Xgk/tcrCXig==
+raw-body@2.5.2:
+ version "2.5.2"
+ resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-2.5.2.tgz#99febd83b90e08975087e8f1f9419a149366b68a"
+ integrity sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==
dependencies:
bytes "3.1.2"
http-errors "2.0.0"
From 78dca4260264469b2ae036155abb91872e886cd5 Mon Sep 17 00:00:00 2001
From: Eli Knebel
Date: Fri, 29 Mar 2024 09:37:27 -0400
Subject: [PATCH 18/42] Add new reply input with disable anonymous option
---
lib/oli_web/live/delivery/student/lesson/annotations.ex | 5 ++++-
1 file changed, 4 insertions(+), 1 deletion(-)
diff --git a/lib/oli_web/live/delivery/student/lesson/annotations.ex b/lib/oli_web/live/delivery/student/lesson/annotations.ex
index 84b95b7da82..bf9973b95ee 100644
--- a/lib/oli_web/live/delivery/student/lesson/annotations.ex
+++ b/lib/oli_web/live/delivery/student/lesson/annotations.ex
@@ -404,7 +404,10 @@ defmodule OliWeb.Delivery.Student.Lesson.Annotations do
<% {_, :loading} -> %>
<% {_, []} -> %>
- <.add_new_reply_input parent_post_id={@post.id} />
+ <.add_new_reply_input
+ parent_post_id={@post.id}
+ disable_anonymous_option={@disable_anonymous_option}
+ />
<% {_, replies} -> %>
<%= for reply <- replies do %>
From eff47354cb7c7617d7a6173e451211e8d402d78c Mon Sep 17 00:00:00 2001
From: Eli Knebel
Date: Fri, 29 Mar 2024 10:42:16 -0400
Subject: [PATCH 19/42] Update comment
---
lib/oli/resources/collaboration.ex | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/lib/oli/resources/collaboration.ex b/lib/oli/resources/collaboration.ex
index ea3e804743c..c6cefb1731b 100644
--- a/lib/oli/resources/collaboration.ex
+++ b/lib/oli/resources/collaboration.ex
@@ -1104,10 +1104,10 @@ defmodule Oli.Resources.Collaboration do
## Examples
- iex> list_posts_for_user_in_point_block(1, 1, 1, "1"))
+ iex> list_posts_for_user_in_point_block(1, 1, 1, :private, "1"))
[%Post{status: :archived}, ...]
- iex> list_posts_for_user_in_point_block(2, 2, 2, "2")
+ iex> list_posts_for_user_in_point_block(2, 2, 2, :private, "2")
[]
"""
def list_posts_for_user_in_point_block(
From 60a448c79c032bdf53cda987a4c23751aa313831 Mon Sep 17 00:00:00 2001
From: Eli Knebel
Date: Fri, 29 Mar 2024 11:06:55 -0400
Subject: [PATCH 20/42] remove unnecessary queries in
list_replies_for_post_in_point_block
---
lib/oli/resources/collaboration.ex | 13 +++----------
1 file changed, 3 insertions(+), 10 deletions(-)
diff --git a/lib/oli/resources/collaboration.ex b/lib/oli/resources/collaboration.ex
index 87d289e0168..6583d8049a6 100644
--- a/lib/oli/resources/collaboration.ex
+++ b/lib/oli/resources/collaboration.ex
@@ -1173,14 +1173,6 @@ defmodule Oli.Resources.Collaboration do
Repo.all(
from(
post in Post,
- join: sr in SectionResource,
- on: sr.resource_id == post.resource_id and sr.section_id == post.section_id,
- join: spp in SectionsProjectsPublications,
- on: spp.section_id == post.section_id and spp.project_id == sr.project_id,
- join: pr in PublishedResource,
- on: pr.publication_id == spp.publication_id and pr.resource_id == post.resource_id,
- join: rev in Revision,
- on: rev.id == pr.revision_id,
join: user in User,
on: post.user_id == user.id,
left_join: urp in UserReadPost,
@@ -1189,8 +1181,9 @@ defmodule Oli.Resources.Collaboration do
post.parent_post_id == ^post_id and
(post.status in [:approved, :archived] or
(post.status == :submitted and post.user_id == ^user_id)),
- select: post,
- order_by: [asc: :updated_at]
+ order_by: [asc: :updated_at],
+ preload: [user: user],
+ select: post
)
)
|> build_metrics_for_reply_posts(user_id)
From 1808aa769036695b265147e95ae522517364243e Mon Sep 17 00:00:00 2001
From: Eli Knebel
Date: Mon, 1 Apr 2024 12:47:53 -0400
Subject: [PATCH 21/42] react to reply
---
lib/oli/resources/collaboration.ex | 4 +-
.../delivery/student/lesson/annotations.ex | 15 ++++++--
.../live/delivery/student/lesson_live.ex | 38 +++++++++++++++++++
3 files changed, 53 insertions(+), 4 deletions(-)
diff --git a/lib/oli/resources/collaboration.ex b/lib/oli/resources/collaboration.ex
index 6bf22ac570e..e9115718d4f 100644
--- a/lib/oli/resources/collaboration.ex
+++ b/lib/oli/resources/collaboration.ex
@@ -1192,16 +1192,18 @@ defmodule Oli.Resources.Collaboration do
on: post.user_id == user.id,
left_join: urp in UserReadPost,
on: urp.post_id == post.id and urp.user_id == ^user_id,
+ left_join: reactions in assoc(post, :reactions),
where:
post.parent_post_id == ^post_id and
(post.status in [:approved, :archived] or
(post.status == :submitted and post.user_id == ^user_id)),
order_by: [asc: :updated_at],
- preload: [user: user],
+ preload: [user: user, reactions: reactions],
select: post
)
)
|> build_metrics_for_reply_posts(user_id)
+ |> count_reactions()
end
def toggle_reaction(post_id, user_id, reaction) do
diff --git a/lib/oli_web/live/delivery/student/lesson/annotations.ex b/lib/oli_web/live/delivery/student/lesson/annotations.ex
index bf9973b95ee..a561d6b0adf 100644
--- a/lib/oli_web/live/delivery/student/lesson/annotations.ex
+++ b/lib/oli_web/live/delivery/student/lesson/annotations.ex
@@ -310,7 +310,11 @@ defmodule OliWeb.Delivery.Student.Lesson.Annotations do
<%= @post.content.message %>
- <.post_actions post={@post} />
+ <.post_actions
+ post={@post}
+ on_toggle_reaction="toggle_reaction"
+ on_toggle_replies="toggle_post_replies"
+ />
<.post_replies
post={@post}
replies={@post_replies}
@@ -346,6 +350,8 @@ defmodule OliWeb.Delivery.Student.Lesson.Annotations do
end
attr :post, Oli.Resources.Collaboration.Post, required: true
+ attr :on_toggle_reaction, :string, default: nil
+ attr :on_toggle_replies, :string, default: nil
defp post_actions(assigns) do
case assigns.post do
@@ -360,8 +366,9 @@ defmodule OliWeb.Delivery.Student.Lesson.Annotations do
~H"""
@@ -373,8 +380,9 @@ defmodule OliWeb.Delivery.Student.Lesson.Annotations do
<% end %>
<.replies_bubble_icon />
@@ -486,6 +494,7 @@ defmodule OliWeb.Delivery.Student.Lesson.Annotations do
<%= @post.content.message %>
+ <.post_actions post={@post} on_toggle_reaction="toggle_reply_reaction" />
"""
end
diff --git a/lib/oli_web/live/delivery/student/lesson_live.ex b/lib/oli_web/live/delivery/student/lesson_live.ex
index fc28f59820f..633816d25c8 100644
--- a/lib/oli_web/live/delivery/student/lesson_live.ex
+++ b/lib/oli_web/live/delivery/student/lesson_live.ex
@@ -360,6 +360,44 @@ defmodule OliWeb.Delivery.Student.LessonLive do
end
end
+ def handle_event(
+ "toggle_reply_reaction",
+ %{"post-id" => post_id, "reaction" => reaction},
+ socket
+ ) do
+ %{current_user: current_user, annotations: %{post_replies: {parent_post_id, post_replies}}} =
+ socket.assigns
+
+ post_id = String.to_integer(post_id)
+ reaction = String.to_existing_atom(reaction)
+
+ case Collaboration.toggle_reaction(post_id, current_user.id, reaction) do
+ {:ok, change} ->
+ {:noreply,
+ assign_annotations(socket,
+ post_replies:
+ {parent_post_id,
+ Enum.map(
+ post_replies,
+ fn post ->
+ if post.id == post_id do
+ %{
+ post
+ | reaction_counts:
+ Map.update(post.reaction_counts, reaction, 1, &(&1 + change))
+ }
+ else
+ post
+ end
+ end
+ )}
+ )}
+
+ {:error, _} ->
+ {:noreply, put_flash(socket, :error, "Failed to update reaction for post")}
+ end
+ end
+
def handle_event("create_reply", %{"content" => ""}, socket) do
{:noreply, put_flash(socket, :error, "Reply cannot be empty")}
end
From c04a7ac466ecc2f945e11d6050e4d1bff7fa2ca9 Mon Sep 17 00:00:00 2001
From: Eli Knebel
Date: Mon, 1 Apr 2024 13:16:52 -0400
Subject: [PATCH 22/42] add reactions to replies render as primary color when
user has reacted
---
lib/oli/resources/collaboration.ex | 22 +++++++++---
lib/oli/resources/collaboration/post.ex | 2 +-
.../delivery/student/lesson/annotations.ex | 35 ++++++++++++++-----
.../live/delivery/student/lesson_live.ex | 20 ++++++++---
4 files changed, 59 insertions(+), 20 deletions(-)
diff --git a/lib/oli/resources/collaboration.ex b/lib/oli/resources/collaboration.ex
index e9115718d4f..8613c2a80c1 100644
--- a/lib/oli/resources/collaboration.ex
+++ b/lib/oli/resources/collaboration.ex
@@ -1149,16 +1149,28 @@ defmodule Oli.Resources.Collaboration do
}
)
)
- |> count_reactions()
+ |> summarize_reactions(user_id)
end
- defp count_reactions(posts) do
+ defp summarize_reactions(posts, current_user_id) do
Enum.map(posts, fn post ->
%{
post
- | reaction_counts:
+ | reaction_summaries:
Enum.reduce(post.reactions, %{}, fn r, acc ->
- Map.update(acc, r.reaction, 1, &(&1 + 1))
+ reacted_by_current_user = r.user_id == current_user_id
+
+ Map.update(
+ acc,
+ r.reaction,
+ %{count: 1, reacted: reacted_by_current_user},
+ fn %{
+ count: count,
+ reacted: reacted
+ } ->
+ %{count: count + 1, reacted: reacted_by_current_user || reacted}
+ end
+ )
end)
}
end)
@@ -1203,7 +1215,7 @@ defmodule Oli.Resources.Collaboration do
)
)
|> build_metrics_for_reply_posts(user_id)
- |> count_reactions()
+ |> summarize_reactions(user_id)
end
def toggle_reaction(post_id, user_id, reaction) do
diff --git a/lib/oli/resources/collaboration/post.ex b/lib/oli/resources/collaboration/post.ex
index 32284544dd4..d187c099fa4 100644
--- a/lib/oli/resources/collaboration/post.ex
+++ b/lib/oli/resources/collaboration/post.ex
@@ -38,7 +38,7 @@ defmodule Oli.Resources.Collaboration.Post do
has_many :reactions, Oli.Resources.Collaboration.UserReactionPost
- field :reaction_counts, :map, virtual: true
+ field :reaction_summaries, :map, virtual: true
timestamps(type: :utc_datetime)
end
diff --git a/lib/oli_web/live/delivery/student/lesson/annotations.ex b/lib/oli_web/live/delivery/student/lesson/annotations.ex
index a561d6b0adf..19c8cdb02f3 100644
--- a/lib/oli_web/live/delivery/student/lesson/annotations.ex
+++ b/lib/oli_web/live/delivery/student/lesson/annotations.ex
@@ -372,11 +372,11 @@ defmodule OliWeb.Delivery.Student.Lesson.Annotations do
phx-value-reaction={:like}
phx-value-post-id={assigns.post.id}
>
- <.like_icon />
- <%= case Map.get(@post.reaction_counts, :like) do %>
+ <%= case Map.get(@post.reaction_summaries, :like) do %>
<% nil -> %>
- <% count -> %>
- <%= if(count > 0, do: count) %>
+ <.like_icon />
+ <% %{count: count, reacted: reacted} -> %>
+ <.like_icon selected={reacted} /> <%= if(count > 0, do: count) %>
<% end %>
-
-
+
+
"""
end
+ attr :selected, :boolean, default: false
+
defp like_icon(assigns) do
~H"""
assign_annotations(
- posts: [%Collaboration.Post{post | replies_count: 0, reaction_counts: 0} | posts],
+ posts: [%Collaboration.Post{post | replies_count: 0, reaction_summaries: %{}} | posts],
post_counts: Map.update(post_counts, selected_point, 1, &(&1 + 1)),
create_new_annotation: false
)
@@ -1131,6 +1129,18 @@ defmodule OliWeb.Delivery.Student.LessonLive do
)
end
+ def update_reaction_summaries(post, reaction, change) do
+ Map.update(
+ post.reaction_summaries,
+ reaction,
+ %{count: 1, reacted: true},
+ &%{
+ count: &1.count + change,
+ reacted: if(change > 0, do: true, else: false)
+ }
+ )
+ end
+
defp check_gating_conditions(section, user, resource_id) do
case Oli.Delivery.Gating.blocked_by(section, user, resource_id) do
[] -> :ok
From 787818b08b97d6f3766af69d8733879612bf65de Mon Sep 17 00:00:00 2001
From: Eli Knebel
Date: Tue, 2 Apr 2024 11:42:38 -0400
Subject: [PATCH 23/42] cleanup async assign tasks add some unit tests
---
lib/oli_web/components/common.ex | 1 +
.../delivery/student/lesson/annotations.ex | 2 +-
.../live/delivery/student/lesson_live.ex | 63 ++++++-----
.../delivery/student/lesson_live_test.exs | 105 ++++++++++++++++++
test/support/test_helpers.ex | 27 +++++
5 files changed, 167 insertions(+), 31 deletions(-)
diff --git a/lib/oli_web/components/common.ex b/lib/oli_web/components/common.ex
index 423db9a0e9d..949c4853981 100644
--- a/lib/oli_web/components/common.ex
+++ b/lib/oli_web/components/common.ex
@@ -857,6 +857,7 @@ defmodule OliWeb.Components.Common do
def loading_spinner(assigns) do
~H"""
+
<.toggle_notes_button>
diff --git a/lib/oli_web/live/delivery/student/lesson_live.ex b/lib/oli_web/live/delivery/student/lesson_live.ex
index a26bff1de23..00a07b75250 100644
--- a/lib/oli_web/live/delivery/student/lesson_live.ex
+++ b/lib/oli_web/live/delivery/student/lesson_live.ex
@@ -32,7 +32,6 @@ defmodule OliWeb.Delivery.Student.LessonLive do
# https://hexdocs.pm/phoenix_live_view/Phoenix.LiveView.html#assign_async/3
if connected?(socket) do
async_load_annotations(
- self(),
socket.assigns.section,
socket.assigns.page_context.page.resource_id,
socket.assigns.current_user,
@@ -209,7 +208,6 @@ defmodule OliWeb.Delivery.Student.LessonLive do
def handle_event("select_annotation_point", %{"point-marker-id" => point_marker_id}, socket) do
async_load_annotations(
- self(),
socket.assigns.section,
socket.assigns.page_context.page.resource_id,
socket.assigns.current_user,
@@ -226,7 +224,6 @@ defmodule OliWeb.Delivery.Student.LessonLive do
def handle_event("select_annotation_point", _params, socket) do
async_load_annotations(
- self(),
socket.assigns.section,
socket.assigns.page_context.page.resource_id,
socket.assigns.current_user,
@@ -299,7 +296,6 @@ defmodule OliWeb.Delivery.Student.LessonLive do
end
async_load_annotations(
- self(),
socket.assigns.section,
socket.assigns.page_context.page.resource_id,
socket.assigns.current_user,
@@ -322,7 +318,7 @@ defmodule OliWeb.Delivery.Student.LessonLive do
{:noreply, assign_annotations(socket, post_replies: nil)}
_ ->
- async_load_post_replies(self(), current_user.id, post_id)
+ async_load_post_replies(current_user.id, post_id)
{:noreply, assign_annotations(socket, post_replies: {post_id, :loading})}
end
@@ -438,19 +434,30 @@ defmodule OliWeb.Delivery.Student.LessonLive do
{:noreply,
socket
|> put_flash(:info, "Reply successfully created")
- |> optimistically_add_reply_post(post, parent_post_id)}
+ |> optimistically_add_reply_post(
+ %Collaboration.Post{post | reaction_summaries: %{}},
+ parent_post_id
+ )}
{:error, _} ->
{:noreply, put_flash(socket, :error, "Failed to create reply")}
end
end
- # handle assigns directly from other sub-tasks and processes
- def handle_info(
- {:assign_annotations, annotations},
- socket
- ) do
- {:noreply, assign_annotations(socket, Enum.into(annotations, socket.assigns.annotations))}
+ # handle assigns directly from async tasks
+ def handle_info({ref, result}, socket) do
+ Process.demonitor(ref, [:flush])
+
+ case result do
+ {:assign_annotations, annotations} ->
+ {:noreply, assign_annotations(socket, Enum.into(annotations, socket.assigns.annotations))}
+
+ {:error, _} ->
+ {:noreply, put_flash(socket, :error, "Failed to load annotations")}
+
+ _ ->
+ {:noreply, socket}
+ end
end
def render(%{view: :practice_page, annotations: %{}} = assigns) do
@@ -1033,7 +1040,6 @@ defmodule OliWeb.Delivery.Student.LessonLive do
end
defp async_load_annotations(
- caller,
section,
resource_id,
current_user,
@@ -1042,7 +1048,7 @@ defmodule OliWeb.Delivery.Student.LessonLive do
point_block_id
) do
if current_user do
- Task.Supervisor.start_child(Oli.TaskSupervisor, fn ->
+ Task.async(fn ->
case course_collab_space_config do
%CollabSpaceConfig{status: :enabled} ->
# load post counts
@@ -1064,15 +1070,12 @@ defmodule OliWeb.Delivery.Student.LessonLive do
point_block_id
)
- send(
- caller,
- {:assign_annotations,
- %{
- post_counts: post_counts,
- posts: posts,
- auto_approve_annotations: course_collab_space_config.auto_accept
- }}
- )
+ {:assign_annotations,
+ %{
+ post_counts: post_counts,
+ posts: posts,
+ auto_approve_annotations: course_collab_space_config.auto_accept
+ }}
_ ->
# do nothing
@@ -1082,14 +1085,11 @@ defmodule OliWeb.Delivery.Student.LessonLive do
end
end
- defp async_load_post_replies(caller, user_id, post_id) do
- Task.Supervisor.start_child(Oli.TaskSupervisor, fn ->
+ defp async_load_post_replies(user_id, post_id) do
+ Task.async(fn ->
post_replies = Collaboration.list_replies_for_post_in_point_block(user_id, post_id)
- send(
- caller,
- {:assign_annotations, %{post_replies: {post_id, post_replies}}}
- )
+ {:assign_annotations, %{post_replies: {post_id, post_replies}}}
end)
end
@@ -1120,7 +1120,10 @@ defmodule OliWeb.Delivery.Student.LessonLive do
posts:
Enum.map(posts, fn post ->
if post.id == parent_post_id do
- %Collaboration.Post{post | replies_count: post.replies_count + 1}
+ %Collaboration.Post{
+ post
+ | replies_count: post.replies_count + 1
+ }
else
post
end
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 5d420dcefff..980eda35db1 100644
--- a/test/oli_web/live/delivery/student/lesson_live_test.exs
+++ b/test/oli_web/live/delivery/student/lesson_live_test.exs
@@ -797,4 +797,109 @@ defmodule OliWeb.Delivery.Student.LessonLiveTest do
)
end
end
+
+ describe "annotations toggle" do
+ setup [:user_conn, :create_elixir_project]
+
+ test "button is not rendered when annotations are disabled", %{
+ conn: conn,
+ section: section,
+ user: user,
+ page_1: page_1
+ } 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, Utils.lesson_live_path(section.slug, page_1.slug))
+
+ assert not has_element?(
+ view,
+ "button[phx-click='toggle_sidebar']"
+ )
+ end
+
+ test "button is rendered when annotations are enabled", %{
+ conn: conn,
+ section: section,
+ user: user,
+ page_1: page_1
+ } do
+ enable_collaborative_spaces(%{section: section})
+
+ Sections.enroll(user.id, section.id, [ContextRoles.get_role(:context_learner)])
+ Sections.mark_section_visited_for_student(section, user)
+
+ {:ok, view, _html} = live(conn, Utils.lesson_live_path(section.slug, page_1.slug))
+
+ assert has_element?(
+ view,
+ "button[phx-click='toggle_sidebar']"
+ )
+ end
+ end
+
+ describe "annotations panel" do
+ setup [:user_conn, :create_elixir_project, :enable_collaborative_spaces]
+
+ test "is toggled open when toolbar button is clicked", %{
+ conn: conn,
+ section: section,
+ user: user,
+ page_1: page_1
+ } 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, Utils.lesson_live_path(section.slug, page_1.slug))
+
+ view
+ |> element(~s{button[phx-click='toggle_sidebar']})
+ |> render_click
+
+ assert has_element?(
+ view,
+ "#annotations_panel"
+ )
+ end
+
+ test "renders empty message when there are no notes", %{
+ conn: conn,
+ section: section,
+ user: user,
+ page_1: page_1
+ } 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, Utils.lesson_live_path(section.slug, page_1.slug))
+
+ view
+ |> element(~s{button[phx-click='toggle_sidebar']})
+ |> render_click
+
+ wait_while(fn -> has_element?(view, "svg.loading") end)
+
+ assert has_element?(
+ view,
+ "div",
+ "There are no notes yet"
+ )
+ end
+ end
+
+ defp enable_collaborative_spaces(%{section: section}) do
+ course_collab_space_config =
+ Oli.Resources.Collaboration.get_course_collab_space_config(section.root_section_resource_id)
+ |> Map.from_struct()
+ |> Map.merge(%{status: :enabled})
+
+ %{resource_id: resource_id} = Oli.Publishing.DeliveryResolver.root_container(section.slug)
+
+ Oli.Delivery.Sections.get_section_resource(section.id, resource_id)
+ |> Oli.Delivery.Sections.update_section_resource(%{
+ collab_space_config: course_collab_space_config
+ })
+
+ %{}
+ end
end
diff --git a/test/support/test_helpers.ex b/test/support/test_helpers.ex
index 2253233e5b1..4c06061ef7b 100644
--- a/test/support/test_helpers.ex
+++ b/test/support/test_helpers.ex
@@ -3507,4 +3507,31 @@ defmodule Oli.TestHelpers do
end
### Ends helper that waits for Tasks to complete ###
+
+ @doc """
+ Waits for the given condition to be true. The condition is checked every `interval` milliseconds.
+ """
+ def wait_while(f, opts \\ []) do
+ wait_while_helper(
+ f,
+ Keyword.get(opts, :interval, 100),
+ Keyword.get(opts, :timeout, 5000),
+ :os.system_time(:millisecond)
+ )
+ end
+
+ defp wait_while_helper(f, interval, timeout, start) do
+ if :os.system_time(:millisecond) - start > timeout do
+ throw("Timeout waiting for condition to be true. Timeout: #{timeout} ms.")
+ else
+ case f.() do
+ true ->
+ :timer.sleep(interval)
+ wait_while_helper(f, interval, timeout, start)
+
+ false ->
+ :ok
+ end
+ end
+ end
end
From 2a275b89f265f38d83dedd1b1eb5593e17a60865 Mon Sep 17 00:00:00 2001
From: Eli Knebel
Date: Tue, 2 Apr 2024 15:52:56 -0400
Subject: [PATCH 24/42] add tests
---
.../delivery/student/lesson/annotations.ex | 2 +-
.../delivery/student/lesson_live_test.exs | 106 ++++++++++++++++++
test/support/factory.ex | 10 +-
3 files changed, 116 insertions(+), 2 deletions(-)
diff --git a/lib/oli_web/live/delivery/student/lesson/annotations.ex b/lib/oli_web/live/delivery/student/lesson/annotations.ex
index 286c86a45d4..15603a6a371 100644
--- a/lib/oli_web/live/delivery/student/lesson/annotations.ex
+++ b/lib/oli_web/live/delivery/student/lesson/annotations.ex
@@ -298,7 +298,7 @@ defmodule OliWeb.Delivery.Student.Lesson.Annotations do
defp post(assigns) do
~H"""
-
+
<%= post_creator(@post, @current_user) %>
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 980eda35db1..551c5a18035 100644
--- a/test/oli_web/live/delivery/student/lesson_live_test.exs
+++ b/test/oli_web/live/delivery/student/lesson_live_test.exs
@@ -885,6 +885,112 @@ defmodule OliWeb.Delivery.Student.LessonLiveTest do
"There are no notes yet"
)
end
+
+ test "renders class notes", %{
+ conn: conn,
+ section: section,
+ user: user,
+ page_1: page_1
+ } do
+ Sections.enroll(user.id, section.id, [ContextRoles.get_role(:context_learner)])
+ Sections.mark_section_visited_for_student(section, user)
+
+ # create a class note
+ create_post(user, section, page_1, "This is a class note")
+ create_post(user, section, page_1, "This is another class note")
+
+ {:ok, view, _html} = live(conn, Utils.lesson_live_path(section.slug, page_1.slug))
+
+ view
+ |> element(~s{button[phx-click='toggle_sidebar']})
+ |> render_click
+
+ view
+ |> element(~s{button[phx-click='select_tab'][phx-value-tab='all_notes']})
+ |> render_click
+
+ wait_while(fn -> has_element?(view, "svg.loading") end)
+
+ assert has_element?(
+ view,
+ "div.post",
+ "This is a class note"
+ )
+
+ assert has_element?(
+ view,
+ "div.post",
+ "This is another class note"
+ )
+ end
+
+ test "renders class note with correct number of likes", %{
+ conn: conn,
+ section: section,
+ user: user,
+ page_1: page_1
+ } do
+ Sections.enroll(user.id, section.id, [ContextRoles.get_role(:context_learner)])
+ Sections.mark_section_visited_for_student(section, user)
+
+ # create a class note
+ {:ok, post_1} = create_post(user, section, page_1, "This is a class note")
+ create_post(user, section, page_1, "This is another class note")
+
+ # like the post by 3 different users
+ user2 = insert(:user)
+ user3 = insert(:user)
+
+ react_to_post(post_1, user, :like)
+ react_to_post(post_1, user2, :like)
+ react_to_post(post_1, user3, :like)
+
+ {:ok, view, _html} = live(conn, Utils.lesson_live_path(section.slug, page_1.slug))
+
+ view
+ |> element(~s{button[phx-click='toggle_sidebar']})
+ |> render_click
+
+ view
+ |> element(~s{button[phx-click='select_tab'][phx-value-tab='all_notes']})
+ |> render_click
+
+ wait_while(fn -> has_element?(view, "svg.loading") end)
+
+ like_button_html =
+ element(
+ view,
+ "button[phx-value-reaction='like'][phx-value-post-id='#{post_1.id}']"
+ )
+ |> render()
+
+ # verify number of likes is correct
+ assert like_button_html =~ "3"
+
+ # verify the like button is styled as primary since it was liked by the current user
+ assert like_button_html =~ "
Date: Fri, 5 Apr 2024 06:58:41 -0400
Subject: [PATCH 25/42] add new deployment target (#4730)
---
.github/workflows/deploy.yml | 1 +
1 file changed, 1 insertion(+)
diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml
index fad798522e0..41c30d9eee3 100644
--- a/.github/workflows/deploy.yml
+++ b/.github/workflows/deploy.yml
@@ -13,6 +13,7 @@ on:
- tokamak.oli.cmu.edu
- stellarator.oli.cmu.edu
- heliotron.oli.cmu.edu
+ - neutron.oli.cmu.edu
- loadtest.oli.cmu.edu
- proton.oli.cmu.edu
From a88081f527090d028afd5bee4759ae0e3aafc77a Mon Sep 17 00:00:00 2001
From: Santiago Simoncelli
Date: Fri, 5 Apr 2024 16:30:02 -0300
Subject: [PATCH 26/42] [MER-3004] Clears user information cache when
locking/unlocking
---
lib/oli_web/pow/user_context.ex | 16 +++++++++++++++-
1 file changed, 15 insertions(+), 1 deletion(-)
diff --git a/lib/oli_web/pow/user_context.ex b/lib/oli_web/pow/user_context.ex
index 6c7ee253146..23fb3e62b32 100644
--- a/lib/oli_web/pow/user_context.ex
+++ b/lib/oli_web/pow/user_context.ex
@@ -7,7 +7,7 @@ defmodule OliWeb.Pow.UserContext do
repo: Oli.Repo,
user: Oli.Accounts.User
- alias Oli.Accounts
+ alias Oli.{AccountLookupCache, Accounts}
alias Oli.Accounts.User
alias Oli.Delivery.Sections
alias Oli.Delivery.Sections.Section
@@ -31,6 +31,13 @@ defmodule OliWeb.Pow.UserContext do
user
|> User.lock_changeset()
|> Repo.update()
+ |> case do
+ {:ok, %User{id: user_id}} ->
+ AccountLookupCache.delete("user_#{user_id}")
+
+ error ->
+ error
+ end
end
@spec unlock(map()) :: {:ok, map()} | {:error, map()}
@@ -38,6 +45,13 @@ defmodule OliWeb.Pow.UserContext do
user
|> User.noauth_changeset(%{locked_at: nil})
|> Repo.update()
+ |> case do
+ {:ok, %User{id: user_id}} ->
+ AccountLookupCache.delete("user_#{user_id}")
+
+ error ->
+ error
+ end
end
@doc """
From 42b4439f3eca526806de8839cff5fa38960389f4 Mon Sep 17 00:00:00 2001
From: Santiago Simoncelli
Date: Fri, 5 Apr 2024 16:30:29 -0300
Subject: [PATCH 27/42] [MER-3004] Adds tests
---
.../delivery/open_and_free_index_test.exs | 28 ++++++++++++++++++-
1 file changed, 27 insertions(+), 1 deletion(-)
diff --git a/test/oli_web/live/delivery/open_and_free_index_test.exs b/test/oli_web/live/delivery/open_and_free_index_test.exs
index 84e12ac4fb7..b1e3adddd3d 100644
--- a/test/oli_web/live/delivery/open_and_free_index_test.exs
+++ b/test/oli_web/live/delivery/open_and_free_index_test.exs
@@ -7,8 +7,9 @@ defmodule OliWeb.Delivery.OpenAndFreeIndexTest do
alias Lti_1p3.Tool.ContextRoles
alias Oli.Delivery.Sections
- alias Oli.Seeder
+ alias Oli.{Accounts, Seeder}
alias Oli.Delivery.Attempts.Core
+ alias OliWeb.Pow.UserContext
defp set_progress(section_id, resource_id, user_id, progress, revision) do
{:ok, resource_access} =
@@ -61,6 +62,31 @@ defmodule OliWeb.Delivery.OpenAndFreeIndexTest do
assert has_element?(view, "p", "You are not enrolled in any courses.")
end
+ test "cannot access when user is locked", %{conn: conn, user: user} do
+ UserContext.lock(user)
+
+ {:error,
+ {:redirect,
+ %{
+ to: "/session/new",
+ flash: %{"error" => "Sorry, your account is locked. Please contact support."}
+ }}} = live(conn, ~p"/sections")
+ end
+
+ test "can access when user is unlocked after being locked", %{conn: conn, user: user} do
+ # Lock the user
+ {:ok, date, _timezone} = DateTime.from_iso8601("2019-05-22 20:30:00Z")
+ {:ok, user} = Accounts.update_user(user, %{locked_at: date})
+
+ # Unlock the user
+ UserContext.unlock(user)
+
+ {:ok, view, _html} = live(conn, ~p"/sections")
+
+ assert has_element?(view, "h3", "Courses available")
+ assert has_element?(view, "p", "You are not enrolled in any courses.")
+ end
+
test "renders product title, image and description in sections index with a link to access to it",
%{
conn: conn,
From e96efcc346d001d4877c580364e5fec0229c9e0e Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Nicol=C3=A1s=20Cirio?=
Date: Mon, 8 Apr 2024 10:49:02 -0300
Subject: [PATCH 28/42] get_student_exception_setting_for_all_resources/3
---
lib/oli/delivery/settings.ex | 51 ++++++++++++++++++++++++++++++++++++
1 file changed, 51 insertions(+)
diff --git a/lib/oli/delivery/settings.ex b/lib/oli/delivery/settings.ex
index cea1c99ca8f..0db9a08b67d 100644
--- a/lib/oli/delivery/settings.ex
+++ b/lib/oli/delivery/settings.ex
@@ -46,6 +46,57 @@ defmodule Oli.Delivery.Settings do
end)
end
+ @doc """
+ For a course section id and user id, return a map of resource_id to student exception settings.
+ The third argument allows to specific the field/s to be returned in the map.
+ If no fields are specified, all fields from the Oli.Delivery.Settings.Combined struct are returned.
+
+ If the are no student exception for a specific resource id, that resource id won't be included in the map.
+ (so if there are no student exceptions for any resources, an empty map will be returned)
+
+ Example:
+
+ ```
+ iex> Oli.Delivery.Settings.get_student_exception_setting_for_all_resources(1, 2)
+ %{
+ 22433 => %{
+ max_attempts: nil,
+ password: nil,
+ end_date: ~U[2024-05-25 13:41:00Z],
+ time_limit: 30,
+ collab_space_config: nil,
+ start_date: nil,
+ resource_id: 22433,
+ retake_mode: nil,
+ late_submit: nil,
+ late_start: nil,
+ grace_period: nil,
+ scoring_strategy_id: nil,
+ review_submission: nil,
+ feedback_mode: nil,
+ feedback_scheduled_date: nil,
+ explanation_strategy: nil
+ }
+ }
+
+ iex> Oli.Delivery.Settings.get_student_exception_setting_for_all_resources(1, 2, [:end_date, :time_limit])
+ %{22433 => %{end_date: ~U[2024-05-25 13:41:00Z], time_limit: 30}}
+
+ iex> Oli.Delivery.Settings.get_student_exception_setting_for_all_resources(1, 5)
+ %{}
+ """
+ def get_student_exception_setting_for_all_resources(section_id, user_id, fields \\ nil) do
+ fields =
+ if !fields do
+ %Oli.Delivery.Settings.StudentException{} |> Map.from_struct() |> Map.keys()
+ else
+ fields
+ end
+
+ get_all_student_exceptions(section_id, user_id)
+ |> Enum.reduce(%{}, fn se, acc -> Map.put(acc, se.resource_id, Map.take(se, fields)) end)
+ end
+
defp get_page_resources_with_settings(section_slug) do
page_id = Oli.Resources.ResourceType.id_for_page()
From acc00d9802de96d3cad64e2095d225349e48fcd0 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Nicol=C3=A1s=20Cirio?=
Date: Mon, 8 Apr 2024 13:16:51 -0300
Subject: [PATCH 29/42] async assign student end date exceptions per resource
id - use it for due dates
---
.../live/delivery/student/learn_live.ex | 49 +++++++++++++------
1 file changed, 35 insertions(+), 14 deletions(-)
diff --git a/lib/oli_web/live/delivery/student/learn_live.ex b/lib/oli_web/live/delivery/student/learn_live.ex
index cb05e39cb79..09d21120066 100644
--- a/lib/oli_web/live/delivery/student/learn_live.ex
+++ b/lib/oli_web/live/delivery/student/learn_live.ex
@@ -52,6 +52,7 @@ defmodule OliWeb.Delivery.Student.LearnLive do
active_tab: :learn,
units: get_or_compute_full_hierarchy(section)["children"],
selected_module_per_unit_resource_id: %{},
+ student_end_date_exceptions_per_resource_id: %{},
student_visited_pages: %{},
student_progress_per_resource_id: %{},
student_raw_avg_score_per_page_id: %{},
@@ -469,7 +470,8 @@ defmodule OliWeb.Delivery.Student.LearnLive do
def handle_info(
{:student_metrics_and_enable_slider_buttons,
{student_visited_pages, student_progress_per_resource_id,
- student_raw_avg_score_per_page_id, student_raw_avg_score_per_container_id}},
+ student_raw_avg_score_per_page_id, student_raw_avg_score_per_container_id,
+ student_end_date_exceptions_per_resource_id}},
socket
) do
full_hierarchy = get_or_compute_full_hierarchy(socket.assigns.section)
@@ -478,6 +480,7 @@ defmodule OliWeb.Delivery.Student.LearnLive do
{:noreply,
assign(socket,
+ student_end_date_exceptions_per_resource_id: student_end_date_exceptions_per_resource_id,
student_visited_pages: student_visited_pages,
student_progress_per_resource_id: student_progress_per_resource_id,
student_raw_avg_score_per_page_id: student_raw_avg_score_per_page_id,
@@ -520,6 +523,7 @@ defmodule OliWeb.Delivery.Student.LearnLive do
unit={unit}
ctx={@ctx}
student_progress_per_resource_id={@student_progress_per_resource_id}
+ student_end_date_exceptions_per_resource_id={@student_end_date_exceptions_per_resource_id}
selected_module_per_unit_resource_id={@selected_module_per_unit_resource_id}
student_raw_avg_score_per_page_id={@student_raw_avg_score_per_page_id}
viewed_intro_video_resource_ids={@viewed_intro_video_resource_ids}
@@ -549,6 +553,7 @@ defmodule OliWeb.Delivery.Student.LearnLive do
attr :ctx, :map, doc: "the context is needed to format the date considering the user's timezone"
attr :student_progress_per_resource_id, :map
attr :student_raw_avg_score_per_page_id, :map
+ attr :student_end_date_exceptions_per_resource_id, :map
attr :selected_module_per_unit_resource_id, :map
attr :progress, :integer
attr :student_id, :integer
@@ -812,6 +817,9 @@ defmodule OliWeb.Delivery.Student.LearnLive do
module={module}
student_raw_avg_score_per_page_id={@student_raw_avg_score_per_page_id}
student_progress_per_resource_id={@student_progress_per_resource_id}
+ student_end_date_exceptions_per_resource_id={
+ @student_end_date_exceptions_per_resource_id
+ }
ctx={@ctx}
student_id={@student_id}
intro_video_viewed={
@@ -899,6 +907,7 @@ defmodule OliWeb.Delivery.Student.LearnLive do
attr :module, :map
attr :student_raw_avg_score_per_page_id, :map
+ attr :student_end_date_exceptions_per_resource_id, :map
attr :ctx, :map
attr :student_id, :integer
attr :intro_video_viewed, :boolean
@@ -1041,14 +1050,14 @@ defmodule OliWeb.Delivery.Student.LearnLive do
resource_id={child["resource_id"]}
student_id={@student_id}
ctx={@ctx}
+ student_end_date_exceptions_per_resource_id={@student_end_date_exceptions_per_resource_id}
due_date={
if child["graded"],
do:
get_due_date_for_student(
child["section_resource"].end_date,
child["resource_id"],
- child["section_resource"].section_id,
- @student_id,
+ @student_end_date_exceptions_per_resource_id,
@ctx,
"{WDshort} {Mshort} {D}, {YYYY}"
)
@@ -1081,6 +1090,7 @@ defmodule OliWeb.Delivery.Student.LearnLive do
attr :graded, :boolean
attr :raw_avg_score, :map
attr :student_raw_avg_score_per_page_id, :map
+ attr :student_end_date_exceptions_per_resource_id, :map
attr :student_progress_per_resource_id, :map
attr :due_date, :string
attr :intro_video_viewed, :boolean
@@ -1158,14 +1168,14 @@ defmodule OliWeb.Delivery.Student.LearnLive do
resource_id={child["resource_id"]}
student_id={@student_id}
ctx={@ctx}
+ student_end_date_exceptions_per_resource_id={@student_end_date_exceptions_per_resource_id}
due_date={
if child["graded"],
do:
get_due_date_for_student(
child["section_resource"].end_date,
child["resource_id"],
- child["section_resource"].section_id,
- @student_id,
+ @student_end_date_exceptions_per_resource_id,
@ctx,
"{WDshort} {Mshort} {D}, {YYYY}"
)
@@ -1641,6 +1651,17 @@ defmodule OliWeb.Delivery.Student.LearnLive do
end
defp get_student_metrics(section, current_user_id) do
+ student_end_date_exceptions_per_resource_id =
+ Oli.Delivery.Settings.get_student_exception_setting_for_all_resources(
+ section.id,
+ current_user_id,
+ [:end_date]
+ )
+ |> Enum.reduce(%{}, fn {resource_id, settings}, acc ->
+ Map.put(acc, resource_id, settings[:end_date])
+ end)
+ |> IO.inspect(label: "Aca!!!")
+
visited_pages_map = Sections.get_visited_pages(section.id, current_user_id)
%{"container" => container_ids, "page" => page_ids} =
@@ -1666,7 +1687,7 @@ defmodule OliWeb.Delivery.Student.LearnLive do
|> Map.filter(fn {_, progress} -> progress not in [nil, 0.0] end)
{visited_pages_map, progress_per_resource_id, raw_avg_score_per_page_id,
- raw_avg_score_per_container_id}
+ raw_avg_score_per_container_id, student_end_date_exceptions_per_resource_id}
end
defp mark_visited_and_completed_pages(
@@ -1798,14 +1819,14 @@ defmodule OliWeb.Delivery.Student.LearnLive do
This function returns the end date for a resource considering the student exception (if any)
"""
- defp get_due_date_for_student(end_date, resource_id, section_id, student_id, context, format) do
- case Oli.Delivery.Settings.get_student_exception(resource_id, section_id, student_id) do
- nil ->
- end_date
-
- student_exception ->
- student_exception.end_date
- end
+ defp get_due_date_for_student(
+ end_date,
+ resource_id,
+ student_end_date_exceptions_per_resource_id,
+ context,
+ format
+ ) do
+ Map.get(student_end_date_exceptions_per_resource_id, resource_id, end_date)
|> FormatDateTime.to_formatted_datetime(context, format)
end
From 3911ad1b920ac8a89af130e8a9a80328d605df17 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Nicol=C3=A1s=20Cirio?=
Date: Mon, 8 Apr 2024 13:26:03 -0300
Subject: [PATCH 30/42] split intro_item function component into intro_item and
intro_video_item
---
.../live/delivery/student/learn_live.ex | 111 +++++++++++-------
1 file changed, 68 insertions(+), 43 deletions(-)
diff --git a/lib/oli_web/live/delivery/student/learn_live.ex b/lib/oli_web/live/delivery/student/learn_live.ex
index 09d21120066..6b54348c793 100644
--- a/lib/oli_web/live/delivery/student/learn_live.ex
+++ b/lib/oli_web/live/delivery/student/learn_live.ex
@@ -1012,22 +1012,12 @@ defmodule OliWeb.Delivery.Student.LearnLive do
Introduction and Learning Objectives
- <.index_item
+ <.intro_video_item
:if={module_has_intro_video(@module)}
- title="Introduction"
- type="intro"
- numbering_index={1}
- numbering_level={3}
- was_visited={false}
- graded={@module["graded"]}
duration_minutes={@module["duration_minutes"]}
- revision_slug={@module["slug"]}
module_resource_id={@module["resource_id"]}
- resource_id="intro"
- raw_avg_score={%{}}
video_url={@module["intro_video"]}
intro_video_viewed={@intro_video_viewed}
- progress={0.0}
/>
<.index_item
@@ -1093,7 +1083,6 @@ defmodule OliWeb.Delivery.Student.LearnLive do
attr :student_end_date_exceptions_per_resource_id, :map
attr :student_progress_per_resource_id, :map
attr :due_date, :string
- attr :intro_video_viewed, :boolean
attr :video_url, :string, default: nil
attr :progress, :float
attr :closed_sections, :list, default: []
@@ -1182,7 +1171,6 @@ defmodule OliWeb.Delivery.Student.LearnLive do
}
raw_avg_score={Map.get(@student_raw_avg_score_per_page_id, child["resource_id"])}
student_raw_avg_score_per_page_id={@student_raw_avg_score_per_page_id}
- intro_video_viewed={@intro_video_viewed}
progress={Map.get(@student_progress_per_resource_id, child["resource_id"])}
student_progress_per_resource_id={@student_progress_per_resource_id}
closed_sections={@closed_sections}
@@ -1202,19 +1190,16 @@ defmodule OliWeb.Delivery.Student.LearnLive do
<.index_item_icon
item_type={@type}
was_visited={@was_visited}
- intro_video_viewed={@intro_video_viewed}
graded={@graded}
raw_avg_score={@raw_avg_score[:score]}
progress={@progress}
/>
<.numbering_index type={@type} index={@numbering_index} />
@@ -1224,8 +1209,8 @@ defmodule OliWeb.Delivery.Student.LearnLive do
<%= "#{@title}" %>
@@ -1252,11 +1237,74 @@ defmodule OliWeb.Delivery.Student.LearnLive do
"""
end
+ attr :duration_minutes, :integer
+ attr :module_resource_id, :integer
+ attr :intro_video_viewed, :boolean
+ attr :video_url, :string, default: nil
+
+ def intro_video_item(assigns) do
+ ~H"""
+
+
+
+
+
+
+ Introduction
+
+
+
+
+ <%= parse_minutes(@duration_minutes) %>
+
+ min
+
+
+
+
+
+
+
+ """
+ end
+
attr :item_type, :string
attr :was_visited, :boolean
attr :graded, :boolean
attr :raw_avg_score, :map
- attr :intro_video_viewed, :boolean
attr :progress, :float
def index_item_icon(assigns) do
@@ -1299,29 +1347,6 @@ defmodule OliWeb.Delivery.Student.LearnLive do
"""
-
- {_, "intro", _, _} ->
- # intro video
- ~H"""
-
- """
end
end
From 79333c6d8fabab451cdcbf8c40be61cf80c66ec8 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Nicol=C3=A1s=20Cirio?=
Date: Mon, 8 Apr 2024 15:40:17 -0300
Subject: [PATCH 31/42] group pages by due date
---
.../live/delivery/student/learn_live.ex | 274 ++++++++++++------
1 file changed, 191 insertions(+), 83 deletions(-)
diff --git a/lib/oli_web/live/delivery/student/learn_live.ex b/lib/oli_web/live/delivery/student/learn_live.ex
index 6b54348c793..971ddee54d8 100644
--- a/lib/oli_web/live/delivery/student/learn_live.ex
+++ b/lib/oli_web/live/delivery/student/learn_live.ex
@@ -384,7 +384,11 @@ defmodule OliWeb.Delivery.Student.LearnLive do
def handle_event(
"expand_section",
- %{"resource_id" => resource_id, "module_resource_id" => module_resource_id},
+ %{
+ "resource_id" => resource_id,
+ "module_resource_id" => module_resource_id,
+ "parent_due_date" => parent_due_date
+ },
socket
) do
full_hierarchy = get_or_compute_full_hierarchy(socket.assigns.section)
@@ -397,13 +401,15 @@ defmodule OliWeb.Delivery.Student.LearnLive do
&(&1["resource_id"] == module_resource_id)
)
+ # we concatenate the resource_id with the parent_due_date to create a unique key
+ # and allow the student to expand and collapse same sections grouping pages with different due dates independently
closed_sections =
socket.assigns.display_props_per_module_id
|> get_in([module_resource_id, :closed_sections], [])
|> then(
- &if Enum.member?(&1, resource_id),
- do: List.delete(&1, resource_id),
- else: [resource_id | &1]
+ &if Enum.member?(&1, "#{resource_id}-#{parent_due_date}"),
+ do: List.delete(&1, "#{resource_id}-#{parent_due_date}"),
+ else: ["#{resource_id}-#{parent_due_date}" | &1]
)
display_props_per_module_id =
@@ -581,7 +587,7 @@ defmodule OliWeb.Delivery.Student.LearnLive do
Due:
- <%= FormatDateTime.to_formatted_datetime(
+ <%= format_date(
@unit["section_resource"].end_date,
@ctx,
"{WDshort}, {Mshort} {D}, {YYYY} ({h12}:{m}{am})"
@@ -653,7 +659,7 @@ defmodule OliWeb.Delivery.Student.LearnLive do
Due:
- <%= FormatDateTime.to_formatted_datetime(
+ <%= format_date(
@unit["section_resource"].end_date,
@ctx,
"{WDshort}, {Mshort} {D}, {YYYY} ({h12}:{m}{am})"
@@ -752,7 +758,7 @@ defmodule OliWeb.Delivery.Student.LearnLive do
] %>
- Due: <%= FormatDateTime.to_formatted_datetime(
+ Due: <%= format_date(
Map.get(@selected_module_per_unit_resource_id, @unit["resource_id"])["section_resource"].end_date,
@ctx,
"{WDshort} {Mshort} {D}, {YYYY}"
@@ -915,6 +921,13 @@ defmodule OliWeb.Delivery.Student.LearnLive do
attr :display_props_per_module_id, :map
def module_index(assigns) do
+ show_completed_pages =
+ get_in(
+ assigns.display_props_per_module_id,
+ [assigns.module["resource_id"], :show_completed_pages],
+ true
+ )
+
assigns =
Map.merge(assigns, %{
closed_sections:
@@ -923,12 +936,17 @@ defmodule OliWeb.Delivery.Student.LearnLive do
[assigns.module["resource_id"], :closed_sections],
[]
),
- show_completed_pages:
- get_in(
- assigns.display_props_per_module_id,
- [assigns.module["resource_id"], :show_completed_pages],
- true
+ show_completed_pages: show_completed_pages,
+ page_due_dates:
+ contained_pages_due_dates(
+ assigns.module,
+ assigns.student_end_date_exceptions_per_resource_id,
+ show_completed_pages
)
+ |> Enum.uniq()
+ |> Enum.sort_by(& &1, {:asc, Date})
+
+ # TODO sort null last and ensure to render it as "Not yet scheduled"
})
~H"""
@@ -1019,48 +1037,56 @@ defmodule OliWeb.Delivery.Student.LearnLive do
video_url={@module["intro_video"]}
intro_video_viewed={@intro_video_viewed}
/>
-
- <.index_item
- :for={child <- @module["children"]}
- :if={display_module_item?(@show_completed_pages, child)}
- title={child["title"]}
- type={
- if is_section?(child),
- do: "section",
- else: "page"
- }
- numbering_index={child["numbering"]["index"]}
- numbering_level={child["numbering"]["level"]}
- children={child["children"]}
- was_visited={child["visited"]}
- duration_minutes={child["duration_minutes"]}
- graded={child["graded"]}
- revision_slug={child["slug"]}
- module_resource_id={@module["resource_id"]}
- resource_id={child["resource_id"]}
- student_id={@student_id}
- ctx={@ctx}
- student_end_date_exceptions_per_resource_id={@student_end_date_exceptions_per_resource_id}
- due_date={
- if child["graded"],
- do:
- get_due_date_for_student(
- child["section_resource"].end_date,
- child["resource_id"],
- @student_end_date_exceptions_per_resource_id,
- @ctx,
- "{WDshort} {Mshort} {D}, {YYYY}"
- )
- }
- student_raw_avg_score_per_page_id={@student_raw_avg_score_per_page_id}
- raw_avg_score={Map.get(@student_raw_avg_score_per_page_id, child["resource_id"])}
- intro_video_viewed={@intro_video_viewed}
- video_url={@module["intro_video"]}
- progress={Map.get(@student_progress_per_resource_id, child["resource_id"])}
- student_progress_per_resource_id={@student_progress_per_resource_id}
- closed_sections={@closed_sections}
- show_completed_pages={@show_completed_pages}
- />
+
+
+
+ <%= "Due: #{format_date(grouped_due_date, @ctx, "{WDshort} {Mshort} {D}, {YYYY}")}" %>
+
+
+ <.index_item
+ :for={child <- @module["children"]}
+ :if={
+ display_module_item?(
+ @show_completed_pages,
+ grouped_due_date,
+ @student_end_date_exceptions_per_resource_id,
+ child
+ )
+ }
+ title={child["title"]}
+ type={
+ if is_section?(child),
+ do: "section",
+ else: "page"
+ }
+ numbering_index={child["numbering"]["index"]}
+ numbering_level={child["numbering"]["level"]}
+ children={child["children"]}
+ was_visited={child["visited"]}
+ duration_minutes={child["duration_minutes"]}
+ graded={child["graded"]}
+ revision_slug={child["slug"]}
+ module_resource_id={@module["resource_id"]}
+ resource_id={child["resource_id"]}
+ student_id={@student_id}
+ ctx={@ctx}
+ student_end_date_exceptions_per_resource_id={@student_end_date_exceptions_per_resource_id}
+ parent_due_date={grouped_due_date}
+ due_date={
+ get_due_date_for_student(
+ child["section_resource"].end_date,
+ child["resource_id"],
+ @student_end_date_exceptions_per_resource_id
+ )
+ }
+ student_raw_avg_score_per_page_id={@student_raw_avg_score_per_page_id}
+ raw_avg_score={Map.get(@student_raw_avg_score_per_page_id, child["resource_id"])}
+ progress={Map.get(@student_progress_per_resource_id, child["resource_id"])}
+ student_progress_per_resource_id={@student_progress_per_resource_id}
+ closed_sections={@closed_sections}
+ show_completed_pages={@show_completed_pages}
+ />
+
"""
end
@@ -1082,8 +1108,8 @@ defmodule OliWeb.Delivery.Student.LearnLive do
attr :student_raw_avg_score_per_page_id, :map
attr :student_end_date_exceptions_per_resource_id, :map
attr :student_progress_per_resource_id, :map
- attr :due_date, :string
- attr :video_url, :string, default: nil
+ attr :due_date, Date
+ attr :parent_due_date, Date
attr :progress, :float
attr :closed_sections, :list, default: []
attr :show_completed_pages, :boolean
@@ -1098,18 +1124,25 @@ defmodule OliWeb.Delivery.Student.LearnLive do
~H"""
<.no_icon />
JS.push("expand_section")
}
phx-value-resource_id={@resource_id}
+ phx-value-parent_due_date={@parent_due_date}
phx-value-module_resource_id={@module_resource_id}
class="flex shrink items-center gap-3 w-full dark:text-white cursor-pointer hover:bg-gray-200/70 dark:hover:bg-gray-800"
>
@@ -1131,15 +1165,22 @@ defmodule OliWeb.Delivery.Student.LearnLive do
<.index_item
:for={child <- @children}
- :if={display_module_item?(@show_completed_pages, child)}
+ :if={
+ display_module_item?(
+ @show_completed_pages,
+ @parent_due_date,
+ @student_end_date_exceptions_per_resource_id,
+ child
+ )
+ }
title={child["title"]}
type={
if is_section?(child),
@@ -1158,16 +1199,13 @@ defmodule OliWeb.Delivery.Student.LearnLive do
student_id={@student_id}
ctx={@ctx}
student_end_date_exceptions_per_resource_id={@student_end_date_exceptions_per_resource_id}
+ parent_due_date={@parent_due_date}
due_date={
- if child["graded"],
- do:
- get_due_date_for_student(
- child["section_resource"].end_date,
- child["resource_id"],
- @student_end_date_exceptions_per_resource_id,
- @ctx,
- "{WDshort} {Mshort} {D}, {YYYY}"
- )
+ get_due_date_for_student(
+ child["section_resource"].end_date,
+ child["resource_id"],
+ @student_end_date_exceptions_per_resource_id
+ )
}
raw_avg_score={Map.get(@student_raw_avg_score_per_page_id, child["resource_id"])}
student_raw_avg_score_per_page_id={@student_raw_avg_score_per_page_id}
@@ -1227,7 +1265,7 @@ defmodule OliWeb.Delivery.Student.LearnLive do
- Due: <%= @due_date %>
+ Due: <%= format_date(@due_date, @ctx, "{WDshort} {Mshort} {D}, {YYYY}") %>
@@ -1768,16 +1806,85 @@ defmodule OliWeb.Delivery.Student.LearnLive do
end)
end
- defp display_module_item?(true, _child), do: true
+ # defp display_module_item?(true, grouped_due_date, child), do: true
- defp display_module_item?(show_completed_pages, child) do
+ defp display_module_item?(
+ show_completed_pages,
+ grouped_due_date,
+ student_end_date_exceptions_per_resource_id,
+ child
+ ) do
if is_section?(child) do
- Enum.any?(child["children"], &display_module_item?(show_completed_pages, &1))
+ Enum.any?(
+ child["children"],
+ &display_module_item?(
+ show_completed_pages,
+ grouped_due_date,
+ student_end_date_exceptions_per_resource_id,
+ &1
+ )
+ )
else
- !child["completed"]
+ # this due date considers the student exception (if any)
+ student_due_date =
+ Map.get(
+ student_end_date_exceptions_per_resource_id,
+ child["resource_id"],
+ child["section_resource"].end_date
+ )
+ |> DateTime.to_date()
+
+ if show_completed_pages do
+ student_due_date == grouped_due_date
+ else
+ !child["completed"] and student_due_date == grouped_due_date
+ end
end
end
+ # In-class Activities should not appear in the course content in the learn page (but only in the schedule) so we can ignore those.
+ # As for 'read by' (lessons) and 'due date' (graded assignments) we assumed that we could group both together and treat the Read By Date as a general Due Date
+ defp contained_pages_due_dates(
+ container,
+ student_end_date_exceptions_per_resource_id,
+ show_completed_pages
+ ) do
+ page_type_id = Oli.Resources.ResourceType.get_id_by_type("page")
+ container_type_id = Oli.Resources.ResourceType.get_id_by_type("container")
+
+ Enum.flat_map(container["children"], fn
+ %{
+ "resource_type_id" => ^page_type_id,
+ "completed" => completed,
+ "section_resource" => %{
+ scheduling_type: scheduling_type,
+ end_date: end_date,
+ resource_id: resource_id
+ }
+ }
+ when scheduling_type in [:due_by, :read_by] ->
+ if completed and !show_completed_pages do
+ []
+ else
+ [
+ DateTime.to_date(
+ Map.get(student_end_date_exceptions_per_resource_id, resource_id, end_date)
+ )
+ ]
+ end
+
+ %{"resource_type_id" => ^container_type_id} = section_or_subsection ->
+ contained_pages_due_dates(
+ section_or_subsection,
+ student_end_date_exceptions_per_resource_id,
+ show_completed_pages
+ )
+
+ _ ->
+ []
+ end)
+ end
+
defp progress_started(student_progress_per_resource_id, resource_id) do
Map.get(student_progress_per_resource_id, resource_id, 0.0) > 0.0
end
@@ -1847,12 +1954,13 @@ defmodule OliWeb.Delivery.Student.LearnLive do
defp get_due_date_for_student(
end_date,
resource_id,
- student_end_date_exceptions_per_resource_id,
- context,
- format
+ student_end_date_exceptions_per_resource_id
) do
Map.get(student_end_date_exceptions_per_resource_id, resource_id, end_date)
- |> FormatDateTime.to_formatted_datetime(context, format)
+ end
+
+ defp format_date(due_date, context, format) do
+ FormatDateTime.to_formatted_datetime(due_date, context, format)
end
defp get_viewed_intro_video_resource_ids(section_slug, current_user_id) do
@@ -2053,7 +2161,7 @@ defmodule OliWeb.Delivery.Student.LearnLive do
end
end
- defp maybe_hidden_section(closed_sections, resource_id) do
- if Enum.member?(closed_sections, resource_id), do: "hidden", else: ""
+ defp maybe_hidden_section(closed_sections, resource_id, parent_due_date) do
+ if Enum.member?(closed_sections, "#{resource_id}-#{parent_due_date}"), do: "hidden", else: ""
end
end
From 55041a28e577e841d05561be0237108f39fb2bfb Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Nicol=C3=A1s=20Cirio?=
Date: Mon, 8 Apr 2024 16:12:47 -0300
Subject: [PATCH 32/42] sort null last and ensure to render it as 'Not yet
scheduled'
---
.../live/delivery/student/learn_live.ex | 40 ++++++++++++++-----
1 file changed, 31 insertions(+), 9 deletions(-)
diff --git a/lib/oli_web/live/delivery/student/learn_live.ex b/lib/oli_web/live/delivery/student/learn_live.ex
index 971ddee54d8..5ca8b280d04 100644
--- a/lib/oli_web/live/delivery/student/learn_live.ex
+++ b/lib/oli_web/live/delivery/student/learn_live.ex
@@ -938,15 +938,11 @@ defmodule OliWeb.Delivery.Student.LearnLive do
),
show_completed_pages: show_completed_pages,
page_due_dates:
- contained_pages_due_dates(
+ get_contained_pages_due_dates(
assigns.module,
assigns.student_end_date_exceptions_per_resource_id,
show_completed_pages
)
- |> Enum.uniq()
- |> Enum.sort_by(& &1, {:asc, Date})
-
- # TODO sort null last and ensure to render it as "Not yet scheduled"
})
~H"""
@@ -1832,7 +1828,7 @@ defmodule OliWeb.Delivery.Student.LearnLive do
child["resource_id"],
child["section_resource"].end_date
)
- |> DateTime.to_date()
+ |> then(&if is_nil(&1), do: "Not yet scheduled", else: DateTime.to_date(&1))
if show_completed_pages do
student_due_date == grouped_due_date
@@ -1844,6 +1840,29 @@ defmodule OliWeb.Delivery.Student.LearnLive do
# In-class Activities should not appear in the course content in the learn page (but only in the schedule) so we can ignore those.
# As for 'read by' (lessons) and 'due date' (graded assignments) we assumed that we could group both together and treat the Read By Date as a general Due Date
+ defp get_contained_pages_due_dates(
+ container,
+ student_end_date_exceptions_per_resource_id,
+ show_completed_pages
+ ) do
+ contained_pages_due_dates(
+ container,
+ student_end_date_exceptions_per_resource_id,
+ show_completed_pages
+ )
+ |> Enum.uniq()
+ |> then(fn dates ->
+ if nil in dates do
+ dates
+ |> Enum.reject(&is_nil/1)
+ |> Enum.sort_by(& &1, {:asc, Date})
+ |> Enum.concat(["Not yet scheduled"])
+ else
+ Enum.sort_by(dates, & &1, {:asc, Date})
+ end
+ end)
+ end
+
defp contained_pages_due_dates(
container,
student_end_date_exceptions_per_resource_id,
@@ -1867,9 +1886,10 @@ defmodule OliWeb.Delivery.Student.LearnLive do
[]
else
[
- DateTime.to_date(
- Map.get(student_end_date_exceptions_per_resource_id, resource_id, end_date)
- )
+ Map.get(student_end_date_exceptions_per_resource_id, resource_id, end_date) &&
+ DateTime.to_date(
+ Map.get(student_end_date_exceptions_per_resource_id, resource_id, end_date)
+ )
]
end
@@ -1959,6 +1979,8 @@ defmodule OliWeb.Delivery.Student.LearnLive do
Map.get(student_end_date_exceptions_per_resource_id, resource_id, end_date)
end
+ defp format_date("Not yet scheduled", _context, _format), do: "Not yet scheduled"
+
defp format_date(due_date, context, format) do
FormatDateTime.to_formatted_datetime(due_date, context, format)
end
From a1af40a64fb08951d28bf27d463e7dcc185b0d30 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Nicol=C3=A1s=20Cirio?=
Date: Mon, 8 Apr 2024 16:18:55 -0300
Subject: [PATCH 33/42] fix gap in module index
---
lib/oli_web/live/delivery/student/learn_live.ex | 7 +++----
1 file changed, 3 insertions(+), 4 deletions(-)
diff --git a/lib/oli_web/live/delivery/student/learn_live.ex b/lib/oli_web/live/delivery/student/learn_live.ex
index 5ca8b280d04..0a8aa68a75e 100644
--- a/lib/oli_web/live/delivery/student/learn_live.ex
+++ b/lib/oli_web/live/delivery/student/learn_live.ex
@@ -948,7 +948,7 @@ defmodule OliWeb.Delivery.Student.LearnLive do
~H"""
-
+
<%= "Due: #{format_date(grouped_due_date, @ctx, "{WDshort} {Mshort} {D}, {YYYY}")}" %>
@@ -1163,7 +1163,7 @@ defmodule OliWeb.Delivery.Student.LearnLive do
@@ -1719,7 +1719,6 @@ defmodule OliWeb.Delivery.Student.LearnLive do
|> Enum.reduce(%{}, fn {resource_id, settings}, acc ->
Map.put(acc, resource_id, settings[:end_date])
end)
- |> IO.inspect(label: "Aca!!!")
visited_pages_map = Sections.get_visited_pages(section.id, current_user_id)
From d5bbe4bdd536e046f6b0142a91981483da972797 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Nicol=C3=A1s=20Cirio?=
Date: Mon, 8 Apr 2024 16:48:19 -0300
Subject: [PATCH 34/42] fix failing tests
---
.../live/delivery/student/learn_live_test.exs | 33 +++++++++++++------
1 file changed, 23 insertions(+), 10 deletions(-)
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 e3e89459e2e..1b47c81f6e6 100644
--- a/test/oli_web/live/delivery/student/learn_live_test.exs
+++ b/test/oli_web/live/delivery/student/learn_live_test.exs
@@ -428,6 +428,19 @@ defmodule OliWeb.Delivery.Student.ContentLiveTest do
end_date: ~U[2023-11-03 20:00:00Z]
})
+ # schedule start and end date for page 11 and 12 section_resource
+ Sections.get_section_resource(section.id, page_11_revision.resource_id)
+ |> Sections.update_section_resource(%{
+ start_date: ~U[2023-11-02 20:00:00Z],
+ end_date: ~U[2023-11-03 20:00:00Z]
+ })
+
+ Sections.get_section_resource(section.id, page_12_revision.resource_id)
+ |> Sections.update_section_resource(%{
+ start_date: ~U[2023-11-02 20:00:00Z],
+ end_date: ~U[2023-11-03 20:00:00Z]
+ })
+
%{
author: author,
section: section,
@@ -841,12 +854,12 @@ defmodule OliWeb.Delivery.Student.ContentLiveTest do
|> render_click()
assert view
- |> element(~s{div[role="intro 1 details"]})
+ |> element(~s{div[role="intro video details"]})
|> render() =~ "Introduction"
assert has_element?(
view,
- ~s{div[role="intro 1 details"] div[role="unseen video icon"]}
+ ~s{div[role="intro video details"] div[role="unseen video icon"]}
)
# module 2 has no intro video
@@ -854,7 +867,7 @@ defmodule OliWeb.Delivery.Student.ContentLiveTest do
|> element(~s{div[role="unit_1"] div[role="card_2"]})
|> render_click()
- refute has_element?(view, ~s{div[role="intro 1 details"] div[role="unseen video icon"]})
+ refute has_element?(view, ~s{div[role="intro video details"] div[role="unseen video icon"]})
end
test "intro video is marked as seen after playing it",
@@ -880,11 +893,11 @@ defmodule OliWeb.Delivery.Student.ContentLiveTest do
assert has_element?(
view,
- ~s{div[role="intro 1 details"] div[role="unseen video icon"]}
+ ~s{div[role="intro video details"] div[role="unseen video icon"]}
)
view
- |> element(~s{div[role="intro 1 details"] div[phx-click="play_video"]})
+ |> element(~s{div[role="intro video details"] div[phx-click="play_video"]})
|> render_click()
# since the video is marked as seen in an async way, we revisit the page to check if the icon changed
@@ -901,7 +914,7 @@ defmodule OliWeb.Delivery.Student.ContentLiveTest do
assert has_element?(
view,
- ~s{div[role="intro 1 details"] div[role="seen video icon"]}
+ ~s{div[role="intro video details"] div[role="seen video icon"]}
)
end
@@ -1270,7 +1283,7 @@ defmodule OliWeb.Delivery.Student.ContentLiveTest do
assert has_element?(
view,
- "#index_item_1_#{section_1.resource_id}"
+ "#index_item_1_#{section_1.resource_id}_2023-11-03"
)
end
@@ -1807,13 +1820,13 @@ defmodule OliWeb.Delivery.Student.ContentLiveTest do
section_1_element =
element(
view,
- "#index_item_1_#{section_1.resource_id}"
+ "#index_item_1_#{section_1.resource_id}_2023-11-03"
)
subsection_1_element =
element(
view,
- "#index_item_1_#{subsection_1.resource_id}"
+ "#index_item_1_#{subsection_1.resource_id}_2023-11-03"
)
assert render(section_1_element) =~ "Why Elixir?"
@@ -1840,7 +1853,7 @@ defmodule OliWeb.Delivery.Student.ContentLiveTest do
assert has_element?(
view,
- "div.hidden #index_item_1_#{subsection_1.resource_id}"
+ "div.hidden #index_item_1_#{subsection_1.resource_id}_2023-11-03"
)
end
end
From 8e937bfaee0c534ca0bf93867855bc5760ab5ba4 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Nicol=C3=A1s=20Cirio?=
Date: Tue, 9 Apr 2024 08:24:56 -0300
Subject: [PATCH 35/42] add tests for learn_live
---
.../live/delivery/student/learn_live.ex | 6 ++-
.../live/delivery/student/learn_live_test.exs | 51 ++++++++++++++++++-
2 files changed, 55 insertions(+), 2 deletions(-)
diff --git a/lib/oli_web/live/delivery/student/learn_live.ex b/lib/oli_web/live/delivery/student/learn_live.ex
index 0a8aa68a75e..8d46bbe0eb7 100644
--- a/lib/oli_web/live/delivery/student/learn_live.ex
+++ b/lib/oli_web/live/delivery/student/learn_live.ex
@@ -1033,7 +1033,11 @@ defmodule OliWeb.Delivery.Student.LearnLive do
video_url={@module["intro_video"]}
intro_video_viewed={@intro_video_viewed}
/>
-
+
<%= "Due: #{format_date(grouped_due_date, @ctx, "{WDshort} {Mshort} {D}, {YYYY}")}" %>
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 1b47c81f6e6..4d72765f7f5 100644
--- a/test/oli_web/live/delivery/student/learn_live_test.exs
+++ b/test/oli_web/live/delivery/student/learn_live_test.exs
@@ -208,12 +208,30 @@ defmodule OliWeb.Delivery.Student.ContentLiveTest do
duration_minutes: 15
)
+ page_13_revision =
+ insert(:revision,
+ resource_type_id: ResourceType.get_id_by_type("page"),
+ title: "Page 13",
+ duration_minutes: 15
+ )
+
+ page_14_revision =
+ insert(:revision,
+ resource_type_id: ResourceType.get_id_by_type("page"),
+ title: "Page 14",
+ duration_minutes: 15
+ )
+
# sections and sub-sections...
subsection_1_revision =
insert(:revision, %{
resource_type_id: Oli.Resources.ResourceType.get_id_by_type("container"),
- children: [page_11_revision.resource_id],
+ children: [
+ page_11_revision.resource_id,
+ page_13_revision.resource_id,
+ page_14_revision.resource_id
+ ],
title: "Erlang as a motivation"
})
@@ -357,6 +375,8 @@ defmodule OliWeb.Delivery.Student.ContentLiveTest do
top_level_page_revision,
page_11_revision,
page_12_revision,
+ page_13_revision,
+ page_14_revision,
section_1_revision,
subsection_1_revision,
module_1_revision,
@@ -1856,6 +1876,35 @@ defmodule OliWeb.Delivery.Student.ContentLiveTest do
"div.hidden #index_item_1_#{subsection_1.resource_id}_2023-11-03"
)
end
+
+ test "groups pages within a module index by due date (even if some pages do not yet have a scheduled date)",
+ %{
+ conn: conn,
+ user: user,
+ section: section
+ } 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, Utils.learn_live_path(section.slug))
+
+ view
+ |> element(~s{div[role="unit_5"] div[role="card_4"]})
+ |> render_click()
+
+ group_by_due_date_div = element(view, ~s{div[id="pages_grouped_by_2023-11-03"]})
+
+ group_by_not_yet_scheduled_div =
+ element(view, ~s{div[id="pages_grouped_by_Not yet scheduled"]})
+
+ assert render(group_by_due_date_div) =~ "Due: Fri Nov 3, 2023"
+ assert render(group_by_due_date_div) =~ "Page 11"
+ assert render(group_by_due_date_div) =~ "Page 12"
+
+ assert render(group_by_not_yet_scheduled_div) =~ "Due: Not yet scheduled"
+ assert render(group_by_not_yet_scheduled_div) =~ "Page 13"
+ assert render(group_by_not_yet_scheduled_div) =~ "Page 14"
+ end
end
describe "sidebar menu" do
From bf8ad430ddb6e7819c5b6ea5930408cf1af8efc1 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Nicol=C3=A1s=20Cirio?=
Date: Tue, 9 Apr 2024 08:25:16 -0300
Subject: [PATCH 36/42] refactor and test
get_student_exception_setting_for_all_resources/3
---
lib/oli/delivery/settings.ex | 18 ++++++----
test/oli/delivery/settings_test.exs | 54 +++++++++++++++++++++++++++++
2 files changed, 65 insertions(+), 7 deletions(-)
diff --git a/lib/oli/delivery/settings.ex b/lib/oli/delivery/settings.ex
index 0db9a08b67d..49adee66f30 100644
--- a/lib/oli/delivery/settings.ex
+++ b/lib/oli/delivery/settings.ex
@@ -85,14 +85,18 @@ defmodule Oli.Delivery.Settings do
iex> Oli.Delivery.Settings.get_student_exception_setting_for_all_resources(1, 5)
%{}
"""
- def get_student_exception_setting_for_all_resources(section_id, user_id, fields \\ nil) do
- fields =
- if !fields do
- %Oli.Delivery.Settings.StudentException{} |> Map.from_struct() |> Map.keys()
- else
- fields
- end
+ def get_student_exception_setting_for_all_resources(section_id, user_id, fields \\ nil)
+
+ def get_student_exception_setting_for_all_resources(section_id, user_id, nil) do
+ fields = %Oli.Delivery.Settings.StudentException{} |> Map.from_struct() |> Map.keys()
+
+ get_all_student_exceptions(section_id, user_id)
+ |> Enum.reduce(%{}, fn se, acc -> Map.put(acc, se.resource_id, Map.take(se, fields)) end)
+ end
+
+ def get_student_exception_setting_for_all_resources(section_id, user_id, fields)
+ when is_list(fields) do
get_all_student_exceptions(section_id, user_id)
|> Enum.reduce(%{}, fn se, acc -> Map.put(acc, se.resource_id, Map.take(se, fields)) end)
end
diff --git a/test/oli/delivery/settings_test.exs b/test/oli/delivery/settings_test.exs
index 95a59a921e5..a14dcda5b96 100644
--- a/test/oli/delivery/settings_test.exs
+++ b/test/oli/delivery/settings_test.exs
@@ -364,4 +364,58 @@ defmodule Oli.Delivery.SettingsTest do
}} =
Settings.update_student_exception(student_exception, %{end_date: nil}, [:end_date])
end
+
+ test "get_student_exception_setting_for_all_resources/3 returns the student exception setting for all resources" do
+ section = insert(:section)
+ student = insert(:user)
+
+ student_exception =
+ insert(:student_exception, %{
+ max_attempts: 10,
+ end_date: ~U[2024-01-10 00:00:00Z],
+ section: section,
+ user: student
+ })
+
+ student_exception_2 =
+ insert(:student_exception, %{
+ max_attempts: 12,
+ end_date: ~U[2024-01-10 00:00:00Z],
+ section: section,
+ user: student
+ })
+
+ # returns all fields
+ result =
+ Settings.get_student_exception_setting_for_all_resources(
+ student_exception.section_id,
+ student_exception.user_id
+ )
+
+ assert result |> Map.get(student_exception.resource_id) |> Map.keys() ==
+ %Oli.Delivery.Settings.StudentException{} |> Map.from_struct() |> Map.keys()
+
+ assert result[student_exception.resource_id].max_attempts == 10
+ assert result[student_exception_2.resource_id].max_attempts == 12
+
+ # only returns the requested fields
+ assert %{
+ student_exception.resource_id => %{max_attempts: 10},
+ student_exception_2.resource_id => %{max_attempts: 12}
+ } ==
+ Settings.get_student_exception_setting_for_all_resources(
+ student_exception.section_id,
+ student_exception.user_id,
+ [:max_attempts]
+ )
+
+ # returns an empty map if there are no exceptions for that section and user
+ section_with_no_exceptions = insert(:section)
+
+ assert %{} ==
+ Settings.get_student_exception_setting_for_all_resources(
+ section_with_no_exceptions.id,
+ student.id
+ )
+ end
end
From bfb049780b13e50567f9066efca34b01363829dc Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Nicol=C3=A1s=20Cirio?=
Date: Tue, 9 Apr 2024 09:27:48 -0300
Subject: [PATCH 37/42] add more tests
---
.../live/delivery/student/learn_live.ex | 8 +-
.../live/delivery/student/learn_live_test.exs | 75 ++++++++++++++++++-
2 files changed, 81 insertions(+), 2 deletions(-)
diff --git a/lib/oli_web/live/delivery/student/learn_live.ex b/lib/oli_web/live/delivery/student/learn_live.ex
index 8d46bbe0eb7..e5dd8e6de5a 100644
--- a/lib/oli_web/live/delivery/student/learn_live.ex
+++ b/lib/oli_web/live/delivery/student/learn_live.ex
@@ -1805,7 +1805,13 @@ defmodule OliWeb.Delivery.Student.LearnLive do
end)
end
- # defp display_module_item?(true, grouped_due_date, child), do: true
+ defp display_module_item?(
+ _show_completed_pages,
+ _grouped_due_date,
+ _student_end_date_exceptions_per_resource_id,
+ %{"section_resource" => %{scheduling_type: :inclass_activity}} = _child
+ ),
+ do: false
defp display_module_item?(
show_completed_pages,
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 4d72765f7f5..8d853771eb1 100644
--- a/test/oli_web/live/delivery/student/learn_live_test.exs
+++ b/test/oli_web/live/delivery/student/learn_live_test.exs
@@ -222,6 +222,13 @@ defmodule OliWeb.Delivery.Student.ContentLiveTest do
duration_minutes: 15
)
+ page_15_revision =
+ insert(:revision,
+ resource_type_id: ResourceType.get_id_by_type("page"),
+ title: "Page 15 - in class activity",
+ duration_minutes: 15
+ )
+
# sections and sub-sections...
subsection_1_revision =
@@ -230,7 +237,8 @@ defmodule OliWeb.Delivery.Student.ContentLiveTest do
children: [
page_11_revision.resource_id,
page_13_revision.resource_id,
- page_14_revision.resource_id
+ page_14_revision.resource_id,
+ page_15_revision.resource_id
],
title: "Erlang as a motivation"
})
@@ -377,6 +385,7 @@ defmodule OliWeb.Delivery.Student.ContentLiveTest do
page_12_revision,
page_13_revision,
page_14_revision,
+ page_15_revision,
section_1_revision,
subsection_1_revision,
module_1_revision,
@@ -461,6 +470,17 @@ defmodule OliWeb.Delivery.Student.ContentLiveTest do
end_date: ~U[2023-11-03 20:00:00Z]
})
+ # set page 15 to in class activity and page 14 to due by
+ Sections.get_section_resource(section.id, page_15_revision.resource_id)
+ |> Sections.update_section_resource(%{
+ scheduling_type: :inclass_activity
+ })
+
+ Sections.get_section_resource(section.id, page_14_revision.resource_id)
+ |> Sections.update_section_resource(%{
+ scheduling_type: :due_by
+ })
+
%{
author: author,
section: section,
@@ -481,6 +501,7 @@ defmodule OliWeb.Delivery.Student.ContentLiveTest do
top_level_page: top_level_page_revision,
page_11: page_11_revision,
page_12: page_12_revision,
+ page_13: page_13_revision,
section_1: section_1_revision,
subsection_1: subsection_1_revision,
module_1: module_1_revision,
@@ -1905,6 +1926,58 @@ defmodule OliWeb.Delivery.Student.ContentLiveTest do
assert render(group_by_not_yet_scheduled_div) =~ "Page 13"
assert render(group_by_not_yet_scheduled_div) =~ "Page 14"
end
+
+ test "considers student exceptions when grouping pages in index by due date", %{
+ conn: conn,
+ user: user,
+ section: section,
+ page_13: page_13
+ } do
+ Sections.enroll(user.id, section.id, [ContextRoles.get_role(:context_learner)])
+ Sections.mark_section_visited_for_student(section, user)
+
+ # add a student exception for page 13
+ insert(:student_exception, %{
+ section: section,
+ user: user,
+ resource: page_13.resource,
+ end_date: ~U[2023-11-10 00:00:00Z]
+ })
+
+ {:ok, view, _html} = live(conn, Utils.learn_live_path(section.slug))
+
+ # when the slider buttons are enabled we know the student async metrics (including student exceptions) were loaded
+ assert_receive({_ref, {:push_event, "enable-slider-buttons", _}}, 2_000)
+
+ view
+ |> element(~s{div[role="unit_5"] div[role="card_4"]})
+ |> render_click()
+
+ group_by_due_date_div = element(view, ~s{div[id="pages_grouped_by_2023-11-10"]})
+
+ # page 13 is due on Nov 10, 2023 as defined in the student exception
+ assert render(group_by_due_date_div) =~ "Due: Fri Nov 10, 2023"
+ assert render(group_by_due_date_div) =~ "Page 13"
+ end
+
+ test "in class activities pages are not listed in the module index", %{
+ conn: conn,
+ user: user,
+ section: section
+ } 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, Utils.learn_live_path(section.slug))
+
+ view
+ |> element(~s{div[role="unit_5"] div[role="card_4"]})
+ |> render_click()
+
+ assert render(view) =~ "Page 13"
+ assert render(view) =~ "Page 14"
+ refute render(view) =~ "Page 15"
+ end
end
describe "sidebar menu" do
From 60e03fa2375dc3e0d17368d8f17b607c7daf1f7f Mon Sep 17 00:00:00 2001
From: tomasferok
Date: Tue, 9 Apr 2024 10:55:17 -0300
Subject: [PATCH 38/42] [MER-3048] replace jsonb_path_match for
jsonb_path_exists and remove unnecessary brackets
---
lib/oli/activities/realizer/query/builder.ex | 6 +++---
1 file changed, 3 insertions(+), 3 deletions(-)
diff --git a/lib/oli/activities/realizer/query/builder.ex b/lib/oli/activities/realizer/query/builder.ex
index b4be1f9cecc..88082baa511 100644
--- a/lib/oli/activities/realizer/query/builder.ex
+++ b/lib/oli/activities/realizer/query/builder.ex
@@ -169,7 +169,7 @@ defmodule Oli.Activities.Realizer.Query.Builder do
:does_not_equal ->
[
- "(NOT ((objectives_count = #{length(value)} AND (#{build_objectives_conjunction(value)})))"
+ "NOT (objectives_count = #{length(value)} AND #{build_objectives_conjunction(value)})"
]
end
@@ -206,7 +206,7 @@ defmodule Oli.Activities.Realizer.Query.Builder do
Enum.map(objective_ids, fn id -> "@ == #{id}" end)
|> Enum.join(" || ")
- "jsonb_path_match(objectives, 'exists($.** ? (#{id_filter}))')"
+ "jsonb_path_exists(objectives, '$.** ? (#{id_filter})')"
end
defp build_objectives_conjunction(objective_ids) do
@@ -214,6 +214,6 @@ defmodule Oli.Activities.Realizer.Query.Builder do
Enum.map(objective_ids, fn id -> build_objectives_disjunction([id]) end)
|> Enum.join(" AND ")
- "(#{clauses})"
+ "#{clauses}"
end
end
From f58b2763de673b097707708e25a2b516cf1e0365 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Nicol=C3=A1s=20Cirio?=
Date: Tue, 9 Apr 2024 12:57:44 -0300
Subject: [PATCH 39/42] fix failing tests
---
test/oli_web/live/delivery/student/learn_live_test.exs | 5 ++++-
1 file changed, 4 insertions(+), 1 deletion(-)
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 8d853771eb1..49878fcb637 100644
--- a/test/oli_web/live/delivery/student/learn_live_test.exs
+++ b/test/oli_web/live/delivery/student/learn_live_test.exs
@@ -1526,6 +1526,9 @@ defmodule OliWeb.Delivery.Student.ContentLiveTest do
{:ok, view, _html} = live(conn, Utils.learn_live_path(section.slug))
+ # when the slider buttons are enabled we know the student async metrics (including progress) were loaded
+ assert_receive({_ref, {:push_event, "enable-slider-buttons", _}}, 2_000)
+
# Progress in module 1 (which has page 2)
assert has_element?(view, ~s{div[role="unit_1"] div[role="card_1_progress"]})
@@ -1590,7 +1593,7 @@ defmodule OliWeb.Delivery.Student.ContentLiveTest do
|> element(
~s{div[id="top_level_page_#{top_level_page.resource_id}"] div[role="header"]}
)
- |> render() =~ "PAGE 14"
+ |> render() =~ "PAGE 17"
assert view
|> element(
From 2a1137a0f1c3fd7e13b9063b45d56537d0445ce0 Mon Sep 17 00:00:00 2001
From: Francisco-Castro
Date: Tue, 9 Apr 2024 16:07:22 -0400
Subject: [PATCH 40/42] [MER-3007] Add Generate Reset Password Link UI
---
lib/oli_web/live/users/user_actions.ex | 37 ++++++++++++++++++++++
lib/oli_web/live/users/user_detail_view.ex | 15 +++++++--
2 files changed, 50 insertions(+), 2 deletions(-)
diff --git a/lib/oli_web/live/users/user_actions.ex b/lib/oli_web/live/users/user_actions.ex
index 1c61ab9d33e..acb14772e38 100644
--- a/lib/oli_web/live/users/user_actions.ex
+++ b/lib/oli_web/live/users/user_actions.ex
@@ -7,6 +7,7 @@ defmodule OliWeb.Users.Actions do
attr(:for_author, :boolean, default: false)
attr(:user, :any, required: true)
attr(:csrf_token, :any, required: true)
+ attr(:password_reset_link, :string, default: "")
def render(assigns) do
{resend, reset} =
@@ -26,6 +27,42 @@ defmodule OliWeb.Users.Actions do
~H"""
+
+
<%= if @user.independent_learner do %>
-
+
<% else %>
No actions available
LTI users are managed by their LMS
@@ -225,6 +230,12 @@ defmodule OliWeb.Users.UsersDetailView do
{:noreply, socket}
end
+ def handle_event("generate_reset_password_link", params, socket) do
+ password_reset_link = OliWeb.PowController.create_user_password_reset_link(params)
+ socket = assign(socket, password_reset_link: password_reset_link)
+ {:noreply, socket}
+ end
+
def handle_event("show_confirm_email_modal", _, socket) do
modal_assigns = %{
user: socket.assigns.user
From 0bd17bf4158e417b93a0ad40945f246a68c36726 Mon Sep 17 00:00:00 2001
From: Francisco-Castro
Date: Tue, 9 Apr 2024 16:07:57 -0400
Subject: [PATCH 41/42] [MER-3007] Add logic to generate a reset password link
---
lib/oli_web/controllers/pow.ex | 39 ++++++++++++++++++++++++++++++++--
1 file changed, 37 insertions(+), 2 deletions(-)
diff --git a/lib/oli_web/controllers/pow.ex b/lib/oli_web/controllers/pow.ex
index efd6295984a..2971336739d 100644
--- a/lib/oli_web/controllers/pow.ex
+++ b/lib/oli_web/controllers/pow.ex
@@ -1,9 +1,32 @@
defmodule OliWeb.PowController do
use OliWeb, :controller
- alias Oli.Repo
- alias Oli.Accounts.User
alias Oli.Accounts.Author
+ alias Oli.Accounts.User
+ alias Pow.Phoenix.Controller
+ alias Pow.Phoenix.Routes, as: PowRoutes
+ alias PowResetPassword.Phoenix.ResetPasswordController
+ alias Oli.Repo
+
+ @secret_key_base Application.compile_env(:oli, OliWeb.Endpoint)[:secret_key_base]
+ @basic_conn_for_pow %Plug.Conn{
+ private: %{phoenix_router: OliWeb.Router, phoenix_endpoint: OliWeb.Endpoint, otp_app: :oli},
+ secret_key_base: @secret_key_base
+ }
+
+ @ttl :timer.minutes(24 * 60)
+ @cache_config {PowResetPassword.Store.ResetTokenCache, ttl: @ttl}
+
+ def create_user_password_reset_link(%{"id" => id}) do
+ %{email: email} = _user = Repo.get(User, id)
+ params = %{"user" => %{"email" => email}}
+
+ @basic_conn_for_pow
+ |> use_pow_config(:user)
+ |> put_reset_password_token_store_into_pow_config()
+ |> ResetPasswordController.process_create(params)
+ |> generate_password_reset_url()
+ end
def send_user_password_reset_link(conn, %{"id" => id}) do
user = Repo.get(User, id)
@@ -66,6 +89,18 @@ defmodule OliWeb.PowController do
|> redirect(to: Routes.live_path(conn, OliWeb.Users.AuthorsDetailView, author.id))
end
+ defp generate_password_reset_url({:ok, %{token: token, user: _user}, conn}) do
+ Controller.routes(conn, PowRoutes).url_for(conn, ResetPasswordController, :edit, [token])
+ end
+
+ defp put_reset_password_token_store_into_pow_config(conn) do
+ update_in(
+ conn,
+ [Access.key(:private), Access.key(:pow_config)],
+ &Keyword.put(&1, :reset_password_token_store, @cache_config)
+ )
+ end
+
defp resend_user_confirmation_email(conn, user) do
PowEmailConfirmation.Phoenix.ControllerCallbacks.send_confirmation_email(user, conn)
conn
From dbeecb6f2781b961f727c30e3781c3f383a60628 Mon Sep 17 00:00:00 2001
From: Francisco-Castro
Date: Wed, 10 Apr 2024 13:02:51 -0400
Subject: [PATCH 42/42] [MER-3007] Apply Darren's feedback and change naming
hmtl attributes to match with the context
---
lib/oli_web/live/users/user_actions.ex | 9 ++++-----
1 file changed, 4 insertions(+), 5 deletions(-)
diff --git a/lib/oli_web/live/users/user_actions.ex b/lib/oli_web/live/users/user_actions.ex
index acb14772e38..36f018663c0 100644
--- a/lib/oli_web/live/users/user_actions.ex
+++ b/lib/oli_web/live/users/user_actions.ex
@@ -33,17 +33,16 @@ defmodule OliWeb.Users.Actions do
Copy