mirror of
https://github.com/MarijnDoeve/TijdVoorDeTest.git
synced 2026-07-05 23:20:18 +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);
|
||||
|
||||
$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->flush();
|
||||
|
||||
@@ -299,13 +308,14 @@ class QuestionBankController extends AbstractController
|
||||
throw new NotFoundHttpException();
|
||||
}
|
||||
|
||||
if ($usage->quiz->hasStartedCandidates()) {
|
||||
$this->addFlash(FlashType::Danger, $this->translator->trans('This quiz has already been filled in and can no longer be altered'));
|
||||
if ($usage->quiz->isLocked()) {
|
||||
$this->addFlash(FlashType::Danger, $this->translator->trans('This quiz can no longer be altered'));
|
||||
|
||||
return $this->redirectToRoute('tvdt_backoffice_question_bank', ['seasonCode' => $season->seasonCode]);
|
||||
}
|
||||
|
||||
$this->questionBankService->syncToQuiz($bankQuestion, $usage);
|
||||
$this->em->flush();
|
||||
$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]);
|
||||
@@ -329,14 +339,20 @@ class QuestionBankController extends AbstractController
|
||||
private function syncUsagesAfterEdit(BankQuestion $bankQuestion): void
|
||||
{
|
||||
$pendingNames = [];
|
||||
$synced = false;
|
||||
foreach ($bankQuestion->usages as $usage) {
|
||||
if (!$usage->quiz->isFinalized()) {
|
||||
if (!$usage->quiz->isLocked()) {
|
||||
$this->questionBankService->syncToQuiz($bankQuestion, $usage);
|
||||
} elseif (!$usage->quiz->hasStartedCandidates()) {
|
||||
$synced = true;
|
||||
} else {
|
||||
$pendingNames[] = $usage->quiz->name;
|
||||
}
|
||||
}
|
||||
|
||||
if ($synced) {
|
||||
$this->em->flush();
|
||||
}
|
||||
|
||||
if ([] !== $pendingNames) {
|
||||
$this->addFlash(
|
||||
FlashType::Warning,
|
||||
|
||||
@@ -297,8 +297,6 @@ class QuizController extends AbstractController
|
||||
{
|
||||
try {
|
||||
$this->quizRepository->clearQuiz($quiz);
|
||||
$quiz->finalizedAt = null;
|
||||
$this->em->flush();
|
||||
$this->addFlash(FlashType::Success, $this->translator->trans('Quiz cleared and no longer finalized'));
|
||||
} catch (ErrorClearingQuizException) {
|
||||
$this->addFlash(FlashType::Danger, $this->translator->trans('Error clearing quiz'));
|
||||
@@ -323,6 +321,8 @@ class QuizController extends AbstractController
|
||||
$quiz->finalizedAt = new DateTimeImmutable();
|
||||
$this->em->flush();
|
||||
$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]);
|
||||
|
||||
@@ -161,7 +161,7 @@ class SeasonController extends AbstractController
|
||||
public function addBlankQuiz(Request $request, Season $season): Response
|
||||
{
|
||||
$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'])
|
||||
->getForm();
|
||||
|
||||
|
||||
@@ -131,6 +131,6 @@ class BankQuestion implements \Stringable
|
||||
|
||||
public function __toString(): string
|
||||
{
|
||||
return $this->question ?? '';
|
||||
return $this->question;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -72,6 +72,20 @@ class QuizRepository extends ServiceEntityRepository
|
||||
DQL)
|
||||
->setParameter('quiz', $quiz)
|
||||
->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
|
||||
catch (\Throwable $throwable) {
|
||||
|
||||
@@ -97,8 +97,6 @@ final readonly class QuestionBankService
|
||||
$answer = $this->objectMapper->map($bankAnswer, Answer::class);
|
||||
$question->addAnswer($answer);
|
||||
}
|
||||
|
||||
$this->entityManager->flush();
|
||||
}
|
||||
|
||||
/** Remove the quiz copy created by this usage and delete the usage record. */
|
||||
|
||||
Reference in New Issue
Block a user