mirror of
https://github.com/MarijnDoeve/TijdVoorDeTest.git
synced 2026-07-06 07:30:17 +02:00
fix: correct lock guards, flush batching, and clearQuiz atomicity in question bank
- syncUsagesAfterEdit: use isLocked() instead of !isFinalized() to avoid syncing quizzes with started candidates (would have destroyed GivenAnswer records) - syncToQuiz action: use isLocked() instead of hasStartedCandidates() to block syncing into finalized quizzes that have no started candidates - QuestionBankService::syncToQuiz: remove internal flush(); callers now flush once (syncUsagesAfterEdit after the loop, syncToQuiz action after the call) - QuizRepository::clearQuiz: delete BankQuestionUsage rows and reset finalized_at inside the transaction (previously orphaned usages blocked bank question reassignment; finalizedAt reset was a separate non-atomic flush) - QuizController::finalizeQuiz: add flash when quiz is already finalized - QuestionBankController::delete: block deletion when any usage references a locked quiz - BankQuestion::__toString: remove dead null-coalescing on non-nullable string - SeasonController::addBlankQuiz: align form field with UploadQuizFormType (add translation_domain: false, use translator for label)
This commit is contained in:
@@ -150,6 +150,15 @@ class QuestionBankController extends AbstractController
|
|||||||
{
|
{
|
||||||
$this->assertSameSeason($season, $bankQuestion->season);
|
$this->assertSameSeason($season, $bankQuestion->season);
|
||||||
|
|
||||||
|
$hasLockedUsages = $bankQuestion->usages->exists(
|
||||||
|
static fn (int $key, BankQuestionUsage $usage): bool => $usage->quiz->isLocked(),
|
||||||
|
);
|
||||||
|
if ($hasLockedUsages) {
|
||||||
|
$this->addFlash(FlashType::Danger, $this->translator->trans('This question cannot be deleted because it is used in a locked or active quiz'));
|
||||||
|
|
||||||
|
return $this->redirectToRoute('tvdt_backoffice_question_bank', ['seasonCode' => $season->seasonCode]);
|
||||||
|
}
|
||||||
|
|
||||||
$this->em->remove($bankQuestion);
|
$this->em->remove($bankQuestion);
|
||||||
$this->em->flush();
|
$this->em->flush();
|
||||||
|
|
||||||
@@ -299,13 +308,14 @@ class QuestionBankController extends AbstractController
|
|||||||
throw new NotFoundHttpException();
|
throw new NotFoundHttpException();
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($usage->quiz->hasStartedCandidates()) {
|
if ($usage->quiz->isLocked()) {
|
||||||
$this->addFlash(FlashType::Danger, $this->translator->trans('This quiz has already been filled in and can no longer be altered'));
|
$this->addFlash(FlashType::Danger, $this->translator->trans('This quiz can no longer be altered'));
|
||||||
|
|
||||||
return $this->redirectToRoute('tvdt_backoffice_question_bank', ['seasonCode' => $season->seasonCode]);
|
return $this->redirectToRoute('tvdt_backoffice_question_bank', ['seasonCode' => $season->seasonCode]);
|
||||||
}
|
}
|
||||||
|
|
||||||
$this->questionBankService->syncToQuiz($bankQuestion, $usage);
|
$this->questionBankService->syncToQuiz($bankQuestion, $usage);
|
||||||
|
$this->em->flush();
|
||||||
$this->addFlash(FlashType::Success, $this->translator->trans('Question synced to quiz %quiz%', ['%quiz%' => $usage->quiz->name]));
|
$this->addFlash(FlashType::Success, $this->translator->trans('Question synced to quiz %quiz%', ['%quiz%' => $usage->quiz->name]));
|
||||||
|
|
||||||
return $this->redirectToRoute('tvdt_backoffice_question_bank', ['seasonCode' => $season->seasonCode]);
|
return $this->redirectToRoute('tvdt_backoffice_question_bank', ['seasonCode' => $season->seasonCode]);
|
||||||
@@ -329,14 +339,20 @@ class QuestionBankController extends AbstractController
|
|||||||
private function syncUsagesAfterEdit(BankQuestion $bankQuestion): void
|
private function syncUsagesAfterEdit(BankQuestion $bankQuestion): void
|
||||||
{
|
{
|
||||||
$pendingNames = [];
|
$pendingNames = [];
|
||||||
|
$synced = false;
|
||||||
foreach ($bankQuestion->usages as $usage) {
|
foreach ($bankQuestion->usages as $usage) {
|
||||||
if (!$usage->quiz->isFinalized()) {
|
if (!$usage->quiz->isLocked()) {
|
||||||
$this->questionBankService->syncToQuiz($bankQuestion, $usage);
|
$this->questionBankService->syncToQuiz($bankQuestion, $usage);
|
||||||
} elseif (!$usage->quiz->hasStartedCandidates()) {
|
$synced = true;
|
||||||
|
} else {
|
||||||
$pendingNames[] = $usage->quiz->name;
|
$pendingNames[] = $usage->quiz->name;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ($synced) {
|
||||||
|
$this->em->flush();
|
||||||
|
}
|
||||||
|
|
||||||
if ([] !== $pendingNames) {
|
if ([] !== $pendingNames) {
|
||||||
$this->addFlash(
|
$this->addFlash(
|
||||||
FlashType::Warning,
|
FlashType::Warning,
|
||||||
|
|||||||
@@ -297,8 +297,6 @@ class QuizController extends AbstractController
|
|||||||
{
|
{
|
||||||
try {
|
try {
|
||||||
$this->quizRepository->clearQuiz($quiz);
|
$this->quizRepository->clearQuiz($quiz);
|
||||||
$quiz->finalizedAt = null;
|
|
||||||
$this->em->flush();
|
|
||||||
$this->addFlash(FlashType::Success, $this->translator->trans('Quiz cleared and no longer finalized'));
|
$this->addFlash(FlashType::Success, $this->translator->trans('Quiz cleared and no longer finalized'));
|
||||||
} catch (ErrorClearingQuizException) {
|
} catch (ErrorClearingQuizException) {
|
||||||
$this->addFlash(FlashType::Danger, $this->translator->trans('Error clearing quiz'));
|
$this->addFlash(FlashType::Danger, $this->translator->trans('Error clearing quiz'));
|
||||||
@@ -323,6 +321,8 @@ class QuizController extends AbstractController
|
|||||||
$quiz->finalizedAt = new DateTimeImmutable();
|
$quiz->finalizedAt = new DateTimeImmutable();
|
||||||
$this->em->flush();
|
$this->em->flush();
|
||||||
$this->addFlash(FlashType::Success, $this->translator->trans('Quiz finalized'));
|
$this->addFlash(FlashType::Success, $this->translator->trans('Quiz finalized'));
|
||||||
|
} else {
|
||||||
|
$this->addFlash(FlashType::Warning, $this->translator->trans('The quiz is already finalized'));
|
||||||
}
|
}
|
||||||
|
|
||||||
return $this->redirectToRoute('tvdt_backoffice_quiz', ['seasonCode' => $quiz->season->seasonCode, 'quiz' => $quiz->id]);
|
return $this->redirectToRoute('tvdt_backoffice_quiz', ['seasonCode' => $quiz->season->seasonCode, 'quiz' => $quiz->id]);
|
||||||
|
|||||||
@@ -161,7 +161,7 @@ class SeasonController extends AbstractController
|
|||||||
public function addBlankQuiz(Request $request, Season $season): Response
|
public function addBlankQuiz(Request $request, Season $season): Response
|
||||||
{
|
{
|
||||||
$form = $this->createFormBuilder(new Quiz())
|
$form = $this->createFormBuilder(new Quiz())
|
||||||
->add('name', TextType::class, ['label' => 'Quiz name'])
|
->add('name', TextType::class, ['label' => $this->translator->trans('Quiz name'), 'translation_domain' => false])
|
||||||
->add('save', SubmitType::class, ['label' => 'Create'])
|
->add('save', SubmitType::class, ['label' => 'Create'])
|
||||||
->getForm();
|
->getForm();
|
||||||
|
|
||||||
|
|||||||
@@ -131,6 +131,6 @@ class BankQuestion implements \Stringable
|
|||||||
|
|
||||||
public function __toString(): string
|
public function __toString(): string
|
||||||
{
|
{
|
||||||
return $this->question ?? '';
|
return $this->question;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -72,6 +72,20 @@ class QuizRepository extends ServiceEntityRepository
|
|||||||
DQL)
|
DQL)
|
||||||
->setParameter('quiz', $quiz)
|
->setParameter('quiz', $quiz)
|
||||||
->execute();
|
->execute();
|
||||||
|
|
||||||
|
$em->createQuery(<<<DQL
|
||||||
|
delete from Tvdt\Entity\BankQuestionUsage bqu
|
||||||
|
where bqu.quiz = :quiz
|
||||||
|
DQL)
|
||||||
|
->setParameter('quiz', $quiz)
|
||||||
|
->execute();
|
||||||
|
|
||||||
|
$em->createQuery(<<<DQL
|
||||||
|
update Tvdt\Entity\Quiz q set q.finalizedAt = null
|
||||||
|
where q = :quiz
|
||||||
|
DQL)
|
||||||
|
->setParameter('quiz', $quiz)
|
||||||
|
->execute();
|
||||||
}
|
}
|
||||||
// @codeCoverageIgnoreStart
|
// @codeCoverageIgnoreStart
|
||||||
catch (\Throwable $throwable) {
|
catch (\Throwable $throwable) {
|
||||||
|
|||||||
@@ -97,8 +97,6 @@ final readonly class QuestionBankService
|
|||||||
$answer = $this->objectMapper->map($bankAnswer, Answer::class);
|
$answer = $this->objectMapper->map($bankAnswer, Answer::class);
|
||||||
$question->addAnswer($answer);
|
$question->addAnswer($answer);
|
||||||
}
|
}
|
||||||
|
|
||||||
$this->entityManager->flush();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Remove the quiz copy created by this usage and delete the usage record. */
|
/** Remove the quiz copy created by this usage and delete the usage record. */
|
||||||
|
|||||||
Reference in New Issue
Block a user