From 953aa9c8e0c840d7e8afaaa924ff20fb3b883b25 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B6rn=20Fromme?= Date: Mon, 2 Oct 2023 12:39:52 +0200 Subject: [PATCH] WIP: Implement profile editing --- config/packages/oneup_uploader.yaml | 6 ++ src/BusProNet/DataProvider/Countries.php | 6 ++ src/BusProNet/DataProvider/Pickups.php | 5 ++ src/BusProNet/ResponseParser.php | 25 +++--- src/Controller/Common/DownloadController.php | 27 +++++++ .../Teamer/Profile/IndexController.php | 2 +- .../Teamer/Profile/License/AddController.php | 77 +++++++++++++++++++ .../Teamer/Profile/SkillsController.php | 12 ++- src/Entity/License.php | 5 +- src/Entity/Upload.php | 1 + src/Form/AbbreviatedDateType.php | 30 ++++++++ src/Form/BpnPickupType.php | 1 + .../DataTransformer/EntityToIdTransformer.php | 38 +++++++++ .../FirstDayOfMonthDateTransformer.php | 29 +++++++ src/Form/DatepickerType.php | 39 ++++++++++ src/Form/HiddenEntityType.php | 45 +++++++++++ src/Form/LicenseType.php | 53 +++++++++++++ src/Model/AjaxModalResponseDto.php | 53 +++++++++++++ src/Security/Voter/UploadVoter.php | 43 +++++++++++ templates/_partials/_ajax_modal.html.twig | 2 +- templates/_partials/_form_errors.html.twig | 14 ++++ .../admin/system/job_profile/index.html.twig | 2 + templates/forms.html.twig | 42 +++++++++- templates/layout.html.twig | 4 +- templates/teamer/profile/index.html.twig | 26 ++----- .../teamer/profile/license/add.html.twig | 25 ++++++ templates/teamer/profile/skills.html.twig | 29 +++++++ 27 files changed, 598 insertions(+), 43 deletions(-) create mode 100644 src/Controller/Common/DownloadController.php create mode 100644 src/Controller/Teamer/Profile/License/AddController.php create mode 100644 src/Form/AbbreviatedDateType.php create mode 100644 src/Form/DataTransformer/EntityToIdTransformer.php create mode 100644 src/Form/DataTransformer/FirstDayOfMonthDateTransformer.php create mode 100644 src/Form/DatepickerType.php create mode 100644 src/Form/HiddenEntityType.php create mode 100644 src/Form/LicenseType.php create mode 100644 src/Model/AjaxModalResponseDto.php create mode 100644 src/Security/Voter/UploadVoter.php create mode 100644 templates/_partials/_form_errors.html.twig create mode 100644 templates/teamer/profile/license/add.html.twig diff --git a/config/packages/oneup_uploader.yaml b/config/packages/oneup_uploader.yaml index a7e0d68..c6d58e2 100644 --- a/config/packages/oneup_uploader.yaml +++ b/config/packages/oneup_uploader.yaml @@ -6,6 +6,12 @@ oneup_uploader: namer: app.upload_namer storage: directory: '%kernel.project_dir%/uploads/photo' + certificate: + frontend: dropzone + use_orphanage: true + namer: app.upload_namer + storage: + directory: '%kernel.project_dir%/uploads/certificate' chunks: maxage: 86400 storage: diff --git a/src/BusProNet/DataProvider/Countries.php b/src/BusProNet/DataProvider/Countries.php index 9c00fa2..7ef775c 100644 --- a/src/BusProNet/DataProvider/Countries.php +++ b/src/BusProNet/DataProvider/Countries.php @@ -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; + } } \ No newline at end of file diff --git a/src/BusProNet/DataProvider/Pickups.php b/src/BusProNet/DataProvider/Pickups.php index 0307c70..9eb970d 100644 --- a/src/BusProNet/DataProvider/Pickups.php +++ b/src/BusProNet/DataProvider/Pickups.php @@ -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; } } \ No newline at end of file diff --git a/src/BusProNet/ResponseParser.php b/src/BusProNet/ResponseParser.php index dc376d8..3a70948 100644 --- a/src/BusProNet/ResponseParser.php +++ b/src/BusProNet/ResponseParser.php @@ -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(); diff --git a/src/Controller/Common/DownloadController.php b/src/Controller/Common/DownloadController.php new file mode 100644 index 0000000..78cf40c --- /dev/null +++ b/src/Controller/Common/DownloadController.php @@ -0,0 +1,27 @@ +uploadHandler->getUploadFilepath($upload); + $originalFilename = $upload->getOriginalFilename(); + + return $this->file($path, $originalFilename); + } +} \ No newline at end of file diff --git a/src/Controller/Teamer/Profile/IndexController.php b/src/Controller/Teamer/Profile/IndexController.php index 0e5dd92..66e4eb0 100644 --- a/src/Controller/Teamer/Profile/IndexController.php +++ b/src/Controller/Teamer/Profile/IndexController.php @@ -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'); diff --git a/src/Controller/Teamer/Profile/License/AddController.php b/src/Controller/Teamer/Profile/License/AddController.php new file mode 100644 index 0000000..223cdee --- /dev/null +++ b/src/Controller/Teamer/Profile/License/AddController.php @@ -0,0 +1,77 @@ +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'); + } +} \ No newline at end of file diff --git a/src/Controller/Teamer/Profile/SkillsController.php b/src/Controller/Teamer/Profile/SkillsController.php index 4cdc8fa..58ff293 100644 --- a/src/Controller/Teamer/Profile/SkillsController.php +++ b/src/Controller/Teamer/Profile/SkillsController.php @@ -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, + ]); } } \ No newline at end of file diff --git a/src/Entity/License.php b/src/Entity/License.php index 4b81d66..2643cd5 100644 --- a/src/Entity/License.php +++ b/src/Entity/License.php @@ -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')] diff --git a/src/Entity/Upload.php b/src/Entity/Upload.php index cd150a8..caa9d05 100644 --- a/src/Entity/Upload.php +++ b/src/Entity/Upload.php @@ -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'; diff --git a/src/Form/AbbreviatedDateType.php b/src/Form/AbbreviatedDateType.php new file mode 100644 index 0000000..e1e9c2e --- /dev/null +++ b/src/Form/AbbreviatedDateType.php @@ -0,0 +1,30 @@ +addViewTransformer(new FirstDayOfMonthDateTransformer()); + } + + public function configureOptions(OptionsResolver $resolver): void + { + $resolver->setDefaults([ + 'input' => 'datetime_immutable', + 'format' => 'y-MMMM-d', + ]); + } +} diff --git a/src/Form/BpnPickupType.php b/src/Form/BpnPickupType.php index 7a455bb..4189bc5 100644 --- a/src/Form/BpnPickupType.php +++ b/src/Form/BpnPickupType.php @@ -22,6 +22,7 @@ class BpnPickupType extends AbstractType { $resolver->setDefaults([ 'choice_loader' => new BpnPickupsChoiceLoader($this->pickups), + 'placeholder' => 'Bitte wählen...', ]); } } \ No newline at end of file diff --git a/src/Form/DataTransformer/EntityToIdTransformer.php b/src/Form/DataTransformer/EntityToIdTransformer.php new file mode 100644 index 0000000..9760995 --- /dev/null +++ b/src/Form/DataTransformer/EntityToIdTransformer.php @@ -0,0 +1,38 @@ +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; + } +} diff --git a/src/Form/DataTransformer/FirstDayOfMonthDateTransformer.php b/src/Form/DataTransformer/FirstDayOfMonthDateTransformer.php new file mode 100644 index 0000000..18fceab --- /dev/null +++ b/src/Form/DataTransformer/FirstDayOfMonthDateTransformer.php @@ -0,0 +1,29 @@ +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; + } +} diff --git a/src/Form/HiddenEntityType.php b/src/Form/HiddenEntityType.php new file mode 100644 index 0000000..d55006c --- /dev/null +++ b/src/Form/HiddenEntityType.php @@ -0,0 +1,45 @@ +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; + } +} diff --git a/src/Form/LicenseType.php b/src/Form/LicenseType.php new file mode 100644 index 0000000..b5ab8db --- /dev/null +++ b/src/Form/LicenseType.php @@ -0,0 +1,53 @@ +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) + ; + } +} \ No newline at end of file diff --git a/src/Model/AjaxModalResponseDto.php b/src/Model/AjaxModalResponseDto.php new file mode 100644 index 0000000..005896e --- /dev/null +++ b/src/Model/AjaxModalResponseDto.php @@ -0,0 +1,53 @@ +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; + } +} diff --git a/src/Security/Voter/UploadVoter.php b/src/Security/Voter/UploadVoter.php new file mode 100644 index 0000000..29b414f --- /dev/null +++ b/src/Security/Voter/UploadVoter.php @@ -0,0 +1,43 @@ +security->isGranted('ROLE_ADMINISTRATIVE')) { + return true; + } + + /** @var Upload $upload */ + $upload = $subject; + + /** @var User $user */ + $user = $this->security->getUser(); + + return $upload->getOwner() === $user; + } +} \ No newline at end of file diff --git a/templates/_partials/_ajax_modal.html.twig b/templates/_partials/_ajax_modal.html.twig index 5a645e5..7396c2a 100644 --- a/templates/_partials/_ajax_modal.html.twig +++ b/templates/_partials/_ajax_modal.html.twig @@ -6,7 +6,7 @@
-
+
{%- endblock -%} +{% block abbreviated_date_widget %} + {%- set class = 'flex items-center space-x-1 ' ~ attr.class|default('') -%} + {%- do form.setRendered -%} + {%- if errors|length -%} + {%- set attr = attr|merge({'class': attr.class|default('') ~ ' ring-red-500 placeholder-red-500 focus:ring-red-500' }) -%} + {%- endif -%} +
+ {{ form_widget(form.month, { 'attr': attr })}} + / + {{ form_widget(form.year, { 'attr': attr })}} +
+{% endblock %} + {%- block money_widget -%} {% set currency_class = 'absolute top-1/2 transform -translate-y-1/2 right-0 mr-2' %} {% if errors|length %} @@ -127,3 +140,24 @@
{%- endblock money_widget -%} + +{%- block datepicker_widget -%} + {%- set minDate = form.vars.min_date ? form.vars.min_date | date('Y-m-d') : null -%} + {%- set maxDate = form.vars.max_date ? form.vars.max_date | date('Y-m-d') : null -%} + {%- set attr = attr|merge({'class': (attr.class|default('') ~ ' block w-full rounded-md border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 focus:ring-2 focus:ring-inset sm:text-sm sm:leading-6')|trim }) -%} + {%- if errors|length -%} + {%- set attr = attr|merge({'class': attr.class|default('') ~ ' ring-red-500 placeholder-red-500 focus:ring-red-500' }) -%} + {% else %} + {%- set attr = attr|merge({'class': attr.class|default('') ~ ' placeholder:text-gray-400 focus:ring-primary' }) -%} + {%- endif -%} + {%- if disabled is defined and disabled == true -%} + {%- set attr = attr|merge({'class': attr.class|default('') ~ ' cursor-not-allowed' }) -%} + {%- endif -%} +
+ +
+ {%- if disabled is defined and disabled == true -%} + + {%- endif -%} +{%- endblock datepicker_widget %} diff --git a/templates/layout.html.twig b/templates/layout.html.twig index b9c11c4..e757712 100644 --- a/templates/layout.html.twig +++ b/templates/layout.html.twig @@ -9,9 +9,9 @@
{% if app.user.teamer.photo %} - {{ teamer.firstName }} + alt="{{ app.user.teamer.firstName }}"> {% endif %}
+ {% endblock %} \ No newline at end of file