Podcast and Episode management (still WIP…)
This commit is contained in:
parent
c39e7039ac
commit
ab35d77b42
151
src/Controller/EpisodeController.php
Normal file
151
src/Controller/EpisodeController.php
Normal file
|
@ -0,0 +1,151 @@
|
|||
<?php
|
||||
|
||||
namespace App\Controller;
|
||||
|
||||
use App\Entity\Episode;
|
||||
use App\Form\EpisodeType;
|
||||
use App\Repository\EpisodeRepository;
|
||||
use App\Repository\PodcastRepository;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||
use Symfony\Component\Form\FormError;
|
||||
use Symfony\Component\Form\FormInterface;
|
||||
use Symfony\Component\HttpFoundation\File\Exception\FileException;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Routing\Annotation\Route;
|
||||
use Symfony\Component\String\Slugger\SluggerInterface;
|
||||
|
||||
#[Route('/manage/episodes')]
|
||||
class EpisodeController extends AbstractController
|
||||
{
|
||||
public function __construct(protected LoggerInterface $logger)
|
||||
{}
|
||||
|
||||
#[Route('/', name: 'app_episode_index', methods: ['GET'])]
|
||||
public function index(EpisodeRepository $episodeRepository): Response
|
||||
{
|
||||
return $this->render('episode/index.html.twig', [
|
||||
'episodes' => $episodeRepository->findAll(),
|
||||
]);
|
||||
}
|
||||
|
||||
#[Route('/new', name: 'app_episode_new', methods: ['GET', 'POST'])]
|
||||
public function new(
|
||||
Request $request,
|
||||
EntityManagerInterface $entityManager,
|
||||
PodcastRepository $podcastRepository,
|
||||
SluggerInterface $slugger,
|
||||
): Response
|
||||
{
|
||||
$queryPodcastId = $request->query->getInt('podcast', 0);
|
||||
|
||||
$episode = new Episode();
|
||||
$form = $this->createForm(
|
||||
EpisodeType::class,
|
||||
$episode,
|
||||
[
|
||||
'owner' => $this->getUser(),
|
||||
'selectedPodcast' => $queryPodcastId,
|
||||
]
|
||||
);
|
||||
$form->handleRequest($request);
|
||||
|
||||
if ($form->isSubmitted() && $form->isValid()) {
|
||||
$this->logger->info("FLUMZO " . json_encode($form->getData()));
|
||||
|
||||
// Check that the user is the owner of the podcast this episode should belong to.
|
||||
$podcastId = $form->get('podcast')->getData();
|
||||
$podcast = $podcastRepository->find($podcastId ?? $queryPodcastId);
|
||||
if (
|
||||
$podcast === null
|
||||
|| $podcast->getOwner()->getId() !== $this->getUser()->getId()
|
||||
) {
|
||||
$form->get('podcast')->addError(new FormError('Invalid podcast.'));
|
||||
return $this->render('episode/new.html.twig', ['episode' => $episode, 'form' => $form]);
|
||||
}
|
||||
$episode->setPodcast($podcast);
|
||||
|
||||
// Ensure the uploaded audio file is saved properly.
|
||||
if (!$this->handleAudioChange($form, $episode, $slugger)) {
|
||||
$form->get('logo')->addError(new FormError('Could not upload audio.'));
|
||||
return $this->render('episode/new.html.twig', ['episode' => $episode, 'form' => $form]);
|
||||
}
|
||||
|
||||
$entityManager->persist($episode);
|
||||
$entityManager->flush();
|
||||
return $this->redirectToRoute('app_episode_index', [], Response::HTTP_SEE_OTHER);
|
||||
}
|
||||
|
||||
return $this->render('episode/new.html.twig', [
|
||||
'episode' => $episode,
|
||||
'form' => $form,
|
||||
]);
|
||||
}
|
||||
|
||||
#[Route('/{id}', name: 'app_episode_show', methods: ['GET'])]
|
||||
public function show(Episode $episode): Response
|
||||
{
|
||||
return $this->render('episode/show.html.twig', [
|
||||
'episode' => $episode,
|
||||
]);
|
||||
}
|
||||
|
||||
#[Route('/{id}/edit', name: 'app_episode_edit', methods: ['GET', 'POST'])]
|
||||
public function edit(Request $request, Episode $episode, EntityManagerInterface $entityManager): Response
|
||||
{
|
||||
$form = $this->createForm(
|
||||
EpisodeType::class,
|
||||
$episode,
|
||||
['selectedPodcast' => $episode->getPodcast()->getId()]
|
||||
);
|
||||
$form->handleRequest($request);
|
||||
|
||||
if ($form->isSubmitted() && $form->isValid()) {
|
||||
$entityManager->flush();
|
||||
|
||||
return $this->redirectToRoute('app_episode_index', [], Response::HTTP_SEE_OTHER);
|
||||
}
|
||||
|
||||
return $this->render('episode/edit.html.twig', [
|
||||
'episode' => $episode,
|
||||
'form' => $form,
|
||||
]);
|
||||
}
|
||||
|
||||
#[Route('/{id}', name: 'app_episode_delete', methods: ['POST'])]
|
||||
public function delete(Request $request, Episode $episode, EntityManagerInterface $entityManager): Response
|
||||
{
|
||||
if ($this->isCsrfTokenValid('delete'.$episode->getId(), $request->request->get('_token'))) {
|
||||
$entityManager->remove($episode);
|
||||
$entityManager->flush();
|
||||
}
|
||||
|
||||
return $this->redirectToRoute('app_episode_index', [], Response::HTTP_SEE_OTHER);
|
||||
}
|
||||
|
||||
protected function handleAudioChange(
|
||||
FormInterface $form,
|
||||
Episode $episode,
|
||||
SluggerInterface $slugger
|
||||
): bool
|
||||
{
|
||||
$audioFile = $form->get('audio')->getData();
|
||||
if ($audioFile) {
|
||||
$originalFilename = pathinfo($audioFile->getClientOriginalName(), PATHINFO_FILENAME);
|
||||
$safeFilename = $slugger->slug($originalFilename);
|
||||
$newFilename = $safeFilename . '-' . uniqid() . '.' . $audioFile->guessExtension();
|
||||
|
||||
try {
|
||||
$audioFile->move($this->getParameter('audio_directory'), $newFilename);
|
||||
} catch (FileException $e) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$episode->setAudioFilename($newFilename);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
|
@ -4,6 +4,7 @@ namespace App\Controller;
|
|||
|
||||
use App\Entity\Podcast;
|
||||
use App\Form\PodcastType;
|
||||
use App\Repository\EpisodeRepository;
|
||||
use App\Repository\PodcastRepository;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||
|
@ -97,7 +98,11 @@ class PodcastController extends AbstractController
|
|||
}
|
||||
|
||||
#[Route('/{id}', name: 'app_podcast_delete', methods: ['POST'])]
|
||||
public function delete(Request $request, Podcast $podcast, EntityManagerInterface $entityManager): Response
|
||||
public function delete(
|
||||
Request $request,
|
||||
Podcast $podcast,
|
||||
EntityManagerInterface $entityManager
|
||||
): Response
|
||||
{
|
||||
if ($this->isCsrfTokenValid('delete'.$podcast->getId(), $request->request->get('_token'))) {
|
||||
$entityManager->remove($podcast);
|
||||
|
@ -130,4 +135,16 @@ class PodcastController extends AbstractController
|
|||
|
||||
return true;
|
||||
}
|
||||
|
||||
#[Route('/{id}/episodes', name: 'app_podcast_episodes_index', methods: ['GET'])]
|
||||
public function episodes_index(
|
||||
Podcast $podcast,
|
||||
EpisodeRepository $episodeRepository
|
||||
): Response
|
||||
{
|
||||
return $this->render('episode/index.html.twig', [
|
||||
'podcast' => $podcast,
|
||||
'episodes' => $episodeRepository->findBy(['podcast' => $podcast]),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
|
71
src/Form/EpisodeType.php
Normal file
71
src/Form/EpisodeType.php
Normal file
|
@ -0,0 +1,71 @@
|
|||
<?php
|
||||
|
||||
namespace App\Form;
|
||||
|
||||
use App\Entity\Episode;
|
||||
use App\Entity\Podcast;
|
||||
use Doctrine\ORM\EntityRepository;
|
||||
use Doctrine\ORM\QueryBuilder;
|
||||
use Symfony\Bridge\Doctrine\Form\Type\EntityType;
|
||||
use Symfony\Component\Form\AbstractType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\FileType;
|
||||
use Symfony\Component\Form\FormBuilderInterface;
|
||||
use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||
use Symfony\Component\Validator\Constraints\File;
|
||||
|
||||
class EpisodeType extends AbstractType
|
||||
{
|
||||
public function buildForm(FormBuilderInterface $builder, array $options): void
|
||||
{
|
||||
$owner = $options['owner'] ?? null;
|
||||
$podcastId = $options['selectedPodcast'] ?? null;
|
||||
|
||||
$builder
|
||||
->add('title')
|
||||
->add('description')
|
||||
->add('audio', FileType::class, [
|
||||
'help' => 'The audio file of your episode.',
|
||||
'mapped' => false,
|
||||
'required' => false,
|
||||
'constraints' => new File([
|
||||
'maxSize' => '4m',
|
||||
'mimeTypes' => ['audio/mpeg', 'audio/ogg', 'audio/opus', 'audio/aac', 'audio/flac', 'audio/webm'],
|
||||
'mimeTypesMessage' => 'Please select an audio file (MP3, OGG audio, Opus, AAC, FLAC, WebM audio).',
|
||||
]),
|
||||
])
|
||||
->add('publicationDate')
|
||||
->add('podcast', EntityType::class, [
|
||||
'help' => 'The podcast this episode belongs to. Cannot be changed after creation.',
|
||||
'class' => Podcast::class,
|
||||
'query_builder' => function (EntityRepository $er) use ($owner, $podcastId): QueryBuilder {
|
||||
$qb = $er->createQueryBuilder('p');
|
||||
if ($owner !== null) {
|
||||
$qb
|
||||
->andWhere('p.owner = :ownerId')
|
||||
->setParameter('ownerId', $owner->getId());
|
||||
}
|
||||
if ($podcastId !== null) {
|
||||
$qb
|
||||
->andWhere('p.id = :podcastId')
|
||||
->setParameter('podcastId', $podcastId);
|
||||
}
|
||||
return $qb->orderBy('p.name', 'ASC');
|
||||
|
||||
},
|
||||
'placeholder' => false,
|
||||
'disabled' => $podcastId !== null,
|
||||
'empty_data' => "$podcastId",
|
||||
])
|
||||
;
|
||||
}
|
||||
|
||||
public function configureOptions(OptionsResolver $resolver): void
|
||||
{
|
||||
$resolver->setDefaults([
|
||||
'data_class' => Episode::class,
|
||||
'owner' => null,
|
||||
'selectedPodcast' => null,
|
||||
]);
|
||||
$resolver->setAllowedTypes('selectedPodcast', 'int');
|
||||
}
|
||||
}
|
|
@ -13,6 +13,12 @@
|
|||
{% endblock %}
|
||||
</head>
|
||||
<body>
|
||||
{% if app.user %}
|
||||
<nav>
|
||||
<a href="{{ path('app_podcast_index' )}}">Manage podcasts</a>
|
||||
<a href="{{ path('app_episode_index' )}}">Manage episodes</a>
|
||||
</nav>
|
||||
{% endif %}
|
||||
{% block body %}{% endblock %}
|
||||
</body>
|
||||
</html>
|
||||
|
|
4
templates/episode/_delete_form.html.twig
Normal file
4
templates/episode/_delete_form.html.twig
Normal file
|
@ -0,0 +1,4 @@
|
|||
<form method="post" action="{{ path('app_episode_delete', {'id': episode.id}) }}" onsubmit="return confirm('Are you sure you want to delete this item?');">
|
||||
<input type="hidden" name="_token" value="{{ csrf_token('delete' ~ episode.id) }}">
|
||||
<button class="btn">Delete</button>
|
||||
</form>
|
4
templates/episode/_form.html.twig
Normal file
4
templates/episode/_form.html.twig
Normal file
|
@ -0,0 +1,4 @@
|
|||
{{ form_start(form) }}
|
||||
{{ form_widget(form) }}
|
||||
<button class="btn">{{ button_label|default('Save') }}</button>
|
||||
{{ form_end(form) }}
|
13
templates/episode/edit.html.twig
Normal file
13
templates/episode/edit.html.twig
Normal file
|
@ -0,0 +1,13 @@
|
|||
{% extends 'base.html.twig' %}
|
||||
|
||||
{% block title %}Edit Episode{% endblock %}
|
||||
|
||||
{% block body %}
|
||||
<h1>Edit Episode</h1>
|
||||
|
||||
{{ include('episode/_form.html.twig', {'button_label': 'Update'}) }}
|
||||
|
||||
<a href="{{ path('app_episode_index') }}">Go back to list</a>
|
||||
|
||||
{{ include('episode/_delete_form.html.twig') }}
|
||||
{% endblock %}
|
56
templates/episode/index.html.twig
Normal file
56
templates/episode/index.html.twig
Normal file
|
@ -0,0 +1,56 @@
|
|||
{% extends 'base.html.twig' %}
|
||||
|
||||
{% block title %}Episode index{% endblock %}
|
||||
|
||||
{% block body %}
|
||||
<h1>
|
||||
{% if podcast is defined %}
|
||||
Episodes for {{ podcast.name }}
|
||||
{% else %}
|
||||
All your episodes
|
||||
{% endif %}
|
||||
</h1>
|
||||
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>ID</th>
|
||||
<th>Title</th>
|
||||
<th>Publication date</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for episode in episodes %}
|
||||
<tr>
|
||||
<td>{{ episode.id }}</td>
|
||||
<td>{{ episode.title }}</td>
|
||||
<td>{{ episode.publicationDate ? episode.publicationDate|date('Y-m-d H:i:s') : '' }}</td>
|
||||
<td>
|
||||
<a href="{{ path('app_episode_show', {'id': episode.id}) }}"
|
||||
><button>Show</button></a>
|
||||
<a href="{{ path('app_episode_edit', {'id': episode.id}) }}"
|
||||
><button>Edit</button></a>
|
||||
</td>
|
||||
</tr>
|
||||
{% else %}
|
||||
<tr>
|
||||
<td colspan="4">No episodes found.</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
{% if podcast is defined %}
|
||||
<a href="{{ path('app_episode_new', {'podcast': podcast.id }) }}"
|
||||
><button>Create new episode of this podcast</button></a>
|
||||
<a href="{{ path('app_podcast_show', {'id': podcast.id}) }}">Back to podcast</a>
|
||||
{% else %}
|
||||
<a href="{{ path('app_episode_new') }}"><button>Create new episode</button></a>
|
||||
<p>
|
||||
To manage episodes of a specific podcast, open the podcast page and use
|
||||
the “Manage podcasts” link.
|
||||
</p>
|
||||
{% endif %}
|
||||
|
||||
{% endblock %}
|
19
templates/episode/new.html.twig
Normal file
19
templates/episode/new.html.twig
Normal file
|
@ -0,0 +1,19 @@
|
|||
{% extends 'base.html.twig' %}
|
||||
|
||||
{% block title %}New episode{% endblock %}
|
||||
|
||||
{% block stylesheets %}
|
||||
{{ parent() }}
|
||||
<style>
|
||||
select { width: auto; background-image: none; }
|
||||
</style>
|
||||
{% endblock %}
|
||||
|
||||
|
||||
{% block body %}
|
||||
<h1>Create new episode</h1>
|
||||
|
||||
{{ include('episode/_form.html.twig') }}
|
||||
|
||||
<a href="{{ path('app_episode_index') }}">Go back to list</a>
|
||||
{% endblock %}
|
49
templates/episode/show.html.twig
Normal file
49
templates/episode/show.html.twig
Normal file
|
@ -0,0 +1,49 @@
|
|||
{% extends 'base.html.twig' %}
|
||||
|
||||
{% block title %}Episode “{{ episode.title }}”{% endblock %}
|
||||
|
||||
{% block stylesheets %}
|
||||
{{ parent() }}
|
||||
<style>
|
||||
th { min-width: 8em; }
|
||||
</style>
|
||||
{% endblock %}
|
||||
|
||||
{% block body %}
|
||||
<h1>Episode “{{ episode.title }}”</h1>
|
||||
|
||||
<table class="table">
|
||||
<tbody>
|
||||
<tr>
|
||||
<th>ID</th>
|
||||
<td>{{ episode.id }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Title</th>
|
||||
<td>{{ episode.title }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Description</th>
|
||||
<td>{{ episode.description }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Audio file name</th>
|
||||
<td>{{ episode.audioFilename }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Audio</th>
|
||||
<td><mark>TODO</mark></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Publication date</th>
|
||||
<td>{{ episode.publicationDate ? episode.publicationDate|date('Y-m-d H:i:s') : '' }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<a href="{{ path('app_episode_index') }}">back to list</a>
|
||||
|
||||
<a href="{{ path('app_episode_edit', {'id': episode.id}) }}">edit</a>
|
||||
|
||||
{{ include('episode/_delete_form.html.twig') }}
|
||||
{% endblock %}
|
|
@ -1,13 +1,13 @@
|
|||
{% extends 'base.html.twig' %}
|
||||
|
||||
{% block title %}Edit Podcast{% endblock %}
|
||||
{% block title %}Edit podcast {{ podcast.name }}{% endblock %}
|
||||
|
||||
{% block body %}
|
||||
<h1>Edit Podcast</h1>
|
||||
<h1>Edit podcast {{ podcast.name }}</h1>
|
||||
|
||||
{{ include('podcast/_form.html.twig', {'button_label': 'Update'}) }}
|
||||
|
||||
<a href="{{ path('app_podcast_index') }}">back to list</a>
|
||||
<a href="{{ path('app_podcast_index') }}">Go back to list</a>
|
||||
|
||||
{{ include('podcast/_delete_form.html.twig') }}
|
||||
{% endblock %}
|
||||
|
|
|
@ -15,11 +15,11 @@
|
|||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Id</th>
|
||||
<th>ID</th>
|
||||
<th>Name</th>
|
||||
<th>Author</th>
|
||||
<th>Logo</th>
|
||||
<th>actions</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
|
@ -44,5 +44,5 @@
|
|||
</tbody>
|
||||
</table>
|
||||
|
||||
<a href="{{ path('app_podcast_new') }}"><button>Create new</button></a>
|
||||
<a href="{{ path('app_podcast_new') }}"><button>Create new podcast</button></a>
|
||||
{% endblock %}
|
||||
|
|
|
@ -1,11 +1,11 @@
|
|||
{% extends 'base.html.twig' %}
|
||||
|
||||
{% block title %}New Podcast{% endblock %}
|
||||
{% block title %}New podcast{% endblock %}
|
||||
|
||||
{% block body %}
|
||||
<h1>Create new Podcast</h1>
|
||||
<h1>Create a new podcast</h1>
|
||||
|
||||
{{ include('podcast/_form.html.twig') }}
|
||||
|
||||
<a href="{{ path('app_podcast_index') }}">back to list</a>
|
||||
<a href="{{ path('app_podcast_index') }}">Go back to list</a>
|
||||
{% endblock %}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{% extends 'base.html.twig' %}
|
||||
|
||||
{% block title %}Podcast{% endblock %}
|
||||
{% block title %}Podcast {{ podcast.name }}{% endblock %}
|
||||
|
||||
{% block stylesheets %}
|
||||
{{ parent() }}
|
||||
|
@ -15,7 +15,7 @@
|
|||
<table class="table">
|
||||
<tbody>
|
||||
<tr>
|
||||
<th>Id</th>
|
||||
<th>ID</th>
|
||||
<td>{{ podcast.id }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
|
@ -43,7 +43,7 @@
|
|||
<td>{{ podcast.email }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>LogoFilename</th>
|
||||
<th>Logo file name</th>
|
||||
<td>{{ podcast.logoFilename }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
|
@ -54,12 +54,25 @@
|
|||
<th>Owner</th>
|
||||
<td>{{ podcast.owner }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Episodes</th>
|
||||
<td>
|
||||
<a href="{{ path('app_podcast_episodes_index', {'id': podcast.id}) }}"
|
||||
>Manage episodes</a>
|
||||
<ol>
|
||||
{% for episode in podcast.episodes %}
|
||||
<li>{{ episode.title }}</li>
|
||||
{% endfor %}
|
||||
</ol>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<a href="{{ path('app_podcast_index') }}">back to list</a>
|
||||
<a href="{{ path('app_podcast_index') }}">Go back to list</a>
|
||||
|
||||
<a href="{{ path('app_podcast_edit', {'id': podcast.id}) }}">edit</a>
|
||||
<a href="{{ path('app_podcast_edit', {'id': podcast.id}) }}"
|
||||
><button>Edit</button></a>
|
||||
|
||||
{{ include('podcast/_delete_form.html.twig') }}
|
||||
{% endblock %}
|
||||
|
|
Loading…
Reference in a new issue