Files
TijdVoorDeTest/src/Repository/QuizCandidateRepository.php
T
Marijn 8382900b9e 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
2026-07-12 18:31:06 +02:00

83 lines
2.8 KiB
PHP

<?php
declare(strict_types=1);
namespace Tvdt\Repository;
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
use Doctrine\Persistence\ManagerRegistry;
use Safe\DateTimeImmutable;
use Tvdt\Entity\Candidate;
use Tvdt\Entity\Quiz;
use Tvdt\Entity\QuizCandidate;
/** @extends ServiceEntityRepository<QuizCandidate> */
class QuizCandidateRepository extends ServiceEntityRepository
{
public function __construct(ManagerRegistry $registry)
{
parent::__construct($registry, QuizCandidate::class);
}
/** @return bool|null true if a new entry was created, false if it already exists, null if candidate is inactive */
public function createIfNotExist(Quiz $quiz, Candidate $candidate): ?bool
{
$quizCandidate = $this->findOneBy(['candidate' => $candidate, 'quiz' => $quiz]);
if (null !== $quizCandidate) {
// Check if candidate is inactive
if (!$quizCandidate->active) {
return null;
}
// If QuizCandidate exists but hasn't started yet, set the started timestamp
if (null === $quizCandidate->started) {
$quizCandidate->started = new DateTimeImmutable();
$this->getEntityManager()->flush();
}
return false;
}
$quizCandidate = new QuizCandidate($quiz, $candidate);
$quizCandidate->started = new DateTimeImmutable();
$this->getEntityManager()->persist($quizCandidate);
$this->getEntityManager()->flush();
return true;
}
public function setCorrectionsForCandidate(Quiz $quiz, Candidate $candidate, float $corrections): void
{
$quizCandidate = $this->findOneBy(['candidate' => $candidate, 'quiz' => $quiz]);
if (!$quizCandidate instanceof QuizCandidate) {
throw new \InvalidArgumentException('Quiz candidate not found');
}
$quizCandidate->corrections = $corrections;
$this->getEntityManager()->flush();
}
public function setPenaltyForCandidate(Quiz $quiz, Candidate $candidate, int $penalty): void
{
$quizCandidate = $this->findOneBy(['candidate' => $candidate, 'quiz' => $quiz]);
if (!$quizCandidate instanceof QuizCandidate) {
throw new \InvalidArgumentException('Quiz candidate not found');
}
$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();
}
}