mirror of
https://github.com/MarijnDoeve/TijdVoorDeTest.git
synced 2026-07-14 13:55:19 +02:00
Add reset progress action for a candidate's quiz attempt, and modal-based candidate editing (#204)
* feat: add reset progress action for a candidate's quiz attempt Admins previously had no way to let a candidate retake a quiz once started. Adds a backoffice action that clears a candidate's given answers and start timestamp for a quiz, closes #20. * fix: wrap candidate progress reset in a transaction Avoids leaving a candidate half-reset (answers deleted but started still set) if the second flush fails. Addresses CodeRabbit review comment on PR #204. * feat: open candidate edit/add forms in modals with dirty-aware dismissal Rename-candidate modal always blocked backdrop-click dismissal via a hardcoded static backdrop; wire it into the existing bo--modal controller so outside clicks/Escape only get blocked once the name field has actually been edited. Also move "Add Candidate" from a full-page navigation into the same turbo-frame modal pattern already used for editing quiz questions, keeping the full-page form as a fallback for non-JS/turbo requests. * feat: show a hint for the candidates textarea and gate translations in CI Add a help hint on the add-candidates form explaining one candidate per line, and translate it plus a few strings from the earlier reset-progress feature that had been missed. Also add a CI step that re-runs translation:extract and fails the build if it produces uncommitted changes, so untranslated/stale strings can't slip through review again. * Just links
This commit is contained in:
@@ -25,6 +25,7 @@ use Tvdt\Entity\QuizCandidate;
|
||||
use Tvdt\Entity\Season;
|
||||
use Tvdt\Enum\FlashType;
|
||||
use Tvdt\Exception\ErrorClearingQuizException;
|
||||
use Tvdt\Repository\GivenAnswerRepository;
|
||||
use Tvdt\Repository\QuizCandidateRepository;
|
||||
use Tvdt\Repository\QuizRepository;
|
||||
use Tvdt\Security\Voter\SeasonVoter;
|
||||
@@ -37,6 +38,7 @@ class QuizController extends AbstractController
|
||||
private readonly QuizRepository $quizRepository,
|
||||
private readonly TranslatorInterface $translator,
|
||||
private readonly QuizCandidateRepository $quizCandidateRepository,
|
||||
private readonly GivenAnswerRepository $givenAnswerRepository,
|
||||
private readonly EntityManagerInterface $em,
|
||||
) {}
|
||||
|
||||
@@ -397,6 +399,26 @@ class QuizController extends AbstractController
|
||||
return $this->redirectToRoute('tvdt_backoffice_quiz_candidates_tab', ['seasonCode' => $quiz->season->seasonCode, 'quiz' => $quiz->id]);
|
||||
}
|
||||
|
||||
#[IsCsrfTokenValid('reset_candidate_progress')]
|
||||
#[IsGranted(SeasonVoter::EDIT, subject: 'quiz')]
|
||||
#[Route(
|
||||
'/backoffice/quiz/{quiz}/candidate/{candidate}/reset',
|
||||
name: 'tvdt_backoffice_reset_candidate_progress',
|
||||
requirements: ['quiz' => Requirement::UUID, 'candidate' => Requirement::UUID],
|
||||
methods: ['POST'],
|
||||
)]
|
||||
public function resetCandidateProgress(Quiz $quiz, Candidate $candidate): RedirectResponse
|
||||
{
|
||||
$this->em->wrapInTransaction(function () use ($quiz, $candidate): void {
|
||||
$this->givenAnswerRepository->deleteAllForCandidateInQuiz($quiz, $candidate);
|
||||
$this->quizCandidateRepository->resetProgressForCandidate($quiz, $candidate);
|
||||
});
|
||||
|
||||
$this->addFlash(FlashType::Success, $this->translator->trans('Candidate progress reset'));
|
||||
|
||||
return $this->redirectToRoute('tvdt_backoffice_quiz_candidates_tab', ['seasonCode' => $quiz->season->seasonCode, 'quiz' => $quiz->id]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Pre-computes per-candidate data (quiz participation and given answer counts) to avoid nested loops in templates.
|
||||
*
|
||||
|
||||
@@ -129,6 +129,8 @@ class SeasonController extends AbstractController
|
||||
)]
|
||||
public function addCandidates(Season $season, Request $request): Response
|
||||
{
|
||||
$isTurboFrame = $request->headers->has('Turbo-Frame');
|
||||
|
||||
$form = $this->createForm(AddCandidatesFormType::class);
|
||||
$form->handleRequest($request);
|
||||
|
||||
@@ -140,10 +142,18 @@ class SeasonController extends AbstractController
|
||||
|
||||
$this->em->flush();
|
||||
|
||||
return $this->redirectToRoute('tvdt_backoffice_season', ['seasonCode' => $season->seasonCode]);
|
||||
if ($isTurboFrame) {
|
||||
return new Response('<turbo-frame id="add-candidates-modal-frame"></turbo-frame>');
|
||||
}
|
||||
|
||||
return $this->redirectToRoute('tvdt_backoffice_season_candidates', ['seasonCode' => $season->seasonCode]);
|
||||
}
|
||||
|
||||
return $this->render('backoffice/season_add_candidates.html.twig', ['form' => $form, 'season' => $season]);
|
||||
$template = $isTurboFrame
|
||||
? 'backoffice/season/_add_candidates_frame.html.twig'
|
||||
: 'backoffice/season_add_candidates.html.twig';
|
||||
|
||||
return $this->render($template, ['form' => $form, 'season' => $season]);
|
||||
}
|
||||
|
||||
#[IsCsrfTokenValid('rename_candidate')]
|
||||
|
||||
Reference in New Issue
Block a user