mirror of
https://github.com/MarijnDoeve/TijdVoorDeTest.git
synced 2026-03-06 04:44:19 +01:00
Add quiz clearing and deletion functionality with UI enhancements
This commit introduces the ability to clear quiz results and delete quizzes directly from the backoffice. It includes new routes, controllers, modals for user confirmation, and updates to translations. The `QuizRepository` now supports dedicated methods for clearing results and deleting quizzes along with error handling. Related database migrations and front-end adjustments are also included.
This commit is contained in:
@@ -1,5 +1,5 @@
|
|||||||
|
import * as bootstrap from 'bootstrap'
|
||||||
import './bootstrap.js';
|
import './bootstrap.js';
|
||||||
import 'bootstrap/dist/css/bootstrap.min.css'
|
import 'bootstrap/dist/css/bootstrap.min.css'
|
||||||
import * as bootstrap from 'bootstrap'
|
|
||||||
|
|
||||||
import './styles/backoffice.scss';
|
import './styles/backoffice.scss';
|
||||||
|
|||||||
17
assets/controllers/bo/quiz_controller.js
Normal file
17
assets/controllers/bo/quiz_controller.js
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
import {Controller} from '@hotwired/stimulus';
|
||||||
|
import * as bootstrap from 'bootstrap'
|
||||||
|
|
||||||
|
export default class extends Controller {
|
||||||
|
connect() {
|
||||||
|
const tooltipTriggerList = document.querySelectorAll('[data-bs-toggle="tooltip"]')
|
||||||
|
const tooltipList = [...tooltipTriggerList].map(tooltipTriggerEl => new bootstrap.Tooltip(tooltipTriggerEl))
|
||||||
|
}
|
||||||
|
|
||||||
|
clearQuiz() {
|
||||||
|
new bootstrap.Modal('#clearQuizModal').show();
|
||||||
|
}
|
||||||
|
|
||||||
|
deleteQuiz() {
|
||||||
|
new bootstrap.Modal('#deleteQuizModal').show();
|
||||||
|
}
|
||||||
|
}
|
||||||
41
migrations/Version20250607154730.php
Normal file
41
migrations/Version20250607154730.php
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace DoctrineMigrations;
|
||||||
|
|
||||||
|
use Doctrine\DBAL\Schema\Schema;
|
||||||
|
use Doctrine\Migrations\AbstractMigration;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Auto-generated Migration: Please modify to your needs!
|
||||||
|
*/
|
||||||
|
final class Version20250607154730 extends AbstractMigration
|
||||||
|
{
|
||||||
|
public function getDescription(): string
|
||||||
|
{
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function up(Schema $schema): void
|
||||||
|
{
|
||||||
|
// this up() migration is auto-generated, please modify it to your needs
|
||||||
|
$this->addSql(<<<'SQL'
|
||||||
|
ALTER TABLE season DROP CONSTRAINT FK_F0E45BA96706D6B
|
||||||
|
SQL);
|
||||||
|
$this->addSql(<<<'SQL'
|
||||||
|
ALTER TABLE season ADD CONSTRAINT FK_F0E45BA96706D6B FOREIGN KEY (active_quiz_id) REFERENCES quiz (id) ON DELETE SET NULL NOT DEFERRABLE INITIALLY IMMEDIATE
|
||||||
|
SQL);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(Schema $schema): void
|
||||||
|
{
|
||||||
|
// this down() migration is auto-generated, please modify it to your needs
|
||||||
|
$this->addSql(<<<'SQL'
|
||||||
|
ALTER TABLE season DROP CONSTRAINT fk_f0e45ba96706d6b
|
||||||
|
SQL);
|
||||||
|
$this->addSql(<<<'SQL'
|
||||||
|
ALTER TABLE season ADD CONSTRAINT fk_f0e45ba96706d6b FOREIGN KEY (active_quiz_id) REFERENCES quiz (id) NOT DEFERRABLE INITIALLY IMMEDIATE
|
||||||
|
SQL);
|
||||||
|
}
|
||||||
|
}
|
||||||
53
migrations/Version20250607184525.php
Normal file
53
migrations/Version20250607184525.php
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace DoctrineMigrations;
|
||||||
|
|
||||||
|
use Doctrine\DBAL\Schema\Schema;
|
||||||
|
use Doctrine\Migrations\AbstractMigration;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Auto-generated Migration: Please modify to your needs!
|
||||||
|
*/
|
||||||
|
final class Version20250607184525 extends AbstractMigration
|
||||||
|
{
|
||||||
|
public function getDescription(): string
|
||||||
|
{
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function up(Schema $schema): void
|
||||||
|
{
|
||||||
|
// this up() migration is auto-generated, please modify it to your needs
|
||||||
|
$this->addSql(<<<'SQL'
|
||||||
|
ALTER TABLE elimination DROP CONSTRAINT FK_5947284F853CD175
|
||||||
|
SQL);
|
||||||
|
$this->addSql(<<<'SQL'
|
||||||
|
ALTER TABLE elimination ADD CONSTRAINT FK_5947284F853CD175 FOREIGN KEY (quiz_id) REFERENCES quiz (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE
|
||||||
|
SQL);
|
||||||
|
$this->addSql(<<<'SQL'
|
||||||
|
ALTER TABLE given_answer DROP CONSTRAINT FK_9AC61A30853CD175
|
||||||
|
SQL);
|
||||||
|
$this->addSql(<<<'SQL'
|
||||||
|
ALTER TABLE given_answer ADD CONSTRAINT FK_9AC61A30853CD175 FOREIGN KEY (quiz_id) REFERENCES quiz (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE
|
||||||
|
SQL);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(Schema $schema): void
|
||||||
|
{
|
||||||
|
// this down() migration is auto-generated, please modify it to your needs
|
||||||
|
$this->addSql(<<<'SQL'
|
||||||
|
ALTER TABLE given_answer DROP CONSTRAINT fk_9ac61a30853cd175
|
||||||
|
SQL);
|
||||||
|
$this->addSql(<<<'SQL'
|
||||||
|
ALTER TABLE given_answer ADD CONSTRAINT fk_9ac61a30853cd175 FOREIGN KEY (quiz_id) REFERENCES quiz (id) NOT DEFERRABLE INITIALLY IMMEDIATE
|
||||||
|
SQL);
|
||||||
|
$this->addSql(<<<'SQL'
|
||||||
|
ALTER TABLE elimination DROP CONSTRAINT fk_5947284f853cd175
|
||||||
|
SQL);
|
||||||
|
$this->addSql(<<<'SQL'
|
||||||
|
ALTER TABLE elimination ADD CONSTRAINT fk_5947284f853cd175 FOREIGN KEY (quiz_id) REFERENCES quiz (id) NOT DEFERRABLE INITIALLY IMMEDIATE
|
||||||
|
SQL);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -27,6 +27,7 @@ return RectorConfig::configure()
|
|||||||
phpunitCodeQuality: true,
|
phpunitCodeQuality: true,
|
||||||
doctrineCodeQuality: true,
|
doctrineCodeQuality: true,
|
||||||
symfonyCodeQuality: true,
|
symfonyCodeQuality: true,
|
||||||
|
// naming: true
|
||||||
)
|
)
|
||||||
->withAttributesSets(all: true)
|
->withAttributesSets(all: true)
|
||||||
->withComposerBased(twig: true, doctrine: true, phpunit: true, symfony: true)
|
->withComposerBased(twig: true, doctrine: true, phpunit: true, symfony: true)
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ use Symfony\Bundle\FrameworkBundle\Controller\AbstractController as AbstractBase
|
|||||||
abstract class AbstractController extends AbstractBaseController
|
abstract class AbstractController extends AbstractBaseController
|
||||||
{
|
{
|
||||||
protected const string SEASON_CODE_REGEX = '[A-Za-z\d]{5}';
|
protected const string SEASON_CODE_REGEX = '[A-Za-z\d]{5}';
|
||||||
|
|
||||||
protected const string CANDIDATE_HASH_REGEX = '[\w\-=]+';
|
protected const string CANDIDATE_HASH_REGEX = '[\w\-=]+';
|
||||||
|
|
||||||
#[\Override]
|
#[\Override]
|
||||||
|
|||||||
@@ -39,9 +39,10 @@ final class PrepareEliminationController extends AbstractController
|
|||||||
$elimination->updateFromInputBag($request->request);
|
$elimination->updateFromInputBag($request->request);
|
||||||
$em->flush();
|
$em->flush();
|
||||||
|
|
||||||
if (true === $request->request->getBoolean('start')) {
|
if ($request->request->getBoolean('start')) {
|
||||||
return $this->redirectToRoute('app_elimination', ['elimination' => $elimination->getId()]);
|
return $this->redirectToRoute('app_elimination', ['elimination' => $elimination->getId()]);
|
||||||
}
|
}
|
||||||
|
|
||||||
$this->addFlash('success', 'Elimination updated');
|
$this->addFlash('success', 'Elimination updated');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -8,8 +8,10 @@ use App\Controller\AbstractController;
|
|||||||
use App\Entity\Candidate;
|
use App\Entity\Candidate;
|
||||||
use App\Entity\Quiz;
|
use App\Entity\Quiz;
|
||||||
use App\Entity\Season;
|
use App\Entity\Season;
|
||||||
|
use App\Exception\ErrorClearingQuizException;
|
||||||
use App\Repository\CandidateRepository;
|
use App\Repository\CandidateRepository;
|
||||||
use App\Repository\QuizCandidateRepository;
|
use App\Repository\QuizCandidateRepository;
|
||||||
|
use App\Repository\QuizRepository;
|
||||||
use App\Security\Voter\SeasonVoter;
|
use App\Security\Voter\SeasonVoter;
|
||||||
use Doctrine\ORM\EntityManagerInterface;
|
use Doctrine\ORM\EntityManagerInterface;
|
||||||
use Symfony\Component\HttpFoundation\RedirectResponse;
|
use Symfony\Component\HttpFoundation\RedirectResponse;
|
||||||
@@ -19,6 +21,7 @@ use Symfony\Component\HttpKernel\Attribute\AsController;
|
|||||||
use Symfony\Component\HttpKernel\Exception\MethodNotAllowedHttpException;
|
use Symfony\Component\HttpKernel\Exception\MethodNotAllowedHttpException;
|
||||||
use Symfony\Component\Routing\Attribute\Route;
|
use Symfony\Component\Routing\Attribute\Route;
|
||||||
use Symfony\Component\Security\Http\Attribute\IsGranted;
|
use Symfony\Component\Security\Http\Attribute\IsGranted;
|
||||||
|
use Symfony\Contracts\Translation\TranslatorInterface;
|
||||||
|
|
||||||
#[AsController]
|
#[AsController]
|
||||||
#[IsGranted('ROLE_USER')]
|
#[IsGranted('ROLE_USER')]
|
||||||
@@ -26,6 +29,7 @@ class QuizController extends AbstractController
|
|||||||
{
|
{
|
||||||
public function __construct(
|
public function __construct(
|
||||||
private readonly CandidateRepository $candidateRepository,
|
private readonly CandidateRepository $candidateRepository,
|
||||||
|
private readonly TranslatorInterface $translator,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
#[Route(
|
#[Route(
|
||||||
@@ -61,6 +65,37 @@ class QuizController extends AbstractController
|
|||||||
return $this->redirectToRoute('app_backoffice_season', ['seasonCode' => $season->getSeasonCode()]);
|
return $this->redirectToRoute('app_backoffice_season', ['seasonCode' => $season->getSeasonCode()]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[Route(
|
||||||
|
'/backoffice/quiz/{quiz}/clear',
|
||||||
|
name: 'app_backoffice_quiz_clear',
|
||||||
|
)]
|
||||||
|
#[IsGranted(SeasonVoter::EDIT, subject: 'quiz')]
|
||||||
|
public function clearQuiz(Quiz $quiz, QuizRepository $quizRepository): RedirectResponse
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$quizRepository->clearQuiz($quiz);
|
||||||
|
$this->addFlash('success', $this->translator->trans('Quiz cleared'));
|
||||||
|
} catch (ErrorClearingQuizException) {
|
||||||
|
$this->addFlash('error', $this->translator->trans('Error clearing quiz'));
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->redirectToRoute('app_backoffice_quiz', ['seasonCode' => $quiz->getSeason()->getSeasonCode(), 'quiz' => $quiz->getId()]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Route(
|
||||||
|
'/backoffice/quiz/{quiz}/delete',
|
||||||
|
name: 'app_backoffice_quiz_delete',
|
||||||
|
)]
|
||||||
|
#[IsGranted(SeasonVoter::DELETE, subject: 'quiz')]
|
||||||
|
public function deleteQuiz(Quiz $quiz, QuizRepository $quizRepository): RedirectResponse
|
||||||
|
{
|
||||||
|
$quizRepository->deleteQuiz($quiz);
|
||||||
|
|
||||||
|
$this->addFlash('success', $this->translator->trans('Quiz deleted'));
|
||||||
|
|
||||||
|
return $this->redirectToRoute('app_backoffice_season', ['seasonCode' => $quiz->getSeason()->getSeasonCode()]);
|
||||||
|
}
|
||||||
|
|
||||||
#[Route(
|
#[Route(
|
||||||
'/backoffice/quiz/{quiz}/modify_correction/{candidate}',
|
'/backoffice/quiz/{quiz}/modify_correction/{candidate}',
|
||||||
name: 'app_backoffice_modify_correction',
|
name: 'app_backoffice_modify_correction',
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ use Symfony\Contracts\Translation\TranslatorInterface;
|
|||||||
#[IsGranted('ROLE_USER')]
|
#[IsGranted('ROLE_USER')]
|
||||||
class SeasonController extends AbstractController
|
class SeasonController extends AbstractController
|
||||||
{
|
{
|
||||||
public function __construct(private readonly TranslatorInterface $translator, private EntityManagerInterface $em,
|
public function __construct(private readonly TranslatorInterface $translator, private readonly EntityManagerInterface $em,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
#[Route(
|
#[Route(
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ use Symfony\Component\Uid\Uuid;
|
|||||||
class Elimination
|
class Elimination
|
||||||
{
|
{
|
||||||
public const string SCREEN_GREEN = 'green';
|
public const string SCREEN_GREEN = 'green';
|
||||||
|
|
||||||
public const string SCREEN_RED = 'red';
|
public const string SCREEN_RED = 'red';
|
||||||
|
|
||||||
#[ORM\Id]
|
#[ORM\Id]
|
||||||
@@ -35,7 +36,7 @@ class Elimination
|
|||||||
|
|
||||||
public function __construct(
|
public function __construct(
|
||||||
#[ORM\ManyToOne(inversedBy: 'eliminations')]
|
#[ORM\ManyToOne(inversedBy: 'eliminations')]
|
||||||
#[ORM\JoinColumn(nullable: false)]
|
#[ORM\JoinColumn(nullable: false, onDelete: 'CASCADE')]
|
||||||
private Quiz $quiz,
|
private Quiz $quiz,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
@@ -66,7 +67,7 @@ class Elimination
|
|||||||
/** @param InputBag<bool|float|int|string> $inputBag */
|
/** @param InputBag<bool|float|int|string> $inputBag */
|
||||||
public function updateFromInputBag(InputBag $inputBag): self
|
public function updateFromInputBag(InputBag $inputBag): self
|
||||||
{
|
{
|
||||||
foreach ($this->data as $name => $screenColour) {
|
foreach (array_keys($this->data) as $name) {
|
||||||
$newColour = $inputBag->get('colour-'.mb_strtolower($name));
|
$newColour = $inputBag->get('colour-'.mb_strtolower($name));
|
||||||
if (\is_string($newColour)) {
|
if (\is_string($newColour)) {
|
||||||
$this->data[$name] = $inputBag->get('colour-'.mb_strtolower($name));
|
$this->data[$name] = $inputBag->get('colour-'.mb_strtolower($name));
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ class GivenAnswer
|
|||||||
private Candidate $candidate,
|
private Candidate $candidate,
|
||||||
|
|
||||||
#[ORM\ManyToOne]
|
#[ORM\ManyToOne]
|
||||||
#[ORM\JoinColumn(nullable: false)]
|
#[ORM\JoinColumn(nullable: false, onDelete: 'CASCADE')]
|
||||||
private Quiz $quiz,
|
private Quiz $quiz,
|
||||||
|
|
||||||
#[ORM\ManyToOne(inversedBy: 'givenAnswers')]
|
#[ORM\ManyToOne(inversedBy: 'givenAnswers')]
|
||||||
|
|||||||
@@ -43,6 +43,7 @@ class Season
|
|||||||
private Collection $owners;
|
private Collection $owners;
|
||||||
|
|
||||||
#[ORM\ManyToOne]
|
#[ORM\ManyToOne]
|
||||||
|
#[ORM\JoinColumn(nullable: true, onDelete: 'SET NULL')]
|
||||||
private ?Quiz $ActiveQuiz = null;
|
private ?Quiz $ActiveQuiz = null;
|
||||||
|
|
||||||
public function __construct()
|
public function __construct()
|
||||||
|
|||||||
7
src/Exception/ErrorClearingQuizException.php
Normal file
7
src/Exception/ErrorClearingQuizException.php
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Exception;
|
||||||
|
|
||||||
|
class ErrorClearingQuizException extends \Exception {}
|
||||||
@@ -4,17 +4,59 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
namespace App\Repository;
|
namespace App\Repository;
|
||||||
|
|
||||||
|
use App\Entity\Elimination;
|
||||||
|
use App\Entity\GivenAnswer;
|
||||||
use App\Entity\Quiz;
|
use App\Entity\Quiz;
|
||||||
|
use App\Entity\QuizCandidate;
|
||||||
|
use App\Exception\ErrorClearingQuizException;
|
||||||
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
|
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
|
||||||
use Doctrine\Persistence\ManagerRegistry;
|
use Doctrine\Persistence\ManagerRegistry;
|
||||||
|
use Psr\Log\LoggerInterface;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @extends ServiceEntityRepository<Quiz>
|
* @extends ServiceEntityRepository<Quiz>
|
||||||
*/
|
*/
|
||||||
class QuizRepository extends ServiceEntityRepository
|
class QuizRepository extends ServiceEntityRepository
|
||||||
{
|
{
|
||||||
public function __construct(ManagerRegistry $registry)
|
public function __construct(ManagerRegistry $registry, private readonly LoggerInterface $logger)
|
||||||
{
|
{
|
||||||
parent::__construct($registry, Quiz::class);
|
parent::__construct($registry, Quiz::class);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** @throws ErrorClearingQuizException */
|
||||||
|
public function clearQuiz(Quiz $quiz): void
|
||||||
|
{
|
||||||
|
$em = $this->getEntityManager();
|
||||||
|
$em->beginTransaction();
|
||||||
|
try {
|
||||||
|
$em->createQueryBuilder()
|
||||||
|
->delete()->from(QuizCandidate::class, 'qc')
|
||||||
|
->where('qc.quiz = :quiz')
|
||||||
|
->setParameter('quiz', $quiz)
|
||||||
|
->getQuery()->execute();
|
||||||
|
|
||||||
|
$em->createQueryBuilder()
|
||||||
|
->delete()->from(GivenAnswer::class, 'ga')
|
||||||
|
->where('ga.quiz = :quiz')
|
||||||
|
->setParameter('quiz', $quiz)
|
||||||
|
->getQuery()->execute();
|
||||||
|
$em->createQueryBuilder()
|
||||||
|
->delete()->from(Elimination::class, 'e')
|
||||||
|
->where('e.quiz = :quiz')
|
||||||
|
->setParameter('quiz', $quiz)
|
||||||
|
->getQuery()->execute();
|
||||||
|
} catch (\Throwable $throwable) {
|
||||||
|
$this->logger->error($throwable->getMessage());
|
||||||
|
$em->rollback();
|
||||||
|
throw new ErrorClearingQuizException(previous: $throwable);
|
||||||
|
}
|
||||||
|
|
||||||
|
$em->commit();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function deleteQuiz(Quiz $quiz): void
|
||||||
|
{
|
||||||
|
$this->getEntityManager()->remove($quiz);
|
||||||
|
$this->getEntityManager()->flush();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,43 +11,45 @@
|
|||||||
{{ 'Add'|trans }}
|
{{ 'Add'|trans }}
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
<table class="table table-hover">
|
{% if seasons %}
|
||||||
<thead>
|
<table class="table table-hover">
|
||||||
<tr>
|
<thead>
|
||||||
{% if is_granted('ROLE_ADMIN') %}
|
<tr>
|
||||||
<th scope="col">{{ 'Owner(s)'|trans }}</th>
|
|
||||||
{% endif %}
|
|
||||||
<th scope="col">{{ 'Name'|trans }}</th>
|
|
||||||
<th scope="col">{{ 'Active Quiz'|trans }}</th>
|
|
||||||
<th scope="col">{{ 'Season Code'|trans }}</th>
|
|
||||||
<th scope="col">{{ 'Manage'|trans }}</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
{% for season in seasons %}
|
|
||||||
<tr class="align-middle">
|
|
||||||
{% if is_granted('ROLE_ADMIN') %}
|
{% if is_granted('ROLE_ADMIN') %}
|
||||||
<td>{{ season.owners|map(o => o.email)|join(', ') }}</td>
|
<th scope="col">{{ 'Owner(s)'|trans }}</th>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<td>{{ season.name }}</td>
|
<th scope="col">{{ 'Name'|trans }}</th>
|
||||||
<td>
|
<th scope="col">{{ 'Active Quiz'|trans }}</th>
|
||||||
{% if season.activeQuiz %}
|
<th scope="col">{{ 'Season Code'|trans }}</th>
|
||||||
{{ season.activeQuiz.name }}
|
<th scope="col">{{ 'Manage'|trans }}</th>
|
||||||
{% else %}
|
|
||||||
{{ 'No active quiz'|trans }}
|
|
||||||
{% endif %}
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
<a {% if season.activeQuiz %}href="{{ path('app_quiz_enter_name', {seasonCode: season.seasonCode}) }}"
|
|
||||||
{% else %}class="disabled" {% endif %}>{{ season.seasonCode }}</a>
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
<a href="{{ path('app_backoffice_season', {seasonCode: season.seasonCode}) }}">{{ 'Manage'|trans }}</a>
|
|
||||||
</td>
|
|
||||||
</tr>
|
</tr>
|
||||||
{% else %}
|
</thead>
|
||||||
EMPTY
|
<tbody>
|
||||||
{% endfor %}
|
{% for season in seasons %}
|
||||||
</tbody>
|
<tr class="align-middle">
|
||||||
</table>
|
{% if is_granted('ROLE_ADMIN') %}
|
||||||
|
<td>{{ season.owners|map(o => o.email)|join(', ') }}</td>
|
||||||
|
{% endif %}
|
||||||
|
<td>{{ season.name }}</td>
|
||||||
|
<td>
|
||||||
|
{% if season.activeQuiz %}
|
||||||
|
{{ season.activeQuiz.name }}
|
||||||
|
{% else %}
|
||||||
|
{{ 'No active quiz'|trans }}
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<a {% if season.activeQuiz %}href="{{ path('app_quiz_enter_name', {seasonCode: season.seasonCode}) }}"
|
||||||
|
{% else %}class="disabled" {% endif %}>{{ season.seasonCode }}</a>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<a href="{{ path('app_backoffice_season', {seasonCode: season.seasonCode}) }}">{{ 'Manage'|trans }}</a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
{% else %}
|
||||||
|
{{ 'You have no seasons yet.'|trans }}
|
||||||
|
{% endif %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
@@ -2,13 +2,19 @@
|
|||||||
|
|
||||||
{% block body %}
|
{% block body %}
|
||||||
<h2 class="py-2">{{ 'Quiz'|trans }}: {{ quiz.season.name }} - {{ quiz.name }}</h2>
|
<h2 class="py-2">{{ 'Quiz'|trans }}: {{ quiz.season.name }} - {{ quiz.name }}</h2>
|
||||||
<div class="py-2 btn-group">
|
<div class="py-2 btn-group" data-controller="bo--quiz">
|
||||||
<a class="btn btn-primary {% if quiz is same as(season.activeQuiz) %}disabled{% endif %}"
|
<a class="btn btn-primary {% if quiz is same as(season.activeQuiz) %}disabled{% endif %}"
|
||||||
href="{{ path('app_backoffice_enable', {seasonCode: season.seasonCode, quiz: quiz.id}) }}">{{ 'Make active'|trans }}</a>
|
href="{{ path('app_backoffice_enable', {seasonCode: season.seasonCode, quiz: quiz.id}) }}">{{ 'Make active'|trans }}</a>
|
||||||
{% if quiz is same as (season.activeQuiz) %}
|
{% if quiz is same as (season.activeQuiz) %}
|
||||||
<a class="btn btn-secondary"
|
<a class="btn btn-secondary"
|
||||||
href="{{ path('app_backoffice_enable', {seasonCode: season.seasonCode, quiz: 'null'}) }}">{{ 'Deactivate Quiz'|trans }}</a>
|
href="{{ path('app_backoffice_enable', {seasonCode: season.seasonCode, quiz: 'null'}) }}">{{ 'Deactivate Quiz'|trans }}</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
<button class="btn btn-danger" data-action="click->bo--quiz#clearQuiz">
|
||||||
|
{{ 'Clear quiz...'|trans }}
|
||||||
|
</button>
|
||||||
|
<button class="btn btn-danger" data-action="click->bo--quiz#deleteQuiz">
|
||||||
|
{{ 'Delete Quiz...'|trans }}
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="questions">
|
<div id="questions">
|
||||||
@@ -111,16 +117,48 @@
|
|||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
{% endblock %}
|
|
||||||
{% block javascripts %}
|
|
||||||
{{ parent() }}
|
|
||||||
<script>
|
|
||||||
document.addEventListener('DOMContentLoaded', function () {
|
|
||||||
const tooltipTriggerList = document.querySelectorAll('[data-bs-toggle="tooltip"]')
|
|
||||||
const tooltipList = [...tooltipTriggerList].map(tooltipTriggerEl => new bootstrap.Tooltip(tooltipTriggerEl))
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
{% endblock javascripts %}
|
|
||||||
{% block title %}
|
|
||||||
|
|
||||||
|
{# Modal Clear #}
|
||||||
|
<div class="modal fade" id="clearQuizModal" data-bs-backdrop="static"
|
||||||
|
tabindex="-1"
|
||||||
|
aria-labelledby="staticBackdropLabel" aria-hidden="true">
|
||||||
|
<div class="modal-dialog">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h1 class="modal-title fs-5" id="staticBackdropLabel">Please Confirm</h1>
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
Are you sure you want to clear all the results? This will also delete al the eliminations.
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">No</button>
|
||||||
|
<a href="{{ path('app_backoffice_quiz_clear', {quiz: quiz.id}) }}" class="btn btn-danger">Yes</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{# Modal Delete #}
|
||||||
|
<div class="modal fade" id="deleteQuizModal" data-bs-backdrop="static"
|
||||||
|
tabindex="-1"
|
||||||
|
aria-labelledby="staticBackdropLabel" aria-hidden="true">
|
||||||
|
<div class="modal-dialog">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h1 class="modal-title fs-5" id="staticBackdropLabel">Please Confirm</h1>
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
Are you sure you want to delete this quiz?
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">No</button>
|
||||||
|
<a href="{{ path('app_backoffice_quiz_delete', {quiz: quiz.id}) }}" class="btn btn-danger">Yes</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
|
{% block title %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
@@ -18,19 +18,19 @@ use PHPUnit\Framework\TestCase;
|
|||||||
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
|
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
|
||||||
use Symfony\Component\Security\Core\Authorization\Voter\VoterInterface;
|
use Symfony\Component\Security\Core\Authorization\Voter\VoterInterface;
|
||||||
|
|
||||||
class SeasonVoterTest extends TestCase
|
final class SeasonVoterTest extends TestCase
|
||||||
{
|
{
|
||||||
private SeasonVoter $seasonVoter;
|
private SeasonVoter $seasonVoter;
|
||||||
|
|
||||||
private TokenInterface&Stub $token;
|
private TokenInterface&Stub $token;
|
||||||
private User&Stub $user;
|
|
||||||
|
|
||||||
protected function setUp(): void
|
protected function setUp(): void
|
||||||
{
|
{
|
||||||
$this->seasonVoter = new SeasonVoter();
|
$this->seasonVoter = new SeasonVoter();
|
||||||
$this->token = $this->createStub(TokenInterface::class);
|
$this->token = $this->createStub(TokenInterface::class);
|
||||||
|
|
||||||
$this->user = $this->createStub(User::class);
|
$user = $this->createStub(User::class);
|
||||||
$this->token->method('getUser')->willReturn($this->user);
|
$this->token->method('getUser')->willReturn($user);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[DataProvider('typesProvider')]
|
#[DataProvider('typesProvider')]
|
||||||
|
|||||||
@@ -49,6 +49,10 @@
|
|||||||
<source>Candidates</source>
|
<source>Candidates</source>
|
||||||
<target>Kandidaten</target>
|
<target>Kandidaten</target>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
|
<trans-unit id="FNY513f" resname="Clear quiz...">
|
||||||
|
<source>Clear quiz...</source>
|
||||||
|
<target>Test leegmaken...</target>
|
||||||
|
</trans-unit>
|
||||||
<trans-unit id="sFpB4C2" resname="Correct Answers">
|
<trans-unit id="sFpB4C2" resname="Correct Answers">
|
||||||
<source>Correct Answers</source>
|
<source>Correct Answers</source>
|
||||||
<target>Goede antwoorden</target>
|
<target>Goede antwoorden</target>
|
||||||
@@ -77,6 +81,10 @@
|
|||||||
<source>Deactivate Quiz</source>
|
<source>Deactivate Quiz</source>
|
||||||
<target>Deactiveer test</target>
|
<target>Deactiveer test</target>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
|
<trans-unit id="p9GNNI3" resname="Delete Quiz...">
|
||||||
|
<source>Delete Quiz...</source>
|
||||||
|
<target>Test verwijderen...</target>
|
||||||
|
</trans-unit>
|
||||||
<trans-unit id="R9yHzHv" resname="Download Template">
|
<trans-unit id="R9yHzHv" resname="Download Template">
|
||||||
<source>Download Template</source>
|
<source>Download Template</source>
|
||||||
<target>Download sjabloon</target>
|
<target>Download sjabloon</target>
|
||||||
@@ -93,6 +101,10 @@
|
|||||||
<source>Enter your name</source>
|
<source>Enter your name</source>
|
||||||
<target>Voor je naam in</target>
|
<target>Voor je naam in</target>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
|
<trans-unit id="HNMwvRn" resname="Error clearing quiz">
|
||||||
|
<source>Error clearing quiz</source>
|
||||||
|
<target>Fout bij leegmaken test</target>
|
||||||
|
</trans-unit>
|
||||||
<trans-unit id="OGiIhMH" resname="Green">
|
<trans-unit id="OGiIhMH" resname="Green">
|
||||||
<source>Green</source>
|
<source>Green</source>
|
||||||
<target>Groen</target>
|
<target>Groen</target>
|
||||||
@@ -177,10 +189,18 @@
|
|||||||
<source>Quiz Added!</source>
|
<source>Quiz Added!</source>
|
||||||
<target>Test toegevoegd!</target>
|
<target>Test toegevoegd!</target>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
|
<trans-unit id="vXN8b2w" resname="Quiz cleared">
|
||||||
|
<source>Quiz cleared</source>
|
||||||
|
<target>Test leeggemaakt</target>
|
||||||
|
</trans-unit>
|
||||||
<trans-unit id="LbVe.2c" resname="Quiz completed">
|
<trans-unit id="LbVe.2c" resname="Quiz completed">
|
||||||
<source>Quiz completed</source>
|
<source>Quiz completed</source>
|
||||||
<target>Test voltooid</target>
|
<target>Test voltooid</target>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
|
<trans-unit id="XdfTTMD" resname="Quiz deleted">
|
||||||
|
<source>Quiz deleted</source>
|
||||||
|
<target>Test verwijderd</target>
|
||||||
|
</trans-unit>
|
||||||
<trans-unit id="frxoIkW" resname="Quiz name">
|
<trans-unit id="frxoIkW" resname="Quiz name">
|
||||||
<source>Quiz name</source>
|
<source>Quiz name</source>
|
||||||
<target>Testnaam</target>
|
<target>Testnaam</target>
|
||||||
@@ -237,10 +257,6 @@
|
|||||||
<source>Sign in</source>
|
<source>Sign in</source>
|
||||||
<target>Log in</target>
|
<target>Log in</target>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="2QO7aYC" resname="Start Elimination">
|
|
||||||
<source>Start Elimination</source>
|
|
||||||
<target>Start eliminatie</target>
|
|
||||||
</trans-unit>
|
|
||||||
<trans-unit id="9m8DOBg" resname="Submit">
|
<trans-unit id="9m8DOBg" resname="Submit">
|
||||||
<source>Submit</source>
|
<source>Submit</source>
|
||||||
<target>Verstuur</target>
|
<target>Verstuur</target>
|
||||||
@@ -253,10 +269,18 @@
|
|||||||
<source>There are no answers for this question</source>
|
<source>There are no answers for this question</source>
|
||||||
<target>Er zijn geen antwoorden voor deze vraag</target>
|
<target>Er zijn geen antwoorden voor deze vraag</target>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
|
<trans-unit id=".LrcTyU" resname="There is no active quiz">
|
||||||
|
<source>There is no active quiz</source>
|
||||||
|
<target>Er is geen test actief</target>
|
||||||
|
</trans-unit>
|
||||||
<trans-unit id="Dptvysv" resname="Time">
|
<trans-unit id="Dptvysv" resname="Time">
|
||||||
<source>Time</source>
|
<source>Time</source>
|
||||||
<target>Tijd</target>
|
<target>Tijd</target>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
|
<trans-unit id="0afY1NF" resname="You have no seasons yet.">
|
||||||
|
<source>You have no seasons yet.</source>
|
||||||
|
<target>Je hebt nog geen seizoenen.</target>
|
||||||
|
</trans-unit>
|
||||||
<trans-unit id="vVQAP9A" resname="Your Seasons">
|
<trans-unit id="vVQAP9A" resname="Your Seasons">
|
||||||
<source>Your Seasons</source>
|
<source>Your Seasons</source>
|
||||||
<target>Jouw seizoenen</target>
|
<target>Jouw seizoenen</target>
|
||||||
|
|||||||
Reference in New Issue
Block a user