Fix duplicate key quiz when duplicate (#182)

* add quiz_responses association to user

* bugfix possible duplicate key entries in multi when adding quiz responses

* remove user_id from casting changeset in QuizResponse

* pass whole user to submit_quiz function

* update test to match changes

* simplify submit_quiz/3 function for inserting quiz response

---------

Co-authored-by: Dimitrije Dimitrijevic <me@dimitrijedimitrijevic.com>
This commit is contained in:
Dimitrije Dimitrijevic
2025-11-04 18:46:29 +01:00
committed by GitHub
parent fc667bb478
commit 16bcce1a60
5 changed files with 56 additions and 20 deletions

View File

@@ -30,6 +30,7 @@ defmodule Claper.Accounts.User do
has_many :events, Claper.Events.Event has_many :events, Claper.Events.Event
has_one :lti_user, Lti13.Users.User has_one :lti_user, Lti13.Users.User
has_many :quiz_responses, Claper.Quizzes.QuizResponse
timestamps() timestamps()
end end

View File

@@ -2,6 +2,8 @@ defmodule Claper.Quizzes do
import Ecto.Query, warn: false import Ecto.Query, warn: false
alias Claper.Repo alias Claper.Repo
alias Claper.Accounts.User
alias Claper.Quizzes.Quiz alias Claper.Quizzes.Quiz
alias Claper.Quizzes.QuizQuestion alias Claper.Quizzes.QuizQuestion
alias Claper.Quizzes.QuizQuestionOpt alias Claper.Quizzes.QuizQuestionOpt
@@ -265,28 +267,62 @@ defmodule Claper.Quizzes do
{:ok, quiz} {:ok, quiz}
""" """
def submit_quiz(user_id, quiz_opts, quiz_id)
when is_number(user_id) and is_list(quiz_opts) do # Pattern match on user, from user we create QuizResponse struct
case Enum.reduce(quiz_opts, Ecto.Multi.new(), fn opt, multi -> # def submit_quiz(user_id, quiz_opts, quiz_id)
Ecto.Multi.update( # when is_number(user_id) and is_list(quiz_opts) do
multi, # quiz_opts = Enum.with_index(quiz_opts)
{:update_quiz_opt, opt.id},
# case Enum.reduce(quiz_opts, Ecto.Multi.new(), fn {opt, index}, multi ->
# unique_key = "#{opt.id}_#{user_id + index}"
# multi
# |> Ecto.Multi.update(
# "update_quiz_opt_#{unique_key}",
# QuizQuestionOpt.changeset(opt, %{"response_count" => opt.response_count + 1})
# )
# |> Ecto.Multi.insert(
# "insert_quiz_response_#{unique_key}",
# QuizResponse.changeset(%QuizResponse{}, %{
# user_id: user_id,
# quiz_question_opt_id: opt.id,
# quiz_question_id: opt.quiz_question_id,
# quiz_id: quiz_id
# })
# )
# end)
# |> Repo.transact() do
# {:ok, _} ->
# quiz = get_quiz!(quiz_id, [:quiz_questions, quiz_questions: :quiz_question_opts])
# Lti13.QuizScoreReporter.report_quiz_score(quiz, user_id)
# {:ok, quiz}
# end
# end
def submit_quiz(%User{} = user, quiz_opts, quiz_id) do
quiz_opts = Enum.with_index(quiz_opts)
case Enum.reduce(quiz_opts, Ecto.Multi.new(), fn {opt, index}, multi ->
unique_key = "#{opt.id}_#{user.id + index}"
multi
|> Ecto.Multi.update(
"update_quiz_opt_#{unique_key}",
QuizQuestionOpt.changeset(opt, %{"response_count" => opt.response_count + 1}) QuizQuestionOpt.changeset(opt, %{"response_count" => opt.response_count + 1})
) )
|> Ecto.Multi.insert( |> Ecto.Multi.insert(
{:insert_quiz_response, opt.id}, "insert_quiz_response_#{unique_key}",
QuizResponse.changeset(%QuizResponse{}, %{ Ecto.build_assoc(user, :quiz_responses, %{
user_id: user_id,
quiz_question_opt_id: opt.id, quiz_question_opt_id: opt.id,
quiz_question_id: opt.quiz_question_id, quiz_question_id: opt.quiz_question_id,
quiz_id: quiz_id quiz_id: quiz_id
}) })
) )
end) end)
|> Repo.transaction() do |> Repo.transact() do
{:ok, _} -> {:ok, _} ->
quiz = get_quiz!(quiz_id, [:quiz_questions, quiz_questions: :quiz_question_opts]) quiz = get_quiz!(quiz_id, [:quiz_questions, quiz_questions: :quiz_question_opts])
Lti13.QuizScoreReporter.report_quiz_score(quiz, user_id) Lti13.QuizScoreReporter.report_quiz_score(quiz, user.id)
{:ok, quiz} {:ok, quiz}
end end
end end
@@ -294,8 +330,8 @@ defmodule Claper.Quizzes do
def submit_quiz(attendee_identifier, quiz_opts, quiz_id) def submit_quiz(attendee_identifier, quiz_opts, quiz_id)
when is_binary(attendee_identifier) and is_list(quiz_opts) do when is_binary(attendee_identifier) and is_list(quiz_opts) do
case Enum.reduce(quiz_opts, Ecto.Multi.new(), fn opt, multi -> case Enum.reduce(quiz_opts, Ecto.Multi.new(), fn opt, multi ->
Ecto.Multi.update( multi
multi, |> Ecto.Multi.update(
{:update_quiz_opt, opt.id}, {:update_quiz_opt, opt.id},
QuizQuestionOpt.changeset(opt, %{"response_count" => opt.response_count + 1}) QuizQuestionOpt.changeset(opt, %{"response_count" => opt.response_count + 1})
) )
@@ -309,7 +345,7 @@ defmodule Claper.Quizzes do
}) })
) )
end) end)
|> Repo.transaction() do |> Repo.transact() do
{:ok, _} -> {:ok, _} ->
quiz = get_quiz!(quiz_id, [:quiz_questions, quiz_questions: :quiz_question_opts]) quiz = get_quiz!(quiz_id, [:quiz_questions, quiz_questions: :quiz_question_opts])
{:ok, quiz} {:ok, quiz}

View File

@@ -20,8 +20,7 @@ defmodule Claper.Quizzes.QuizResponse do
:attendee_identifier, :attendee_identifier,
:quiz_id, :quiz_id,
:quiz_question_id, :quiz_question_id,
:quiz_question_opt_id, :quiz_question_opt_id
:user_id
]) ])
|> validate_required([:quiz_id, :quiz_question_id, :quiz_question_opt_id]) |> validate_required([:quiz_id, :quiz_question_id, :quiz_question_opt_id])
end end

View File

@@ -693,7 +693,7 @@ defmodule ClaperWeb.EventLive.Show do
) )
when is_map(current_user) do when is_map(current_user) do
case Claper.Quizzes.submit_quiz( case Claper.Quizzes.submit_quiz(
current_user.id, current_user,
opts, opts,
socket.assigns.current_interaction.id socket.assigns.current_interaction.id
) do ) do

View File

@@ -113,14 +113,14 @@ defmodule Claper.QuizzesTest do
assert length(new_question.quiz_question_opts) == 2 assert length(new_question.quiz_question_opts) == 2
end end
test "submit_quiz/3 with user_id records responses and updates counts" do test "submit_quiz/3 with user records responses and updates counts" do
quiz = quiz_fixture() quiz = quiz_fixture()
user = user_fixture() user = user_fixture()
question = List.first(quiz.quiz_questions) question = List.first(quiz.quiz_questions)
option = List.first(question.quiz_question_opts) option = List.first(question.quiz_question_opts)
assert {:ok, updated_quiz} = assert {:ok, updated_quiz} =
Quizzes.submit_quiz(user.id, [option], quiz.id) Quizzes.submit_quiz(user, [option], quiz.id)
updated_option = updated_option =
updated_quiz.quiz_questions updated_quiz.quiz_questions
@@ -151,7 +151,7 @@ defmodule Claper.QuizzesTest do
correct_option = Enum.find(question.quiz_question_opts, & &1.is_correct) correct_option = Enum.find(question.quiz_question_opts, & &1.is_correct)
# Submit correct answer # Submit correct answer
{:ok, _} = Quizzes.submit_quiz(user.id, [correct_option], quiz.id) {:ok, _} = Quizzes.submit_quiz(user, [correct_option], quiz.id)
assert {1, 1} = Quizzes.calculate_user_score(user.id, quiz.id) assert {1, 1} = Quizzes.calculate_user_score(user.id, quiz.id)
end end