mirror of
https://github.com/MarijnDoeve/TijdVoorDeTest.git
synced 2026-03-06 04:44:19 +01:00
Add Sheet upload function
This commit is contained in:
@@ -4,19 +4,28 @@ declare(strict_types=1);
|
||||
|
||||
namespace App\Controller;
|
||||
|
||||
use App\Entity\Candidate;
|
||||
use App\Entity\Quiz;
|
||||
use App\Entity\Season;
|
||||
use App\Entity\User;
|
||||
use App\Enum\FlashType;
|
||||
use App\Form\AddCandidatesFormType;
|
||||
use App\Form\CreateSeasonFormType;
|
||||
use App\Form\UploadQuizFormType;
|
||||
use App\Repository\CandidateRepository;
|
||||
use App\Repository\SeasonRepository;
|
||||
use App\Security\Voter\SeasonVoter;
|
||||
use App\Service\QuizSpreadsheetService;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Symfony\Bundle\SecurityBundle\Security;
|
||||
use Symfony\Component\HttpFoundation\File\UploadedFile;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\HttpFoundation\StreamedResponse;
|
||||
use Symfony\Component\HttpKernel\Attribute\AsController;
|
||||
use Symfony\Component\Routing\Attribute\Route;
|
||||
use Symfony\Component\Security\Http\Attribute\IsGranted;
|
||||
use Symfony\Component\Serializer\SerializerInterface;
|
||||
use Symfony\Contracts\Translation\TranslatorInterface;
|
||||
|
||||
#[AsController]
|
||||
#[IsGranted('ROLE_USER')]
|
||||
@@ -26,6 +35,7 @@ final class BackofficeController extends AbstractController
|
||||
private readonly SeasonRepository $seasonRepository,
|
||||
private readonly CandidateRepository $candidateRepository,
|
||||
private readonly Security $security,
|
||||
private readonly TranslatorInterface $translator,
|
||||
) {}
|
||||
|
||||
#[Route('/backoffice/', name: 'app_backoffice_index')]
|
||||
@@ -43,6 +53,30 @@ final class BackofficeController extends AbstractController
|
||||
]);
|
||||
}
|
||||
|
||||
#[Route('/backoffice/add', name: 'app_backoffice_season_add', priority: 10)]
|
||||
public function seasonAdd(Request $request, EntityManagerInterface $em): Response
|
||||
{
|
||||
$season = new Season();
|
||||
$form = $this->createForm(CreateSeasonFormType::class, $season);
|
||||
|
||||
$form->handleRequest($request);
|
||||
|
||||
if ($form->isSubmitted() && $form->isValid()) {
|
||||
$user = $this->getUser();
|
||||
\assert($user instanceof User);
|
||||
|
||||
$season->addOwner($user);
|
||||
$season->generateSeasonCode();
|
||||
|
||||
$em->persist($season);
|
||||
$em->flush();
|
||||
|
||||
return $this->redirectToRoute('app_backoffice_season', ['seasonCode' => $season->getSeasonCode()]);
|
||||
}
|
||||
|
||||
return $this->render('backoffice/season_add.html.twig', ['form' => $form]);
|
||||
}
|
||||
|
||||
#[Route('/backoffice/{seasonCode}', name: 'app_backoffice_season')]
|
||||
#[IsGranted(SeasonVoter::EDIT, subject: 'season')]
|
||||
public function season(Season $season): Response
|
||||
@@ -72,9 +106,58 @@ final class BackofficeController extends AbstractController
|
||||
return $this->redirectToRoute('app_backoffice_season', ['seasonCode' => $season->getSeasonCode()]);
|
||||
}
|
||||
|
||||
#[Route('/backoffice/{seasonCode}/{quiz}/yaml')]
|
||||
public function testRoute(Season $season, Quiz $quiz, SerializerInterface $serializer): Response
|
||||
#[Route('/backoffice/{seasonCode}/add_candidate', name: 'app_backoffice_add_candidates', priority: 10)]
|
||||
public function addCandidates(Season $season, Request $request, EntityManagerInterface $em): Response
|
||||
{
|
||||
return new Response($serializer->serialize(\App\Resource\Quiz::fromEntity($quiz)->questions, 'yaml', ['yaml_inline' => 100, 'yaml_flags' => 0]), headers: ['Content-Type' => 'text/yaml']);
|
||||
$form = $this->createForm(AddCandidatesFormType::class);
|
||||
$form->handleRequest($request);
|
||||
|
||||
if ($form->isSubmitted() && $form->isValid()) {
|
||||
$candidates = $form->get('candidates')->getData();
|
||||
foreach (explode("\r\n", $candidates) as $candidate) {
|
||||
$season->addCandidate(new Candidate($candidate));
|
||||
}
|
||||
$em->flush();
|
||||
|
||||
return $this->redirectToRoute('app_backoffice_season', ['seasonCode' => $season->getSeasonCode()]);
|
||||
}
|
||||
|
||||
return $this->render('backoffice/season_add_candidates.html.twig', ['form' => $form]);
|
||||
}
|
||||
|
||||
#[Route('/backoffice/{seasonCode}/add', name: 'app_backoffice_quiz_add', priority: 10)]
|
||||
public function addQuiz(Request $request, Season $season, QuizSpreadsheetService $quizSpreadsheet, EntityManagerInterface $em): Response
|
||||
{
|
||||
$quiz = new Quiz();
|
||||
$form = $this->createForm(UploadQuizFormType::class, $quiz);
|
||||
|
||||
$form->handleRequest($request);
|
||||
|
||||
if ($form->isSubmitted() && $form->isValid()) {
|
||||
/* @var UploadedFile $sheet */
|
||||
$sheet = $form->get('sheet')->getData();
|
||||
|
||||
$quizSpreadsheet->xlsxToQuiz($quiz, $sheet);
|
||||
|
||||
$quiz->setSeason($season);
|
||||
$em->persist($quiz);
|
||||
$em->flush();
|
||||
|
||||
$this->addFlash(FlashType::Success, $this->translator->trans('Quiz Added!'));
|
||||
|
||||
return $this->redirectToRoute('app_backoffice_season', ['seasonCode' => $season->getSeasonCode()]);
|
||||
}
|
||||
|
||||
return $this->render('/backoffice/quiz_add.html.twig', ['form' => $form, 'season' => $season]);
|
||||
}
|
||||
|
||||
#[Route('/backoffice/template', name: 'app_backoffice_template', priority: 10)]
|
||||
public function getTemplate(QuizSpreadsheetService $excel): Response
|
||||
{
|
||||
$response = new StreamedResponse($excel->generateTemplate());
|
||||
$response->headers->set('Content-Type', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet');
|
||||
$response->headers->set('Content-Disposition', 'attachment; filename="template.xlsx"');
|
||||
|
||||
return $response;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -52,6 +52,7 @@ class KrtekFixtures extends Fixture
|
||||
->setQuestion('Is de Krtek een man of een vrouw?')
|
||||
->addAnswer(new Answer('Vrouw', true))
|
||||
->addAnswer(new Answer('Man'))
|
||||
->setOrdering(1)
|
||||
)
|
||||
|
||||
->addQuestion((new Question())
|
||||
@@ -59,6 +60,7 @@ class KrtekFixtures extends Fixture
|
||||
->addAnswer(new Answer('Geen', true))
|
||||
->addAnswer(new Answer('1'))
|
||||
->addAnswer(new Answer('2'))
|
||||
->setOrdering(2)
|
||||
)
|
||||
|
||||
->addQuestion((new Question())
|
||||
@@ -68,12 +70,14 @@ class KrtekFixtures extends Fixture
|
||||
->addAnswer(new Answer('Koningsdag'))
|
||||
->addAnswer(new Answer('Kerst', true))
|
||||
->addAnswer(new Answer('Oud en Nieuw'))
|
||||
->setOrdering(3)
|
||||
)
|
||||
|
||||
->addQuestion((new Question())
|
||||
->setQuestion('Hoe kwam de Krtek naar Kersteren vandaag?')
|
||||
->addAnswer(new Answer('Met het OV', true))
|
||||
->addAnswer(new Answer('Met de auto'))
|
||||
->setOrdering(4)
|
||||
)
|
||||
->addQuestion((new Question())
|
||||
->setQuestion('Met wie keek de Krtek video bij binnenkomst?')
|
||||
@@ -90,6 +94,7 @@ class KrtekFixtures extends Fixture
|
||||
->addAnswer(new Answer('Remy'))
|
||||
->addAnswer(new Answer('Robbert'))
|
||||
->addAnswer(new Answer('Tom', true))
|
||||
->setOrdering(5)
|
||||
)
|
||||
|
||||
->addQuestion((new Question())
|
||||
@@ -102,6 +107,7 @@ class KrtekFixtures extends Fixture
|
||||
->addAnswer(new Answer('Probeer ook eens buiten de lijntjes te kleuren', true))
|
||||
->addAnswer(new Answer('Ga als je groot bent op groepsreis! '))
|
||||
->addAnswer(new Answer('Trek minder aan van de mening van anderen, het is oké om anders te zijn.'))
|
||||
->setOrdering(6)
|
||||
)
|
||||
|
||||
->addQuestion((new Question())
|
||||
@@ -112,6 +118,7 @@ class KrtekFixtures extends Fixture
|
||||
->addAnswer(new Answer('Pantoffels'))
|
||||
->addAnswer(new Answer('Hakken'))
|
||||
->addAnswer(new Answer('Geen schoenen, alleen sokken'))
|
||||
->setOrdering(7)
|
||||
)
|
||||
|
||||
->addQuestion((new Question())
|
||||
@@ -119,12 +126,14 @@ class KrtekFixtures extends Fixture
|
||||
->addAnswer(new Answer('Fiets', true))
|
||||
->addAnswer(new Answer('Auto'))
|
||||
->addAnswer(new Answer('Trein'))
|
||||
->setOrdering(8)
|
||||
)
|
||||
|
||||
->addQuestion((new Question())
|
||||
->setQuestion('Heeft de Krtek een eigen auto?')
|
||||
->addAnswer(new Answer('Ja'))
|
||||
->addAnswer(new Answer('Nee', true))
|
||||
->setOrdering(9)
|
||||
)
|
||||
|
||||
->addQuestion((new Question())
|
||||
@@ -144,12 +153,14 @@ class KrtekFixtures extends Fixture
|
||||
->addAnswer(new Answer('Pieter'))
|
||||
->addAnswer(new Answer('Renée Fokker'))
|
||||
->addAnswer(new Answer('Sam, Davy', true))
|
||||
->setOrdering(10)
|
||||
)
|
||||
|
||||
->addQuestion((new Question())
|
||||
->setQuestion('Zou de Krtek molboekjes, jokers, vrijstellingen of topito’s uit iemands rugzak stelen om te kunnen winnen?')
|
||||
->addAnswer(new Answer('Ja'))
|
||||
->addAnswer(new Answer('Nee', true))
|
||||
->setOrdering(11)
|
||||
)
|
||||
|
||||
->addQuestion((new Question())
|
||||
@@ -157,6 +168,7 @@ class KrtekFixtures extends Fixture
|
||||
->addAnswer(new Answer('Éénpersoons, losstaand bed'))
|
||||
->addAnswer(new Answer('Éénpersoonsbed, tegen een ander bed aan', true))
|
||||
->addAnswer(new Answer('Tweepersoons bed'))
|
||||
->setOrdering(12)
|
||||
)
|
||||
|
||||
->addQuestion((new Question())
|
||||
@@ -165,12 +177,14 @@ class KrtekFixtures extends Fixture
|
||||
->addAnswer(new Answer('6', true))
|
||||
->addAnswer(new Answer('7'))
|
||||
->addAnswer(new Answer('8'))
|
||||
->setOrdering(13)
|
||||
)
|
||||
|
||||
->addQuestion((new Question())
|
||||
->setQuestion('Waar zat de Krtek aan tafel bij het diner?')
|
||||
->addAnswer(new Answer('Met de rug naar de accommodatie'))
|
||||
->addAnswer(new Answer('Met de rug naar de buitenmuur', true))
|
||||
->setOrdering(14)
|
||||
)
|
||||
|
||||
->addQuestion((new Question())
|
||||
@@ -188,6 +202,7 @@ class KrtekFixtures extends Fixture
|
||||
->addAnswer(new Answer('Remy'))
|
||||
->addAnswer(new Answer('Robbert'))
|
||||
->addAnswer(new Answer('Tom'))
|
||||
->setOrdering(15)
|
||||
)
|
||||
;
|
||||
}
|
||||
@@ -202,6 +217,7 @@ class KrtekFixtures extends Fixture
|
||||
->setQuestion('Is de Krtek een man of een vrouw?')
|
||||
->addAnswer(new Answer('Man'))
|
||||
->addAnswer(new Answer('Vrouw', true))
|
||||
->setOrdering(1)
|
||||
)
|
||||
|
||||
->addQuestion((new Question())
|
||||
@@ -213,6 +229,7 @@ class KrtekFixtures extends Fixture
|
||||
->addAnswer(new Answer('De Krtek heeft een intolerantie'))
|
||||
->addAnswer(new Answer('De Krtek eet geen rundvlees'))
|
||||
->addAnswer(new Answer('De Krtek eet geen waterdieren'))
|
||||
->setOrdering(2)
|
||||
)
|
||||
|
||||
->addQuestion((new Question())
|
||||
@@ -224,6 +241,7 @@ class KrtekFixtures extends Fixture
|
||||
->addAnswer(new Answer('Tom'))
|
||||
->addAnswer(new Answer('De huisdieren van de Krtek hebben geen naam'))
|
||||
->addAnswer(new Answer('De Krtek heeft geen huisdieren', true))
|
||||
->setOrdering(3)
|
||||
)
|
||||
|
||||
->addQuestion((new Question())
|
||||
@@ -234,6 +252,7 @@ class KrtekFixtures extends Fixture
|
||||
->addAnswer(new Answer('Melk'))
|
||||
->addAnswer(new Answer('Sap'))
|
||||
->addAnswer(new Answer('Niks'))
|
||||
->setOrdering(4)
|
||||
)
|
||||
|
||||
->addQuestion((new Question())
|
||||
@@ -245,6 +264,7 @@ class KrtekFixtures extends Fixture
|
||||
->addAnswer(new Answer('Oostenrijk'))
|
||||
->addAnswer(new Answer('Turkije'))
|
||||
->addAnswer(new Answer('Zweden', true))
|
||||
->setOrdering(5)
|
||||
)
|
||||
|
||||
->addQuestion((new Question())
|
||||
@@ -254,6 +274,7 @@ class KrtekFixtures extends Fixture
|
||||
->addAnswer(new Answer('Het derde groepje'))
|
||||
->addAnswer(new Answer('Het vierde groepje'))
|
||||
->addAnswer(new Answer('Het vijfde groepje'))
|
||||
->setOrdering(6)
|
||||
)
|
||||
|
||||
->addQuestion((new Question())
|
||||
@@ -262,12 +283,14 @@ class KrtekFixtures extends Fixture
|
||||
->addAnswer(new Answer('Het universum', true))
|
||||
->addAnswer(new Answer('Toeval'))
|
||||
->addAnswer(new Answer('De Krtek is hindoeïstisch'))
|
||||
->setOrdering(7)
|
||||
)
|
||||
|
||||
->addQuestion((new Question())
|
||||
->setQuestion('At de Krtek op vrijdagavond heksenkaas tijdens het diner?')
|
||||
->addAnswer(new Answer('Ja', true))
|
||||
->addAnswer(new Answer('Nee'))
|
||||
->setOrdering(8)
|
||||
)
|
||||
|
||||
->addQuestion((new Question())
|
||||
@@ -276,6 +299,7 @@ class KrtekFixtures extends Fixture
|
||||
->addAnswer(new Answer('Tussen 1:00 en 1:59 uur', true))
|
||||
->addAnswer(new Answer('Tussen 2:00 en 2:59 uur'))
|
||||
->addAnswer(new Answer('Na 3:00'))
|
||||
->setOrdering(9)
|
||||
)
|
||||
|
||||
->addQuestion((new Question())
|
||||
@@ -284,6 +308,7 @@ class KrtekFixtures extends Fixture
|
||||
->addAnswer(new Answer('2'))
|
||||
->addAnswer(new Answer('3'))
|
||||
->addAnswer(new Answer('geen', true))
|
||||
->setOrdering(10)
|
||||
)
|
||||
|
||||
->addQuestion((new Question())
|
||||
@@ -294,6 +319,7 @@ class KrtekFixtures extends Fixture
|
||||
->addAnswer(new Answer('Sesamstraat'))
|
||||
->addAnswer(new Answer('Spongebob Squarepants'))
|
||||
->addAnswer(new Answer('Teletubbies'))
|
||||
->setOrdering(11)
|
||||
)
|
||||
|
||||
->addQuestion((new Question())
|
||||
@@ -301,6 +327,7 @@ class KrtekFixtures extends Fixture
|
||||
->addAnswer(new Answer('In koffer(s)', true))
|
||||
->addAnswer(new Answer('In losse tas(sen)'))
|
||||
->addAnswer(new Answer('In een rugzak'))
|
||||
->setOrdering(12)
|
||||
)
|
||||
|
||||
->addQuestion((new Question())
|
||||
@@ -313,12 +340,14 @@ class KrtekFixtures extends Fixture
|
||||
->addAnswer(new Answer('Servies dat tegen elkaar klettert'))
|
||||
->addAnswer(new Answer('Het geroekoe van een duif', true))
|
||||
->addAnswer(new Answer('Piepschuim'))
|
||||
->setOrdering(13)
|
||||
)
|
||||
|
||||
->addQuestion((new Question())
|
||||
->setQuestion('Wilde de Krtek penningmeester worden?')
|
||||
->addAnswer(new Answer('Ja'))
|
||||
->addAnswer(new Answer('Nee', true))
|
||||
->setOrdering(14)
|
||||
)
|
||||
|
||||
->addQuestion((new Question())
|
||||
@@ -336,6 +365,7 @@ class KrtekFixtures extends Fixture
|
||||
->addAnswer(new Answer('Remy'))
|
||||
->addAnswer(new Answer('Robbert'))
|
||||
->addAnswer(new Answer('Tom'))
|
||||
->setOrdering(15)
|
||||
)
|
||||
;
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ namespace App\Entity;
|
||||
use App\Repository\AnswerRepository;
|
||||
use Doctrine\Common\Collections\ArrayCollection;
|
||||
use Doctrine\Common\Collections\Collection;
|
||||
use Doctrine\DBAL\Types\Types;
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
use Symfony\Bridge\Doctrine\IdGenerator\UuidGenerator;
|
||||
use Symfony\Bridge\Doctrine\Types\UuidType;
|
||||
@@ -21,6 +22,9 @@ class Answer
|
||||
#[ORM\CustomIdGenerator(class: UuidGenerator::class)]
|
||||
private Uuid $id;
|
||||
|
||||
#[ORM\Column(type: Types::SMALLINT, options: ['default' => 0])]
|
||||
private int $ordering;
|
||||
|
||||
#[ORM\ManyToOne(inversedBy: 'answers')]
|
||||
#[ORM\JoinColumn(nullable: false)]
|
||||
private Question $question;
|
||||
@@ -121,4 +125,16 @@ class Answer
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getOrdering(): int
|
||||
{
|
||||
return $this->ordering;
|
||||
}
|
||||
|
||||
public function setOrdering(int $ordering): self
|
||||
{
|
||||
$this->ordering = $ordering;
|
||||
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ namespace App\Entity;
|
||||
use App\Repository\QuestionRepository;
|
||||
use Doctrine\Common\Collections\ArrayCollection;
|
||||
use Doctrine\Common\Collections\Collection;
|
||||
use Doctrine\DBAL\Types\Types;
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
use Symfony\Bridge\Doctrine\IdGenerator\UuidGenerator;
|
||||
use Symfony\Bridge\Doctrine\Types\UuidType;
|
||||
@@ -21,7 +22,10 @@ class Question
|
||||
#[ORM\CustomIdGenerator(class: UuidGenerator::class)]
|
||||
private ?Uuid $id = null;
|
||||
|
||||
#[ORM\Column(length: 255, nullable: false)]
|
||||
#[ORM\Column(type: Types::SMALLINT, options: ['default' => 0])]
|
||||
private int $ordering;
|
||||
|
||||
#[ORM\Column(type: Types::STRING, length: 255)]
|
||||
private string $question;
|
||||
|
||||
#[ORM\ManyToOne(inversedBy: 'questions')]
|
||||
@@ -33,6 +37,7 @@ class Question
|
||||
|
||||
/** @var Collection<int, Answer> */
|
||||
#[ORM\OneToMany(targetEntity: Answer::class, mappedBy: 'question', cascade: ['persist'], orphanRemoval: true)]
|
||||
#[ORM\OrderBy(['ordering' => 'ASC'])]
|
||||
private Collection $answers;
|
||||
|
||||
public function __construct()
|
||||
@@ -115,4 +120,16 @@ class Question
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public function getOrdering(): int
|
||||
{
|
||||
return $this->ordering;
|
||||
}
|
||||
|
||||
public function setOrdering(int $ordering): static
|
||||
{
|
||||
$this->ordering = $ordering;
|
||||
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,6 +30,7 @@ class Quiz
|
||||
|
||||
/** @var Collection<int, Question> */
|
||||
#[ORM\OneToMany(targetEntity: Question::class, mappedBy: 'quiz', cascade: ['persist'], orphanRemoval: true)]
|
||||
#[ORM\OrderBy(['ordering' => 'ASC'])]
|
||||
private Collection $questions;
|
||||
|
||||
/** @var Collection<int, Correction> */
|
||||
|
||||
@@ -15,6 +15,8 @@ use Symfony\Component\Uid\Uuid;
|
||||
#[ORM\Entity(repositoryClass: SeasonRepository::class)]
|
||||
class Season
|
||||
{
|
||||
private const string SEASON_CODE_CHARACTERS = 'bcdfghjklmnpqrstvwxz';
|
||||
|
||||
#[ORM\Id]
|
||||
#[ORM\Column(type: UuidType::NAME)]
|
||||
#[ORM\GeneratedValue(strategy: 'CUSTOM')]
|
||||
@@ -148,4 +150,18 @@ class Season
|
||||
{
|
||||
return $this->owners->contains($user);
|
||||
}
|
||||
|
||||
public function generateSeasonCode(): self
|
||||
{
|
||||
$code = '';
|
||||
$len = mb_strlen(self::SEASON_CODE_CHARACTERS) - 1;
|
||||
|
||||
for ($i = 0; $i < 5; ++$i) {
|
||||
$code .= self::SEASON_CODE_CHARACTERS[random_int(0, $len)];
|
||||
}
|
||||
|
||||
$this->seasonCode = $code;
|
||||
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
|
||||
18
src/Exception/SpreadsheetDataException.php
Normal file
18
src/Exception/SpreadsheetDataException.php
Normal file
@@ -0,0 +1,18 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Exception;
|
||||
|
||||
class SpreadsheetDataException extends SpreadsheetException
|
||||
{
|
||||
/** @param list<string> $errors */
|
||||
public function __construct(
|
||||
public readonly array $errors,
|
||||
string $message = '',
|
||||
int $code = 0,
|
||||
?\Throwable $previous = null,
|
||||
) {
|
||||
parent::__construct($message, $code, $previous);
|
||||
}
|
||||
}
|
||||
7
src/Exception/SpreadsheetException.php
Normal file
7
src/Exception/SpreadsheetException.php
Normal file
@@ -0,0 +1,7 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Exception;
|
||||
|
||||
class SpreadsheetException extends \Exception {}
|
||||
34
src/Form/CreateSeasonFormType.php
Normal file
34
src/Form/CreateSeasonFormType.php
Normal file
@@ -0,0 +1,34 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Form;
|
||||
|
||||
use App\Entity\Season;
|
||||
use Symfony\Component\Form\AbstractType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\TextType;
|
||||
use Symfony\Component\Form\FormBuilderInterface;
|
||||
use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||
use Symfony\Contracts\Translation\TranslatorInterface;
|
||||
|
||||
/** @extends AbstractType<Season> */
|
||||
class CreateSeasonFormType extends AbstractType
|
||||
{
|
||||
public function __construct(private readonly TranslatorInterface $translator) {}
|
||||
|
||||
public function buildForm(FormBuilderInterface $builder, array $options): void
|
||||
{
|
||||
$builder
|
||||
->add('name', TextType::class, [
|
||||
'label' => $this->translator->trans('Season Name'),
|
||||
])
|
||||
;
|
||||
}
|
||||
|
||||
public function configureOptions(OptionsResolver $resolver): void
|
||||
{
|
||||
$resolver->setDefaults([
|
||||
'data_class' => Season::class,
|
||||
]);
|
||||
}
|
||||
}
|
||||
50
src/Form/UploadQuizFormType.php
Normal file
50
src/Form/UploadQuizFormType.php
Normal file
@@ -0,0 +1,50 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Form;
|
||||
|
||||
use App\Entity\Quiz;
|
||||
use Symfony\Component\Form\AbstractType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\FileType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\TextType;
|
||||
use Symfony\Component\Form\FormBuilderInterface;
|
||||
use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||
use Symfony\Component\Validator\Constraints\File;
|
||||
use Symfony\Contracts\Translation\TranslatorInterface;
|
||||
|
||||
/** @extends AbstractType<Quiz> */
|
||||
class UploadQuizFormType extends AbstractType
|
||||
{
|
||||
public function __construct(private readonly TranslatorInterface $translator) {}
|
||||
|
||||
public function buildForm(FormBuilderInterface $builder, array $options): void
|
||||
{
|
||||
$builder
|
||||
->add('name', TextType::class, [
|
||||
'label' => $this->translator->trans('Quiz name'),
|
||||
])
|
||||
->add('sheet', FileType::class, [
|
||||
'label' => $this->translator->trans('Quiz (xlsx)'),
|
||||
'mapped' => false,
|
||||
'required' => true,
|
||||
'constraints' => [
|
||||
new File([
|
||||
'maxSize' => '1024k',
|
||||
'mimeTypes' => [
|
||||
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
||||
],
|
||||
'mimeTypesMessage' => $this->translator->trans('Please upload a valid XLSX file'),
|
||||
]),
|
||||
],
|
||||
])
|
||||
;
|
||||
}
|
||||
|
||||
public function configureOptions(OptionsResolver $resolver): void
|
||||
{
|
||||
$resolver->setDefaults([
|
||||
'data_class' => Quiz::class,
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -1,42 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Resource;
|
||||
|
||||
use App\Entity\Answer;
|
||||
use App\Entity\Quiz as QuizEntity;
|
||||
use Doctrine\Common\Collections\Collection;
|
||||
|
||||
final class Quiz
|
||||
{
|
||||
/** @param array<string, array<string, bool>> $questions*/
|
||||
public function __construct(
|
||||
public array $questions,
|
||||
) {}
|
||||
|
||||
public static function fromEntity(QuizEntity $quiz): self
|
||||
{
|
||||
$questions = [];
|
||||
|
||||
foreach ($quiz->getQuestions() as $question) {
|
||||
$questions[$question->getQuestion()] = self::answerArray($question->getAnswers());
|
||||
}
|
||||
|
||||
return new self($questions);
|
||||
}
|
||||
|
||||
/** @param Collection<int, Answer> $answers
|
||||
* @return array<string, bool>
|
||||
**/
|
||||
private static function answerArray(Collection $answers): array
|
||||
{
|
||||
$result = [];
|
||||
|
||||
foreach ($answers as $answer) {
|
||||
$result[$answer->getText()] = $answer->isRightAnswer();
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
}
|
||||
123
src/Service/QuizSpreadsheetService.php
Normal file
123
src/Service/QuizSpreadsheetService.php
Normal file
@@ -0,0 +1,123 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Service;
|
||||
|
||||
use App\Entity\Answer;
|
||||
use App\Entity\Question;
|
||||
use App\Entity\Quiz;
|
||||
use App\Exception\SpreadsheetDataException;
|
||||
use PhpOffice\PhpSpreadsheet\Spreadsheet;
|
||||
use PhpOffice\PhpSpreadsheet\Writer\Xlsx;
|
||||
use Symfony\Component\HttpFoundation\File\File;
|
||||
|
||||
class QuizSpreadsheetService
|
||||
{
|
||||
public function generateTemplate(bool $fillExample = true): \Closure
|
||||
{
|
||||
$spreadsheet = new Spreadsheet();
|
||||
|
||||
$sheet = $spreadsheet->getActiveSheet();
|
||||
|
||||
$sheet->getStyle('1:1')->getFont()->setBold(true);
|
||||
|
||||
$sheet->setCellValue('A1', 'Question');
|
||||
$sheet->getColumnDimension('A')->setWidth(30);
|
||||
$sheet->getStyle('A:A')->getAlignment()->setWrapText(true);
|
||||
|
||||
$counter = 1;
|
||||
foreach (range('B', 'L', 2) as $column) {
|
||||
$sheet->setCellValue($column.'1', 'Answer '.$counter++);
|
||||
$sheet->getColumnDimension($column)->setWidth(30);
|
||||
$sheet->getStyle($column.':'.$column)->getAlignment()->setWrapText(true);
|
||||
}
|
||||
|
||||
foreach (range('C', 'M', 2) as $column) {
|
||||
$sheet->setCellValue($column.'1', 'Correct');
|
||||
$sheet->getColumnDimension($column)->setAutoSize(true);
|
||||
}
|
||||
|
||||
if ($fillExample) {
|
||||
$sheet->setCellValue('B2', 'Man');
|
||||
$sheet->setCellValue('C2', true);
|
||||
|
||||
$sheet->setCellValue('D2', 'Vrouw');
|
||||
$sheet->setCellValue('E2', false);
|
||||
|
||||
$sheet->setCellValue('A2', 'Is de mol een man of een vrouw?');
|
||||
}
|
||||
|
||||
return $this->toXlsx($spreadsheet);
|
||||
}
|
||||
|
||||
/** @throws SpreadsheetDataException */
|
||||
public function xlsxToQuiz(Quiz $quiz, File $file): Quiz
|
||||
{
|
||||
$spreadsheet = $this->readSheet($file);
|
||||
$sheet = $spreadsheet->getSheet($spreadsheet->getFirstSheetIndex());
|
||||
|
||||
$answerLines = \array_slice($sheet->toArray(formatData: false), 1);
|
||||
|
||||
return $this->fillQuizFromArray($quiz, $answerLines);
|
||||
}
|
||||
|
||||
private function readSheet(File $file): Spreadsheet
|
||||
{
|
||||
return (new \PhpOffice\PhpSpreadsheet\Reader\Xlsx())->setReadDataOnly(true)->load($file->getRealPath());
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<int, array<int, string|bool|null>> $sheet
|
||||
*
|
||||
* @throws SpreadsheetDataException
|
||||
*/
|
||||
private function fillQuizFromArray(Quiz $quiz, array $sheet): Quiz
|
||||
{
|
||||
$errors = [];
|
||||
|
||||
$questionCounter = 1;
|
||||
foreach ($sheet as $questionArr) {
|
||||
if (null === $questionArr[0]) {
|
||||
break;
|
||||
}
|
||||
|
||||
$question = new Question();
|
||||
$question->setQuestion((string) $questionArr[0]);
|
||||
$question->setOrdering($questionCounter++);
|
||||
|
||||
$answerCounter = 1;
|
||||
$arrCounter = 1;
|
||||
|
||||
while (true) {
|
||||
if (null === $questionArr[$arrCounter]) {
|
||||
if (1 === $answerCounter) {
|
||||
$errors[] = \sprintf('Question %d has no answers', $answerCounter);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
$answer = new Answer((string) $questionArr[$arrCounter++], (bool) $questionArr[$arrCounter++]);
|
||||
$answer->setOrdering($answerCounter++);
|
||||
$question->addAnswer($answer);
|
||||
}
|
||||
|
||||
$quiz->addQuestion($question);
|
||||
}
|
||||
|
||||
if ([] !== $errors) {
|
||||
throw new SpreadsheetDataException($errors);
|
||||
}
|
||||
|
||||
return $quiz;
|
||||
}
|
||||
|
||||
public function quizToXlsx(Quiz $quiz): void {}
|
||||
|
||||
private function toXlsx(Spreadsheet $spreadsheet): \Closure
|
||||
{
|
||||
$writer = new Xlsx($spreadsheet);
|
||||
|
||||
return static fn () => $writer->save('php://output');
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user