From bfd24948c100fbcccae69cde98a03380b32df384 Mon Sep 17 00:00:00 2001
From: dece
Date: Mon, 9 Oct 2023 00:03:03 +0200
Subject: [PATCH] Make first moves toward dropping EasyAdmin
Anything more than running your own CRUD seems too cumbersome.
---
README.md | 11 ++
config/packages/security.yaml | 3 +-
config/services.yaml | 2 +
.../Admin/PodcastCrudController.php | 2 +
src/Controller/PodcastController.php | 133 ++++++++++++++++++
src/Entity/Podcast.php | 9 ++
src/Entity/User.php | 5 +
src/Form/PodcastType.php | 63 +++++++++
src/Repository/EpisodeRepository.php | 25 ----
src/Repository/PodcastRepository.php | 28 +---
src/Repository/UserRepository.php | 25 ----
templates/base.html.twig | 2 +
templates/feed/podcast.html.twig | 3 +-
templates/login/index.html.twig | 47 ++++---
templates/podcast/_delete_form.html.twig | 4 +
templates/podcast/_form.html.twig | 4 +
templates/podcast/edit.html.twig | 13 ++
templates/podcast/index.html.twig | 46 ++++++
templates/podcast/new.html.twig | 11 ++
templates/podcast/show.html.twig | 58 ++++++++
20 files changed, 398 insertions(+), 96 deletions(-)
create mode 100644 src/Controller/PodcastController.php
create mode 100644 src/Form/PodcastType.php
create mode 100644 templates/podcast/_delete_form.html.twig
create mode 100644 templates/podcast/_form.html.twig
create mode 100644 templates/podcast/edit.html.twig
create mode 100644 templates/podcast/index.html.twig
create mode 100644 templates/podcast/new.html.twig
create mode 100644 templates/podcast/show.html.twig
diff --git a/README.md b/README.md
index 71d75e0..bfa2a06 100644
--- a/README.md
+++ b/README.md
@@ -6,6 +6,17 @@ LSBC
A small platform to create podcasts and episodes, host the audio files and share
the RSS feed, with external sources download abilities.
+Features include:
+
+- Lightweight, bare minimum pages;
+- Simple backoffice;
+- Separate users manage their own podcasts;
+- TODO an API
+- TODO the API let's you quickly add whole episodes from Youtube links
+
+Podcasts follow mostly open standards but the “target” client is the fantastic
+[AntennaPod](https://antennapod.org/)
+
Install
diff --git a/config/packages/security.yaml b/config/packages/security.yaml
index db1155d..291bd6d 100644
--- a/config/packages/security.yaml
+++ b/config/packages/security.yaml
@@ -25,7 +25,8 @@ security:
role_hierarchy:
ROLE_ADMIN: ROLE_USER
access_control:
- - { path: ^/admin, roles: ROLE_USER }
+ - { path: ^/admin, roles: ROLE_ADMIN }
+ - { path: ^/manage, roles: ROLE_USER }
when@test:
security:
diff --git a/config/services.yaml b/config/services.yaml
index 2d6a76f..816ae00 100644
--- a/config/services.yaml
+++ b/config/services.yaml
@@ -4,6 +4,8 @@
# Put parameters here that don't need to change on each machine where the app is deployed
# https://symfony.com/doc/current/best_practices.html#use-parameters-for-application-configuration
parameters:
+ audio_directory: '%kernel.project_dir%/public/uploads/files'
+ images_directory: '%kernel.project_dir%/public/uploads/images'
services:
# default configuration for services in *this* file
diff --git a/src/Controller/Admin/PodcastCrudController.php b/src/Controller/Admin/PodcastCrudController.php
index 3d05b03..60d67da 100644
--- a/src/Controller/Admin/PodcastCrudController.php
+++ b/src/Controller/Admin/PodcastCrudController.php
@@ -31,4 +31,6 @@ class PodcastCrudController extends AbstractCrudController
->setBasePath(Constants::IMAGES_BASE_PATH),
];
}
+
+
}
diff --git a/src/Controller/PodcastController.php b/src/Controller/PodcastController.php
new file mode 100644
index 0000000..ef9b7a0
--- /dev/null
+++ b/src/Controller/PodcastController.php
@@ -0,0 +1,133 @@
+render('podcast/index.html.twig', [
+ 'podcasts' => $podcastRepository->findAllOrderedById(),
+ ]);
+ }
+
+ #[Route('/new', name: 'app_podcast_new', methods: ['GET', 'POST'])]
+ public function new(
+ Request $request,
+ EntityManagerInterface $entityManager,
+ SluggerInterface $slugger,
+ ): Response
+ {
+ $podcast = new Podcast();
+ $form = $this->createForm(PodcastType::class, $podcast);
+ $form->handleRequest($request);
+
+ if ($form->isSubmitted() && $form->isValid()) {
+ if (!$this->handleLogoChange($form, $podcast, $slugger)) {
+ $form->get('logo')->addError(new FormError('Could not upload logo.'));
+ return $this->render('podcast/new.html.twig', [
+ 'podcast' => $podcast,
+ 'form' => $form,
+ ]);
+ }
+
+ if (($owner = $form->get('owner')->getData()) === null) {
+ $owner = $this->getUser();
+ }
+ $podcast->setOwner($owner);
+
+ $entityManager->persist($podcast);
+ $entityManager->flush();
+ return $this->redirectToRoute('app_podcast_index', [], Response::HTTP_SEE_OTHER);
+ }
+
+ return $this->render('podcast/new.html.twig', [
+ 'podcast' => $podcast,
+ 'form' => $form,
+ ]);
+ }
+
+ #[Route('/{id}', name: 'app_podcast_show', methods: ['GET'])]
+ public function show(Podcast $podcast): Response
+ {
+ return $this->render('podcast/show.html.twig', [
+ 'podcast' => $podcast,
+ ]);
+ }
+
+ #[Route('/{id}/edit', name: 'app_podcast_edit', methods: ['GET', 'POST'])]
+ public function edit(
+ Request $request,
+ Podcast $podcast,
+ EntityManagerInterface $entityManager,
+ SluggerInterface $slugger,
+ ): Response
+ {
+ $form = $this->createForm(PodcastType::class, $podcast);
+ $form->handleRequest($request);
+
+ if ($form->isSubmitted() && $form->isValid()) {
+ if (!$this->handleLogoChange($form, $podcast, $slugger)) {
+ $form->get('logo')->addError(new FormError('Could not upload logo.'));
+ } else {
+ $entityManager->flush();
+ return $this->redirectToRoute('app_podcast_index', [], Response::HTTP_SEE_OTHER);
+ }
+ }
+
+ return $this->render('podcast/edit.html.twig', [
+ 'podcast' => $podcast,
+ 'form' => $form,
+ ]);
+ }
+
+ #[Route('/{id}', name: 'app_podcast_delete', methods: ['POST'])]
+ public function delete(Request $request, Podcast $podcast, EntityManagerInterface $entityManager): Response
+ {
+ if ($this->isCsrfTokenValid('delete'.$podcast->getId(), $request->request->get('_token'))) {
+ $entityManager->remove($podcast);
+ $entityManager->flush();
+ }
+
+ return $this->redirectToRoute('app_podcast_index', [], Response::HTTP_SEE_OTHER);
+ }
+
+ protected function handleLogoChange(
+ FormInterface $form,
+ Podcast $podcast,
+ SluggerInterface $slugger
+ ): bool
+ {
+ $logoFile = $form->get('logo')->getData();
+ if ($logoFile) {
+ $originalFilename = pathinfo($logoFile->getClientOriginalName(), PATHINFO_FILENAME);
+ $safeFilename = $slugger->slug($originalFilename);
+ $newFilename = $safeFilename . '-' . uniqid() . '.' . $logoFile->guessExtension();
+
+ try {
+ $logoFile->move($this->getParameter('images_directory'), $newFilename);
+ } catch (FileException $e) {
+ return false;
+ }
+
+ $podcast->setLogoFilename($newFilename);
+ }
+
+ return true;
+ }
+}
diff --git a/src/Entity/Podcast.php b/src/Entity/Podcast.php
index 6b815cf..2d742e5 100644
--- a/src/Entity/Podcast.php
+++ b/src/Entity/Podcast.php
@@ -182,6 +182,15 @@ class Podcast
return $this;
}
+ public function getLogoPath(): ?string
+ {
+ $logoFilename = $this->logoFilename;
+ if ($logoFilename === null)
+ return null;
+
+ return Constants::IMAGES_BASE_PATH . $logoFilename;
+ }
+
public function getOwner(): ?User
{
return $this->owner;
diff --git a/src/Entity/User.php b/src/Entity/User.php
index ff381ac..39162f3 100644
--- a/src/Entity/User.php
+++ b/src/Entity/User.php
@@ -40,6 +40,11 @@ class User implements UserInterface, PasswordAuthenticatedUserInterface
$this->podcasts = new ArrayCollection();
}
+ public function __toString()
+ {
+ return "$this->email ($this->id)";
+ }
+
public function getId(): ?int
{
return $this->id;
diff --git a/src/Form/PodcastType.php b/src/Form/PodcastType.php
new file mode 100644
index 0000000..7343d63
--- /dev/null
+++ b/src/Form/PodcastType.php
@@ -0,0 +1,63 @@
+add('name', TextType::class, [
+ 'help' => 'Your podcast\'s public name.',
+ ])
+ ->add('slug', TextType::class, [
+ 'help' => 'Your podcast\'s slug, used in its URL and to reference it in API calls. Automatically set.',
+ 'disabled' => true,
+ ])
+ ->add('website', TextType::class, [
+ 'help' => 'A website linked to your podcast. Can be this instance URL if you have no idea.',
+ ])
+ ->add('description', TextType::class, [
+ 'help' => 'Description.',
+ ])
+ ->add('author', TextType::class, [
+ 'help' => 'An author name.',
+ ])
+ ->add('email', TextType::class, [
+ 'help' => 'An author contact email address.',
+ ])
+ ->add('logo', FileType::class, [
+ 'help' => 'Your logo; any image will do but keep it lightweight (4 MB max).',
+ 'mapped' => false,
+ 'required' => false,
+ 'constraints' => new File([
+ 'maxSize' => '4m',
+ 'mimeTypes' => ['image/jpeg', 'image/png'],
+ 'mimeTypesMessage' => 'Please upload a valid JPG or PNG file.',
+ ]),
+ ])
+ ->add('owner', EntityType::class, [
+ 'help' => 'The podcast owner is the only user of this instance able to see and modify it.',
+ 'class' => User::class,
+ 'disabled' => true,
+ ])
+ ;
+ }
+
+ public function configureOptions(OptionsResolver $resolver): void
+ {
+ $resolver->setDefaults([
+ 'data_class' => Podcast::class,
+ ]);
+ }
+}
diff --git a/src/Repository/EpisodeRepository.php b/src/Repository/EpisodeRepository.php
index 59c5196..46c672b 100644
--- a/src/Repository/EpisodeRepository.php
+++ b/src/Repository/EpisodeRepository.php
@@ -38,29 +38,4 @@ class EpisodeRepository extends ServiceEntityRepository
$this->getEntityManager()->flush();
}
}
-
-// /**
-// * @return Episode[] Returns an array of Episode objects
-// */
-// public function findByExampleField($value): array
-// {
-// return $this->createQueryBuilder('e')
-// ->andWhere('e.exampleField = :val')
-// ->setParameter('val', $value)
-// ->orderBy('e.id', 'ASC')
-// ->setMaxResults(10)
-// ->getQuery()
-// ->getResult()
-// ;
-// }
-
-// public function findOneBySomeField($value): ?Episode
-// {
-// return $this->createQueryBuilder('e')
-// ->andWhere('e.exampleField = :val')
-// ->setParameter('val', $value)
-// ->getQuery()
-// ->getOneOrNullResult()
-// ;
-// }
}
diff --git a/src/Repository/PodcastRepository.php b/src/Repository/PodcastRepository.php
index d29dcf6..4ad6110 100644
--- a/src/Repository/PodcastRepository.php
+++ b/src/Repository/PodcastRepository.php
@@ -39,28 +39,8 @@ class PodcastRepository extends ServiceEntityRepository
}
}
-// /**
-// * @return Podcast[] Returns an array of Podcast objects
-// */
-// public function findByExampleField($value): array
-// {
-// return $this->createQueryBuilder('p')
-// ->andWhere('p.exampleField = :val')
-// ->setParameter('val', $value)
-// ->orderBy('p.id', 'ASC')
-// ->setMaxResults(10)
-// ->getQuery()
-// ->getResult()
-// ;
-// }
-
-// public function findOneBySomeField($value): ?Podcast
-// {
-// return $this->createQueryBuilder('p')
-// ->andWhere('p.exampleField = :val')
-// ->setParameter('val', $value)
-// ->getQuery()
-// ->getOneOrNullResult()
-// ;
-// }
+ public function findAllOrderedById(): array
+ {
+ return $this->findBy([], ['id' => 'ASC']);
+ }
}
diff --git a/src/Repository/UserRepository.php b/src/Repository/UserRepository.php
index 9b60483..8f7b0e1 100644
--- a/src/Repository/UserRepository.php
+++ b/src/Repository/UserRepository.php
@@ -55,29 +55,4 @@ class UserRepository extends ServiceEntityRepository implements PasswordUpgrader
$this->save($user, true);
}
-
-// /**
-// * @return User[] Returns an array of User objects
-// */
-// public function findByExampleField($value): array
-// {
-// return $this->createQueryBuilder('u')
-// ->andWhere('u.exampleField = :val')
-// ->setParameter('val', $value)
-// ->orderBy('u.id', 'ASC')
-// ->setMaxResults(10)
-// ->getQuery()
-// ->getResult()
-// ;
-// }
-
-// public function findOneBySomeField($value): ?User
-// {
-// return $this->createQueryBuilder('u')
-// ->andWhere('u.exampleField = :val')
-// ->setParameter('val', $value)
-// ->getQuery()
-// ->getOneOrNullResult()
-// ;
-// }
}
diff --git a/templates/base.html.twig b/templates/base.html.twig
index dba00ec..5991ecc 100644
--- a/templates/base.html.twig
+++ b/templates/base.html.twig
@@ -8,6 +8,8 @@
{% block stylesheets %}
{{ encore_entry_link_tags('app') }}
{% endblock %}
diff --git a/templates/feed/podcast.html.twig b/templates/feed/podcast.html.twig
index 13ed4e5..77a50f3 100644
--- a/templates/feed/podcast.html.twig
+++ b/templates/feed/podcast.html.twig
@@ -1,6 +1,6 @@
{% extends 'base.html.twig' %}
-{% block body %}{{ podcast.name }}{% endblock %}
+{% block title %}{{ podcast.name }} — LSBC{% endblock %}
{% block body %}
{{ podcast.name }}
@@ -10,4 +10,5 @@
{% set feed = url('app_podcast_feed', {slug: podcast.slug}) %}
Feed URL: {{ feed }}
+
{% endblock %}
diff --git a/templates/login/index.html.twig b/templates/login/index.html.twig
index b8829ad..6418537 100644
--- a/templates/login/index.html.twig
+++ b/templates/login/index.html.twig
@@ -2,24 +2,31 @@
{% block title %}Login{% endblock %}
-{% block body %}
- Login
- {% if error %}
- {{ error.messageKey|trans(error.messageData, 'security') }}
- {% endif %}
-
-
+{% block stylesheets %}
+ {{ parent() }}
+
+{% endblock %}
+
+{% block body %}
+ Login
+ {% if error %}
+ {{ error.messageKey|trans(error.messageData, 'security') }}
+ {% endif %}
+
+
{% endblock %}
diff --git a/templates/podcast/_delete_form.html.twig b/templates/podcast/_delete_form.html.twig
new file mode 100644
index 0000000..aae9488
--- /dev/null
+++ b/templates/podcast/_delete_form.html.twig
@@ -0,0 +1,4 @@
+
diff --git a/templates/podcast/_form.html.twig b/templates/podcast/_form.html.twig
new file mode 100644
index 0000000..bf20b98
--- /dev/null
+++ b/templates/podcast/_form.html.twig
@@ -0,0 +1,4 @@
+{{ form_start(form) }}
+ {{ form_widget(form) }}
+
+{{ form_end(form) }}
diff --git a/templates/podcast/edit.html.twig b/templates/podcast/edit.html.twig
new file mode 100644
index 0000000..6d80131
--- /dev/null
+++ b/templates/podcast/edit.html.twig
@@ -0,0 +1,13 @@
+{% extends 'base.html.twig' %}
+
+{% block title %}Edit Podcast{% endblock %}
+
+{% block body %}
+ Edit Podcast
+
+ {{ include('podcast/_form.html.twig', {'button_label': 'Update'}) }}
+
+ back to list
+
+ {{ include('podcast/_delete_form.html.twig') }}
+{% endblock %}
diff --git a/templates/podcast/index.html.twig b/templates/podcast/index.html.twig
new file mode 100644
index 0000000..daa245a
--- /dev/null
+++ b/templates/podcast/index.html.twig
@@ -0,0 +1,46 @@
+{% extends 'base.html.twig' %}
+
+{% block title %}Podcast index{% endblock %}
+
+{% block stylesheets %}
+ {{ parent() }}
+
+{% endblock %}
+
+{% block body %}
+ Podcast index
+
+
+
+
+ Id |
+ Name |
+ Author |
+ Logo |
+ actions |
+
+
+
+ {% for podcast in podcasts %}
+
+ {{ podcast.id }} |
+ {{ podcast.name }} ({{ podcast.slug }} ) |
+ {{ podcast.author }} |
+ |
+
+ show
+ edit
+ |
+
+ {% else %}
+
+ no records found |
+
+ {% endfor %}
+
+
+
+ Create new
+{% endblock %}
diff --git a/templates/podcast/new.html.twig b/templates/podcast/new.html.twig
new file mode 100644
index 0000000..f47fe0c
--- /dev/null
+++ b/templates/podcast/new.html.twig
@@ -0,0 +1,11 @@
+{% extends 'base.html.twig' %}
+
+{% block title %}New Podcast{% endblock %}
+
+{% block body %}
+ Create new Podcast
+
+ {{ include('podcast/_form.html.twig') }}
+
+ back to list
+{% endblock %}
diff --git a/templates/podcast/show.html.twig b/templates/podcast/show.html.twig
new file mode 100644
index 0000000..f2f993c
--- /dev/null
+++ b/templates/podcast/show.html.twig
@@ -0,0 +1,58 @@
+{% extends 'base.html.twig' %}
+
+{% block title %}Podcast{% endblock %}
+
+{% block body %}
+ Podcast
+
+
+
+
+ Id |
+ {{ podcast.id }} |
+
+
+ Name |
+ {{ podcast.name }} |
+
+
+ Slug |
+ {{ podcast.slug }} |
+
+
+ Website |
+ {{ podcast.website }} |
+
+
+ Description |
+ {{ podcast.description }} |
+
+
+ Author |
+ {{ podcast.author }} |
+
+
+ Email |
+ {{ podcast.email }} |
+
+
+ LogoFilename |
+ {{ podcast.logoFilename }} |
+
+
+ Logo |
+ |
+
+
+ Owner |
+ {{ podcast.owner }} |
+
+
+
+
+ back to list
+
+ edit
+
+ {{ include('podcast/_delete_form.html.twig') }}
+{% endblock %}