Skip to content

Commit c745f37

Browse files
committed
Version 2.3.1
1 parent 5941705 commit c745f37

28 files changed

+926
-665
lines changed

CHANGELOG.md

+11
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,14 @@
1+
### v.2.3.1
2+
3+
### Fixes and improvements
4+
5+
- Improve performance of presentation to load slides faster
6+
- Fix manager layout on small screens
7+
- Add clickable hyperlinks in messages
8+
- Improve quiz export
9+
- Add option to force login to submit quizzes
10+
- Fix url with question mark being flagged as a question
11+
112
### v.2.3.0
213

314
### Features

assets/js/manager.js

+6-7
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ export class Manager {
55
this.context = context;
66
this.currentPage = parseInt(context.el.dataset.currentPage);
77
this.maxPage = parseInt(context.el.dataset.maxPage);
8+
9+
localStorage.setItem("slide-position", this.currentPage);
810
}
911

1012
init() {
@@ -31,19 +33,14 @@ export class Manager {
3133

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

3637
switch (e.key) {
37-
case "ArrowUp":
38-
this.prevPage();
39-
break;
4038
case "ArrowLeft":
39+
e.preventDefault();
4140
this.prevPage();
4241
break;
4342
case "ArrowRight":
44-
this.nextPage();
45-
break;
46-
case "ArrowDown":
43+
e.preventDefault();
4744
this.nextPage();
4845
break;
4946
}
@@ -168,6 +165,7 @@ export class Manager {
168165
if (this.currentPage == this.maxPage - 1) return;
169166

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

179177
this.currentPage -= 1;
178+
localStorage.setItem("slide-position", this.currentPage);
180179
this.context.pushEventTo(this.context.el, "current-page", {
181180
page: this.currentPage.toString(),
182181
});

assets/js/presenter.js

+19-11
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ export class Presenter {
1919
controls: false,
2020
swipeAngle: false,
2121
startIndex: this.currentPage,
22+
speed: 0,
2223
loop: false,
2324
nav: false,
2425
});
@@ -29,8 +30,13 @@ export class Presenter {
2930

3031
this.context.handleEvent("page", (data) => {
3132
//set current page
33+
if (this.currentPage == data.current_page) {
34+
return;
35+
}
36+
3237
this.currentPage = parseInt(data.current_page);
3338
this.slider.goTo(data.current_page);
39+
3440
});
3541

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

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

108113
switch (e.key) {
109114
case "f": // F
115+
e.preventDefault();
110116
this.fullscreen();
111117
break;
112-
case "ArrowUp":
113-
window.opener.dispatchEvent(
114-
new KeyboardEvent("keydown", { key: "ArrowUp" })
115-
);
116-
break;
117118
case "ArrowLeft":
119+
e.preventDefault();
118120
window.opener.dispatchEvent(
119121
new KeyboardEvent("keydown", { key: "ArrowLeft" })
120122
);
121123
break;
122124
case "ArrowRight":
125+
e.preventDefault();
123126
window.opener.dispatchEvent(
124127
new KeyboardEvent("keydown", { key: "ArrowRight" })
125128
);
126129
break;
127-
case "ArrowDown":
128-
window.opener.dispatchEvent(
129-
new KeyboardEvent("keydown", { key: "ArrowDown" })
130-
);
131-
break;
132130
}
133131
}
134132
});
133+
134+
window.addEventListener("storage", (e) => {
135+
console.log(e)
136+
if (e.key == "slide-position") {
137+
console.log("settings new value " + Date.now())
138+
this.currentPage = parseInt(e.newValue);
139+
this.slider.goTo(e.newValue);
140+
141+
}
142+
})
135143
}
136144

137145
update() {

lib/claper/events.ex

+1-1
Original file line numberDiff line numberDiff line change
@@ -123,7 +123,7 @@ defmodule Claper.Events do
123123
query =
124124
from(e in Event,
125125
where: e.user_id == ^user_id and not is_nil(e.expired_at),
126-
order_by: [desc: e.inserted_at]
126+
order_by: [desc: e.expired_at]
127127
)
128128

129129
Repo.paginate(query, page: page, page_size: page_size, preload: preload)

lib/claper/quizzes.ex

+20
Original file line numberDiff line numberDiff line change
@@ -443,6 +443,26 @@ defmodule Claper.Quizzes do
443443
end
444444
end
445445

446+
@doc """
447+
Get number of submissions for a given quiz_id
448+
449+
## Examples
450+
451+
iex> get_number_submissions(quiz_id)
452+
12
453+
454+
"""
455+
def get_submission_count(quiz_id) do
456+
from(r in QuizResponse,
457+
where: r.quiz_id == ^quiz_id,
458+
select:
459+
count(
460+
fragment("DISTINCT COALESCE(?, CAST(? AS varchar))", r.attendee_identifier, r.user_id)
461+
)
462+
)
463+
|> Repo.one()
464+
end
465+
446466
@doc """
447467
Calculate percentage of all quiz questions for a given quiz.
448468

lib/claper/quizzes/quiz.ex

+3-1
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,8 @@ defmodule Claper.Quizzes.Quiz do
66
field :title, :string
77
field :position, :integer, default: 0
88
field :enabled, :boolean, default: false
9-
field :show_results, :boolean, default: false
9+
field :show_results, :boolean, default: true
10+
field :allow_anonymous, :boolean, default: false
1011
field :lti_line_item_url, :string
1112

1213
belongs_to :presentation_file, Claper.Presentations.PresentationFile
@@ -30,6 +31,7 @@ defmodule Claper.Quizzes.Quiz do
3031
:presentation_file_id,
3132
:enabled,
3233
:show_results,
34+
:allow_anonymous,
3335
:lti_resource_id,
3436
:lti_line_item_url
3537
])

lib/claper_web/controllers/stat_controller.ex

+72-28
Original file line numberDiff line numberDiff line change
@@ -86,43 +86,87 @@ defmodule ClaperWeb.StatController do
8686
with quiz <-
8787
Quizzes.get_quiz!(quiz_id, [
8888
:quiz_questions,
89+
:quiz_responses,
8990
quiz_questions: :quiz_question_opts,
91+
quiz_responses: [:quiz_question_opt, :user],
9092
presentation_file: :event
9193
]),
9294
event <- quiz.presentation_file.event,
9395
:ok <- authorize_event_access(current_user, event) do
94-
# Create headers for the CSV
95-
headers = ["Question", "Correct Answers", "Total Responses", "Response Distribution (%)"]
96-
97-
# Format data rows
98-
data =
99-
quiz.quiz_questions
100-
|> Enum.map(fn question ->
101-
[
102-
question.content,
103-
# Correct answers
104-
question.quiz_question_opts
105-
|> Enum.filter(& &1.is_correct)
106-
|> Enum.map_join(", ", & &1.content),
107-
# Total responses
108-
question.quiz_question_opts
109-
|> Enum.map(& &1.response_count)
110-
|> Enum.sum()
111-
|> to_string(),
112-
# Response distribution
113-
question.quiz_question_opts
114-
|> Enum.map_join(", ", fn opt ->
115-
"#{opt.content}: #{opt.percentage}%"
116-
end)
117-
]
118-
end)
96+
questions = quiz.quiz_questions
97+
headers = build_quiz_headers(questions)
11998

120-
export_as_csv(conn, headers, data, "quiz-#{sanitize(quiz.title)}")
121-
else
122-
:unauthorized -> send_resp(conn, 403, "Forbidden")
99+
# Group responses by user/attendee and question
100+
responses_by_user =
101+
Enum.group_by(
102+
quiz.quiz_responses,
103+
fn response -> response.user_id || response.attendee_identifier end
104+
)
105+
106+
# Format data rows - one row per user with their answers and score
107+
data = Enum.map(responses_by_user, &process_user_responses(&1, questions))
108+
109+
csv_content =
110+
CSV.encode([headers | data])
111+
|> Enum.to_list()
112+
|> to_string()
113+
114+
send_download(conn, {:binary, csv_content},
115+
filename: "quiz_#{quiz.id}_results.csv",
116+
content_type: "text/csv"
117+
)
123118
end
124119
end
125120

121+
defp build_quiz_headers(questions) do
122+
question_headers =
123+
questions
124+
|> Enum.with_index(1)
125+
|> Enum.map(fn {question, _index} -> question.content end)
126+
127+
["Attendee identifier", "User email"] ++ question_headers ++ ["Total"]
128+
end
129+
130+
defp process_user_responses({_user_id, responses}, questions) do
131+
user_identifier = format_attendee_identifier(List.first(responses).attendee_identifier)
132+
user_email = Map.get(List.first(responses).user || %{}, :email, "N/A")
133+
responses_by_question = Enum.group_by(responses, & &1.quiz_question_id)
134+
135+
answers_with_correctness = process_question_responses(questions, responses_by_question)
136+
answers = Enum.map(answers_with_correctness, fn {answer, _} -> answer || "" end)
137+
correct_count = Enum.count(answers_with_correctness, fn {_, correct} -> correct end)
138+
total = "#{correct_count}/#{length(questions)}"
139+
140+
[user_identifier, user_email] ++ answers ++ [total]
141+
end
142+
143+
defp process_question_responses(questions, responses_by_question) do
144+
Enum.map(questions, fn question ->
145+
question_responses = Map.get(responses_by_question, question.id, [])
146+
147+
correct_opt_ids =
148+
question.quiz_question_opts
149+
|> Enum.filter(& &1.is_correct)
150+
|> Enum.map(& &1.id)
151+
|> MapSet.new()
152+
153+
format_question_response(question_responses, correct_opt_ids)
154+
end)
155+
end
156+
157+
defp format_question_response([], _correct_opt_ids), do: {nil, false}
158+
159+
defp format_question_response(question_responses, correct_opt_ids) do
160+
answers = Enum.map(question_responses, & &1.quiz_question_opt.content)
161+
162+
all_correct =
163+
Enum.all?(question_responses, fn r ->
164+
MapSet.member?(correct_opt_ids, r.quiz_question_opt_id)
165+
end)
166+
167+
{Enum.join(answers, ", "), all_correct}
168+
end
169+
126170
@doc """
127171
Exports quiz as QTI format.
128172
Requires user to be either an event leader or the event owner.

lib/claper_web/helpers.ex

+22
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
defmodule ClaperWeb.Helpers do
2+
def format_body(body) do
3+
url_regex = ~r/(https?:\/\/[^\s]+)/
4+
5+
body
6+
|> String.split(url_regex, include_captures: true)
7+
|> Enum.map(fn
8+
"http" <> _rest = url ->
9+
Phoenix.HTML.raw(
10+
~s(<a href="#{url}" target="_blank" class="cursor-pointer text-primary-500 hover:underline font-medium">#{url}</a>)
11+
)
12+
13+
text ->
14+
text
15+
end)
16+
end
17+
18+
def body_without_links(text) do
19+
url_regex = ~r/(https?:\/\/[^\s]+)/
20+
String.replace(text, url_regex, "")
21+
end
22+
end

lib/claper_web/live/event_live/event_card_component.ex

+7-2
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,14 @@ defmodule ClaperWeb.EventLive.EventCardComponent do
1414
<div class="px-4 py-4 sm:px-6">
1515
<div class="flex items-center justify-between">
1616
<div class="flex items-center">
17-
<p class="text-lg font-medium text-primary-600 truncate">
17+
<a
18+
data-phx-link="patch"
19+
data-phx-link-state="push"
20+
class="text-lg font-medium text-primary-600 truncate"
21+
href={~p"/e/#{@event.code}/manage"}
22+
>
1823
<%= @event.name %>
19-
</p>
24+
</a>
2025
<p
2126
:if={@event.lti_resource}
2227
class="text-xs text-white rounded-md px-2 py-0.5 bg-gray-500 mx-2 flex items-center space-x-1"

lib/claper_web/live/event_live/event_form_component.html.heex

+1-1
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
data-finish-label={gettext("Finish")}
88
data-group="create-event"
99
>
10-
<div id="product-tour-btn-form" class="hidden absolute bottom-5 right-5 z-30">
10+
<div id="product-tour-btn-form" class="hidden fixed bottom-5 right-5 z-30">
1111
<button class="close absolute -top-1.5 -right-1.5 bg-red-500 text-white rounded-full">
1212
<svg
1313
xmlns="http://www.w3.org/2000/svg"

lib/claper_web/live/event_live/index.html.heex

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
<div class="mx-3 md:max-w-3xl lg:max-w-5xl md:mx-auto">
2-
<div id="product-tour-btn" class="hidden absolute bottom-5 right-5 z-30">
2+
<div id="product-tour-btn" class="hidden fixed bottom-5 right-5 z-30">
33
<button class="close absolute -top-1.5 -right-1.5 bg-red-500 text-white rounded-full">
44
<svg
55
xmlns="http://www.w3.org/2000/svg"

lib/claper_web/live/event_live/manage.ex

+3-2
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,7 @@ defmodule ClaperWeb.EventLive.Manage do
9292
|> stream_insert(:posts, post)
9393
|> update(:post_count, fn post_count -> post_count + 1 end)
9494

95-
case post.body =~ "?" do
95+
case ClaperWeb.Helpers.body_without_links(post.body) =~ "?" do
9696
true ->
9797
{:noreply,
9898
socket
@@ -130,7 +130,7 @@ defmodule ClaperWeb.EventLive.Manage do
130130
end)
131131
|> update(:post_count, fn post_count -> post_count - 1 end)
132132

133-
case deleted_post.body =~ "?" do
133+
case ClaperWeb.Helpers.body_without_links(deleted_post.body) =~ "?" do
134134
true ->
135135
{:noreply,
136136
socket
@@ -920,6 +920,7 @@ defmodule ClaperWeb.EventLive.Manage do
920920

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

925926
defp list_form_submits(_socket, presentation_file_id) do

0 commit comments

Comments
 (0)