From 16bcce1a60132263701263ae7f6138ffe86202d6 Mon Sep 17 00:00:00 2001 From: Dimitrije Dimitrijevic Date: Tue, 4 Nov 2025 18:46:29 +0100 Subject: [PATCH] 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 --- lib/claper/accounts/user.ex | 1 + lib/claper/quizzes.ex | 64 ++++++++++++++++++++------ lib/claper/quizzes/quiz_response.ex | 3 +- lib/claper_web/live/event_live/show.ex | 2 +- test/claper/quizzes_test.exs | 6 +-- 5 files changed, 56 insertions(+), 20 deletions(-) diff --git a/lib/claper/accounts/user.ex b/lib/claper/accounts/user.ex index d342307..5c08bfd 100644 --- a/lib/claper/accounts/user.ex +++ b/lib/claper/accounts/user.ex @@ -30,6 +30,7 @@ defmodule Claper.Accounts.User do has_many :events, Claper.Events.Event has_one :lti_user, Lti13.Users.User + has_many :quiz_responses, Claper.Quizzes.QuizResponse timestamps() end diff --git a/lib/claper/quizzes.ex b/lib/claper/quizzes.ex index d1595bf..8b2c0b0 100644 --- a/lib/claper/quizzes.ex +++ b/lib/claper/quizzes.ex @@ -2,6 +2,8 @@ defmodule Claper.Quizzes do import Ecto.Query, warn: false alias Claper.Repo + alias Claper.Accounts.User + alias Claper.Quizzes.Quiz alias Claper.Quizzes.QuizQuestion alias Claper.Quizzes.QuizQuestionOpt @@ -265,28 +267,62 @@ defmodule Claper.Quizzes do {:ok, quiz} """ - def submit_quiz(user_id, quiz_opts, quiz_id) - when is_number(user_id) and is_list(quiz_opts) do - case Enum.reduce(quiz_opts, Ecto.Multi.new(), fn opt, multi -> - Ecto.Multi.update( - multi, - {:update_quiz_opt, opt.id}, + + # Pattern match on user, from user we create QuizResponse struct + # def submit_quiz(user_id, quiz_opts, quiz_id) + # when is_number(user_id) and is_list(quiz_opts) 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}) + # ) + # |> 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}) ) |> Ecto.Multi.insert( - {:insert_quiz_response, opt.id}, - QuizResponse.changeset(%QuizResponse{}, %{ - user_id: user_id, + "insert_quiz_response_#{unique_key}", + Ecto.build_assoc(user, :quiz_responses, %{ quiz_question_opt_id: opt.id, quiz_question_id: opt.quiz_question_id, quiz_id: quiz_id }) ) end) - |> Repo.transaction() do + |> Repo.transact() do {:ok, _} -> 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} end end @@ -294,8 +330,8 @@ defmodule Claper.Quizzes do def submit_quiz(attendee_identifier, quiz_opts, quiz_id) when is_binary(attendee_identifier) and is_list(quiz_opts) do case Enum.reduce(quiz_opts, Ecto.Multi.new(), fn opt, multi -> - Ecto.Multi.update( - multi, + multi + |> Ecto.Multi.update( {:update_quiz_opt, opt.id}, QuizQuestionOpt.changeset(opt, %{"response_count" => opt.response_count + 1}) ) @@ -309,7 +345,7 @@ defmodule Claper.Quizzes do }) ) end) - |> Repo.transaction() do + |> Repo.transact() do {:ok, _} -> quiz = get_quiz!(quiz_id, [:quiz_questions, quiz_questions: :quiz_question_opts]) {:ok, quiz} diff --git a/lib/claper/quizzes/quiz_response.ex b/lib/claper/quizzes/quiz_response.ex index a1233dc..9055e21 100644 --- a/lib/claper/quizzes/quiz_response.ex +++ b/lib/claper/quizzes/quiz_response.ex @@ -20,8 +20,7 @@ defmodule Claper.Quizzes.QuizResponse do :attendee_identifier, :quiz_id, :quiz_question_id, - :quiz_question_opt_id, - :user_id + :quiz_question_opt_id ]) |> validate_required([:quiz_id, :quiz_question_id, :quiz_question_opt_id]) end diff --git a/lib/claper_web/live/event_live/show.ex b/lib/claper_web/live/event_live/show.ex index a337006..26b5b38 100644 --- a/lib/claper_web/live/event_live/show.ex +++ b/lib/claper_web/live/event_live/show.ex @@ -693,7 +693,7 @@ defmodule ClaperWeb.EventLive.Show do ) when is_map(current_user) do case Claper.Quizzes.submit_quiz( - current_user.id, + current_user, opts, socket.assigns.current_interaction.id ) do diff --git a/test/claper/quizzes_test.exs b/test/claper/quizzes_test.exs index 6a8300c..0ee044b 100644 --- a/test/claper/quizzes_test.exs +++ b/test/claper/quizzes_test.exs @@ -113,14 +113,14 @@ defmodule Claper.QuizzesTest do assert length(new_question.quiz_question_opts) == 2 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() user = user_fixture() question = List.first(quiz.quiz_questions) option = List.first(question.quiz_question_opts) assert {:ok, updated_quiz} = - Quizzes.submit_quiz(user.id, [option], quiz.id) + Quizzes.submit_quiz(user, [option], quiz.id) updated_option = updated_quiz.quiz_questions @@ -151,7 +151,7 @@ defmodule Claper.QuizzesTest do correct_option = Enum.find(question.quiz_question_opts, & &1.is_correct) # 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) end