mirror of
https://github.com/MarijnDoeve/TijdVoorDeTest.git
synced 2026-03-06 04:44:19 +01:00
Add settings management for seasons, update templates, migrations, and commands
This commit is contained in:
48
migrations/Version20250610210417.php
Normal file
48
migrations/Version20250610210417.php
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace DoctrineMigrations;
|
||||||
|
|
||||||
|
use Doctrine\DBAL\Schema\Schema;
|
||||||
|
use Doctrine\Migrations\AbstractMigration;
|
||||||
|
|
||||||
|
final class Version20250610210417 extends AbstractMigration
|
||||||
|
{
|
||||||
|
public function getDescription(): string
|
||||||
|
{
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function up(Schema $schema): void
|
||||||
|
{
|
||||||
|
$this->addSql(<<<'SQL'
|
||||||
|
CREATE TABLE season_settings (id UUID NOT NULL, show_numbers BOOLEAN DEFAULT false NOT NULL, confirm_answers BOOLEAN DEFAULT false NOT NULL, PRIMARY KEY(id))
|
||||||
|
SQL);
|
||||||
|
$this->addSql(<<<'SQL'
|
||||||
|
ALTER TABLE season ADD settings_id UUID DEFAULT NULL
|
||||||
|
SQL);
|
||||||
|
$this->addSql(<<<'SQL'
|
||||||
|
ALTER TABLE season ADD CONSTRAINT FK_F0E45BA959949888 FOREIGN KEY (settings_id) REFERENCES season_settings (id) NOT DEFERRABLE INITIALLY IMMEDIATE
|
||||||
|
SQL);
|
||||||
|
$this->addSql(<<<'SQL'
|
||||||
|
CREATE UNIQUE INDEX UNIQ_F0E45BA959949888 ON season (settings_id)
|
||||||
|
SQL);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(Schema $schema): void
|
||||||
|
{
|
||||||
|
$this->addSql(<<<'SQL'
|
||||||
|
DROP TABLE season_settings
|
||||||
|
SQL);
|
||||||
|
$this->addSql(<<<'SQL'
|
||||||
|
ALTER TABLE season DROP CONSTRAINT FK_F0E45BA959949888
|
||||||
|
SQL);
|
||||||
|
$this->addSql(<<<'SQL'
|
||||||
|
DROP INDEX UNIQ_F0E45BA959949888
|
||||||
|
SQL);
|
||||||
|
$this->addSql(<<<'SQL'
|
||||||
|
ALTER TABLE season DROP settings_id
|
||||||
|
SQL);
|
||||||
|
}
|
||||||
|
}
|
||||||
40
src/Command/AddSettingsCommand.php
Normal file
40
src/Command/AddSettingsCommand.php
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Command;
|
||||||
|
|
||||||
|
use App\Entity\SeasonSettings;
|
||||||
|
use App\Repository\SeasonRepository;
|
||||||
|
use Doctrine\ORM\EntityManagerInterface;
|
||||||
|
use Symfony\Component\Console\Attribute\AsCommand;
|
||||||
|
use Symfony\Component\Console\Command\Command;
|
||||||
|
use Symfony\Component\Console\Input\InputInterface;
|
||||||
|
use Symfony\Component\Console\Output\OutputInterface;
|
||||||
|
use Symfony\Component\Console\Style\SymfonyStyle;
|
||||||
|
|
||||||
|
#[AsCommand(
|
||||||
|
name: 'app:add-settings',
|
||||||
|
description: 'Add a short description for your command',
|
||||||
|
)]
|
||||||
|
readonly class AddSettingsCommand
|
||||||
|
{
|
||||||
|
public function __construct(private SeasonRepository $seasonRepository, private EntityManagerInterface $entityManager) {}
|
||||||
|
|
||||||
|
public function __invoke(InputInterface $input, OutputInterface $output): int
|
||||||
|
{
|
||||||
|
$io = new SymfonyStyle($input, $output);
|
||||||
|
|
||||||
|
foreach ($this->seasonRepository->findAll() as $season) {
|
||||||
|
if (null !== $season->getSettings()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$io->text('Adding settings to season : '.$season->getSeasonCode());
|
||||||
|
$season->setSettings(new SeasonSettings());
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->entityManager->flush();
|
||||||
|
|
||||||
|
return Command::SUCCESS;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -10,6 +10,7 @@ use App\Entity\Quiz;
|
|||||||
use App\Entity\Season;
|
use App\Entity\Season;
|
||||||
use App\Enum\FlashType;
|
use App\Enum\FlashType;
|
||||||
use App\Form\AddCandidatesFormType;
|
use App\Form\AddCandidatesFormType;
|
||||||
|
use App\Form\SettingsForm;
|
||||||
use App\Form\UploadQuizFormType;
|
use App\Form\UploadQuizFormType;
|
||||||
use App\Security\Voter\SeasonVoter;
|
use App\Security\Voter\SeasonVoter;
|
||||||
use App\Service\QuizSpreadsheetService;
|
use App\Service\QuizSpreadsheetService;
|
||||||
@@ -26,7 +27,9 @@ 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 readonly EntityManagerInterface $em,
|
public function __construct(
|
||||||
|
private readonly TranslatorInterface $translator,
|
||||||
|
private readonly EntityManagerInterface $em,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
#[Route(
|
#[Route(
|
||||||
@@ -35,10 +38,19 @@ class SeasonController extends AbstractController
|
|||||||
requirements: ['seasonCode' => self::SEASON_CODE_REGEX],
|
requirements: ['seasonCode' => self::SEASON_CODE_REGEX],
|
||||||
)]
|
)]
|
||||||
#[IsGranted(SeasonVoter::EDIT, subject: 'season')]
|
#[IsGranted(SeasonVoter::EDIT, subject: 'season')]
|
||||||
public function index(Season $season): Response
|
public function index(Season $season, Request $request): Response
|
||||||
{
|
{
|
||||||
|
$form = $this->createForm(SettingsForm::class, $season->getSettings());
|
||||||
|
|
||||||
|
$form->handleRequest($request);
|
||||||
|
|
||||||
|
if ($form->isSubmitted() && $form->isValid()) {
|
||||||
|
$this->em->flush();
|
||||||
|
}
|
||||||
|
|
||||||
return $this->render('backoffice/season.html.twig', [
|
return $this->render('backoffice/season.html.twig', [
|
||||||
'season' => $season,
|
'season' => $season,
|
||||||
|
'form' => $form,
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -125,6 +125,6 @@ final class QuizController extends AbstractController
|
|||||||
|
|
||||||
$quizCandidateRepository->createIfNotExist($quiz, $candidate);
|
$quizCandidateRepository->createIfNotExist($quiz, $candidate);
|
||||||
|
|
||||||
return $this->render('quiz/question.twig', ['candidate' => $candidate, 'question' => $question]);
|
return $this->render('quiz/question.twig', ['candidate' => $candidate, 'question' => $question, 'season' => $season]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -46,8 +46,13 @@ class Season
|
|||||||
#[ORM\JoinColumn(nullable: true, onDelete: 'SET NULL')]
|
#[ORM\JoinColumn(nullable: true, onDelete: 'SET NULL')]
|
||||||
private ?Quiz $ActiveQuiz = null;
|
private ?Quiz $ActiveQuiz = null;
|
||||||
|
|
||||||
|
#[ORM\OneToOne(cascade: ['persist', 'remove'])]
|
||||||
|
#[ORM\JoinColumn(nullable: true)]
|
||||||
|
private ?SeasonSettings $settings = null;
|
||||||
|
|
||||||
public function __construct()
|
public function __construct()
|
||||||
{
|
{
|
||||||
|
$this->settings = new SeasonSettings();
|
||||||
$this->quizzes = new ArrayCollection();
|
$this->quizzes = new ArrayCollection();
|
||||||
$this->candidates = new ArrayCollection();
|
$this->candidates = new ArrayCollection();
|
||||||
$this->owners = new ArrayCollection();
|
$this->owners = new ArrayCollection();
|
||||||
@@ -166,4 +171,16 @@ class Season
|
|||||||
|
|
||||||
return $this;
|
return $this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function getSettings(): ?SeasonSettings
|
||||||
|
{
|
||||||
|
return $this->settings;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setSettings(SeasonSettings $settings): static
|
||||||
|
{
|
||||||
|
$this->settings = $settings;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
57
src/Entity/SeasonSettings.php
Normal file
57
src/Entity/SeasonSettings.php
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Entity;
|
||||||
|
|
||||||
|
use App\Repository\SeasonSettingsRepository;
|
||||||
|
use Doctrine\DBAL\Types\Types;
|
||||||
|
use Doctrine\ORM\Mapping as ORM;
|
||||||
|
use Symfony\Bridge\Doctrine\IdGenerator\UuidGenerator;
|
||||||
|
use Symfony\Bridge\Doctrine\Types\UuidType;
|
||||||
|
use Symfony\Component\Uid\Uuid;
|
||||||
|
|
||||||
|
#[ORM\Entity(repositoryClass: SeasonSettingsRepository::class)]
|
||||||
|
class SeasonSettings
|
||||||
|
{
|
||||||
|
#[ORM\Id]
|
||||||
|
#[ORM\GeneratedValue(strategy: 'CUSTOM')]
|
||||||
|
#[ORM\Column(type: UuidType::NAME)]
|
||||||
|
#[ORM\CustomIdGenerator(class: UuidGenerator::class)]
|
||||||
|
private Uuid $id;
|
||||||
|
|
||||||
|
#[ORM\Column(type: Types::BOOLEAN, options: ['default' => false])]
|
||||||
|
private bool $showNumbers = false;
|
||||||
|
|
||||||
|
#[ORM\Column(type: Types::BOOLEAN, options: ['default' => false])]
|
||||||
|
private bool $confirmAnswers = false;
|
||||||
|
|
||||||
|
public function getId(): Uuid
|
||||||
|
{
|
||||||
|
return $this->id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function isShowNumbers(): bool
|
||||||
|
{
|
||||||
|
return $this->showNumbers;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setShowNumbers(bool $showNumbers): self
|
||||||
|
{
|
||||||
|
$this->showNumbers = $showNumbers;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function isConfirmAnswers(): bool
|
||||||
|
{
|
||||||
|
return $this->confirmAnswers;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setConfirmAnswers(bool $confirmAnswers): self
|
||||||
|
{
|
||||||
|
$this->confirmAnswers = $confirmAnswers;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
}
|
||||||
35
src/Form/SettingsForm.php
Normal file
35
src/Form/SettingsForm.php
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Form;
|
||||||
|
|
||||||
|
use App\Entity\SeasonSettings;
|
||||||
|
use Symfony\Component\Form\AbstractType;
|
||||||
|
use Symfony\Component\Form\Extension\Core\Type\SubmitType;
|
||||||
|
use Symfony\Component\Form\FormBuilderInterface;
|
||||||
|
use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||||
|
|
||||||
|
/** @extends AbstractType<SeasonSettings> */
|
||||||
|
class SettingsForm extends AbstractType
|
||||||
|
{
|
||||||
|
public function buildForm(FormBuilderInterface $builder, array $options): void
|
||||||
|
{
|
||||||
|
$builder
|
||||||
|
->add('showNumbers', options: [
|
||||||
|
'label_attr' => ['class' => 'checkbox-switch'],
|
||||||
|
'attr' => ['role' => 'switch', 'switch' => null]])
|
||||||
|
->add('confirmAnswers', options: [
|
||||||
|
'label_attr' => ['class' => 'checkbox-switch'],
|
||||||
|
'attr' => ['role' => 'switch', 'switch' => null]])
|
||||||
|
->add('save', SubmitType::class)
|
||||||
|
;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function configureOptions(OptionsResolver $resolver): void
|
||||||
|
{
|
||||||
|
$resolver->setDefaults([
|
||||||
|
'data_class' => SeasonSettings::class,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
20
src/Repository/SeasonSettingsRepository.php
Normal file
20
src/Repository/SeasonSettingsRepository.php
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Repository;
|
||||||
|
|
||||||
|
use App\Entity\SeasonSettings;
|
||||||
|
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
|
||||||
|
use Doctrine\Persistence\ManagerRegistry;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @extends ServiceEntityRepository<SeasonSettings>
|
||||||
|
*/
|
||||||
|
class SeasonSettingsRepository extends ServiceEntityRepository
|
||||||
|
{
|
||||||
|
public function __construct(ManagerRegistry $registry)
|
||||||
|
{
|
||||||
|
parent::__construct($registry, SeasonSettings::class);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -21,13 +21,18 @@
|
|||||||
<div class="d-flex flex-row align-items-center">
|
<div class="d-flex flex-row align-items-center">
|
||||||
<h4 class="py-2 pe-2">{{ 'Candidates'|trans }}</h4>
|
<h4 class="py-2 pe-2">{{ 'Candidates'|trans }}</h4>
|
||||||
<a class="link"
|
<a class="link"
|
||||||
href="{{ path('app_backoffice_add_candidates', {seasonCode: season.seasonCode}) }}">{{ 'Add Candidate'|trans }}</a>
|
href="{{ path('app_backoffice_add_candidates', {seasonCode: season.seasonCode}) }}">{{ 'Add Candidate'|trans }}
|
||||||
|
</a>
|
||||||
</div>
|
</div>
|
||||||
<ul>
|
<ul>
|
||||||
{% for candidate in season.candidates %}
|
{% for candidate in season.candidates %}
|
||||||
<li>{{ candidate.name }}</li>{% endfor %}
|
<li>{{ candidate.name }}</li>{% endfor %}
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
|
<div class="d-flex flex-row align-items-center">
|
||||||
|
<h4 class="py-2 pe-2">{{ 'Settings'|trans }}</h4>
|
||||||
|
</div>
|
||||||
|
{{ form(form) }}
|
||||||
</div>
|
</div>
|
||||||
<div class="col-12 col-md-3"></div>
|
<div class="col-12 col-md-3"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,17 +1,35 @@
|
|||||||
{% extends 'quiz/base.html.twig' %}
|
{% extends 'quiz/base.html.twig' %}
|
||||||
{% block body %}
|
{% block body %}
|
||||||
{{ question.question }}<br/>
|
<h4>
|
||||||
|
{% if season.settings.showNumbers %}
|
||||||
|
({{ question.ordering }}/{{ question.quiz.questions.count }})
|
||||||
|
{% endif %}{{ question.question }}<br/>
|
||||||
|
</h4>
|
||||||
<form method="post">
|
<form method="post">
|
||||||
<input type="hidden" name="token" value="{{ csrf_token('question') }}">
|
<input type="hidden" name="token" value="{{ csrf_token('question') }}">
|
||||||
{% for answer in question.answers %}
|
{% if season.settings.confirmAnswers == false %}
|
||||||
<div>
|
{% for answer in question.answers %}
|
||||||
<button class="btn btn-outline-success"
|
<div class="py-2">
|
||||||
type="submit"
|
<button class="btn btn-outline-success"
|
||||||
name="answer"
|
type="submit"
|
||||||
value="{{ answer.id }}">{{ answer.text }}</button>
|
name="answer"
|
||||||
</div>
|
value="{{ answer.id }}">{{ answer.text }}</button>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
{% else %}
|
{% else %}
|
||||||
Weirdly enough this question has no answers...
|
{% for answer in question.answers %}
|
||||||
{% endfor %}
|
<div class="py-1">
|
||||||
|
<input type="radio" class="btn-check" name="answer" id="answer-{{ loop.index0 }}" autocomplete="off"
|
||||||
|
value="{{ answer.id }}">
|
||||||
|
<label class="btn btn-outline-secondary" for="answer-{{ loop.index0 }}">{{ answer.text }}</label>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
<div class="py-2">
|
||||||
|
<button class="btn btn-success"
|
||||||
|
type="submit"
|
||||||
|
>{{ 'Next'|trans }}</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% endif %}
|
||||||
</form>
|
</form>
|
||||||
{% endblock body %}
|
{% endblock body %}
|
||||||
|
|||||||
@@ -137,6 +137,10 @@
|
|||||||
<source>Name</source>
|
<source>Name</source>
|
||||||
<target>Naam</target>
|
<target>Naam</target>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
|
<trans-unit id="gefhnBC" resname="Next">
|
||||||
|
<source>Next</source>
|
||||||
|
<target>Volgende</target>
|
||||||
|
</trans-unit>
|
||||||
<trans-unit id="nOHriCl" resname="No active quiz">
|
<trans-unit id="nOHriCl" resname="No active quiz">
|
||||||
<source>No active quiz</source>
|
<source>No active quiz</source>
|
||||||
<target>Geen actieve test</target>
|
<target>Geen actieve test</target>
|
||||||
@@ -253,6 +257,10 @@
|
|||||||
<source>Seasons</source>
|
<source>Seasons</source>
|
||||||
<target>Seizoenen</target>
|
<target>Seizoenen</target>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
|
<trans-unit id="VXFwlwn" resname="Settings">
|
||||||
|
<source>Settings</source>
|
||||||
|
<target>Instellingen</target>
|
||||||
|
</trans-unit>
|
||||||
<trans-unit id="pNIxNSX" resname="Sign in">
|
<trans-unit id="pNIxNSX" resname="Sign in">
|
||||||
<source>Sign in</source>
|
<source>Sign in</source>
|
||||||
<target>Log in</target>
|
<target>Log in</target>
|
||||||
|
|||||||
Reference in New Issue
Block a user