WIP: Implement profile editing

This commit is contained in:
Björn Fromme
2023-10-02 12:39:52 +02:00
parent d776d37cd5
commit 953aa9c8e0
27 changed files with 598 additions and 43 deletions
+6
View File
@@ -4,6 +4,7 @@ namespace App\BusProNet\DataProvider;
use App\BusProNet\ApiClient;
use App\BusProNet\ApiClientException;
use App\BusProNet\Model\Country;
use Symfony\Contracts\Cache\CacheInterface;
use Symfony\Contracts\Cache\ItemInterface;
@@ -30,4 +31,9 @@ class Countries
return $countries;
}
public function get(int $id): ?Country
{
return $this->getAll()[$id] ?? null;
}
}
+5
View File
@@ -4,6 +4,7 @@ namespace App\BusProNet\DataProvider;
use App\BusProNet\ApiClient;
use App\BusProNet\ApiClientException;
use App\BusProNet\Model\Pickup;
use Symfony\Contracts\Cache\CacheInterface;
use Symfony\Contracts\Cache\ItemInterface;
@@ -29,6 +30,10 @@ class Pickups
}
return $pickups;
}
public function get(int $busProId): ?Pickup
{
return $this->getAll()[$busProId] ?? null;
}
}
+14 -11
View File
@@ -176,14 +176,15 @@ class ResponseParser
$countries = [];
foreach ($xml->xpath('laender/land') as $item) {
$id = (int) $item->attributes()['id'];
$country = new Country();
$country
->setId((int)$item->attributes()['id'])
->setName((string)$item->attributes()['bezeichnung'])
->setToken((string)$item->attributes()['kuerzel'])
->setNationality((string)$item->attributes()['nationalitaet'])
->setId($id)
->setName((string) $item->attributes()['bezeichnung'])
->setToken((string) $item->attributes()['kuerzel'])
->setNationality((string) $item->attributes()['nationalitaet'])
;
$countries[] = $country;
$countries[$id] = $country;
}
$response = new BaseDataResponse();
@@ -197,15 +198,17 @@ class ResponseParser
$pickups = [];
foreach ($xml->xpath('zustieg') as $item) {
$id = (int) $item->attributes()['id'];
$busProId = (int) $item->attributes()['idbuspro'];
$pickup = new Pickup();
$pickup
->setId((int)$item->attributes()['id'])
->setBusProId((int)$item->attributes()['idbuspro'])
->setCode((string)$item->attributes()['code'])
->setCity((string)$item->xpath('ort')[0])
->setStreet((string)$item->xpath('strasse')[0])
->setId($id)
->setBusProId($busProId)
->setCode((string) $item->attributes()['code'])
->setCity((string) $item->xpath('ort')[0])
->setStreet((string) $item->xpath('strasse')[0])
;
$pickups[] = $pickup;
$pickups[$busProId] = $pickup;
}
$response = new BaseDataResponse();
@@ -0,0 +1,27 @@
<?php
namespace App\Controller\Common;
use App\Entity\Upload;
use App\Service\Upload\UploadHandler;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\Security\Http\Attribute\IsGranted;
class DownloadController extends AbstractController
{
public function __construct(private readonly UploadHandler $uploadHandler)
{
}
#[Route('/download/{uuid}', name: 'app_common_download')]
#[IsGranted('DOWNLOAD', subject: 'upload')]
public function index(Upload $upload): Response
{
$path = $this->uploadHandler->getUploadFilepath($upload);
$originalFilename = $upload->getOriginalFilename();
return $this->file($path, $originalFilename);
}
}
@@ -92,7 +92,7 @@ class IndexController extends AbstractController
$upload = Upload::fromUploadDto($uploadSession->getUploads()->first(), $user, Upload::TYPE_PHOTO);
$teamer->setPhoto($upload);
$this->entityManager->flush();
$this->uploadHandler->moveUploadSessionFilesFromOrphanage('photo', $uploadSession);
$this->uploadHandler->moveUploadSessionFilesFromOrphanage(Upload::TYPE_PHOTO, $uploadSession);
$this->uploadHandler->destroyUploadSession();
$this->addFlash('success', 'Dein Profilbild wurde aktualisiert');
@@ -0,0 +1,77 @@
<?php
namespace App\Controller\Teamer\Profile\License;
use App\Entity\License;
use App\Entity\Upload;
use App\Entity\User;
use App\Form\LicenseType;
use App\Model\AjaxModalResponseDto;
use App\Model\UploadSessionDto;
use App\Service\Upload\UploadHandler;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\Security\Http\Attribute\IsGranted;
class AddController extends AbstractController
{
public function __construct(
private readonly EntityManagerInterface $entityManager,
private readonly UploadHandler $uploadHandler
) {
}
#[Route('/teamer/profile/license/add', name: 'app_teamer_profile_license_add')]
#[IsGranted('ROLE_TEAMER')]
public function index(Request $request): JsonResponse
{
$response = new AjaxModalResponseDto();
$action = $this->generateUrl('app_teamer_profile_license_add');
/** @var User $user */
$user = $this->getUser();
$license = new License();
// Handle upload independently from form submission to avoid issues with failing validation
$uploadSession = $this->uploadHandler->getUploadSession();
if (true === $request->isMethod('POST') && 0 < $uploadSession->getCount()) {
$this->updateCertificate($user, $license, $uploadSession);
}
$form = $this->createForm(LicenseType::class, $license, ['action' => $action, 'ajax_submit' => true, 'upload_session' => $uploadSession]);
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
$teamer = $user->getTeamer();
$teamer->addLicense($license);
$this->entityManager->persist($license);
$this->entityManager->flush();
$redirectUrl = $this->generateUrl('app_teamer_profile_skills');
$response->setCloseAndRedirect($redirectUrl);
} else {
$content = $this->renderView('teamer/profile/license/add.html.twig', [
'form' => $form,
]);
$response->setContent($content);
}
return $this->json($response);
}
private function updateCertificate(User $user, License $license, UploadSessionDto $uploadSession): void
{
$upload = Upload::fromUploadDto($uploadSession->getUploads()->first(), $user, Upload::TYPE_CERTIFICATE);
$license->setCertificate($upload);
$this->entityManager->flush();
$this->uploadHandler->moveUploadSessionFilesFromOrphanage(Upload::TYPE_CERTIFICATE, $uploadSession);
$this->uploadHandler->destroyUploadSession();
$this->addFlash('success', 'Der Nachweis wurde hochgeladen');
}
}
@@ -2,7 +2,9 @@
namespace App\Controller\Teamer\Profile;
use App\Entity\User;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\Security\Http\Attribute\IsGranted;
@@ -11,8 +13,14 @@ class SkillsController extends AbstractController
{
#[Route('/teamer/profile/skills', name: 'app_teamer_profile_skills')]
#[IsGranted('ROLE_TEAMER')]
public function index(): Response
public function index(Request $request): Response
{
return $this->render('teamer/profile/skills.html.twig');
/** @var User $user */
$user = $this->getUser();
$teamer = $user->getTeamer();
return $this->render('teamer/profile/skills.html.twig', [
'teamer' => $teamer,
]);
}
}
+4 -1
View File
@@ -7,6 +7,7 @@ use App\Repository\LicenseRepository;
use Doctrine\DBAL\Types\Types;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Uid\Uuid;
use Symfony\Component\Validator\Constraints as Assert;
#[ORM\Entity(repositoryClass: LicenseRepository::class)]
class License implements TimestampableEntityInterface
@@ -25,12 +26,14 @@ class License implements TimestampableEntityInterface
private string $uuid;
#[ORM\Column(length: 64)]
private ?string $type = null;
private ?string $type;
#[ORM\Column(type: Types::DATE_IMMUTABLE)]
#[Assert\NotNull(message: 'Bitte gib das Datum an')]
private ?\DateTimeImmutable $date = null;
#[ORM\OneToOne(cascade: ['persist', 'remove'])]
#[Assert\NotNull(message: 'Bitte lade den Nachweis hoch')]
private ?Upload $certificate = null;
#[ORM\ManyToOne(inversedBy: 'licenses')]
+1
View File
@@ -16,6 +16,7 @@ class Upload implements BlameableEntityInterface, TimestampableEntityInterface
use TimestampableEntity;
public const TYPE_PHOTO = 'photo';
public const TYPE_CERTIFICATE = 'certificate';
public const STATUS_NEW = 'new';
public const STATUS_IN_PROCESS = 'in_process';
+30
View File
@@ -0,0 +1,30 @@
<?php
namespace App\Form;
use App\Form\DataTransformer\FirstDayOfMonthDateTransformer;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\DateType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
class AbbreviatedDateType extends AbstractType
{
public function getParent(): string
{
return DateType::class;
}
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$builder->addViewTransformer(new FirstDayOfMonthDateTransformer());
}
public function configureOptions(OptionsResolver $resolver): void
{
$resolver->setDefaults([
'input' => 'datetime_immutable',
'format' => 'y-MMMM-d',
]);
}
}
+1
View File
@@ -22,6 +22,7 @@ class BpnPickupType extends AbstractType
{
$resolver->setDefaults([
'choice_loader' => new BpnPickupsChoiceLoader($this->pickups),
'placeholder' => 'Bitte wählen...',
]);
}
}
@@ -0,0 +1,38 @@
<?php
namespace App\Form\DataTransformer;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\Form\DataTransformerInterface;
use Symfony\Component\Form\Exception\TransformationFailedException;
class EntityToIdTransformer implements DataTransformerInterface
{
public function __construct(private readonly EntityManagerInterface $entityManager, private readonly string $class)
{
}
public function transform($value)
{
if (null === $value) {
return null;
}
return $value->getId();
}
public function reverseTransform($value)
{
if (empty($value)) {
return null;
}
$entity = $this->entityManager->getRepository($this->class)->find($value);
if (null === $entity) {
throw new TransformationFailedException(sprintf('No %s with id %d found', $this->class, $value));
}
return $entity;
}
}
@@ -0,0 +1,29 @@
<?php
namespace App\Form\DataTransformer;
use Symfony\Component\Form\DataTransformerInterface;
class FirstDayOfMonthDateTransformer implements DataTransformerInterface
{
public function transform($value)
{
return $value;
}
public function reverseTransform($value)
{
if (!is_array($value)) {
return $value;
}
if (empty($value['year'])) {
return $value;
}
$value['month'] = $value['month'] ?? 1;
$value['day'] = 1;
return $value;
}
}
+39
View File
@@ -0,0 +1,39 @@
<?php
namespace App\Form;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\DateType;
use Symfony\Component\Form\FormInterface;
use Symfony\Component\Form\FormView;
use Symfony\Component\OptionsResolver\OptionsResolver;
class DatepickerType extends AbstractType
{
public function configureOptions(OptionsResolver $resolver): void
{
$resolver->setDefaults([
'html5' => false,
'widget' => 'single_text',
'input' => 'datetime_immutable',
'min_date' => null,
'max_date' => null,
'disable_weekends' => false,
]);
$resolver->setAllowedTypes('min_date', ['null', \DateTimeImmutable::class]);
$resolver->setAllowedTypes('max_date', ['null', \DateTimeImmutable::class]);
$resolver->setAllowedTypes('disable_weekends', 'bool');
}
public function buildView(FormView $view, FormInterface $form, array $options): void
{
$view->vars['min_date'] = $options['min_date'];
$view->vars['max_date'] = $options['max_date'];
$view->vars['disable_weekends'] = $options['disable_weekends'];
}
public function getParent(): string
{
return DateType::class;
}
}
+45
View File
@@ -0,0 +1,45 @@
<?php
namespace App\Form;
use App\Form\DataTransformer\EntityToIdTransformer;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\HiddenType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\Form\FormInterface;
use Symfony\Component\Form\FormView;
use Symfony\Component\OptionsResolver\OptionsResolver;
class HiddenEntityType extends AbstractType
{
public function __construct(private readonly EntityManagerInterface $entityManager)
{
}
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$transformer = new EntityToIdTransformer($this->entityManager, $options['class']);
$builder->addModelTransformer($transformer);
}
public function buildView(FormView $view, FormInterface $form, array $options): void
{
$view->vars['entity'] = $form->getData();
}
public function configureOptions(OptionsResolver $resolver): void
{
$resolver
->setRequired(['class'])
->setDefaults([
'invalid_message' => 'The entity does not exist.',
])
;
}
public function getParent(): string
{
return HiddenType::class;
}
}
+53
View File
@@ -0,0 +1,53 @@
<?php
namespace App\Form;
use App\Entity\License;
use App\Model\UploadSessionDto;
use App\Service\Upload\UploadHandler;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\Form\FormInterface;
use Symfony\Component\Form\FormView;
use Symfony\Component\OptionsResolver\OptionsResolver;
class LicenseType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$builder
->add('type', ChoiceType::class, [
'label' => 'Art der Lizenz',
'choices' => [
'Skilehrer*in' => License::TYPE_SKI,
'Snowboardlehrer*in' => License::TYPE_SNOWBOARD,
],
])
->add('date', AbbreviatedDateType::class, [
'label' => 'Datum',
'years' => range((int) date('Y'), (int) date('Y') - 15)
])
;
}
public function buildView(FormView $view, FormInterface $form, array $options): void
{
/** @var UploadSessionDto $uploadSession */
$uploadSession = $options['upload_session'];
$view->vars['upload_session_params'] = [
UploadHandler::SESSION_KEY => $uploadSession->getUid(),
];
}
public function configureOptions(OptionsResolver $resolver): void
{
$resolver
->setDefaults([
'data_class' => License::class,
])
->setRequired(['upload_session'])
->setAllowedTypes('upload_session', UploadSessionDto::class)
;
}
}
+53
View File
@@ -0,0 +1,53 @@
<?php
namespace App\Model;
class AjaxModalResponseDto
{
private string $content;
private string $redirect;
private bool $close = false;
public function getContent(): ?string
{
return $this->content;
}
public function setContent(string $content): self
{
$this->content = $content;
return $this;
}
public function getRedirect(): ?string
{
return $this->redirect;
}
public function setRedirect(string $redirect): self
{
$this->redirect = $redirect;
$this->close = false;
return $this;
}
public function setCloseAndRedirect(string $redirect): self
{
$this->redirect = $redirect;
$this->close = true;
return $this;
}
public function isClose(): bool
{
return $this->close;
}
public function setClose(bool $close): void
{
$this->close = $close;
}
}
+43
View File
@@ -0,0 +1,43 @@
<?php
namespace App\Security\Voter;
use App\Entity\Upload;
use App\Entity\User;
use Symfony\Bundle\SecurityBundle\Security;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Authorization\Voter\Voter;
class UploadVoter extends Voter
{
public const VIEW = 'VIEW';
public const DELETE = 'DELETE';
public const DOWNLOAD = 'DOWNLOAD';
public function __construct(private readonly Security $security)
{}
protected function supports(string $attribute, mixed $subject): bool
{
if (! $subject instanceof Upload) {
return false;
}
return in_array($attribute, [static::VIEW, static::DELETE, static::DOWNLOAD]);
}
protected function voteOnAttribute(string $attribute, mixed $subject, TokenInterface $token): bool
{
if (true === $this->security->isGranted('ROLE_ADMINISTRATIVE')) {
return true;
}
/** @var Upload $upload */
$upload = $subject;
/** @var User $user */
$user = $this->security->getUser();
return $upload->getOwner() === $user;
}
}