Skip to content

Commit

Permalink
Version 2.3.1
Browse files Browse the repository at this point in the history
  • Loading branch information
alxlion committed Jan 3, 2025
1 parent 5941705 commit c745f37
Show file tree
Hide file tree
Showing 28 changed files with 926 additions and 665 deletions.
11 changes: 11 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,14 @@
### v.2.3.1

### Fixes and improvements

- Improve performance of presentation to load slides faster
- Fix manager layout on small screens
- Add clickable hyperlinks in messages
- Improve quiz export
- Add option to force login to submit quizzes
- Fix url with question mark being flagged as a question

### v.2.3.0

### Features
Expand Down
13 changes: 6 additions & 7 deletions assets/js/manager.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ export class Manager {
this.context = context;
this.currentPage = parseInt(context.el.dataset.currentPage);
this.maxPage = parseInt(context.el.dataset.maxPage);

localStorage.setItem("slide-position", this.currentPage);
}

init() {
Expand All @@ -31,19 +33,14 @@ export class Manager {

window.addEventListener("keydown", (e) => {
if ((e.target.tagName || "").toLowerCase() != "input") {
e.preventDefault();

switch (e.key) {
case "ArrowUp":
this.prevPage();
break;
case "ArrowLeft":
e.preventDefault();
this.prevPage();
break;
case "ArrowRight":
this.nextPage();
break;
case "ArrowDown":
e.preventDefault();
this.nextPage();
break;
}
Expand Down Expand Up @@ -168,6 +165,7 @@ export class Manager {
if (this.currentPage == this.maxPage - 1) return;

this.currentPage += 1;
localStorage.setItem("slide-position", this.currentPage);
this.context.pushEventTo(this.context.el, "current-page", {
page: this.currentPage.toString(),
});
Expand All @@ -177,6 +175,7 @@ export class Manager {
if (this.currentPage == 0) return;

this.currentPage -= 1;
localStorage.setItem("slide-position", this.currentPage);
this.context.pushEventTo(this.context.el, "current-page", {
page: this.currentPage.toString(),
});
Expand Down
30 changes: 19 additions & 11 deletions assets/js/presenter.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ export class Presenter {
controls: false,
swipeAngle: false,
startIndex: this.currentPage,
speed: 0,
loop: false,
nav: false,
});
Expand All @@ -29,8 +30,13 @@ export class Presenter {

this.context.handleEvent("page", (data) => {
//set current page
if (this.currentPage == data.current_page) {
return;
}

this.currentPage = parseInt(data.current_page);
this.slider.goTo(data.current_page);

});

this.context.handleEvent("chat-visible", (data) => {
Expand Down Expand Up @@ -103,35 +109,37 @@ export class Presenter {

window.addEventListener("keyup", (e) => {
if (e.target.tagName.toLowerCase() != "input") {
e.preventDefault();

switch (e.key) {
case "f": // F
e.preventDefault();
this.fullscreen();
break;
case "ArrowUp":
window.opener.dispatchEvent(
new KeyboardEvent("keydown", { key: "ArrowUp" })
);
break;
case "ArrowLeft":
e.preventDefault();
window.opener.dispatchEvent(
new KeyboardEvent("keydown", { key: "ArrowLeft" })
);
break;
case "ArrowRight":
e.preventDefault();
window.opener.dispatchEvent(
new KeyboardEvent("keydown", { key: "ArrowRight" })
);
break;
case "ArrowDown":
window.opener.dispatchEvent(
new KeyboardEvent("keydown", { key: "ArrowDown" })
);
break;
}
}
});

window.addEventListener("storage", (e) => {
console.log(e)
if (e.key == "slide-position") {
console.log("settings new value " + Date.now())
this.currentPage = parseInt(e.newValue);
this.slider.goTo(e.newValue);

}
})
}

update() {
Expand Down
2 changes: 1 addition & 1 deletion lib/claper/events.ex
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,7 @@ defmodule Claper.Events do
query =
from(e in Event,
where: e.user_id == ^user_id and not is_nil(e.expired_at),
order_by: [desc: e.inserted_at]
order_by: [desc: e.expired_at]
)

Repo.paginate(query, page: page, page_size: page_size, preload: preload)
Expand Down
20 changes: 20 additions & 0 deletions lib/claper/quizzes.ex
Original file line number Diff line number Diff line change
Expand Up @@ -443,6 +443,26 @@ defmodule Claper.Quizzes do
end
end

@doc """
Get number of submissions for a given quiz_id
## Examples
iex> get_number_submissions(quiz_id)
12
"""
def get_submission_count(quiz_id) do
from(r in QuizResponse,
where: r.quiz_id == ^quiz_id,
select:
count(
fragment("DISTINCT COALESCE(?, CAST(? AS varchar))", r.attendee_identifier, r.user_id)
)
)
|> Repo.one()
end

@doc """
Calculate percentage of all quiz questions for a given quiz.
Expand Down
4 changes: 3 additions & 1 deletion lib/claper/quizzes/quiz.ex
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@ defmodule Claper.Quizzes.Quiz do
field :title, :string
field :position, :integer, default: 0
field :enabled, :boolean, default: false
field :show_results, :boolean, default: false
field :show_results, :boolean, default: true
field :allow_anonymous, :boolean, default: false
field :lti_line_item_url, :string

belongs_to :presentation_file, Claper.Presentations.PresentationFile
Expand All @@ -30,6 +31,7 @@ defmodule Claper.Quizzes.Quiz do
:presentation_file_id,
:enabled,
:show_results,
:allow_anonymous,
:lti_resource_id,
:lti_line_item_url
])
Expand Down
100 changes: 72 additions & 28 deletions lib/claper_web/controllers/stat_controller.ex
Original file line number Diff line number Diff line change
Expand Up @@ -86,43 +86,87 @@ defmodule ClaperWeb.StatController do
with quiz <-
Quizzes.get_quiz!(quiz_id, [
:quiz_questions,
:quiz_responses,
quiz_questions: :quiz_question_opts,
quiz_responses: [:quiz_question_opt, :user],
presentation_file: :event
]),
event <- quiz.presentation_file.event,
:ok <- authorize_event_access(current_user, event) do
# Create headers for the CSV
headers = ["Question", "Correct Answers", "Total Responses", "Response Distribution (%)"]

# Format data rows
data =
quiz.quiz_questions
|> Enum.map(fn question ->
[
question.content,
# Correct answers
question.quiz_question_opts
|> Enum.filter(& &1.is_correct)
|> Enum.map_join(", ", & &1.content),
# Total responses
question.quiz_question_opts
|> Enum.map(& &1.response_count)
|> Enum.sum()
|> to_string(),
# Response distribution
question.quiz_question_opts
|> Enum.map_join(", ", fn opt ->
"#{opt.content}: #{opt.percentage}%"
end)
]
end)
questions = quiz.quiz_questions
headers = build_quiz_headers(questions)

export_as_csv(conn, headers, data, "quiz-#{sanitize(quiz.title)}")
else
:unauthorized -> send_resp(conn, 403, "Forbidden")
# Group responses by user/attendee and question
responses_by_user =
Enum.group_by(
quiz.quiz_responses,
fn response -> response.user_id || response.attendee_identifier end
)

# Format data rows - one row per user with their answers and score
data = Enum.map(responses_by_user, &process_user_responses(&1, questions))

csv_content =
CSV.encode([headers | data])
|> Enum.to_list()
|> to_string()

send_download(conn, {:binary, csv_content},
filename: "quiz_#{quiz.id}_results.csv",
content_type: "text/csv"
)
end
end

defp build_quiz_headers(questions) do
question_headers =
questions
|> Enum.with_index(1)
|> Enum.map(fn {question, _index} -> question.content end)

["Attendee identifier", "User email"] ++ question_headers ++ ["Total"]
end

defp process_user_responses({_user_id, responses}, questions) do
user_identifier = format_attendee_identifier(List.first(responses).attendee_identifier)
user_email = Map.get(List.first(responses).user || %{}, :email, "N/A")
responses_by_question = Enum.group_by(responses, & &1.quiz_question_id)

answers_with_correctness = process_question_responses(questions, responses_by_question)
answers = Enum.map(answers_with_correctness, fn {answer, _} -> answer || "" end)
correct_count = Enum.count(answers_with_correctness, fn {_, correct} -> correct end)
total = "#{correct_count}/#{length(questions)}"

[user_identifier, user_email] ++ answers ++ [total]
end

defp process_question_responses(questions, responses_by_question) do
Enum.map(questions, fn question ->
question_responses = Map.get(responses_by_question, question.id, [])

correct_opt_ids =
question.quiz_question_opts
|> Enum.filter(& &1.is_correct)
|> Enum.map(& &1.id)
|> MapSet.new()

format_question_response(question_responses, correct_opt_ids)
end)
end

defp format_question_response([], _correct_opt_ids), do: {nil, false}

defp format_question_response(question_responses, correct_opt_ids) do
answers = Enum.map(question_responses, & &1.quiz_question_opt.content)

all_correct =
Enum.all?(question_responses, fn r ->
MapSet.member?(correct_opt_ids, r.quiz_question_opt_id)
end)

{Enum.join(answers, ", "), all_correct}
end

@doc """
Exports quiz as QTI format.
Requires user to be either an event leader or the event owner.
Expand Down
22 changes: 22 additions & 0 deletions lib/claper_web/helpers.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
defmodule ClaperWeb.Helpers do
def format_body(body) do
url_regex = ~r/(https?:\/\/[^\s]+)/

body
|> String.split(url_regex, include_captures: true)
|> Enum.map(fn
"http" <> _rest = url ->
Phoenix.HTML.raw(
~s(<a href="#{url}" target="_blank" class="cursor-pointer text-primary-500 hover:underline font-medium">#{url}</a>)
)

text ->
text
end)
end

def body_without_links(text) do
url_regex = ~r/(https?:\/\/[^\s]+)/
String.replace(text, url_regex, "")
end
end
9 changes: 7 additions & 2 deletions lib/claper_web/live/event_live/event_card_component.ex
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,14 @@ defmodule ClaperWeb.EventLive.EventCardComponent do
<div class="px-4 py-4 sm:px-6">
<div class="flex items-center justify-between">
<div class="flex items-center">
<p class="text-lg font-medium text-primary-600 truncate">
<a
data-phx-link="patch"
data-phx-link-state="push"
class="text-lg font-medium text-primary-600 truncate"
href={~p"/e/#{@event.code}/manage"}
>
<%= @event.name %>
</p>
</a>
<p
:if={@event.lti_resource}
class="text-xs text-white rounded-md px-2 py-0.5 bg-gray-500 mx-2 flex items-center space-x-1"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
data-finish-label={gettext("Finish")}
data-group="create-event"
>
<div id="product-tour-btn-form" class="hidden absolute bottom-5 right-5 z-30">
<div id="product-tour-btn-form" class="hidden fixed bottom-5 right-5 z-30">
<button class="close absolute -top-1.5 -right-1.5 bg-red-500 text-white rounded-full">
<svg
xmlns="http://www.w3.org/2000/svg"
Expand Down
2 changes: 1 addition & 1 deletion lib/claper_web/live/event_live/index.html.heex
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<div class="mx-3 md:max-w-3xl lg:max-w-5xl md:mx-auto">
<div id="product-tour-btn" class="hidden absolute bottom-5 right-5 z-30">
<div id="product-tour-btn" class="hidden fixed bottom-5 right-5 z-30">
<button class="close absolute -top-1.5 -right-1.5 bg-red-500 text-white rounded-full">
<svg
xmlns="http://www.w3.org/2000/svg"
Expand Down
5 changes: 3 additions & 2 deletions lib/claper_web/live/event_live/manage.ex
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ defmodule ClaperWeb.EventLive.Manage do
|> stream_insert(:posts, post)
|> update(:post_count, fn post_count -> post_count + 1 end)

case post.body =~ "?" do
case ClaperWeb.Helpers.body_without_links(post.body) =~ "?" do
true ->
{:noreply,
socket
Expand Down Expand Up @@ -130,7 +130,7 @@ defmodule ClaperWeb.EventLive.Manage do
end)
|> update(:post_count, fn post_count -> post_count - 1 end)

case deleted_post.body =~ "?" do
case ClaperWeb.Helpers.body_without_links(deleted_post.body) =~ "?" do
true ->
{:noreply,
socket
Expand Down Expand Up @@ -920,6 +920,7 @@ defmodule ClaperWeb.EventLive.Manage do

defp list_all_questions(_socket, event_id, sort \\ "date") do
Claper.Posts.list_questions(event_id, [:event, :reactions], String.to_atom(sort))
|> Enum.filter(&(ClaperWeb.Helpers.body_without_links(&1.body) =~ "?"))
end

defp list_form_submits(_socket, presentation_file_id) do
Expand Down
Loading

0 comments on commit c745f37

Please sign in to comment.