diff --git a/src/Controller/Backoffice/QuizController.php b/src/Controller/Backoffice/QuizController.php index 6d54259..60078c5 100644 --- a/src/Controller/Backoffice/QuizController.php +++ b/src/Controller/Backoffice/QuizController.php @@ -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,24 @@ 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->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. * diff --git a/src/Repository/GivenAnswerRepository.php b/src/Repository/GivenAnswerRepository.php index 331eec8..fbd51e5 100644 --- a/src/Repository/GivenAnswerRepository.php +++ b/src/Repository/GivenAnswerRepository.php @@ -6,7 +6,9 @@ namespace Tvdt\Repository; use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository; use Doctrine\Persistence\ManagerRegistry; +use Tvdt\Entity\Candidate; use Tvdt\Entity\GivenAnswer; +use Tvdt\Entity\Quiz; /** @extends ServiceEntityRepository */ class GivenAnswerRepository extends ServiceEntityRepository @@ -15,4 +17,15 @@ class GivenAnswerRepository extends ServiceEntityRepository { parent::__construct($registry, GivenAnswer::class); } + + public function deleteAllForCandidateInQuiz(Quiz $quiz, Candidate $candidate): void + { + $givenAnswers = $this->findBy(['quiz' => $quiz, 'candidate' => $candidate]); + + foreach ($givenAnswers as $givenAnswer) { + $this->getEntityManager()->remove($givenAnswer); + } + + $this->getEntityManager()->flush(); + } } diff --git a/src/Repository/QuizCandidateRepository.php b/src/Repository/QuizCandidateRepository.php index 931ef82..d247031 100644 --- a/src/Repository/QuizCandidateRepository.php +++ b/src/Repository/QuizCandidateRepository.php @@ -68,4 +68,15 @@ class QuizCandidateRepository extends ServiceEntityRepository $quizCandidate->penaltySeconds = $penalty; $this->getEntityManager()->flush(); } + + public function resetProgressForCandidate(Quiz $quiz, Candidate $candidate): void + { + $quizCandidate = $this->findOneBy(['candidate' => $candidate, 'quiz' => $quiz]); + if (!$quizCandidate instanceof QuizCandidate) { + return; + } + + $quizCandidate->started = null; + $this->getEntityManager()->flush(); + } } diff --git a/templates/backoffice/quiz/tab_candidates_list.html.twig b/templates/backoffice/quiz/tab_candidates_list.html.twig index 3db6809..1aae8b5 100644 --- a/templates/backoffice/quiz/tab_candidates_list.html.twig +++ b/templates/backoffice/quiz/tab_candidates_list.html.twig @@ -37,7 +37,7 @@ {% endif %} -
+
+ {% if quizCandidate and quizCandidate.started %} +
+ + +
+ {% endif %} {% endfor %} diff --git a/tests/Controller/Backoffice/QuizControllerTest.php b/tests/Controller/Backoffice/QuizControllerTest.php index 04db72a..072b55e 100644 --- a/tests/Controller/Backoffice/QuizControllerTest.php +++ b/tests/Controller/Backoffice/QuizControllerTest.php @@ -173,6 +173,47 @@ final class QuizControllerTest extends AbstractControllerWebTestCase $this->assertTrue($updated->active); } + public function testResetCandidateProgressDeletesGivenAnswersAndClearsStarted(): void + { + $quiz = $this->getQuizByName('Quiz 1'); + $candidate = $this->getCandidate('Tom'); + + $quizCandidate = new QuizCandidate($quiz, $candidate); + $quizCandidate->started = new DateTimeImmutable(); + + $this->entityManager->persist($quizCandidate); + $firstQuestion = $quiz->questions->first(); + $this->assertInstanceOf(Question::class, $firstQuestion); + $answer = $firstQuestion->answers->first(); + $this->assertInstanceOf(Answer::class, $answer); + $this->entityManager->persist(new GivenAnswer($candidate, $quiz, $answer)); + $this->entityManager->flush(); + + $crawler = $this->client->request(Request::METHOD_GET, \sprintf('/backoffice/season/krtek/quiz/%s/candidates-list', $quiz->id)); + self::assertResponseIsSuccessful(); + $token = (string) $crawler->filter(\sprintf('form[action*="/%s/reset"] input[name="_token"]', $candidate->id))->first()->attr('value'); + + $this->client->request(Request::METHOD_POST, \sprintf('/backoffice/quiz/%s/candidate/%s/reset', $quiz->id, $candidate->id), [ + '_token' => $token, + ]); + + self::assertResponseRedirects(); + $this->entityManager->clear(); + + $updated = $this->entityManager->getRepository(QuizCandidate::class)->findOneBy([ + 'quiz' => $this->getQuizByName('Quiz 1'), + 'candidate' => $this->getCandidate('Tom'), + ]); + $this->assertInstanceOf(QuizCandidate::class, $updated); + $this->assertNotInstanceOf(\DateTimeImmutable::class, $updated->started); + + $remainingAnswers = $this->entityManager->getRepository(GivenAnswer::class)->findBy([ + 'quiz' => $quiz, + 'candidate' => $candidate, + ]); + $this->assertCount(0, $remainingAnswers); + } + public function testModifyCorrection(): void { $quiz = $this->getQuizByName('Quiz 1');