From dbb4030da49f9ea1f61759fb7c66d71fc29ea4a1 Mon Sep 17 00:00:00 2001
From: Ola Hneini <ola.hneini@gmail.com>
Date: Tue, 7 Jun 2022 11:13:06 +0000
Subject: [PATCH] =?UTF-8?q?feat:=20add=20permanent=20delete=20feature=20fo?=
 =?UTF-8?q?r=20podcasts=20=F0=9F=8E=89?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

closes #89
---
 .../2020-05-30-101500_add_podcasts.php        |   4 -
 app/Database/Seeds/AuthSeeder.php             |   6 -
 app/Entities/Podcast.php                      |   1 -
 app/Models/PodcastModel.php                   |   5 -
 modules/Admin/Config/Routes.php               |   3 +
 .../Admin/Controllers/EpisodeController.php   |   8 +-
 .../Admin/Controllers/PodcastController.php   | 171 +++++++++++++++++-
 modules/Admin/Language/en/Episode.php         |   2 +-
 modules/Admin/Language/en/Podcast.php         |  20 ++
 modules/Auth/Database/Seeds/AuthSeeder.php    |   6 -
 themes/cp_admin/_message_block.php            |   4 +
 themes/cp_admin/podcast/delete.php            |  27 +++
 themes/cp_admin/podcast/edit.php              |   3 +-
 13 files changed, 228 insertions(+), 32 deletions(-)
 create mode 100644 themes/cp_admin/podcast/delete.php

diff --git a/app/Database/Migrations/2020-05-30-101500_add_podcasts.php b/app/Database/Migrations/2020-05-30-101500_add_podcasts.php
index 82b9552abb..bc992195d9 100644
--- a/app/Database/Migrations/2020-05-30-101500_add_podcasts.php
+++ b/app/Database/Migrations/2020-05-30-101500_add_podcasts.php
@@ -183,10 +183,6 @@ class AddPodcasts extends Migration
             'updated_at' => [
                 'type' => 'DATETIME',
             ],
-            'deleted_at' => [
-                'type' => 'DATETIME',
-                'null' => true,
-            ],
         ]);
 
         $this->forge->addPrimaryKey('id');
diff --git a/app/Database/Seeds/AuthSeeder.php b/app/Database/Seeds/AuthSeeder.php
index 889ab902e8..8f09f54ad4 100644
--- a/app/Database/Seeds/AuthSeeder.php
+++ b/app/Database/Seeds/AuthSeeder.php
@@ -127,12 +127,6 @@ class AuthSeeder extends Seeder
             ],
             [
                 'name' => 'delete',
-                'description' =>
-                    'Delete a podcast without removing it from database',
-                'has_permission' => ['superadmin'],
-            ],
-            [
-                'name' => 'delete_permanently',
                 'description' => 'Delete any podcast from the database',
                 'has_permission' => ['superadmin'],
             ],
diff --git a/app/Entities/Podcast.php b/app/Entities/Podcast.php
index b943ce8b4a..84e99cda07 100644
--- a/app/Entities/Podcast.php
+++ b/app/Entities/Podcast.php
@@ -81,7 +81,6 @@ use RuntimeException;
  * @property int $updated_by
  * @property Time $created_at;
  * @property Time $updated_at;
- * @property Time|null $deleted_at;
  *
  * @property Episode[] $episodes
  * @property Person[] $persons
diff --git a/app/Models/PodcastModel.php b/app/Models/PodcastModel.php
index febf44e140..b28c9e2d15 100644
--- a/app/Models/PodcastModel.php
+++ b/app/Models/PodcastModel.php
@@ -73,11 +73,6 @@ class PodcastModel extends Model
      */
     protected $returnType = Podcast::class;
 
-    /**
-     * @var bool
-     */
-    protected $useSoftDeletes = true;
-
     /**
      * @var bool
      */
diff --git a/modules/Admin/Config/Routes.php b/modules/Admin/Config/Routes.php
index b263686ee7..64c394f0be 100644
--- a/modules/Admin/Config/Routes.php
+++ b/modules/Admin/Config/Routes.php
@@ -127,6 +127,9 @@ $routes->group(
                     'as' => 'podcast-delete',
                     'filter' => 'permission:podcasts-delete',
                 ]);
+                $routes->post('delete', 'PodcastController::attemptDelete/$1', [
+                    'filter' => 'permission:podcasts-delete',
+                ]);
 
                 $routes->group('persons', function ($routes): void {
                     $routes->get('/', 'PodcastPersonController/$1', [
diff --git a/modules/Admin/Controllers/EpisodeController.php b/modules/Admin/Controllers/EpisodeController.php
index 9521c8381d..90a17a2d93 100644
--- a/modules/Admin/Controllers/EpisodeController.php
+++ b/modules/Admin/Controllers/EpisodeController.php
@@ -739,8 +739,6 @@ class EpisodeController extends BaseController
                 ->with('error', lang('Episode.messages.deletePublishedEpisodeError'));
         }
 
-        $audio = $this->episode->audio;
-
         $db = db_connect();
 
         $db->transStart();
@@ -755,7 +753,7 @@ class EpisodeController extends BaseController
                 ->with('errors', $episodeModel->errors());
         }
 
-        $episodeMediaList = [$this->episode->transcript, $this->episode->chapters, $audio];
+        $episodeMediaList = [$this->episode->transcript, $this->episode->chapters, $this->episode->audio];
 
         //only delete episode cover if different from podcast's
         if ($this->episode->cover_id !== null) {
@@ -775,6 +773,8 @@ class EpisodeController extends BaseController
             }
         }
 
+        $db->transComplete();
+
         $warnings = [];
 
         //remove episode media files from disk
@@ -787,8 +787,6 @@ class EpisodeController extends BaseController
             }
         }
 
-        $db->transComplete();
-
         if ($warnings !== []) {
             return redirect()
                 ->route('episode-list', [$this->podcast->id])
diff --git a/modules/Admin/Controllers/PodcastController.php b/modules/Admin/Controllers/PodcastController.php
index 8fbbee4d94..aef52ed50b 100644
--- a/modules/Admin/Controllers/PodcastController.php
+++ b/modules/Admin/Controllers/PodcastController.php
@@ -21,6 +21,15 @@ use App\Models\PodcastModel;
 use CodeIgniter\Exceptions\PageNotFoundException;
 use CodeIgniter\HTTP\RedirectResponse;
 use Config\Services;
+use Modules\Analytics\Models\AnalyticsPodcastByCountryModel;
+use Modules\Analytics\Models\AnalyticsPodcastByEpisodeModel;
+use Modules\Analytics\Models\AnalyticsPodcastByHourModel;
+use Modules\Analytics\Models\AnalyticsPodcastByPlayerModel;
+use Modules\Analytics\Models\AnalyticsPodcastByRegionModel;
+use Modules\Analytics\Models\AnalyticsPodcastModel;
+use Modules\Analytics\Models\AnalyticsWebsiteByBrowserModel;
+use Modules\Analytics\Models\AnalyticsWebsiteByEntryPageModel;
+use Modules\Analytics\Models\AnalyticsWebsiteByRefererModel;
 
 class PodcastController extends BaseController
 {
@@ -420,10 +429,166 @@ class PodcastController extends BaseController
         ]);
     }
 
-    public function delete(): RedirectResponse
+    public function delete(): string
     {
-        (new PodcastModel())->delete($this->podcast->id);
+        helper(['form']);
 
-        return redirect()->route('podcast-list');
+        $data = [
+            'podcast' => $this->podcast,
+        ];
+
+        replace_breadcrumb_params([
+            0 => $this->podcast->title,
+        ]);
+        return view('podcast/delete', $data);
+    }
+
+    public function attemptDelete(): RedirectResponse
+    {
+        $rules = [
+            'understand' => 'required',
+        ];
+
+        if (! $this->validate($rules)) {
+            return redirect()
+                ->back()
+                ->withInput()
+                ->with('errors', $this->validator->getErrors());
+        }
+
+        $db = db_connect();
+
+        $db->transStart();
+
+        //delete podcast episodes
+        $podcastEpisodes = (new EpisodeModel())->where('podcast_id', $this->podcast->id)
+            ->findAll();
+
+        foreach ($podcastEpisodes as $podcastEpisode) {
+            $episodeModel = new EpisodeModel();
+
+            if (! $episodeModel->delete($podcastEpisode->id)) {
+                $db->transRollback();
+                return redirect()
+                    ->back()
+                    ->withInput()
+                    ->with('errors', $episodeModel->errors());
+            }
+
+            $episodeMediaList = [$podcastEpisode->transcript, $podcastEpisode->chapters, $podcastEpisode->audio];
+
+            //only delete episode cover if different from podcast's
+            if ($podcastEpisode->cover_id !== null) {
+                $episodeMediaList[] = $podcastEpisode->cover;
+            }
+
+            foreach ($episodeMediaList as $episodeMedia) {
+                if ($episodeMedia !== null && ! $episodeMedia->delete()) {
+                    $db->transRollback();
+                    return redirect()
+                        ->back()
+                        ->withInput()
+                        ->with('error', lang('Podcast.messages.deleteEpisodeMediaError', [
+                            'episode_slug' => $podcastEpisode->slug,
+                            'type' => $episodeMedia->type,
+                        ]));
+                }
+            }
+        }
+
+        //delete podcast
+        $podcastModel = new PodcastModel();
+
+        if (! $podcastModel->delete($this->podcast->id)) {
+            $db->transRollback();
+            return redirect()
+                ->back()
+                ->withInput()
+                ->with('errors', $podcastModel->errors());
+        }
+
+        //delete podcast media
+        $podcastMediaList = [
+            [
+                'type' => 'cover',
+                'file' => $this->podcast->cover,
+            ],
+            [
+                'type' => 'banner',
+                'file' => $this->podcast->banner,
+            ],
+        ];
+
+        foreach ($podcastMediaList as $podcastMedia) {
+            if ($podcastMedia['file'] !== null && ! $podcastMedia['file']->delete()) {
+                $db->transRollback();
+                return redirect()
+                    ->back()
+                    ->withInput()
+                    ->with('error', lang('Podcast.messages.deletePodcastMediaError', [
+                        'type' => $podcastMedia['type'],
+                    ]));
+            }
+        }
+
+        //delete podcast actor
+        $actorModel = new ActorModel();
+
+        if (! $actorModel->delete($this->podcast->actor_id)) {
+            $db->transRollback();
+            return redirect()
+                ->back()
+                ->withInput()
+                ->with('errors', $actorModel->errors());
+        }
+
+        //delete podcast analytics
+        $analyticsModels = [
+            new AnalyticsPodcastModel(),
+            new AnalyticsPodcastByCountryModel(),
+            new AnalyticsPodcastByEpisodeModel(),
+            new AnalyticsPodcastByHourModel(),
+            new AnalyticsPodcastByPlayerModel(),
+            new AnalyticsPodcastByRegionModel(),
+            new AnalyticsWebsiteByBrowserModel(),
+            new AnalyticsWebsiteByEntryPageModel(),
+            new AnalyticsWebsiteByRefererModel(),
+        ];
+        foreach ($analyticsModels as $analyticsModel) {
+            if (! $analyticsModel->where([
+                'podcast_id' => $this->podcast->id,
+            ])->delete()) {
+                $db->transRollback();
+                return redirect()
+                    ->back()
+                    ->withInput()
+                    ->with('errors', $analyticsModel->errors());
+            }
+        }
+
+        $db->transComplete();
+
+        //delete podcast media files and folder
+        $folder = 'podcasts/' . $this->podcast->handle;
+
+        $mediaRoot = config('App')
+            ->mediaRoot . '/' . $folder;
+
+        helper('filesystem');
+
+        if (! delete_files($mediaRoot) || ! rmdir($mediaRoot)) {
+            return redirect()->route('podcast-list')
+                ->with('message', lang('Podcast.messages.deleteSuccess', [
+                    'podcast_handle' => $this->podcast->handle,
+                ]))
+                ->with('warning', lang('Podcast.messages.deletePodcastMediaFolderError', [
+                    'folder_path' => $folder,
+                ]));
+        }
+
+        return redirect()->route('podcast-list')
+            ->with('message', lang('Podcast.messages.deleteSuccess', [
+                'podcast_handle' => $this->podcast->handle,
+            ]));
     }
 }
diff --git a/modules/Admin/Language/en/Episode.php b/modules/Admin/Language/en/Episode.php
index 718bca1f93..bd92e883df 100644
--- a/modules/Admin/Language/en/Episode.php
+++ b/modules/Admin/Language/en/Episode.php
@@ -63,7 +63,7 @@ return [
             image {cover}
             audio {audio}
             other {media}
-        } file {file_path}. You must manually remove it from your disk.',
+        } file {file_path}. You may manually remove it from your disk.',
         'sameSlugError' => 'An episode with the chosen slug already exists.',
     ],
     'form' => [
diff --git a/modules/Admin/Language/en/Podcast.php b/modules/Admin/Language/en/Podcast.php
index 67335c104f..eb0f30f19c 100644
--- a/modules/Admin/Language/en/Podcast.php
+++ b/modules/Admin/Language/en/Podcast.php
@@ -26,6 +26,20 @@ return [
         'createSuccess' => 'Podcast has been successfully created!',
         'editSuccess' => 'Podcast has been successfully updated!',
         'importSuccess' => 'Podcast has been successfully imported!',
+        'deleteSuccess' => 'Podcast @{podcast_handle} successfully deleted!',
+        'deletePodcastMediaError' => 'Failed to delete podcast {type, select,
+            cover {cover}
+            banner {banner}
+            other {media}
+        }.',
+        'deleteEpisodeMediaError' => 'Failed to delete podcast episode {episode_slug} {type, select,
+            transcript {transcript}
+            chapters {chapters}
+            image {cover}
+            audio {audio}
+            other {media}
+        }.',
+        'deletePodcastMediaFolderError' => 'Failed to delete podcast media folder {folder_path}. You may manually remove it from your disk.',
     ],
     'form' => [
         'identity_section_title' => 'Podcast identity',
@@ -219,6 +233,12 @@ return [
         'film_reviews' => 'Film Reviews',
         'tv_reviews' => 'TV Reviews',
     ],
+    'delete_form' => [
+        'disclaimer' =>
+            "Deleting the podcast will delete all episodes, media files, posts and analytics associated with it. This action is irreversible, you will not be able to retrieve them afterwards.",
+        'understand' => 'I understand, I want the podcast to be permanently deleted',
+        'submit' => 'Delete',
+    ],
     'by' => 'By {publisher}',
     'season' => 'Season {seasonNumber}',
     'list_of_episodes_year' => '{year} episodes ({episodeCount})',
diff --git a/modules/Auth/Database/Seeds/AuthSeeder.php b/modules/Auth/Database/Seeds/AuthSeeder.php
index cb4c819aa1..976c904b72 100644
--- a/modules/Auth/Database/Seeds/AuthSeeder.php
+++ b/modules/Auth/Database/Seeds/AuthSeeder.php
@@ -115,12 +115,6 @@ class AuthSeeder extends Seeder
             ],
             [
                 'name' => 'delete',
-                'description' =>
-                    'Delete a podcast without removing it from database',
-                'has_permission' => ['superadmin'],
-            ],
-            [
-                'name' => 'delete_permanently',
                 'description' => 'Delete any podcast from the database',
                 'has_permission' => ['superadmin'],
             ],
diff --git a/themes/cp_admin/_message_block.php b/themes/cp_admin/_message_block.php
index 8e3ee6df5c..5f1ba623b5 100644
--- a/themes/cp_admin/_message_block.php
+++ b/themes/cp_admin/_message_block.php
@@ -18,6 +18,10 @@ if (session()->has('message')): ?>
     </Alert>
 <?php endif; ?>
 
+<?php if (session()->has('warning')): ?>
+    <Alert variant="warning" class="mb-4"><?= esc(session('warning')) ?></Alert>
+<?php endif; ?>
+
 <?php if (session()->has('warnings')): ?>
     <Alert variant="warning" class="mb-4">
         <ul>
diff --git a/themes/cp_admin/podcast/delete.php b/themes/cp_admin/podcast/delete.php
new file mode 100644
index 0000000000..28251e149a
--- /dev/null
+++ b/themes/cp_admin/podcast/delete.php
@@ -0,0 +1,27 @@
+<?= $this->extend('_layout') ?>
+
+<?= $this->section('title') ?>
+<?= lang('Podcast.delete') ?>
+<?= $this->endSection() ?>
+
+<?= $this->section('pageTitle') ?>
+<?= lang('Podcast.delete') ?>
+<?= $this->endSection() ?>
+
+<?= $this->section('content') ?>
+
+<form action="<?= route_to('podcast-delete', $podcast->id) ?>" method="POST" class="flex flex-col w-full max-w-xl mx-auto">
+<?= csrf_field() ?>
+
+<Alert variant="danger" glyph="alert" class="font-semibold"><?= lang('Podcast.delete_form.disclaimer') ?></Alert>
+
+<Forms.Checkbox class="mt-2" name="understand" required="true" isChecked="false"><?= lang('Podcast.delete_form.understand') ?></Forms.Checkbox>
+
+<div class="self-end mt-4">
+    <Button uri="<?= route_to('podcast-view', $podcast->id) ?>"><?= lang('Common.cancel') ?></Button>
+    <Button type="submit" variant="danger"><?= lang('Podcast.delete_form.submit') ?></Button>
+</div>
+
+</form>
+
+<?= $this->endSection() ?>
diff --git a/themes/cp_admin/podcast/edit.php b/themes/cp_admin/podcast/edit.php
index 527e3544df..ef31830e9e 100644
--- a/themes/cp_admin/podcast/edit.php
+++ b/themes/cp_admin/podcast/edit.php
@@ -244,9 +244,10 @@
     </Forms.Toggler>
 </Forms.Section>
 
-<Button variant="primary" type="submit" class="self-end"><?= lang('Podcast.form.submit_edit') ?></Button>
 </div>
 
 </form>
 
+<Button class="mt-8" variant="danger" uri="<?= route_to('podcast-delete', $podcast->id) ?>" iconLeft="delete-bin"><?= lang('Podcast.delete') ?></Button>
+
 <?= $this->endSection() ?>
-- 
GitLab