Compare commits

..

2 Commits

Author SHA1 Message Date
dependabot[bot] c2637481f2 build(deps-dev): bump friendsofphp/php-cs-fixer (#207)
Bumps the dev-dependencies group with 1 update: [friendsofphp/php-cs-fixer](https://github.com/PHP-CS-Fixer/PHP-CS-Fixer).


Updates `friendsofphp/php-cs-fixer` from 3.95.12 to 3.95.13
- [Release notes](https://github.com/PHP-CS-Fixer/PHP-CS-Fixer/releases)
- [Changelog](https://github.com/PHP-CS-Fixer/PHP-CS-Fixer/blob/master/CHANGELOG.md)
- [Commits](https://github.com/PHP-CS-Fixer/PHP-CS-Fixer/compare/v3.95.12...v3.95.13)

---
updated-dependencies:
- dependency-name: friendsofphp/php-cs-fixer
  dependency-version: 3.95.13
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: dev-dependencies
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-10 23:55:45 +00:00
Marijn 938456087a fix: support more than 25 columns in data export sheets (#205)
range('A', ...) silently truncates its end argument to one byte once a
sheet needs a column past 'Z' (e.g. 26+ questions), triggering a PHP
warning and mis-sizing columns. Replace it with a helper that walks
column indexes via Coordinate::stringFromColumnIndex, which handles
multi-letter columns correctly.

Fixes PHP-SYMFONY-3Z
2026-07-10 19:30:37 +02:00
16 changed files with 81 additions and 219 deletions
-10
View File
@@ -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:
-1
View File
@@ -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
View File
@@ -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.
* *
+2 -12
View File
@@ -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]); 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')] #[IsCsrfTokenValid('rename_candidate')]
+1 -3
View File
@@ -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,
]) ])
; ;
} }
-13
View File
@@ -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();
}
} }
+19 -3
View File
@@ -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);
}
}
-16
View File
@@ -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>