diff --git a/app/Database/Migrations/2020-06-05-170000_add_episodes.php b/app/Database/Migrations/2020-06-05-170000_add_episodes.php index 522400a8..5804c460 100644 --- a/app/Database/Migrations/2020-06-05-170000_add_episodes.php +++ b/app/Database/Migrations/2020-06-05-170000_add_episodes.php @@ -147,10 +147,6 @@ class AddEpisodes extends Migration 'updated_at' => [ 'type' => 'DATETIME', ], - 'deleted_at' => [ - 'type' => 'DATETIME', - 'null' => true, - ], ]); $this->forge->addPrimaryKey('id'); $this->forge->addUniqueKey(['podcast_id', 'slug']); diff --git a/app/Database/Seeds/AuthSeeder.php b/app/Database/Seeds/AuthSeeder.php index dff5c8eb..889ab902 100644 --- a/app/Database/Seeds/AuthSeeder.php +++ b/app/Database/Seeds/AuthSeeder.php @@ -207,12 +207,6 @@ class AuthSeeder extends Seeder ], [ 'name' => 'delete', - 'description' => - 'Delete an episode of a podcast without removing it from the database', - 'has_permission' => ['podcast_admin'], - ], - [ - 'name' => 'delete_permanently', 'description' => 'Delete all occurrences of an episode of a podcast from the database', 'has_permission' => ['podcast_admin'], diff --git a/app/Entities/Episode.php b/app/Entities/Episode.php index c706585c..32223685 100644 --- a/app/Entities/Episode.php +++ b/app/Entities/Episode.php @@ -78,7 +78,6 @@ use RuntimeException; * @property Time|null $published_at; * @property Time $created_at; * @property Time $updated_at; - * @property Time|null $deleted_at; * * @property Person[] $persons; * @property Soundbite[] $soundbites; diff --git a/app/Entities/Media/BaseMedia.php b/app/Entities/Media/BaseMedia.php index a45ede67..2acf221d 100644 --- a/app/Entities/Media/BaseMedia.php +++ b/app/Entities/Media/BaseMedia.php @@ -10,6 +10,8 @@ declare(strict_types=1); namespace App\Entities\Media; +use App\Models\MediaModel; +use CodeIgniter\Database\BaseResult; use CodeIgniter\Entity\Entity; use CodeIgniter\Files\File; @@ -101,9 +103,15 @@ class BaseMedia extends Entity return $this; } - public function deleteFile(): void + public function deleteFile(): bool { helper('media'); - unlink(media_path($this->file_path)); + return unlink(media_path($this->file_path)); + } + + public function delete(): bool|BaseResult + { + $mediaModel = new MediaModel(); + return $mediaModel->delete($this->id, true); } } diff --git a/app/Entities/Media/Image.php b/app/Entities/Media/Image.php index e57426c0..c1524806 100644 --- a/app/Entities/Media/Image.php +++ b/app/Entities/Media/Image.php @@ -69,11 +69,13 @@ class Image extends BaseMedia return $this; } - public function deleteFile(): void + public function deleteFile(): bool { - parent::deleteFile(); + if (parent::deleteFile()) { + return $this->deleteSizes(); + } - $this->deleteSizes(); + return false; } public function saveSizes(): void @@ -89,12 +91,16 @@ class Image extends BaseMedia } } - private function deleteSizes(): void + private function deleteSizes(): bool { // delete all derived sizes foreach (array_keys($this->sizes) as $name) { $pathProperty = $name . '_path'; - unlink(media_path($this->{$pathProperty})); + if (! unlink(media_path($this->{$pathProperty}))) { + return false; + } } + + return true; } } diff --git a/app/Entities/Media/Transcript.php b/app/Entities/Media/Transcript.php index a0dbebdf..988e2488 100644 --- a/app/Entities/Media/Transcript.php +++ b/app/Entities/Media/Transcript.php @@ -63,12 +63,16 @@ class Transcript extends BaseMedia return $this; } - public function deleteFile(): void + public function deleteFile(): bool { - parent::deleteFile(); + if (! parent::deleteFile()) { + return false; + } if ($this->json_path) { - unlink($this->json_path); + return unlink($this->json_path); } + + return true; } } diff --git a/app/Entities/Podcast.php b/app/Entities/Podcast.php index a54b7f62..b943ce8b 100644 --- a/app/Entities/Podcast.php +++ b/app/Entities/Podcast.php @@ -327,6 +327,18 @@ class Podcast extends Entity return $this->episodes; } + /** + * Returns the podcast's episodes count + */ + public function getEpisodesCount(): int|string + { + if ($this->id === null) { + throw new RuntimeException('Podcast must be created before getting number of episodes.'); + } + + return (new EpisodeModel())->getPodcastEpisodesCount($this->id); + } + /** * Returns the podcast's persons * diff --git a/app/Models/EpisodeModel.php b/app/Models/EpisodeModel.php index 573b6e76..3b20c07b 100644 --- a/app/Models/EpisodeModel.php +++ b/app/Models/EpisodeModel.php @@ -94,11 +94,6 @@ class EpisodeModel extends Model */ protected $returnType = Episode::class; - /** - * @var bool - */ - protected $useSoftDeletes = true; - /** * @var bool */ @@ -249,6 +244,18 @@ class EpisodeModel extends Model return $found; } + /** + * Returns number of episodes of a podcast + */ + public function getPodcastEpisodesCount(int $podcastId): int|string + { + return $this + ->where([ + 'podcast_id' => $podcastId, + ]) + ->countAllResults(); + } + /** * Returns the timestamp difference in seconds between the next episode to publish and the current timestamp Returns * false if there's no episode to publish diff --git a/app/Views/Components/Button.php b/app/Views/Components/Button.php index 79c96ac5..2c2429b3 100644 --- a/app/Views/Components/Button.php +++ b/app/Views/Components/Button.php @@ -38,6 +38,7 @@ class Button extends Component 'danger' => 'text-white bg-red-600 hover:bg-red-700', 'warning' => 'text-black bg-yellow-500 hover:bg-yellow-600', 'info' => 'text-white bg-blue-500 hover:bg-blue-600', + 'disabled' => 'text-black bg-gray-300 cursor-not-allowed', ]; $sizeClass = [ diff --git a/modules/Admin/Controllers/EpisodeController.php b/modules/Admin/Controllers/EpisodeController.php index 6b1373cf..9521c838 100644 --- a/modules/Admin/Controllers/EpisodeController.php +++ b/modules/Admin/Controllers/EpisodeController.php @@ -131,6 +131,18 @@ class EpisodeController extends BaseController ->with('errors', $this->validator->getErrors()); } + if ((new EpisodeModel()) + ->where([ + 'slug' => $this->request->getPost('slug'), + 'podcast_id' => $this->podcast->id, + ]) + ->first()) { + return redirect() + ->back() + ->withInput() + ->with('error', lang('Episode.messages.sameSlugError')); + } + $db = db_connect(); $db->transStart(); @@ -681,6 +693,11 @@ class EpisodeController extends BaseController ->with('errors', $episodeModel->errors()); } + // set podcast is_published_on_hubs to false to trigger websub push + (new PodcastModel())->update($this->episode->podcast->id, [ + 'is_published_on_hubs' => false, + ]); + $db->transComplete(); return redirect()->route('episode-view', [$this->podcast->id, $this->episode->id]); @@ -715,43 +732,74 @@ class EpisodeController extends BaseController ->with('errors', $this->validator->getErrors()); } + if ($this->episode->published_at !== null) { + return redirect() + ->back() + ->withInput() + ->with('error', lang('Episode.messages.deletePublishedEpisodeError')); + } + + $audio = $this->episode->audio; + $db = db_connect(); $db->transStart(); - $allPostsLinkedToEpisode = (new PostModel()) - ->where([ - 'episode_id' => $this->episode->id, - ]) - ->findAll(); - foreach ($allPostsLinkedToEpisode as $post) { - (new PostModel())->removePost($post); + $episodeModel = new EpisodeModel(); + + if (! $episodeModel->delete($this->episode->id)) { + $db->transRollback(); + return redirect() + ->back() + ->withInput() + ->with('errors', $episodeModel->errors()); } - // set podcast is_published_on_hubs to false to trigger websub push - (new PodcastModel())->update($this->episode->podcast->id, [ - 'is_published_on_hubs' => false, - ]); + $episodeMediaList = [$this->episode->transcript, $this->episode->chapters, $audio]; - $episodeModel = new EpisodeModel(); - if ($this->episode->published_at !== null) { - // if episode is published, set episode published_at to null to unpublish before deletion - $this->episode->published_at = null; + //only delete episode cover if different from podcast's + if ($this->episode->cover_id !== null) { + $episodeMediaList[] = $this->episode->cover; + } - if (! $episodeModel->update($this->episode->id, $this->episode)) { + //delete episode media records from database + foreach ($episodeMediaList as $episodeMedia) { + if ($episodeMedia !== null && ! $episodeMedia->delete()) { $db->transRollback(); return redirect() ->back() ->withInput() - ->with('errors', $episodeModel->errors()); + ->with('error', lang('Episode.messages.deleteError', [ + 'type' => $episodeMedia->type, + ])); } } - $episodeModel->delete($this->episode->id); + $warnings = []; + + //remove episode media files from disk + foreach ($episodeMediaList as $episodeMedia) { + if ($episodeMedia !== null && ! $episodeMedia->deleteFile()) { + $warnings[] = lang('Episode.messages.deleteFileError', [ + 'type' => $episodeMedia->type, + 'file_path' => $episodeMedia->file_path, + ]); + } + } $db->transComplete(); - return redirect()->route('episode-list', [$this->podcast->id]); + if ($warnings !== []) { + return redirect() + ->route('episode-list', [$this->podcast->id]) + ->with('message', lang('Episode.messages.deleteSuccess')) + ->with('warnings', $warnings); + } + + return redirect()->route('episode-list', [$this->podcast->id])->with( + 'message', + lang('Episode.messages.deleteSuccess') + ); } public function embed(): string diff --git a/modules/Admin/Language/en/Episode.php b/modules/Admin/Language/en/Episode.php index 82ed40dc..718bca1f 100644 --- a/modules/Admin/Language/en/Episode.php +++ b/modules/Admin/Language/en/Episode.php @@ -47,6 +47,24 @@ return [ 'createSuccess' => 'Episode has been successfully created!', 'editSuccess' => 'Episode has been successfully updated!', 'publishCancelSuccess' => 'Episode publication successfully cancelled!', + 'unpublishBeforeDeleteTip' => 'You must unpublish the episode before deleting it.', + 'deletePublishedEpisodeError' => 'Please unpublish the episode before deleting it.', + 'deleteSuccess' => 'Episode successfully deleted!', + 'deleteError' => 'Failed to delete episode {type, select, + transcript {transcript} + chapters {chapters} + image {cover} + audio {audio} + other {media} + }.', + 'deleteFileError' => 'Failed to delete {type, select, + transcript {transcript} + chapters {chapters} + image {cover} + audio {audio} + other {media} + } file {file_path}. You must manually remove it from your disk.', + 'sameSlugError' => 'An episode with the chosen slug already exists.', ], 'form' => [ 'file_size_error' => @@ -147,7 +165,7 @@ return [ ], 'delete_form' => [ 'disclaimer' => - "Deleting the episode will delete all the posts associated with it and remove it from the podcast's RSS feed.", + "Deleting the episode will delete all media files, comments, video clips and soundbites associated with it.", 'understand' => 'I understand, I want to delete the episode', 'submit' => 'Delete', ], diff --git a/modules/Admin/Language/fr/Episode.php b/modules/Admin/Language/fr/Episode.php index ee1ba07a..4b8d039f 100644 --- a/modules/Admin/Language/fr/Episode.php +++ b/modules/Admin/Language/fr/Episode.php @@ -147,7 +147,7 @@ return [ ], 'delete_form' => [ 'disclaimer' => - "Supprimer l’épisode supprimera toutes les publications qui lui sont associées et le retirera du flux RSS du podcast.", + "Supprimer l’épisode supprimera tous les fichiers multimédia, commentaires, extraits vidéos et extraits sonores qui lui sont associés.", 'understand' => 'Je comprends, je veux supprimer l’épisode', 'submit' => 'Supprimer', ], diff --git a/modules/Auth/Database/Seeds/AuthSeeder.php b/modules/Auth/Database/Seeds/AuthSeeder.php index 87e2bfcd..cb4c819a 100644 --- a/modules/Auth/Database/Seeds/AuthSeeder.php +++ b/modules/Auth/Database/Seeds/AuthSeeder.php @@ -195,12 +195,6 @@ class AuthSeeder extends Seeder ], [ 'name' => 'delete', - 'description' => - 'Delete an episode of a podcast without removing it from the database', - 'has_permission' => ['podcast_admin'], - ], - [ - 'name' => 'delete_permanently', 'description' => 'Delete all occurrences of an episode of a podcast from the database', 'has_permission' => ['podcast_admin'], diff --git a/modules/WebSub/Database/Migrations/2022-03-07-180000_add_is_published_on_hubs_to_podcasts.php b/modules/WebSub/Database/Migrations/2022-03-07-180000_add_is_published_on_hubs_to_podcasts.php index 523ffdfd..fb3d68ca 100644 --- a/modules/WebSub/Database/Migrations/2022-03-07-180000_add_is_published_on_hubs_to_podcasts.php +++ b/modules/WebSub/Database/Migrations/2022-03-07-180000_add_is_published_on_hubs_to_podcasts.php @@ -28,8 +28,6 @@ class AddIsPublishedOnHubsToPodcasts extends Migration public function down(): void { - $prefix = $this->db->getPrefix(); - - $this->forge->dropColumn($prefix . 'podcasts', 'is_published_on_hubs'); + $this->forge->dropColumn('podcasts', 'is_published_on_hubs'); } } diff --git a/modules/WebSub/Database/Migrations/2022-03-07-181500_add_is_published_on_hubs_to_episodes.php b/modules/WebSub/Database/Migrations/2022-03-07-181500_add_is_published_on_hubs_to_episodes.php index 3bb71966..dfe92a08 100644 --- a/modules/WebSub/Database/Migrations/2022-03-07-181500_add_is_published_on_hubs_to_episodes.php +++ b/modules/WebSub/Database/Migrations/2022-03-07-181500_add_is_published_on_hubs_to_episodes.php @@ -28,8 +28,6 @@ class AddIsPublishedOnHubsToEpisodes extends Migration public function down(): void { - $prefix = $this->db->getPrefix(); - - $this->forge->dropColumn($prefix . 'episodes', 'is_published_on_hubs'); + $this->forge->dropColumn('episodes', 'is_published_on_hubs'); } } diff --git a/themes/cp_admin/_message_block.php b/themes/cp_admin/_message_block.php index 1504aa37..8e3ee6df 100644 --- a/themes/cp_admin/_message_block.php +++ b/themes/cp_admin/_message_block.php @@ -17,3 +17,13 @@ if (session()->has('message')): ?> + +has('warnings')): ?> + + + + diff --git a/themes/cp_admin/episode/_card.php b/themes/cp_admin/episode/_card.php index 543d45e0..0d675fe9 100644 --- a/themes/cp_admin/episode/_card.php +++ b/themes/cp_admin/episode/_card.php @@ -11,7 +11,7 @@ - + ]; + } else { + $label = lang('Episode.delete'); + $icon = icon('forbid', 'mr-2'); + $title = lang('Episode.messages.unpublishBeforeDeleteTip'); + $items[] = [ + 'type' => 'html', + 'content' => esc(<<{$icon}{$label} + CODE_SAMPLE), + ]; + } ?> + \ No newline at end of file diff --git a/themes/cp_admin/episode/edit.php b/themes/cp_admin/episode/edit.php index 5611b60b..9191635d 100644 --- a/themes/cp_admin/episode/edit.php +++ b/themes/cp_admin/episode/edit.php @@ -278,7 +278,11 @@ - +published_at === null): ?> + + + + endSection() ?> diff --git a/themes/cp_admin/episode/list.php b/themes/cp_admin/episode/list.php index b491ea96..a7d2762b 100644 --- a/themes/cp_admin/episode/list.php +++ b/themes/cp_admin/episode/list.php @@ -73,50 +73,63 @@ [ 'header' => lang('Episode.list.actions'), 'cell' => function ($episode, $podcast) { + $items = [ + [ + 'type' => 'link', + 'title' => lang('Episode.go_to_page'), + 'uri' => route_to('episode', esc($podcast->handle), esc($episode->slug)), + ], + [ + 'type' => 'link', + 'title' => lang('Episode.edit'), + 'uri' => route_to('episode-edit', $podcast->id, $episode->id), + ], + [ + 'type' => 'link', + 'title' => lang('Episode.embed.title'), + 'uri' => route_to('embed-add', $podcast->id, $episode->id), + ], + [ + 'type' => 'link', + 'title' => lang('Person.persons'), + 'uri' => route_to('episode-persons-manage', $podcast->id, $episode->id), + ], + [ + 'type' => 'link', + 'title' => lang('VideoClip.list.title'), + 'uri' => route_to('video-clips-list', $episode->podcast->id, $episode->id), + ], + [ + 'type' => 'link', + 'title' => lang('Soundbite.list.title'), + 'uri' => route_to('soundbites-list', $podcast->id, $episode->id), + ], + [ + 'type' => 'separator', + ], + ]; + if ($episode->published_at === null) { + $items[] = [ + 'type' => 'link', + 'title' => lang('Episode.delete'), + 'uri' => route_to('episode-delete', $podcast->id, $episode->id), + 'class' => 'font-semibold text-red-600', + ]; + } else { + $label = lang('Episode.delete'); + $icon = icon('forbid'); + $title = lang('Episode.messages.unpublishBeforeDeleteTip'); + $items[] = [ + 'type' => 'html', + 'content' => esc(<<{$icon}{$label} + CODE_SAMPLE), + ]; + } return '' . - ''; + ''; }, ], ], diff --git a/themes/cp_admin/podcast/_sidebar.php b/themes/cp_admin/podcast/_sidebar.php index dadafde8..619e1f4c 100644 --- a/themes/cp_admin/podcast/_sidebar.php +++ b/themes/cp_admin/podcast/_sidebar.php @@ -33,7 +33,13 @@ $podcastNavigation = [ 'platforms-funding', ], ], -]; ?> +]; + +$counts = [ + 'episode-list' => $podcast->getEpisodesCount(), +]; + +?>
id)); ?> +
  • + ) ?>">