mirror of
https://github.com/MarijnDoeve/TijdVoorDeTest.git
synced 2026-07-11 12:28:23 +02:00
Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| c2637481f2 | |||
| 938456087a |
@@ -95,15 +95,6 @@ jobs:
|
|||||||
id: rector
|
id: rector
|
||||||
continue-on-error: true
|
continue-on-error: true
|
||||||
run: docker compose exec -T php vendor/bin/rector process --dry-run --no-progress-bar --output-format=github
|
run: docker compose exec -T php vendor/bin/rector process --dry-run --no-progress-bar --output-format=github
|
||||||
- name: Translations
|
|
||||||
id: translations
|
|
||||||
continue-on-error: true
|
|
||||||
run: |
|
|
||||||
docker compose exec -T php bin/console translation:extract --force --format=xliff --sort=asc nl
|
|
||||||
if ! git diff --exit-code -- translations; then
|
|
||||||
echo "::error::Translations are out of date. Run 'just translations' and commit the changes."
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
- name: Check HTTP reachability
|
- name: Check HTTP reachability
|
||||||
run: curl -v --fail-with-body http://localhost
|
run: curl -v --fail-with-body http://localhost
|
||||||
- name: Assert all checks passed
|
- name: Assert all checks passed
|
||||||
@@ -122,7 +113,6 @@ jobs:
|
|||||||
check "Twig Coding Style" "${{ steps.twig_cs.outcome }}"
|
check "Twig Coding Style" "${{ steps.twig_cs.outcome }}"
|
||||||
check "PHPStan" "${{ steps.phpstan.outcome }}"
|
check "PHPStan" "${{ steps.phpstan.outcome }}"
|
||||||
check "Rector" "${{ steps.rector.outcome }}"
|
check "Rector" "${{ steps.rector.outcome }}"
|
||||||
check "Translations" "${{ steps.translations.outcome }}"
|
|
||||||
exit $failed
|
exit $failed
|
||||||
|
|
||||||
tests:
|
tests:
|
||||||
|
|||||||
@@ -306,7 +306,6 @@ GitHub Actions workflow (`.github/workflows/ci.yml`):
|
|||||||
- Twig-CS-Fixer style check
|
- Twig-CS-Fixer style check
|
||||||
- PHPStan static analysis
|
- PHPStan static analysis
|
||||||
- Rector dry-run
|
- Rector dry-run
|
||||||
- Translation extraction check (fails if `translation:extract` produces uncommitted changes)
|
|
||||||
3. **Integration Tests**:
|
3. **Integration Tests**:
|
||||||
- Docker image build and start services
|
- Docker image build and start services
|
||||||
- Database creation and migration
|
- Database creation and migration
|
||||||
|
|||||||
Generated
+6
-6
@@ -9408,16 +9408,16 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "friendsofphp/php-cs-fixer",
|
"name": "friendsofphp/php-cs-fixer",
|
||||||
"version": "v3.95.12",
|
"version": "v3.95.13",
|
||||||
"source": {
|
"source": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer.git",
|
"url": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer.git",
|
||||||
"reference": "b1b9055997a98dce3c2338e884626e718a25a923"
|
"reference": "ea941114a002eb5e5876f190223deec1066c482c"
|
||||||
},
|
},
|
||||||
"dist": {
|
"dist": {
|
||||||
"type": "zip",
|
"type": "zip",
|
||||||
"url": "https://api.github.com/repos/PHP-CS-Fixer/PHP-CS-Fixer/zipball/b1b9055997a98dce3c2338e884626e718a25a923",
|
"url": "https://api.github.com/repos/PHP-CS-Fixer/PHP-CS-Fixer/zipball/ea941114a002eb5e5876f190223deec1066c482c",
|
||||||
"reference": "b1b9055997a98dce3c2338e884626e718a25a923",
|
"reference": "ea941114a002eb5e5876f190223deec1066c482c",
|
||||||
"shasum": ""
|
"shasum": ""
|
||||||
},
|
},
|
||||||
"require": {
|
"require": {
|
||||||
@@ -9501,7 +9501,7 @@
|
|||||||
],
|
],
|
||||||
"support": {
|
"support": {
|
||||||
"issues": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer/issues",
|
"issues": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer/issues",
|
||||||
"source": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer/tree/v3.95.12"
|
"source": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer/tree/v3.95.13"
|
||||||
},
|
},
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
@@ -9509,7 +9509,7 @@
|
|||||||
"type": "github"
|
"type": "github"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"time": "2026-07-07T13:29:36+00:00"
|
"time": "2026-07-10T09:23:21+00:00"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "myclabs/deep-copy",
|
"name": "myclabs/deep-copy",
|
||||||
|
|||||||
@@ -25,7 +25,6 @@ use Tvdt\Entity\QuizCandidate;
|
|||||||
use Tvdt\Entity\Season;
|
use Tvdt\Entity\Season;
|
||||||
use Tvdt\Enum\FlashType;
|
use Tvdt\Enum\FlashType;
|
||||||
use Tvdt\Exception\ErrorClearingQuizException;
|
use Tvdt\Exception\ErrorClearingQuizException;
|
||||||
use Tvdt\Repository\GivenAnswerRepository;
|
|
||||||
use Tvdt\Repository\QuizCandidateRepository;
|
use Tvdt\Repository\QuizCandidateRepository;
|
||||||
use Tvdt\Repository\QuizRepository;
|
use Tvdt\Repository\QuizRepository;
|
||||||
use Tvdt\Security\Voter\SeasonVoter;
|
use Tvdt\Security\Voter\SeasonVoter;
|
||||||
@@ -38,7 +37,6 @@ class QuizController extends AbstractController
|
|||||||
private readonly QuizRepository $quizRepository,
|
private readonly QuizRepository $quizRepository,
|
||||||
private readonly TranslatorInterface $translator,
|
private readonly TranslatorInterface $translator,
|
||||||
private readonly QuizCandidateRepository $quizCandidateRepository,
|
private readonly QuizCandidateRepository $quizCandidateRepository,
|
||||||
private readonly GivenAnswerRepository $givenAnswerRepository,
|
|
||||||
private readonly EntityManagerInterface $em,
|
private readonly EntityManagerInterface $em,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
@@ -399,26 +397,6 @@ class QuizController extends AbstractController
|
|||||||
return $this->redirectToRoute('tvdt_backoffice_quiz_candidates_tab', ['seasonCode' => $quiz->season->seasonCode, 'quiz' => $quiz->id]);
|
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.
|
* Pre-computes per-candidate data (quiz participation and given answer counts) to avoid nested loops in templates.
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -129,8 +129,6 @@ class SeasonController extends AbstractController
|
|||||||
)]
|
)]
|
||||||
public function addCandidates(Season $season, Request $request): Response
|
public function addCandidates(Season $season, Request $request): Response
|
||||||
{
|
{
|
||||||
$isTurboFrame = $request->headers->has('Turbo-Frame');
|
|
||||||
|
|
||||||
$form = $this->createForm(AddCandidatesFormType::class);
|
$form = $this->createForm(AddCandidatesFormType::class);
|
||||||
$form->handleRequest($request);
|
$form->handleRequest($request);
|
||||||
|
|
||||||
@@ -142,18 +140,10 @@ class SeasonController extends AbstractController
|
|||||||
|
|
||||||
$this->em->flush();
|
$this->em->flush();
|
||||||
|
|
||||||
if ($isTurboFrame) {
|
return $this->redirectToRoute('tvdt_backoffice_season', ['seasonCode' => $season->seasonCode]);
|
||||||
return new Response('<turbo-frame id="add-candidates-modal-frame"></turbo-frame>');
|
|
||||||
}
|
|
||||||
|
|
||||||
return $this->redirectToRoute('tvdt_backoffice_season_candidates', ['seasonCode' => $season->seasonCode]);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
$template = $isTurboFrame
|
return $this->render('backoffice/season_add_candidates.html.twig', ['form' => $form, 'season' => $season]);
|
||||||
? 'backoffice/season/_add_candidates_frame.html.twig'
|
|
||||||
: 'backoffice/season_add_candidates.html.twig';
|
|
||||||
|
|
||||||
return $this->render($template, ['form' => $form, 'season' => $season]);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[IsCsrfTokenValid('rename_candidate')]
|
#[IsCsrfTokenValid('rename_candidate')]
|
||||||
|
|||||||
@@ -19,9 +19,7 @@ class AddCandidatesFormType extends AbstractType
|
|||||||
{
|
{
|
||||||
$builder
|
$builder
|
||||||
->add('candidates', TextareaType::class, [
|
->add('candidates', TextareaType::class, [
|
||||||
'label' => $this->translator->trans('Candidates'),
|
'label' => $this->translator->trans('Candidates'), 'translation_domain' => false,
|
||||||
'help' => $this->translator->trans('One candidate per line'),
|
|
||||||
'translation_domain' => false,
|
|
||||||
])
|
])
|
||||||
;
|
;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,9 +6,7 @@ namespace Tvdt\Repository;
|
|||||||
|
|
||||||
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
|
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
|
||||||
use Doctrine\Persistence\ManagerRegistry;
|
use Doctrine\Persistence\ManagerRegistry;
|
||||||
use Tvdt\Entity\Candidate;
|
|
||||||
use Tvdt\Entity\GivenAnswer;
|
use Tvdt\Entity\GivenAnswer;
|
||||||
use Tvdt\Entity\Quiz;
|
|
||||||
|
|
||||||
/** @extends ServiceEntityRepository<GivenAnswer> */
|
/** @extends ServiceEntityRepository<GivenAnswer> */
|
||||||
class GivenAnswerRepository extends ServiceEntityRepository
|
class GivenAnswerRepository extends ServiceEntityRepository
|
||||||
@@ -17,15 +15,4 @@ class GivenAnswerRepository extends ServiceEntityRepository
|
|||||||
{
|
{
|
||||||
parent::__construct($registry, GivenAnswer::class);
|
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();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -68,15 +68,4 @@ class QuizCandidateRepository extends ServiceEntityRepository
|
|||||||
$quizCandidate->penaltySeconds = $penalty;
|
$quizCandidate->penaltySeconds = $penalty;
|
||||||
$this->getEntityManager()->flush();
|
$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();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -263,7 +263,7 @@ class DataExportService
|
|||||||
}
|
}
|
||||||
|
|
||||||
$lastColumnIndex = 1 + \count($questions);
|
$lastColumnIndex = 1 + \count($questions);
|
||||||
foreach (range('A', Coordinate::stringFromColumnIndex($lastColumnIndex)) as $column) {
|
foreach ($this->columnLetters($lastColumnIndex) as $column) {
|
||||||
$sheet->getColumnDimension($column)->setWidth(30);
|
$sheet->getColumnDimension($column)->setWidth(30);
|
||||||
$sheet->getStyle($column.':'.$column)->getAlignment()->setWrapText(true);
|
$sheet->getStyle($column.':'.$column)->getAlignment()->setWrapText(true);
|
||||||
}
|
}
|
||||||
@@ -297,7 +297,7 @@ class DataExportService
|
|||||||
++$row;
|
++$row;
|
||||||
}
|
}
|
||||||
|
|
||||||
foreach (range('A', Coordinate::stringFromColumnIndex(2 + \count($candidates))) as $column) {
|
foreach ($this->columnLetters(2 + \count($candidates)) as $column) {
|
||||||
$sheet->getColumnDimension($column)->setAutoSize(true);
|
$sheet->getColumnDimension($column)->setAutoSize(true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -405,7 +405,7 @@ class DataExportService
|
|||||||
}
|
}
|
||||||
|
|
||||||
$lastColumnIndex = $answerStartColumnIndex + max(1, 2 * $maxAnswers);
|
$lastColumnIndex = $answerStartColumnIndex + max(1, 2 * $maxAnswers);
|
||||||
foreach (range('A', Coordinate::stringFromColumnIndex($lastColumnIndex)) as $column) {
|
foreach ($this->columnLetters($lastColumnIndex) as $column) {
|
||||||
$sheet->getColumnDimension($column)->setAutoSize(true);
|
$sheet->getColumnDimension($column)->setAutoSize(true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -426,6 +426,22 @@ class DataExportService
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Column letters from 'A' up to and including the given 1-based column index.
|
||||||
|
* Unlike range('A', ...), this works past 'Z' (e.g. index 27 => 'AA').
|
||||||
|
*
|
||||||
|
* @return list<string>
|
||||||
|
*/
|
||||||
|
private function columnLetters(int $lastColumnIndex): array
|
||||||
|
{
|
||||||
|
$letters = [];
|
||||||
|
for ($columnIndex = 1; $columnIndex <= $lastColumnIndex; ++$columnIndex) {
|
||||||
|
$letters[] = Coordinate::stringFromColumnIndex($columnIndex);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $letters;
|
||||||
|
}
|
||||||
|
|
||||||
/** @throws FilesystemException */
|
/** @throws FilesystemException */
|
||||||
private function writeToTempFile(Spreadsheet $spreadsheet): string
|
private function writeToTempFile(Spreadsheet $spreadsheet): string
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -37,7 +37,7 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<form action="{{ path('tvdt_backoffice_toggle_candidate', {quiz: quiz.id, candidate: candidate.id}) }}" method="POST" class="d-inline">
|
<form action="{{ path('tvdt_backoffice_toggle_candidate', {quiz: quiz.id, candidate: candidate.id}) }}" method="POST">
|
||||||
<input type="hidden" name="_token" value="{{ csrf_token('toggle_candidate') }}">
|
<input type="hidden" name="_token" value="{{ csrf_token('toggle_candidate') }}">
|
||||||
<button type="submit" class="btn btn-sm btn-outline-secondary">
|
<button type="submit" class="btn btn-sm btn-outline-secondary">
|
||||||
{% if quizCandidate == null or quizCandidate.active %}
|
{% if quizCandidate == null or quizCandidate.active %}
|
||||||
@@ -47,12 +47,6 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
{% if quizCandidate and quizCandidate.started %}
|
|
||||||
<form action="{{ path('tvdt_backoffice_reset_candidate_progress', {quiz: quiz.id, candidate: candidate.id}) }}" method="POST" class="d-inline" onsubmit="return confirm('{{ 'Are you sure you want to reset progress for this candidate? Their given answers for this quiz will be deleted.'|trans|e('js') }}');">
|
|
||||||
<input type="hidden" name="_token" value="{{ csrf_token('reset_candidate_progress') }}">
|
|
||||||
<button type="submit" class="btn btn-sm btn-outline-danger">{{ 'Reset progress'|trans }}</button>
|
|
||||||
</form>
|
|
||||||
{% endif %}
|
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
|||||||
@@ -1,11 +0,0 @@
|
|||||||
<turbo-frame id="add-candidates-modal-frame">
|
|
||||||
{{ form_start(form, {attr: {novalidate: 'novalidate'}}) }}
|
|
||||||
<div class="modal-body">
|
|
||||||
{{ form_row(form.candidates) }}
|
|
||||||
</div>
|
|
||||||
<div class="modal-footer">
|
|
||||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">{{ 'Cancel'|trans }}</button>
|
|
||||||
<button type="submit" class="btn btn-primary">{{ 'Submit'|trans }}</button>
|
|
||||||
</div>
|
|
||||||
{{ form_end(form) }}
|
|
||||||
</turbo-frame>
|
|
||||||
@@ -1,10 +1,8 @@
|
|||||||
<div class="row" data-controller="bo--modal" data-action="turbo:submit-end->bo--modal#frameSubmitEnd">
|
<div class="row">
|
||||||
<div class="col-md-6 col-12">
|
<div class="col-md-6 col-12">
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<button type="button" class="btn btn-sm btn-outline-primary"
|
<a class="btn btn-sm btn-outline-primary"
|
||||||
data-action="click->bo--modal#open"
|
href="{{ path('tvdt_backoffice_add_candidates', {seasonCode: season.seasonCode}) }}">{{ 'Add Candidate'|trans }}</a>
|
||||||
data-src="{{ path('tvdt_backoffice_add_candidates', {seasonCode: season.seasonCode}) }}"
|
|
||||||
data-modal-title="{{ 'Add Candidate'|trans }}">{{ 'Add Candidate'|trans }}</button>
|
|
||||||
</div>
|
</div>
|
||||||
<ul class="list-group mb-3">
|
<ul class="list-group mb-3">
|
||||||
{% for candidate in season.candidates %}
|
{% for candidate in season.candidates %}
|
||||||
@@ -19,9 +17,7 @@
|
|||||||
title="{{ 'Delete'|trans }}"><i class="bi bi-trash"></i></button>
|
title="{{ 'Delete'|trans }}"><i class="bi bi-trash"></i></button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="modal fade" id="renameCandidate-{{ candidate.id }}"
|
<div class="modal fade" id="renameCandidate-{{ candidate.id }}" data-bs-backdrop="static"
|
||||||
data-controller="bo--modal" data-bo--modal-target="modal"
|
|
||||||
data-action="hidden.bs.modal->bo--modal#resetDirty"
|
|
||||||
tabindex="-1" aria-labelledby="renameCandidate-{{ candidate.id }}Label" aria-hidden="true">
|
tabindex="-1" aria-labelledby="renameCandidate-{{ candidate.id }}Label" aria-hidden="true">
|
||||||
<div class="modal-dialog">
|
<div class="modal-dialog">
|
||||||
<div class="modal-content">
|
<div class="modal-content">
|
||||||
@@ -35,8 +31,7 @@
|
|||||||
<input type="hidden" name="_token" value="{{ csrf_token('rename_candidate') }}">
|
<input type="hidden" name="_token" value="{{ csrf_token('rename_candidate') }}">
|
||||||
<label class="form-label" for="renameCandidateName-{{ candidate.id }}">{{ 'Name'|trans }}</label>
|
<label class="form-label" for="renameCandidateName-{{ candidate.id }}">{{ 'Name'|trans }}</label>
|
||||||
<input type="text" class="form-control" id="renameCandidateName-{{ candidate.id }}"
|
<input type="text" class="form-control" id="renameCandidateName-{{ candidate.id }}"
|
||||||
name="name" value="{{ candidate.name }}" maxlength="16" required autofocus
|
name="name" value="{{ candidate.name }}" maxlength="16" required autofocus>
|
||||||
data-action="input->bo--modal#markDirty change->bo--modal#markDirty">
|
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-footer">
|
<div class="modal-footer">
|
||||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">{{ 'Cancel'|trans }}</button>
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">{{ 'Cancel'|trans }}</button>
|
||||||
@@ -74,23 +69,6 @@
|
|||||||
{{ 'No candidates'|trans }}
|
{{ 'No candidates'|trans }}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
<div class="modal fade" tabindex="-1"
|
|
||||||
data-bo--modal-target="modal"
|
|
||||||
data-action="hidden.bs.modal->bo--modal#resetDirty"
|
|
||||||
aria-labelledby="addCandidatesModalLabel" aria-hidden="true">
|
|
||||||
<div class="modal-dialog">
|
|
||||||
<div class="modal-content">
|
|
||||||
<div class="modal-header">
|
|
||||||
<h1 class="modal-title fs-5" id="addCandidatesModalLabel">{{ 'Add Candidate'|trans }}</h1>
|
|
||||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
|
||||||
</div>
|
|
||||||
<turbo-frame id="add-candidates-modal-frame"
|
|
||||||
data-bo--modal-target="frame"
|
|
||||||
data-action="input->bo--modal#markDirty change->bo--modal#markDirty"></turbo-frame>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-6 col-12">
|
<div class="col-md-6 col-12">
|
||||||
{{ include('backoffice/help/season_candidates.html.twig') }}
|
{{ include('backoffice/help/season_candidates.html.twig') }}
|
||||||
|
|||||||
@@ -173,47 +173,6 @@ final class QuizControllerTest extends AbstractControllerWebTestCase
|
|||||||
$this->assertTrue($updated->active);
|
$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
|
public function testModifyCorrection(): void
|
||||||
{
|
{
|
||||||
$quiz = $this->getQuizByName('Quiz 1');
|
$quiz = $this->getQuizByName('Quiz 1');
|
||||||
|
|||||||
@@ -104,42 +104,6 @@ final class SeasonControllerTest extends AbstractControllerWebTestCase
|
|||||||
$this->assertNotInstanceOf(Candidate::class, $this->entityManager->getRepository(Candidate::class)->find($candidateId));
|
$this->assertNotInstanceOf(Candidate::class, $this->entityManager->getRepository(Candidate::class)->find($candidateId));
|
||||||
}
|
}
|
||||||
|
|
||||||
public function testAddCandidates(): void
|
|
||||||
{
|
|
||||||
$this->client->request(Request::METHOD_GET, '/backoffice/season/krtek/add-candidate');
|
|
||||||
$form = $this->client->getCrawler()->filter('form')->form([
|
|
||||||
'add_candidates_form[candidates]' => "Nora\nPiet",
|
|
||||||
]);
|
|
||||||
$this->client->submit($form);
|
|
||||||
|
|
||||||
self::assertResponseRedirects('/backoffice/season/krtek/candidates');
|
|
||||||
$this->entityManager->clear();
|
|
||||||
|
|
||||||
$season = $this->entityManager->getRepository(Season::class)->findOneBy(['seasonCode' => 'krtek']);
|
|
||||||
$this->assertInstanceOf(Season::class, $season);
|
|
||||||
$names = array_map(static fn (Candidate $candidate): string => $candidate->name, $season->candidates->toArray());
|
|
||||||
$this->assertContains('Nora', $names);
|
|
||||||
$this->assertContains('Piet', $names);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function testAddCandidatesViaTurboFrameReturnsEmptyFrame(): void
|
|
||||||
{
|
|
||||||
$this->client->xmlHttpRequest(Request::METHOD_GET, '/backoffice/season/krtek/add-candidate', server: ['HTTP_TURBO-FRAME' => 'add-candidates-modal-frame']);
|
|
||||||
$form = $this->client->getCrawler()->filter('form')->form([
|
|
||||||
'add_candidates_form[candidates]' => 'Sanne',
|
|
||||||
]);
|
|
||||||
$this->client->submit($form, [], ['HTTP_TURBO-FRAME' => 'add-candidates-modal-frame']);
|
|
||||||
|
|
||||||
self::assertResponseIsSuccessful();
|
|
||||||
$this->assertStringContainsString('<turbo-frame id="add-candidates-modal-frame"></turbo-frame>', (string) $this->client->getResponse()->getContent());
|
|
||||||
$this->entityManager->clear();
|
|
||||||
|
|
||||||
$season = $this->entityManager->getRepository(Season::class)->findOneBy(['seasonCode' => 'krtek']);
|
|
||||||
$this->assertInstanceOf(Season::class, $season);
|
|
||||||
$names = array_map(static fn (Candidate $candidate): string => $candidate->name, $season->candidates->toArray());
|
|
||||||
$this->assertContains('Sanne', $names);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function testRenameCandidateIsDeniedForNonOwner(): void
|
public function testRenameCandidateIsDeniedForNonOwner(): void
|
||||||
{
|
{
|
||||||
$candidate = $this->getCandidate('Tom');
|
$candidate = $this->getCandidate('Tom');
|
||||||
|
|||||||
@@ -0,0 +1,47 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Tvdt\Tests\Service;
|
||||||
|
|
||||||
|
use Doctrine\ORM\EntityManagerInterface;
|
||||||
|
use PhpOffice\PhpSpreadsheet\Spreadsheet;
|
||||||
|
use PHPUnit\Framework\Attributes\CoversClass;
|
||||||
|
use PHPUnit\Framework\TestCase;
|
||||||
|
use Tvdt\Entity\Question;
|
||||||
|
use Tvdt\Entity\Quiz;
|
||||||
|
use Tvdt\Repository\QuizRepository;
|
||||||
|
use Tvdt\Service\DataExportService;
|
||||||
|
use Tvdt\Service\QuizSpreadsheetService;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reproduces a Sentry warning (PHP-SYMFONY-3Z): with more than 25 questions, the last raw-answers
|
||||||
|
* column goes past 'Z' (e.g. 'AA'), and range('A', 'AA') is invalid because range()'s second argument
|
||||||
|
* must be a single byte.
|
||||||
|
*/
|
||||||
|
#[CoversClass(DataExportService::class)]
|
||||||
|
final class DataExportServiceRawAnswersColumnsTest extends TestCase
|
||||||
|
{
|
||||||
|
public function testFillRawAnswersSheetHandlesMoreThanTwentyFiveQuestions(): void
|
||||||
|
{
|
||||||
|
$subject = new DataExportService(
|
||||||
|
$this->createStub(EntityManagerInterface::class),
|
||||||
|
$this->createStub(QuizSpreadsheetService::class),
|
||||||
|
$this->createStub(QuizRepository::class),
|
||||||
|
);
|
||||||
|
|
||||||
|
$quiz = new Quiz();
|
||||||
|
for ($i = 0; $i < 26; ++$i) {
|
||||||
|
$question = new Question();
|
||||||
|
$question->question = 'Question '.($i + 1);
|
||||||
|
$quiz->addQuestion($question);
|
||||||
|
}
|
||||||
|
|
||||||
|
$sheet = new Spreadsheet()->getActiveSheet();
|
||||||
|
|
||||||
|
$method = new \ReflectionMethod(DataExportService::class, 'fillRawAnswersSheet');
|
||||||
|
$method->invoke($subject, $sheet, $quiz);
|
||||||
|
|
||||||
|
$this->assertEqualsWithDelta(30.0, $sheet->getColumnDimension('AA')->getWidth(), \PHP_FLOAT_EPSILON);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -117,10 +117,6 @@
|
|||||||
<source>Are you sure you want to delete this quiz?</source>
|
<source>Are you sure you want to delete this quiz?</source>
|
||||||
<target>Weet je zeker dat je deze test wilt verwijderen?</target>
|
<target>Weet je zeker dat je deze test wilt verwijderen?</target>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="6XTrab." resname="Are you sure you want to reset progress for this candidate? Their given answers for this quiz will be deleted.">
|
|
||||||
<source>Are you sure you want to reset progress for this candidate? Their given answers for this quiz will be deleted.</source>
|
|
||||||
<target>Weet je zeker dat je de voortgang van deze kandidaat wilt resetten? De ingevulde antwoorden voor deze test worden verwijderd.</target>
|
|
||||||
</trans-unit>
|
|
||||||
<trans-unit id="4bcq6sL" resname="Assign">
|
<trans-unit id="4bcq6sL" resname="Assign">
|
||||||
<source>Assign</source>
|
<source>Assign</source>
|
||||||
<target>Toewijzen</target>
|
<target>Toewijzen</target>
|
||||||
@@ -165,10 +161,6 @@
|
|||||||
<source>Candidate not found</source>
|
<source>Candidate not found</source>
|
||||||
<target>Kandidaat niet gevonden</target>
|
<target>Kandidaat niet gevonden</target>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="fl.hjdA" resname="Candidate progress reset">
|
|
||||||
<source>Candidate progress reset</source>
|
|
||||||
<target>Voortgang kandidaat gereset</target>
|
|
||||||
</trans-unit>
|
|
||||||
<trans-unit id="QH4e_Ho" resname="Candidate renamed">
|
<trans-unit id="QH4e_Ho" resname="Candidate renamed">
|
||||||
<source>Candidate renamed</source>
|
<source>Candidate renamed</source>
|
||||||
<target>Kandidaat hernoemd</target>
|
<target>Kandidaat hernoemd</target>
|
||||||
@@ -549,10 +541,6 @@
|
|||||||
<source>Number of dropouts:</source>
|
<source>Number of dropouts:</source>
|
||||||
<target>Aantal afvallers:</target>
|
<target>Aantal afvallers:</target>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="vxaREnf" resname="One candidate per line">
|
|
||||||
<source>One candidate per line</source>
|
|
||||||
<target>Eén kandidaat per regel</target>
|
|
||||||
</trans-unit>
|
|
||||||
<trans-unit id="_SqArFZ" resname="Open">
|
<trans-unit id="_SqArFZ" resname="Open">
|
||||||
<source>Open</source>
|
<source>Open</source>
|
||||||
<target>Openen</target>
|
<target>Openen</target>
|
||||||
@@ -773,10 +761,6 @@
|
|||||||
<source>Reset password</source>
|
<source>Reset password</source>
|
||||||
<target>Wachtwoord herstellen</target>
|
<target>Wachtwoord herstellen</target>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="SSMxy68" resname="Reset progress">
|
|
||||||
<source>Reset progress</source>
|
|
||||||
<target>Voortgang resetten</target>
|
|
||||||
</trans-unit>
|
|
||||||
<trans-unit id="eyayNGN" resname="Reset your password">
|
<trans-unit id="eyayNGN" resname="Reset your password">
|
||||||
<source>Reset your password</source>
|
<source>Reset your password</source>
|
||||||
<target>Wachtwoord herstellen</target>
|
<target>Wachtwoord herstellen</target>
|
||||||
|
|||||||
Reference in New Issue
Block a user