From 8acd011f13e99492ef4b44b327685bb006fe5f8f Mon Sep 17 00:00:00 2001 From: Benjamin Bellamy Date: Wed, 10 Feb 2021 16:20:01 +0000 Subject: [PATCH] feat(person): add podcastindex.org namespace person tag --- .gitignore | 12 ++ app/Config/Routes.php | 75 ++++++++ app/Controllers/Admin/EpisodePerson.php | 111 +++++++++++ app/Controllers/Admin/Person.php | 147 ++++++++++++++ app/Controllers/Admin/PodcastImport.php | 181 ++++++++++++++---- app/Controllers/Admin/PodcastPerson.php | 89 +++++++++ app/Controllers/Episode.php | 44 +++++ app/Controllers/Page.php | 135 +++++++++++++ app/Controllers/Podcast.php | 44 +++++ .../2020-06-05-190000_add_platforms.php | 6 +- ...20-06-08-160000_add_podcasts_platforms.php | 6 - .../2020-12-25-120000_add_persons.php | 74 +++++++ ...2020-12-25-130000_add_podcasts_persons.php | 59 ++++++ ...2020-12-25-140000_add_episodes_persons.php | 65 +++++++ .../2020-12-25-150000_add_credit_view.php | 43 +++++ app/Database/Seeds/AuthSeeder.php | 27 +++ app/Database/Seeds/PlatformSeeder.php | 7 + app/Entities/Credit.php | 94 +++++++++ app/Entities/Episode.php | 29 +++ app/Entities/EpisodePerson.php | 36 ++++ app/Entities/Person.php | 59 ++++++ app/Entities/Podcast.php | 28 +++ app/Entities/PodcastPerson.php | 35 ++++ app/Helpers/page_helper.php | 3 + app/Helpers/rss_helper.php | 111 +++++++++-- app/Language/en/AdminNavigation.php | 3 + app/Language/en/Breadcrumb.php | 1 + app/Language/en/Person.php | 64 +++++++ app/Language/en/Podcast.php | 3 +- app/Language/en/PodcastNavigation.php | 2 + app/Language/fr/AdminNavigation.php | 3 + app/Language/fr/Breadcrumb.php | 1 + app/Language/fr/Person.php | 66 +++++++ app/Language/fr/Podcast.php | 3 +- app/Language/fr/PodcastNavigation.php | 2 + app/Models/CreditModel.php | 20 ++ app/Models/EpisodeModel.php | 23 ++- app/Models/EpisodePersonModel.php | 150 +++++++++++++++ app/Models/PersonModel.php | 134 +++++++++++++ app/Models/PlatformModel.php | 46 +++-- app/Models/PodcastModel.php | 8 +- app/Models/PodcastPersonModel.php | 119 ++++++++++++ app/Views/_assets/icons/folder-user.svg | 1 + .../images/platforms/podcasting/breaker.svg | 11 ++ app/Views/_layout.php | 22 ++- app/Views/admin/_sidebar.php | 4 + app/Views/admin/episode/list.php | 5 + app/Views/admin/episode/person.php | 131 +++++++++++++ app/Views/admin/episode/view.php | 6 + app/Views/admin/person/create.php | 95 +++++++++ app/Views/admin/person/edit.php | 95 +++++++++ app/Views/admin/person/list.php | 65 +++++++ app/Views/admin/person/view.php | 38 ++++ app/Views/admin/podcast/_sidebar.php | 4 + app/Views/admin/podcast/latest_episodes.php | 10 + app/Views/admin/podcast/person.php | 131 +++++++++++++ app/Views/credits.php | 49 +++++ app/Views/episode.php | 19 +- app/Views/podcast.php | 20 ++ composer.json | 13 +- composer.lock | 29 +++ 61 files changed, 2815 insertions(+), 101 deletions(-) create mode 100644 app/Controllers/Admin/EpisodePerson.php create mode 100644 app/Controllers/Admin/Person.php create mode 100644 app/Controllers/Admin/PodcastPerson.php create mode 100644 app/Database/Migrations/2020-12-25-120000_add_persons.php create mode 100644 app/Database/Migrations/2020-12-25-130000_add_podcasts_persons.php create mode 100644 app/Database/Migrations/2020-12-25-140000_add_episodes_persons.php create mode 100644 app/Database/Migrations/2020-12-25-150000_add_credit_view.php create mode 100644 app/Entities/Credit.php create mode 100644 app/Entities/EpisodePerson.php create mode 100644 app/Entities/Person.php create mode 100644 app/Entities/PodcastPerson.php create mode 100644 app/Language/en/Person.php create mode 100644 app/Language/fr/Person.php create mode 100644 app/Models/CreditModel.php create mode 100644 app/Models/EpisodePersonModel.php create mode 100644 app/Models/PersonModel.php create mode 100644 app/Models/PodcastPersonModel.php create mode 100644 app/Views/_assets/icons/folder-user.svg create mode 100644 app/Views/_assets/images/platforms/podcasting/breaker.svg create mode 100644 app/Views/admin/episode/person.php create mode 100644 app/Views/admin/person/create.php create mode 100644 app/Views/admin/person/edit.php create mode 100644 app/Views/admin/person/list.php create mode 100644 app/Views/admin/person/view.php create mode 100644 app/Views/admin/podcast/person.php create mode 100644 app/Views/credits.php diff --git a/.gitignore b/.gitignore index 28030f56..1baa8fe3 100644 --- a/.gitignore +++ b/.gitignore @@ -30,6 +30,9 @@ $RECYCLE.BIN/ # Linux *~ +# vim +*.swp + # KDE directory preferences .directory @@ -135,6 +138,7 @@ node_modules # public folder public/* !public/media +!public/media/~person !public/.htaccess !public/favicon.ico !public/index.php @@ -144,6 +148,14 @@ public/* public/media/* !public/media/index.html +# public person folder +public/media/~person/* +!public/media/~person/index.html + +# Generated files +app/Language/en/PersonsTaxonomy.php +app/Language/fr/PersonsTaxonomy.php + #------------------------- # Docker volumes #------------------------- diff --git a/app/Config/Routes.php b/app/Config/Routes.php index cfbc02c9..91de34a6 100644 --- a/app/Config/Routes.php +++ b/app/Config/Routes.php @@ -85,6 +85,37 @@ $routes->group( 'as' => 'my-podcasts', ]); + $routes->group('persons', function ($routes) { + $routes->get('/', 'Person', [ + 'as' => 'person-list', + 'filter' => 'permission:person-list', + ]); + $routes->get('new', 'Person::create', [ + 'as' => 'person-create', + 'filter' => 'permission:person-create', + ]); + $routes->post('new', 'Person::attemptCreate', [ + 'filter' => 'permission:person-create', + ]); + $routes->group('(:num)', function ($routes) { + $routes->get('/', 'Person::view/$1', [ + 'as' => 'person-view', + 'filter' => 'permission:person-view', + ]); + $routes->get('edit', 'Person::edit/$1', [ + 'as' => 'person-edit', + 'filter' => 'permission:person-edit', + ]); + $routes->post('edit', 'Person::attemptEdit/$1', [ + 'filter' => 'permission:person-edit', + ]); + $routes->add('delete', 'Person::delete/$1', [ + 'as' => 'person-delete', + 'filter' => 'permission:person-delete', + ]); + }); + }); + // Podcasts $routes->group('podcasts', function ($routes) { $routes->get('/', 'Podcast::list', [ @@ -124,6 +155,25 @@ $routes->group( 'filter' => 'permission:podcasts-delete', ]); + $routes->group('persons', function ($routes) { + $routes->get('/', 'PodcastPerson/$1', [ + 'as' => 'podcast-person-manage', + 'filter' => 'permission:podcast-edit', + ]); + $routes->post('/', 'PodcastPerson::attemptAdd/$1', [ + 'filter' => 'permission:podcast-edit', + ]); + + $routes->get( + '(:num)/remove', + 'PodcastPerson::remove/$1/$2', + [ + 'as' => 'podcast-person-remove', + 'filter' => 'permission:podcast-edit', + ] + ); + }); + $routes->group('analytics', function ($routes) { $routes->get('/', 'Podcast::viewAnalytics/$1', [ 'as' => 'podcast-analytics', @@ -276,6 +326,30 @@ $routes->group( 'filter' => 'permission:podcast_episodes-edit', ] ); + + $routes->group('persons', function ($routes) { + $routes->get('/', 'EpisodePerson/$1/$2', [ + 'as' => 'episode-person-manage', + 'filter' => 'permission:podcast_episodes-edit', + ]); + $routes->post( + '/', + 'EpisodePerson::attemptAdd/$1/$2', + [ + 'filter' => + 'permission:podcast_episodes-edit', + ] + ); + $routes->get( + '(:num)/remove', + 'EpisodePerson::remove/$1/$2/$3', + [ + 'as' => 'episode-person-remove', + 'filter' => + 'permission:podcast_episodes-edit', + ] + ); + }); }); }); @@ -497,6 +571,7 @@ $routes->group('@(:podcastName)', function ($routes) { $routes->head('feed.xml', 'Feed/$1', ['as' => 'podcast_feed']); $routes->get('feed.xml', 'Feed/$1', ['as' => 'podcast_feed']); }); +$routes->get('/credits', 'Page::credits', ['as' => 'credits']); $routes->get('/(:slug)', 'Page/$1', ['as' => 'page']); /** diff --git a/app/Controllers/Admin/EpisodePerson.php b/app/Controllers/Admin/EpisodePerson.php new file mode 100644 index 00000000..9d35dd9c --- /dev/null +++ b/app/Controllers/Admin/EpisodePerson.php @@ -0,0 +1,111 @@ + 1) { + if ( + !($this->podcast = (new PodcastModel())->getPodcastById( + $params[0] + )) + ) { + throw \CodeIgniter\Exceptions\PageNotFoundException::forPageNotFound(); + } + if ( + !($this->episode = (new EpisodeModel()) + ->where([ + 'id' => $params[1], + 'podcast_id' => $params[0], + ]) + ->first()) + ) { + throw \CodeIgniter\Exceptions\PageNotFoundException::forPageNotFound(); + } + } else { + throw \CodeIgniter\Exceptions\PageNotFoundException::forPageNotFound(); + } + unset($params[1]); + unset($params[0]); + + return $this->$method(...$params); + } + + public function index() + { + helper('form'); + + $data = [ + 'episode' => $this->episode, + 'podcast' => $this->podcast, + 'episodePersons' => (new EpisodePersonModel())->getPersonsByEpisodeId( + $this->podcast->id, + $this->episode->id + ), + 'personOptions' => (new PersonModel())->getPersonOptions(), + 'taxonomyOptions' => (new PersonModel())->getTaxonomyOptions(), + ]; + replace_breadcrumb_params([ + 0 => $this->podcast->title, + 1 => $this->episode->title, + ]); + return view('admin/episode/person', $data); + } + + public function attemptAdd() + { + $rules = [ + 'person' => 'required', + ]; + + if (!$this->validate($rules)) { + return redirect() + ->back() + ->withInput() + ->with('errors', $this->validator->getErrors()); + } + + (new EpisodePersonModel())->addEpisodePersons( + $this->podcast->id, + $this->episode->id, + $this->request->getPost('person'), + $this->request->getPost('person_group_role') + ); + + return redirect()->back(); + } + + public function remove($episodePersonId) + { + (new EpisodePersonModel())->removeEpisodePersons( + $this->podcast->id, + $this->episode->id, + $episodePersonId + ); + + return redirect()->back(); + } +} diff --git a/app/Controllers/Admin/Person.php b/app/Controllers/Admin/Person.php new file mode 100644 index 00000000..f78631ff --- /dev/null +++ b/app/Controllers/Admin/Person.php @@ -0,0 +1,147 @@ + 0) { + if ( + !($this->person = (new PersonModel())->getPersonById( + $params[0] + )) + ) { + throw \CodeIgniter\Exceptions\PageNotFoundException::forPageNotFound(); + } + } + + return $this->$method(); + } + + public function index() + { + $data = ['persons' => (new PersonModel())->findAll()]; + + return view('admin/person/list', $data); + } + + public function view() + { + $data = ['person' => $this->person]; + + replace_breadcrumb_params([0 => $this->person->full_name]); + return view('admin/person/view', $data); + } + + public function create() + { + helper(['form']); + + return view('admin/person/create'); + } + + public function attemptCreate() + { + $rules = [ + 'image' => + 'is_image[image]|ext_in[image,jpg,jpeg,png]|min_dims[image,400,400]|is_image_squared[image]', + ]; + + if (!$this->validate($rules)) { + return redirect() + ->back() + ->withInput() + ->with('errors', $this->validator->getErrors()); + } + + $person = new \App\Entities\Person([ + 'full_name' => $this->request->getPost('full_name'), + 'unique_name' => $this->request->getPost('unique_name'), + 'information_url' => $this->request->getPost('information_url'), + 'image' => $this->request->getFile('image'), + 'created_by' => user()->id, + 'updated_by' => user()->id, + ]); + + $personModel = new PersonModel(); + + if (!$personModel->insert($person)) { + return redirect() + ->back() + ->withInput() + ->with('errors', $personModel->errors()); + } + + return redirect()->route('person-list'); + } + + public function edit() + { + helper('form'); + + $data = [ + 'person' => $this->person, + ]; + + replace_breadcrumb_params([0 => $this->person->full_name]); + return view('admin/person/edit', $data); + } + + public function attemptEdit() + { + $rules = [ + 'image' => + 'is_image[image]|ext_in[image,jpg,jpeg,png]|min_dims[image,400,400]|is_image_squared[image]', + ]; + + if (!$this->validate($rules)) { + return redirect() + ->back() + ->withInput() + ->with('errors', $this->validator->getErrors()); + } + + $this->person->full_name = $this->request->getPost('full_name'); + $this->person->unique_name = $this->request->getPost('unique_name'); + $this->person->information_url = $this->request->getPost( + 'information_url' + ); + $image = $this->request->getFile('image'); + if ($image->isValid()) { + $this->person->image = $image; + } + + $this->updated_by = user(); + + $personModel = new PersonModel(); + if (!$personModel->update($this->person->id, $this->person)) { + return redirect() + ->back() + ->withInput() + ->with('errors', $personModel->errors()); + } + + return redirect()->route('person-view', [$this->person->id]); + } + + public function delete() + { + (new PersonModel())->delete($this->person->id); + + return redirect()->route('person-list'); + } +} diff --git a/app/Controllers/Admin/PodcastImport.php b/app/Controllers/Admin/PodcastImport.php index 0ae92f15..05625077 100644 --- a/app/Controllers/Admin/PodcastImport.php +++ b/app/Controllers/Admin/PodcastImport.php @@ -13,6 +13,9 @@ use App\Models\LanguageModel; use App\Models\PodcastModel; use App\Models\EpisodeModel; use App\Models\PlatformModel; +use App\Models\PersonModel; +use App\Models\PodcastPersonModel; +use App\Models\EpisodePersonModel; use Config\Services; use League\HTMLToMarkdown\HtmlConverter; @@ -150,7 +153,7 @@ class PodcastImport extends BaseController : $nsItunes->complete === 'yes', 'location_name' => !$nsPodcast->location ? null - : $nsPodcast->location->attributes()['name'], + : $nsPodcast->location, 'location_geo' => !$nsPodcast->location || empty($nsPodcast->location->attributes()['geo']) @@ -158,9 +161,9 @@ class PodcastImport extends BaseController : $nsPodcast->location->attributes()['geo'], 'location_osmid' => !$nsPodcast->location || - empty($nsPodcast->location->attributes()['osmid']) + empty($nsPodcast->location->attributes()['osm']) ? null - : $nsPodcast->location->attributes()['osmid'], + : $nsPodcast->location->attributes()['osm'], 'created_by' => user(), 'updated_by' => user(), ]); @@ -200,40 +203,40 @@ class PodcastImport extends BaseController $podcastAdminGroup->id ); - $platformModel = new PlatformModel(); $podcastsPlatformsData = []; - foreach ($nsPodcast->id as $podcastingPlatform) { - $slug = $podcastingPlatform->attributes()['platform']; - $platformModel->getOrCreatePlatform($slug, 'podcasting'); - array_push($podcastsPlatformsData, [ - 'platform_slug' => $slug, - 'podcast_id' => $newPodcastId, - 'link_url' => $podcastingPlatform->attributes()['url'], - 'link_content' => $podcastingPlatform->attributes()['id'], - 'is_visible' => false, - ]); - } - foreach ($nsPodcast->social as $socialPlatform) { - $slug = $socialPlatform->attributes()['platform']; - $platformModel->getOrCreatePlatform($slug, 'social'); - array_push($podcastsPlatformsData, [ - 'platform_slug' => $socialPlatform->attributes()['platform'], - 'podcast_id' => $newPodcastId, - 'link_url' => $socialPlatform->attributes()['url'], - 'link_content' => $socialPlatform, - 'is_visible' => false, - ]); - } - foreach ($nsPodcast->funding as $fundingPlatform) { - $slug = $fundingPlatform->attributes()['platform']; - $platformModel->getOrCreatePlatform($slug, 'funding'); - array_push($podcastsPlatformsData, [ - 'platform_slug' => $fundingPlatform->attributes()['platform'], - 'podcast_id' => $newPodcastId, - 'link_url' => $fundingPlatform->attributes()['url'], - 'link_content' => $fundingPlatform->attributes()['id'], - 'is_visible' => false, - ]); + $platformTypes = [ + ['name' => 'podcasting', 'elements' => $nsPodcast->id], + ['name' => 'social', 'elements' => $nsPodcast->social], + ['name' => 'funding', 'elements' => $nsPodcast->funding], + ]; + $platformModel = new PlatformModel(); + foreach ($platformTypes as $platformType) { + foreach ($platformType['elements'] as $platform) { + $platformLabel = $platform->attributes()['platform']; + $platformSlug = slugify($platformLabel); + if (!$platformModel->getPlatform($platformSlug)) { + if ( + !$platformModel->createPlatform( + $platformSlug, + $platformType['name'], + $platformLabel, + '' + ) + ) { + return redirect() + ->back() + ->withInput() + ->with('errors', $platformModel->errors()); + } + } + array_push($podcastsPlatformsData, [ + 'platform_slug' => $platformSlug, + 'podcast_id' => $newPodcastId, + 'link_url' => $platform->attributes()['url'], + 'link_content' => $platform->attributes()['id'], + 'is_visible' => false, + ]); + } } if (count($podcastsPlatformsData) > 1) { $platformModel->createPodcastPlatforms( @@ -242,6 +245,54 @@ class PodcastImport extends BaseController ); } + foreach ($nsPodcast->person as $podcastPerson) { + $personModel = new PersonModel(); + $newPersonId = null; + if ($newPerson = $personModel->getPerson($podcastPerson)) { + $newPersonId = $newPerson->id; + } else { + if ( + !($newPersonId = $personModel->createPerson( + $podcastPerson, + $podcastPerson->attributes()['href'], + $podcastPerson->attributes()['img'] + )) + ) { + return redirect() + ->back() + ->withInput() + ->with('errors', $personModel->errors()); + } + } + + $personGroup = empty($podcastPerson->attributes()['group']) + ? ['slug' => ''] + : \Podlibre\PodcastNamespace\ReversedTaxonomy::$taxonomy[ + (string) $podcastPerson->attributes()['group'] + ]; + $personRole = + empty($podcastPerson->attributes()['role']) || + empty($personGroup) + ? ['slug' => ''] + : $personGroup['roles'][ + strval($podcastPerson->attributes()['role']) + ]; + $newPodcastPerson = new \App\Entities\PodcastPerson([ + 'podcast_id' => $newPodcastId, + 'person_id' => $newPersonId, + 'person_group' => $personGroup['slug'], + 'person_role' => $personRole['slug'], + ]); + $podcastPersonModel = new PodcastPersonModel(); + + if (!$podcastPersonModel->insert($newPodcastPerson)) { + return redirect() + ->back() + ->withInput() + ->with('errors', $podcastPersonModel->errors()); + } + } + $numberItems = $feed->channel[0]->item->count(); $lastItem = !empty($this->request->getPost('max_episodes')) && @@ -251,6 +302,7 @@ class PodcastImport extends BaseController $slugs = []; + ////////////////////////////////////////////////////////////////// // For each Episode: for ($itemNumber = 1; $itemNumber <= $lastItem; $itemNumber++) { $item = $feed->channel[0]->item[$numberItems - $itemNumber]; @@ -326,7 +378,7 @@ class PodcastImport extends BaseController : $nsItunes->block === 'yes', 'location_name' => !$nsPodcast->location ? null - : $nsPodcast->location->attributes()['name'], + : $nsPodcast->location, 'location_geo' => !$nsPodcast->location || empty($nsPodcast->location->attributes()['geo']) @@ -334,9 +386,9 @@ class PodcastImport extends BaseController : $nsPodcast->location->attributes()['geo'], 'location_osmid' => !$nsPodcast->location || - empty($nsPodcast->location->attributes()['osmid']) + empty($nsPodcast->location->attributes()['osm']) ? null - : $nsPodcast->location->attributes()['osmid'], + : $nsPodcast->location->attributes()['osm'], 'created_by' => user(), 'updated_by' => user(), 'published_at' => strtotime($item->pubDate), @@ -344,13 +396,62 @@ class PodcastImport extends BaseController $episodeModel = new EpisodeModel(); - if (!$episodeModel->insert($newEpisode)) { + if (!($newEpisodeId = $episodeModel->insert($newEpisode, true))) { // FIXME: What shall we do? return redirect() ->back() ->withInput() ->with('errors', $episodeModel->errors()); } + + foreach ($nsPodcast->person as $episodePerson) { + $personModel = new PersonModel(); + $newPersonId = null; + if ($newPerson = $personModel->getPerson($episodePerson)) { + $newPersonId = $newPerson->id; + } else { + if ( + !($newPersonId = $personModel->createPerson( + $episodePerson, + $episodePerson->attributes()['href'], + $episodePerson->attributes()['img'] + )) + ) { + return redirect() + ->back() + ->withInput() + ->with('errors', $personModel->errors()); + } + } + + $personGroup = empty($episodePerson->attributes()['group']) + ? ['slug' => ''] + : \Podlibre\PodcastNamespace\ReversedTaxonomy::$taxonomy[ + strval($episodePerson->attributes()['group']) + ]; + $personRole = + empty($episodePerson->attributes()['role']) || + empty($personGroup) + ? ['slug' => ''] + : $personGroup['roles'][ + strval($episodePerson->attributes()['role']) + ]; + $newEpisodePerson = new \App\Entities\PodcastPerson([ + 'podcast_id' => $newPodcastId, + 'episode_id' => $newEpisodeId, + 'person_id' => $newPersonId, + 'person_group' => $personGroup['slug'], + 'person_role' => $personRole['slug'], + ]); + $episodePersonModel = new EpisodePersonModel(); + + if (!$episodePersonModel->insert($newEpisodePerson)) { + return redirect() + ->back() + ->withInput() + ->with('errors', $episodePersonModel->errors()); + } + } } $db->transComplete(); diff --git a/app/Controllers/Admin/PodcastPerson.php b/app/Controllers/Admin/PodcastPerson.php new file mode 100644 index 00000000..67603700 --- /dev/null +++ b/app/Controllers/Admin/PodcastPerson.php @@ -0,0 +1,89 @@ + 0) { + if ( + !($this->podcast = (new PodcastModel())->getPodcastById( + $params[0] + )) + ) { + throw \CodeIgniter\Exceptions\PageNotFoundException::forPageNotFound(); + } + } else { + throw \CodeIgniter\Exceptions\PageNotFoundException::forPageNotFound(); + } + unset($params[0]); + + return $this->$method(...$params); + } + + public function index() + { + helper('form'); + + $data = [ + 'podcast' => $this->podcast, + 'podcastPersons' => (new PodcastPersonModel())->getPersonsByPodcastId( + $this->podcast->id + ), + 'personOptions' => (new PersonModel())->getPersonOptions(), + 'taxonomyOptions' => (new PersonModel())->getTaxonomyOptions(), + ]; + replace_breadcrumb_params([ + 0 => $this->podcast->title, + ]); + return view('admin/podcast/person', $data); + } + + public function attemptAdd() + { + $rules = [ + 'person' => 'required', + ]; + + if (!$this->validate($rules)) { + return redirect() + ->back() + ->withInput() + ->with('errors', $this->validator->getErrors()); + } + + (new PodcastPersonModel())->addPodcastPersons( + $this->podcast->id, + $this->request->getPost('person'), + $this->request->getPost('person_group_role') + ); + + return redirect()->back(); + } + + public function remove($podcastPersonId) + { + (new PodcastPersonModel())->removePodcastPersons( + $this->podcast->id, + $podcastPersonId + ); + + return redirect()->back(); + } +} diff --git a/app/Controllers/Episode.php b/app/Controllers/Episode.php index 7b5dc9f7..3df89dda 100644 --- a/app/Controllers/Episode.php +++ b/app/Controllers/Episode.php @@ -54,11 +54,55 @@ class Episode extends BaseController $this->podcast->type ); + $persons = []; + foreach ($this->episode->episode_persons as $episodePerson) { + if (array_key_exists($episodePerson->person->id, $persons)) { + $persons[$episodePerson->person->id]['roles'] .= + empty($episodePerson->person_group) || + empty($episodePerson->person_role) + ? '' + : (empty( + $persons[$episodePerson->person->id][ + 'roles' + ] + ) + ? '' + : ', ') . + lang( + 'PersonsTaxonomy.persons.' . + $episodePerson->person_group . + '.roles.' . + $episodePerson->person_role . + '.label' + ); + } else { + $persons[$episodePerson->person->id] = [ + 'full_name' => $episodePerson->person->full_name, + 'information_url' => + $episodePerson->person->information_url, + 'thumbnail_url' => + $episodePerson->person->image->thumbnail_url, + 'roles' => + empty($episodePerson->person_group) || + empty($episodePerson->person_role) + ? '' + : lang( + 'PersonsTaxonomy.persons.' . + $episodePerson->person_group . + '.roles.' . + $episodePerson->person_role . + '.label' + ), + ]; + } + } + $data = [ 'previousEpisode' => $previousNextEpisodes['previous'], 'nextEpisode' => $previousNextEpisodes['next'], 'podcast' => $this->podcast, 'episode' => $this->episode, + 'persons' => $persons, ]; $secondsToNextUnpublishedEpisode = $episodeModel->getSecondsToNextUnpublishedEpisode( diff --git a/app/Controllers/Page.php b/app/Controllers/Page.php index b30b5fd6..74735dee 100644 --- a/app/Controllers/Page.php +++ b/app/Controllers/Page.php @@ -9,6 +9,8 @@ namespace App\Controllers; use App\Models\PageModel; +use App\Models\CreditModel; +use App\Models\PodcastModel; class Page extends BaseController { @@ -42,4 +44,137 @@ class Page extends BaseController ]; return view('page', $data); } + + public function credits() + { + $locale = service('request')->getLocale(); + $model = new PodcastModel(); + $allPodcasts = $model->findAll(); + + if (!($found = cache("credits_{$locale}"))) { + $page = new \App\Entities\Page([ + 'title' => lang('Person.credits', [], $locale), + 'slug' => 'credits', + 'content' => '', + ]); + + $creditModel = (new CreditModel())->findAll(); + + // Unlike the carpenter, we make a tree from a table: + + $person_group = null; + $person_id = null; + $person_role = null; + $credits = []; + foreach ($creditModel as $credit) { + if ($person_group !== $credit->person_group) { + $person_group = $credit->person_group; + $person_id = $credit->person_id; + $person_role = $credit->person_role; + $credits[$person_group] = [ + 'group_label' => $credit->group_label, + 'persons' => [ + $person_id => [ + 'full_name' => $credit->person->full_name, + 'thumbnail_url' => + $credit->person->image->thumbnail_url, + 'information_url' => + $credit->person->information_url, + 'roles' => [ + $person_role => [ + 'role_label' => $credit->role_label, + 'is_in' => [ + [ + 'link' => $credit->episode + ? $credit->episode->link + : $credit->podcast->link, + 'title' => $credit->episode + ? (count($allPodcasts) > 1 + ? "{$credit->podcast->title} ▸ " + : '') . + "(S{$credit->episode->season_number}E{$credit->episode->number}) {$credit->episode->title}" + : $credit->podcast->title, + ], + ], + ], + ], + ], + ], + ]; + } elseif ($person_id !== $credit->person_id) { + $person_id = $credit->person_id; + $person_role = $credit->person_role; + $credits[$person_group]['persons'][$person_id] = [ + 'full_name' => $credit->person->full_name, + 'thumbnail_url' => + $credit->person->image->thumbnail_url, + 'information_url' => $credit->person->information_url, + 'roles' => [ + $person_role => [ + 'role_label' => $credit->role_label, + 'is_in' => [ + [ + 'link' => $credit->episode + ? $credit->episode->link + : $credit->podcast->link, + 'title' => $credit->episode + ? (count($allPodcasts) > 1 + ? "{$credit->podcast->title} ▸ " + : '') . + "(S{$credit->episode->season_number}E{$credit->episode->number}) {$credit->episode->title}" + : $credit->podcast->title, + ], + ], + ], + ], + ]; + } elseif ($person_role !== $credit->person_role) { + $person_role = $credit->person_role; + $credits[$person_group]['persons'][$person_id]['roles'][ + $person_role + ] = [ + 'role_label' => $credit->role_label, + 'is_in' => [ + [ + 'link' => $credit->episode + ? $credit->episode->link + : $credit->podcast->link, + 'title' => $credit->episode + ? (count($allPodcasts) > 1 + ? "{$credit->podcast->title} ▸ " + : '') . + "(S{$credit->episode->season_number}E{$credit->episode->number}) {$credit->episode->title}" + : $credit->podcast->title, + ], + ], + ]; + } else { + $credits[$person_group]['persons'][$person_id]['roles'][ + $person_role + ]['is_in'][] = [ + 'link' => $credit->episode + ? $credit->episode->link + : $credit->podcast->link, + 'title' => $credit->episode + ? (count($allPodcasts) > 1 + ? "{$credit->podcast->title} ▸ " + : '') . + "(S{$credit->episode->season_number}E{$credit->episode->number}) {$credit->episode->title}" + : $credit->podcast->title, + ]; + } + } + + $data = [ + 'page' => $page, + 'credits' => $credits, + ]; + + $found = view('credits', $data); + + cache()->save("credits_{$locale}", $found, DECADE); + } + + return $found; + } } diff --git a/app/Controllers/Podcast.php b/app/Controllers/Podcast.php index 1e1dbda3..9c6fc675 100644 --- a/app/Controllers/Podcast.php +++ b/app/Controllers/Podcast.php @@ -109,6 +109,49 @@ class Podcast extends BaseController ]); } + $persons = []; + foreach ($this->podcast->podcast_persons as $podcastPerson) { + if (array_key_exists($podcastPerson->person->id, $persons)) { + $persons[$podcastPerson->person->id]['roles'] .= + empty($podcastPerson->person_group) || + empty($podcastPerson->person_role) + ? '' + : (empty( + $persons[$podcastPerson->person->id][ + 'roles' + ] + ) + ? '' + : ', ') . + lang( + 'PersonsTaxonomy.persons.' . + $podcastPerson->person_group . + '.roles.' . + $podcastPerson->person_role . + '.label' + ); + } else { + $persons[$podcastPerson->person->id] = [ + 'full_name' => $podcastPerson->person->full_name, + 'information_url' => + $podcastPerson->person->information_url, + 'thumbnail_url' => + $podcastPerson->person->image->thumbnail_url, + 'roles' => + empty($podcastPerson->person_group) || + empty($podcastPerson->person_role) + ? '' + : lang( + 'PersonsTaxonomy.persons.' . + $podcastPerson->person_group . + '.roles.' . + $podcastPerson->person_role . + '.label' + ), + ]; + } + } + $data = [ 'podcast' => $this->podcast, 'episodesNav' => $episodesNavigation, @@ -119,6 +162,7 @@ class Podcast extends BaseController $yearQuery, $seasonQuery ), + 'personArray' => $persons, ]; $secondsToNextUnpublishedEpisode = $episodeModel->getSecondsToNextUnpublishedEpisode( diff --git a/app/Database/Migrations/2020-06-05-190000_add_platforms.php b/app/Database/Migrations/2020-06-05-190000_add_platforms.php index bbb231e9..b79e7939 100644 --- a/app/Database/Migrations/2020-06-05-190000_add_platforms.php +++ b/app/Database/Migrations/2020-06-05-190000_add_platforms.php @@ -41,11 +41,9 @@ class AddPlatforms extends Migration 'default' => null, ], ]); + $this->forge->addField('`created_at` timestamp NOT NULL DEFAULT NOW()'); $this->forge->addField( - '`created_at` timestamp NOT NULL DEFAULT current_timestamp()' - ); - $this->forge->addField( - '`updated_at` timestamp NOT NULL DEFAULT current_timestamp() ON UPDATE current_timestamp()' + '`updated_at` timestamp NOT NULL DEFAULT NOW() ON UPDATE NOW()' ); $this->forge->addKey('slug', true); $this->forge->createTable('platforms'); diff --git a/app/Database/Migrations/2020-06-08-160000_add_podcasts_platforms.php b/app/Database/Migrations/2020-06-08-160000_add_podcasts_platforms.php index c353a5d7..045add85 100644 --- a/app/Database/Migrations/2020-06-08-160000_add_podcasts_platforms.php +++ b/app/Database/Migrations/2020-06-08-160000_add_podcasts_platforms.php @@ -40,12 +40,6 @@ class AddPodcastsPlatforms extends Migration 'constraint' => 1, 'default' => 0, ], - 'created_at' => [ - 'type' => 'DATETIME', - ], - 'updated_at' => [ - 'type' => 'DATETIME', - ], ]); $this->forge->addPrimaryKey(['podcast_id', 'platform_slug']); diff --git a/app/Database/Migrations/2020-12-25-120000_add_persons.php b/app/Database/Migrations/2020-12-25-120000_add_persons.php new file mode 100644 index 00000000..bacdafcc --- /dev/null +++ b/app/Database/Migrations/2020-12-25-120000_add_persons.php @@ -0,0 +1,74 @@ +forge->addField([ + 'id' => [ + 'type' => 'INT', + 'unsigned' => true, + 'auto_increment' => true, + ], + 'full_name' => [ + 'type' => 'VARCHAR', + 'constraint' => 192, + 'comment' => 'This is the full name or alias of the person.', + ], + 'unique_name' => [ + 'type' => 'VARCHAR', + 'constraint' => 192, + 'comment' => 'This is the slug name or alias of the person.', + 'unique' => true, + ], + 'information_url' => [ + 'type' => 'VARCHAR', + 'constraint' => 512, + 'comment' => + 'The url to a relevant resource of information about the person, such as a homepage or third-party profile platform.', + 'null' => true, + ], + 'image_uri' => [ + 'type' => 'VARCHAR', + 'constraint' => 255, + ], + 'created_by' => [ + 'type' => 'INT', + 'unsigned' => true, + ], + 'updated_by' => [ + 'type' => 'INT', + 'unsigned' => true, + ], + 'created_at' => [ + 'type' => 'DATETIME', + ], + 'updated_at' => [ + 'type' => 'DATETIME', + ], + ]); + + $this->forge->addKey('id', true); + $this->forge->addForeignKey('created_by', 'users', 'id'); + $this->forge->addForeignKey('updated_by', 'users', 'id'); + $this->forge->createTable('persons'); + } + + public function down() + { + $this->forge->dropTable('persons'); + } +} diff --git a/app/Database/Migrations/2020-12-25-130000_add_podcasts_persons.php b/app/Database/Migrations/2020-12-25-130000_add_podcasts_persons.php new file mode 100644 index 00000000..1e7bc16b --- /dev/null +++ b/app/Database/Migrations/2020-12-25-130000_add_podcasts_persons.php @@ -0,0 +1,59 @@ +forge->addField([ + 'id' => [ + 'type' => 'INT', + 'unsigned' => true, + 'auto_increment' => true, + ], + 'podcast_id' => [ + 'type' => 'INT', + 'unsigned' => true, + ], + 'person_id' => [ + 'type' => 'INT', + 'unsigned' => true, + ], + 'person_group' => [ + 'type' => 'VARCHAR', + 'constraint' => 32, + ], + 'person_role' => [ + 'type' => 'VARCHAR', + 'constraint' => 32, + ], + ]); + $this->forge->addKey('id', true); + $this->forge->addUniqueKey([ + 'podcast_id', + 'person_id', + 'person_group', + 'person_role', + ]); + $this->forge->addForeignKey('podcast_id', 'podcasts', 'id'); + $this->forge->addForeignKey('person_id', 'persons', 'id'); + $this->forge->createTable('podcasts_persons'); + } + + public function down() + { + $this->forge->dropTable('podcasts_persons'); + } +} diff --git a/app/Database/Migrations/2020-12-25-140000_add_episodes_persons.php b/app/Database/Migrations/2020-12-25-140000_add_episodes_persons.php new file mode 100644 index 00000000..4c1c6383 --- /dev/null +++ b/app/Database/Migrations/2020-12-25-140000_add_episodes_persons.php @@ -0,0 +1,65 @@ +forge->addField([ + 'id' => [ + 'type' => 'INT', + 'unsigned' => true, + 'auto_increment' => true, + ], + 'podcast_id' => [ + 'type' => 'INT', + 'unsigned' => true, + ], + 'episode_id' => [ + 'type' => 'INT', + 'unsigned' => true, + ], + 'person_id' => [ + 'type' => 'INT', + 'unsigned' => true, + ], + 'person_group' => [ + 'type' => 'VARCHAR', + 'constraint' => 32, + ], + 'person_role' => [ + 'type' => 'VARCHAR', + 'constraint' => 32, + ], + ]); + $this->forge->addKey('id', true); + $this->forge->addUniqueKey([ + 'podcast_id', + 'episode_id', + 'person_id', + 'person_group', + 'person_role', + ]); + $this->forge->addForeignKey('podcast_id', 'podcasts', 'id'); + $this->forge->addForeignKey('episode_id', 'episodes', 'id'); + $this->forge->addForeignKey('person_id', 'persons', 'id'); + $this->forge->createTable('episodes_persons'); + } + + public function down() + { + $this->forge->dropTable('episodes_persons'); + } +} diff --git a/app/Database/Migrations/2020-12-25-150000_add_credit_view.php b/app/Database/Migrations/2020-12-25-150000_add_credit_view.php new file mode 100644 index 00000000..42731dfc --- /dev/null +++ b/app/Database/Migrations/2020-12-25-150000_add_credit_view.php @@ -0,0 +1,43 @@ +db->prefixTable('credits'); + $personTable = $this->db->prefixTable('persons'); + $podcastPersonTable = $this->db->prefixTable('podcasts_persons'); + $episodePersonTable = $this->db->prefixTable('episodes_persons'); + $createQuery = <<db->query($createQuery); + } + + public function down() + { + $viewName = $this->db->prefixTable('credits'); + $this->db->query("DROP VIEW IF EXISTS `$viewName`"); + } +} diff --git a/app/Database/Seeds/AuthSeeder.php b/app/Database/Seeds/AuthSeeder.php index 8c509669..eb567ad0 100644 --- a/app/Database/Seeds/AuthSeeder.php +++ b/app/Database/Seeds/AuthSeeder.php @@ -198,6 +198,33 @@ class AuthSeeder extends Seeder 'has_permission' => ['podcast_admin'], ], ], + 'person' => [ + [ + 'name' => 'create', + 'description' => 'Add a new person', + 'has_permission' => ['superadmin'], + ], + [ + 'name' => 'list', + 'description' => 'List all persons', + 'has_permission' => ['superadmin'], + ], + [ + 'name' => 'view', + 'description' => 'View any person', + 'has_permission' => ['superadmin'], + ], + [ + 'name' => 'edit', + 'description' => 'Edit a person', + 'has_permission' => ['superadmin'], + ], + [ + 'name' => 'delete_permanently', + 'description' => 'Delete any person from the database', + 'has_permission' => ['superadmin'], + ], + ], ]; static function getGroupIdByName($name, $dataGroups) diff --git a/app/Database/Seeds/PlatformSeeder.php b/app/Database/Seeds/PlatformSeeder.php index c6df88a0..4e0bdb63 100644 --- a/app/Database/Seeds/PlatformSeeder.php +++ b/app/Database/Seeds/PlatformSeeder.php @@ -47,6 +47,13 @@ class PlatformSeeder extends Seeder 'home_url' => 'https://www.blubrry.com/', 'submit_url' => 'https://www.blubrry.com/addpodcast.php', ], + [ + 'slug' => 'breaker', + 'type' => 'podcasting', + 'label' => 'Breaker', + 'home_url' => 'https://www.breaker.audio/', + 'submit_url' => 'https://podcasters.breaker.audio/', + ], [ 'slug' => 'castbox', 'type' => 'podcasting', diff --git a/app/Entities/Credit.php b/app/Entities/Credit.php new file mode 100644 index 00000000..0988e7ca --- /dev/null +++ b/app/Entities/Credit.php @@ -0,0 +1,94 @@ +getPodcastById( + $this->attributes['podcast_id'] + ); + } + + public function getEpisode() + { + if (empty($this->attributes['episode_id'])) { + return null; + } else { + return (new EpisodeModel())->getEpisodeById( + $this->attributes['podcast_id'], + $this->attributes['episode_id'] + ); + } + } + + public function getPerson() + { + return (new PersonModel())->getPersonById( + $this->attributes['person_id'] + ); + } + + public function getGroupLabel() + { + if (empty($this->attributes['person_group'])) { + return null; + } else { + return lang( + "PersonsTaxonomy.persons.{$this->attributes['person_group']}.label" + ); + } + } + + public function getRoleLabel() + { + if ( + empty($this->attributes['person_group']) || + empty($this->attributes['person_role']) + ) { + return null; + } else { + return lang( + "PersonsTaxonomy.persons.{$this->attributes['person_group']}.roles.{$this->attributes['person_role']}.label" + ); + } + } +} diff --git a/app/Entities/Episode.php b/app/Entities/Episode.php index 4249defe..964d51b2 100644 --- a/app/Entities/Episode.php +++ b/app/Entities/Episode.php @@ -10,6 +10,7 @@ namespace App\Entities; use App\Models\PodcastModel; use App\Models\SoundbiteModel; +use App\Models\EpisodePersonModel; use CodeIgniter\Entity; use CodeIgniter\I18n\Time; use League\CommonMark\CommonMarkConverter; @@ -76,6 +77,11 @@ class Episode extends Entity */ protected $chapters_url; + /** + * @var \App\Entities\EpisodePerson[] + */ + protected $episode_persons; + /** * @var \App\Entities\Soundbite[] */ @@ -358,6 +364,29 @@ class Episode extends Entity : null; } + /** + * Returns the episode's persons + * + * @return \App\Entities\EpisodePerson[] + */ + public function getEpisodePersons() + { + if (empty($this->id)) { + throw new \RuntimeException( + 'Episode must be created before getting persons.' + ); + } + + if (empty($this->episode_persons)) { + $this->episode_persons = (new EpisodePersonModel())->getPersonsByEpisodeId( + $this->podcast_id, + $this->id + ); + } + + return $this->episode_persons; + } + /** * Returns the episode’s soundbites * diff --git a/app/Entities/EpisodePerson.php b/app/Entities/EpisodePerson.php new file mode 100644 index 00000000..6c0a6388 --- /dev/null +++ b/app/Entities/EpisodePerson.php @@ -0,0 +1,36 @@ + 'integer', + 'podcast_id' => 'integer', + 'episode_id' => 'integer', + 'person_id' => 'integer', + 'person_group' => '?string', + 'person_role' => '?string', + ]; + + public function getPerson() + { + return (new PersonModel())->getPersonById( + $this->attributes['person_id'] + ); + } +} diff --git a/app/Entities/Person.php b/app/Entities/Person.php new file mode 100644 index 00000000..8f20885c --- /dev/null +++ b/app/Entities/Person.php @@ -0,0 +1,59 @@ + 'integer', + 'full_name' => 'string', + 'unique_name' => 'string', + 'information_url' => '?string', + 'image_uri' => 'string', + 'created_by' => 'integer', + 'updated_by' => 'integer', + ]; + + /** + * Saves a picture in `public/media/~person/` + * + * @param \CodeIgniter\HTTP\Files\UploadedFile|\CodeIgniter\Files\File $image + * + */ + public function setImage($image = null) + { + if ($image) { + helper('media'); + + $this->attributes['image_uri'] = save_podcast_media( + $image, + '~person', + $this->attributes['unique_name'] + ); + $this->image = new \App\Entities\Image( + $this->attributes['image_uri'] + ); + $this->image->saveSizes(); + } + + return $this; + } + + public function getImage() + { + return new \App\Entities\Image($this->attributes['image_uri']); + } +} diff --git a/app/Entities/Podcast.php b/app/Entities/Podcast.php index 8782a0a7..f35cd759 100644 --- a/app/Entities/Podcast.php +++ b/app/Entities/Podcast.php @@ -11,6 +11,7 @@ namespace App\Entities; use App\Models\CategoryModel; use App\Models\EpisodeModel; use App\Models\PlatformModel; +use App\Models\PodcastPersonModel; use CodeIgniter\Entity; use App\Models\UserModel; use League\CommonMark\CommonMarkConverter; @@ -32,6 +33,11 @@ class Podcast extends Entity */ protected $episodes; + /** + * @var \App\Entities\PodcastPerson[] + */ + protected $podcast_persons; + /** * @var \App\Entities\Category */ @@ -167,6 +173,28 @@ class Podcast extends Entity return $this->episodes; } + /** + * Returns the podcast's persons + * + * @return \App\Entities\PodcastPerson[] + */ + public function getPodcastPersons() + { + if (empty($this->id)) { + throw new \RuntimeException( + 'Podcast must be created before getting persons.' + ); + } + + if (empty($this->podcast_persons)) { + $this->podcast_persons = (new PodcastPersonModel())->getPersonsByPodcastId( + $this->id + ); + } + + return $this->podcast_persons; + } + /** * Returns the podcast category entity * diff --git a/app/Entities/PodcastPerson.php b/app/Entities/PodcastPerson.php new file mode 100644 index 00000000..95dec77c --- /dev/null +++ b/app/Entities/PodcastPerson.php @@ -0,0 +1,35 @@ + 'integer', + 'podcast_id' => 'integer', + 'person_id' => 'integer', + 'person_group' => '?string', + 'person_role' => '?string', + ]; + + public function getPerson() + { + return (new PersonModel())->getPersonById( + $this->attributes['person_id'] + ); + } +} diff --git a/app/Helpers/page_helper.php b/app/Helpers/page_helper.php index 74b64220..5dda5b38 100644 --- a/app/Helpers/page_helper.php +++ b/app/Helpers/page_helper.php @@ -20,6 +20,9 @@ function render_page_links($class = null) $links = anchor(route_to('home'), lang('Common.home'), [ 'class' => 'px-2 underline hover:no-underline', ]); + $links .= anchor(route_to('credits'), lang('Person.credits'), [ + 'class' => 'px-2 underline hover:no-underline', + ]); foreach ($pages as $page) { $links .= anchor($page->link, $page->title, [ 'class' => 'px-2 underline hover:no-underline', diff --git a/app/Helpers/rss_helper.php b/app/Helpers/rss_helper.php index bfc17984..69a08539 100644 --- a/app/Helpers/rss_helper.php +++ b/app/Helpers/rss_helper.php @@ -68,18 +68,14 @@ function get_rss_feed($podcast, $serviceSlug = '') if (!empty($podcast->location_name)) { $locationElement = $channel->addChild( 'location', - null, + htmlspecialchars($podcast->location_name), $podcast_namespace ); - $locationElement->addAttribute( - 'name', - htmlspecialchars($podcast->location_name) - ); if (!empty($podcast->location_geo)) { $locationElement->addAttribute('geo', $podcast->location_geo); } if (!empty($podcast->location_osmid)) { - $locationElement->addAttribute('osmid', $podcast->location_osmid); + $locationElement->addAttribute('osm', $podcast->location_osmid); } } if (!empty($podcast->payment_pointer)) { @@ -105,7 +101,7 @@ function get_rss_feed($podcast, $serviceSlug = '') ) ->addAttribute('owner', $podcast->owner_email); if (!empty($podcast->imported_feed_url)) { - $channel->addChildWithCDATA( + $channel->addChild( 'previousUrl', $podcast->imported_feed_url, $podcast_namespace @@ -169,6 +165,51 @@ function get_rss_feed($podcast, $serviceSlug = '') } } + foreach ($podcast->podcast_persons as $podcastPerson) { + $podcastPersonElement = $channel->addChild( + 'person', + htmlspecialchars($podcastPerson->person->full_name), + $podcast_namespace + ); + if ( + !empty($podcastPerson->person_role) && + !empty($podcastPerson->person_group) + ) { + $podcastPersonElement->addAttribute( + 'role', + htmlspecialchars( + lang( + "PersonsTaxonomy.persons.{$podcastPerson->person_group}.roles.{$podcastPerson->person_role}.label", + [], + 'en' + ) + ) + ); + } + if (!empty($podcastPerson->person_group)) { + $podcastPersonElement->addAttribute( + 'group', + htmlspecialchars( + lang( + "PersonsTaxonomy.persons.{$podcastPerson->person_group}.label", + [], + 'en' + ) + ) + ); + } + $podcastPersonElement->addAttribute( + 'img', + $podcastPerson->person->image->large_url + ); + if (!empty($podcastPerson->person->information_url)) { + $podcastPersonElement->addAttribute( + 'href', + $podcastPerson->person->information_url + ); + } + } + // set main category first, then other categories as apple add_category_tag($channel, $podcast->category); foreach ($podcast->other_categories as $other_category) { @@ -222,21 +263,14 @@ function get_rss_feed($podcast, $serviceSlug = '') if (!empty($episode->location_name)) { $locationElement = $item->addChild( 'location', - null, + htmlspecialchars($episode->location_name), $podcast_namespace ); - $locationElement->addAttribute( - 'name', - htmlspecialchars($episode->location_name) - ); if (!empty($episode->location_geo)) { $locationElement->addAttribute('geo', $episode->location_geo); } if (!empty($episode->location_osmid)) { - $locationElement->addAttribute( - 'osmid', - $episode->location_osmid - ); + $locationElement->addAttribute('osm', $episode->location_osmid); } } $item->addChildWithCDATA('description', $episode->description_html); @@ -312,6 +346,51 @@ function get_rss_feed($podcast, $serviceSlug = '') $soundbiteElement->addAttribute('duration', $soundbite->duration); } + foreach ($episode->episode_persons as $episodePerson) { + $episodePersonElement = $item->addChild( + 'person', + htmlspecialchars($episodePerson->person->full_name), + $podcast_namespace + ); + if ( + !empty($episodePerson->person_role) && + !empty($episodePerson->person_group) + ) { + $episodePersonElement->addAttribute( + 'role', + htmlspecialchars( + lang( + "PersonsTaxonomy.persons.{$episodePerson->person_group}.roles.{$episodePerson->person_role}.label", + [], + 'en' + ) + ) + ); + } + if (!empty($episodePerson->person_group)) { + $episodePersonElement->addAttribute( + 'group', + htmlspecialchars( + lang( + "PersonsTaxonomy.persons.{$episodePerson->person_group}.label", + [], + 'en' + ) + ) + ); + } + $episodePersonElement->addAttribute( + 'img', + $episodePerson->person->image->large_url + ); + if (!empty($episodePerson->person->information_url)) { + $episodePersonElement->addAttribute( + 'href', + $episodePerson->person->information_url + ); + } + } + $episode->is_blocked && $item->addChild('block', 'Yes', $itunes_namespace); } diff --git a/app/Language/en/AdminNavigation.php b/app/Language/en/AdminNavigation.php index d9c2ada0..aa36aabf 100644 --- a/app/Language/en/AdminNavigation.php +++ b/app/Language/en/AdminNavigation.php @@ -14,6 +14,9 @@ return [ 'podcast-list' => 'All podcasts', 'podcast-create' => 'New podcast', 'podcast-import' => 'Import a podcast', + 'persons' => 'Persons', + 'person-list' => 'All persons', + 'person-create' => 'New person', 'users' => 'Users', 'user-list' => 'All users', 'user-create' => 'New user', diff --git a/app/Language/en/Breadcrumb.php b/app/Language/en/Breadcrumb.php index 27301ab7..03db0bce 100644 --- a/app/Language/en/Breadcrumb.php +++ b/app/Language/en/Breadcrumb.php @@ -16,6 +16,7 @@ return [ 'add' => 'add', 'new' => 'new', 'edit' => 'edit', + 'persons' => 'persons', 'users' => 'users', 'my-account' => 'my account', 'change-password' => 'change password', diff --git a/app/Language/en/Person.php b/app/Language/en/Person.php new file mode 100644 index 00000000..3d504b2a --- /dev/null +++ b/app/Language/en/Person.php @@ -0,0 +1,64 @@ + 'Persons', + 'all_persons' => 'All persons', + 'no_person' => 'Nobody found!', + 'create' => 'Create a person', + 'view' => 'View person', + 'edit' => 'Edit person', + 'delete' => 'Delete person', + 'form' => [ + 'identity_section_title' => 'Identity', + 'identity_section_subtitle' => 'Who is working on the podcast', + 'full_name' => 'Full name', + 'full_name_hint' => 'This is the full name or alias of the person.', + 'unique_name' => 'Unique name', + 'unique_name_hint' => 'Used for URLs', + 'information_url' => 'Information URL', + 'information_url_hint' => + 'Url to a relevant resource of information about the person, such as a homepage or third-party profile platform.', + 'image' => 'Picture, avatar, image', + 'image_size_hint' => + 'Image must be squared with at least 400px wide and tall.', + 'submit_create' => 'Create person', + 'submit_edit' => 'Save person', + ], + 'podcast_form' => [ + 'title' => 'Manage persons', + 'manage_section_title' => 'Management', + 'manage_section_subtitle' => 'Remove persons from this podcast', + 'add_section_title' => 'Add persons to this podcast', + 'add_section_subtitle' => 'You may pick several persons and roles.', + 'person' => 'Persons', + 'person_hint' => + 'You may select one or several persons with the same roles. You need to create the persons first.', + 'group_role' => 'Groups and roles', + 'group_role_hint' => + 'You may select none, one or several groups and roles for a person.', + 'submit_add' => 'Add person(s)', + 'remove' => 'Remove', + ], + 'episode_form' => [ + 'title' => 'Manage persons', + 'manage_section_title' => 'Management', + 'manage_section_subtitle' => 'Remove persons from this episode', + 'add_section_title' => 'Add persons to this episode', + 'add_section_subtitle' => 'You may pick several persons and roles', + 'person' => 'Persons', + 'person_hint' => + 'You may select one or several persons with the same roles. You need to create the persons first.', + 'group_role' => 'Groups and roles', + 'group_role_hint' => + 'You may select none, one or several groups and roles for a person.', + 'submit_add' => 'Add person(s)', + 'remove' => 'Remove', + ], + 'credits' => 'Credits', +]; diff --git a/app/Language/en/Podcast.php b/app/Language/en/Podcast.php index 86f7b4c9..f7c87572 100644 --- a/app/Language/en/Podcast.php +++ b/app/Language/en/Podcast.php @@ -27,7 +27,8 @@ return [ 'image' => 'Cover image', 'title' => 'Title', 'name' => 'Name', - 'name_hint' => 'Used for generating the podcast URL.', + 'name_hint' => + 'Used for generating the podcast URL. Uppercase, lowercase, numbers and underscores are accepted.', 'type' => [ 'label' => 'Type', 'hint' => diff --git a/app/Language/en/PodcastNavigation.php b/app/Language/en/PodcastNavigation.php index 048ca8be..49062fe3 100644 --- a/app/Language/en/PodcastNavigation.php +++ b/app/Language/en/PodcastNavigation.php @@ -15,6 +15,8 @@ return [ 'episode-list' => 'All episodes', 'episode-create' => 'New episode', 'analytics' => 'Analytics', + 'persons' => 'Persons', + 'podcast-person-manage' => 'Manage persons', 'contributors' => 'Contributors', 'contributor-list' => 'All contributors', 'contributor-add' => 'Add contributor', diff --git a/app/Language/fr/AdminNavigation.php b/app/Language/fr/AdminNavigation.php index ea79018d..b22523f3 100644 --- a/app/Language/fr/AdminNavigation.php +++ b/app/Language/fr/AdminNavigation.php @@ -14,6 +14,9 @@ return [ 'podcast-list' => 'Tous les podcasts', 'podcast-create' => 'Créer un podcast', 'podcast-import' => 'Importer un podcast', + 'persons' => 'Intervenants', + 'person-list' => 'Tous les intervenants', + 'person-create' => 'Nouvel intervenant', 'users' => 'Utilisateurs', 'user-list' => 'Tous les utilisateurs', 'user-create' => 'Créer un utilisateur', diff --git a/app/Language/fr/Breadcrumb.php b/app/Language/fr/Breadcrumb.php index 71d8c331..961d403c 100644 --- a/app/Language/fr/Breadcrumb.php +++ b/app/Language/fr/Breadcrumb.php @@ -16,6 +16,7 @@ return [ 'add' => 'ajouter', 'new' => 'créer', 'edit' => 'modifier', + 'persons' => 'intervenants', 'users' => 'utilisateurs', 'my-account' => 'mon compte', 'change-password' => 'changer le mot de passe', diff --git a/app/Language/fr/Person.php b/app/Language/fr/Person.php new file mode 100644 index 00000000..aef667f0 --- /dev/null +++ b/app/Language/fr/Person.php @@ -0,0 +1,66 @@ + 'Intervenants', + 'all_persons' => 'Tous les intervenants', + 'no_person' => 'Aucun intervenant trouvé !', + 'create' => 'Créer un intervenant', + 'view' => 'Voir l’intervenant', + 'edit' => 'Modifier l’intervenant', + 'delete' => 'Supprimer l’intervenant', + 'form' => [ + 'identity_section_title' => 'Identité', + 'identity_section_subtitle' => 'Qui intervient sur le podcast', + 'full_name' => 'Nom complet', + 'full_name_hint' => 'Le nom complet ou le pseudonyme de l’intervenant', + 'unique_name' => 'Nom unique', + 'unique_name_hint' => 'Utilisé pour les URLs', + 'information_url' => 'Adresse d’information', + 'information_url_hint' => + 'URL pointant vers des informations relatives à l’intervenant, telle qu’une page personnelle ou une page de profil sur une plateforme tierce.', + 'image' => 'Photo, avatar, image', + 'image_size_hint' => + 'L’image doit être carrée et avoir au moins 400px de largeur et de hauteur.', + 'submit_create' => 'Créer l’intervenant', + 'submit_edit' => 'Enregistrer l’intervenant', + ], + 'podcast_form' => [ + 'title' => 'Gérer les intervenants', + 'manage_section_title' => 'Gestion', + 'manage_section_subtitle' => 'Retirer des intervenants de ce podcast', + 'add_section_title' => 'Ajouter des intervenants à ce podcast', + 'add_section_subtitle' => + 'Vous pouvez sélectionner plusieurs intervenants et rôles.', + 'person' => 'Intervenants', + 'person_hint' => + 'Vous pouvez selectionner un ou plusieurs intervenants ayant les mêmes rôles. Les intervenants doivent avoir été préalablement créés.', + 'group_role' => 'Groupes et rôles', + 'group_role_hint' => + 'Vous pouvez sélectionner aucun, un ou plusieurs groupes et rôles par intervenant.', + 'submit_add' => 'Ajouter un/des intervenant(s)', + 'remove' => 'Retirer', + ], + 'episode_form' => [ + 'title' => 'Gérer les intervenants', + 'manage_section_title' => 'Gestion', + 'manage_section_subtitle' => 'Retirer des intervenants de cet épisode', + 'add_section_title' => 'Ajouter des intervenants à cet épisode', + 'add_section_subtitle' => + 'Vous pouvez sélectionner plusieurs intervenants et rôles.', + 'person' => 'Intervenants', + 'person_hint' => + 'Vous pouvez selectionner un ou plusieurs intervenants ayant les mêmes rôles. Les intervenants doivent avoir été préalablement créés.', + 'group_role' => 'Groupes et rôles', + 'group_role_hint' => + 'Vous pouvez sélectionner aucun, un ou plusieurs groupes et rôles par intervenant.', + 'submit_add' => 'Ajouter un/des intervenant(s)', + 'remove' => 'Retirer', + ], + 'credits' => 'Crédits', +]; diff --git a/app/Language/fr/Podcast.php b/app/Language/fr/Podcast.php index 49131cbf..20672a4c 100644 --- a/app/Language/fr/Podcast.php +++ b/app/Language/fr/Podcast.php @@ -28,7 +28,8 @@ return [ 'image' => 'Image de couverture', 'title' => 'Titre', 'name' => 'Nom', - 'name_hint' => 'Utilisé pour l’adresse du podcast.', + 'name_hint' => + 'Utilisé pour l’adresse du podcast. Les majuscules, les minuscules, les chiffres et le caractère souligné « _ » sont acceptés.', 'type' => [ 'label' => 'Type', 'hint' => diff --git a/app/Language/fr/PodcastNavigation.php b/app/Language/fr/PodcastNavigation.php index 5ac59cd2..1b5414bc 100644 --- a/app/Language/fr/PodcastNavigation.php +++ b/app/Language/fr/PodcastNavigation.php @@ -15,6 +15,8 @@ return [ 'episode-list' => 'Tous les épisodes', 'episode-create' => 'Créer un épisode', 'analytics' => 'Mesures d’audience', + 'persons' => 'Intervenants', + 'podcast-person-manage' => 'Gestion des intervenants', 'contributors' => 'Contributeurs', 'contributor-list' => 'Tous les contributeurs', 'contributor-add' => 'Ajouter un contributeur', diff --git a/app/Models/CreditModel.php b/app/Models/CreditModel.php new file mode 100644 index 00000000..00121757 --- /dev/null +++ b/app/Models/CreditModel.php @@ -0,0 +1,20 @@ +where([ + 'podcast_id' => $podcastId, + 'id' => $episodeId, + ]) + ->where('published_at <=', 'NOW()') + ->first(); + + cache()->save( + "podcast{$podcastId}_episode{$episodeId}", + $found, + DECADE + ); + } + + return $found; + } + /** * Returns the previous episode based on episode ordering */ @@ -334,7 +354,7 @@ class EpisodeModel extends Model return $data; } - protected function clearCache(array $data) + public function clearCache(array $data) { $episodeModel = new EpisodeModel(); $episode = (new EpisodeModel())->find( @@ -366,6 +386,7 @@ class EpisodeModel extends Model cache()->delete( "page_podcast{$episode->podcast->id}_episode{$episode->id}_{$locale}" ); + cache()->delete("credits_{$locale}"); } foreach ($years as $year) { diff --git a/app/Models/EpisodePersonModel.php b/app/Models/EpisodePersonModel.php new file mode 100644 index 00000000..1ed80d1e --- /dev/null +++ b/app/Models/EpisodePersonModel.php @@ -0,0 +1,150 @@ + 'required', + 'person_id' => 'required', + ]; + protected $validationMessages = []; + + protected $afterInsert = ['clearCache']; + protected $beforeDelete = ['clearCache']; + + public function getPersonsByEpisodeId($podcastId, $episodeId) + { + if ( + !($found = cache( + "podcast{$podcastId}_episodes{$episodeId}_persons" + )) + ) { + $found = $this->select('episodes_persons.*') + ->where('episode_id', $episodeId) + ->join( + 'persons', + 'person_id=persons.id' + ) + ->orderby('full_name') + ->findAll(); + + cache()->save( + "podcast{$podcastId}_episodes{$episodeId}_persons", + $found, + DECADE + ); + } + return $found; + } + + /** + * Add persons to episode + * + * @param int podcastId + * @param int $episodeId + * @param array $persons + * @param array $groups_roles + * + * @return integer|false Number of rows inserted or FALSE on failure + */ + public function addEpisodePersons( + $podcastId, + $episodeId, + $persons, + $groups_roles + ) { + if (!empty($persons)) { + $this->clearCache([ + 'id' => [ + 'podcast_id' => $podcastId, + 'episode_id' => $episodeId, + ], + ]); + $data = []; + foreach ($persons as $person) { + if ($groups_roles) { + foreach ($groups_roles as $group_role) { + $group_role = explode(',', $group_role); + $data[] = [ + 'podcast_id' => $podcastId, + 'episode_id' => $episodeId, + 'person_id' => $person, + 'person_group' => $group_role[0], + 'person_role' => $group_role[1], + ]; + } + } else { + $data[] = [ + 'podcast_id' => $podcastId, + 'episode_id' => $episodeId, + 'person_id' => $person, + ]; + } + } + return $this->insertBatch($data); + } + return 0; + } + + public function removeEpisodePersons( + $podcastId, + $episodeId, + $episodePersonId + ) { + return $this->delete([ + 'podcast_id' => $podcastId, + 'episode_id' => $episodeId, + 'id' => $episodePersonId, + ]); + } + + protected function clearCache(array $data) + { + $podcastId = null; + $episodeId = null; + if ( + isset($data['id']['podcast_id']) && + isset($data['id']['episode_id']) + ) { + $podcastId = $data['id']['podcast_id']; + $episodeId = $data['id']['episode_id']; + } else { + $episodePerson = (new EpisodePersonModel())->find( + is_array($data['id']) ? $data['id']['id'] : $data['id'] + ); + $podcastId = $episodePerson->podcast_id; + $episodeId = $episodePerson->episode_id; + } + + cache()->delete("podcast{$podcastId}_episodes{$episodeId}_persons"); + (new EpisodeModel())->clearCache(['id' => $episodeId]); + + return $data; + } +} diff --git a/app/Models/PersonModel.php b/app/Models/PersonModel.php new file mode 100644 index 00000000..ac8661c8 --- /dev/null +++ b/app/Models/PersonModel.php @@ -0,0 +1,134 @@ + 'required', + 'unique_name' => + 'required|regex_match[/^[a-z0-9\-]{1,191}$/]|is_unique[persons.unique_name,id,{id}]', + 'image_uri' => 'required', + 'created_by' => 'required', + 'updated_by' => 'required', + ]; + protected $validationMessages = []; + + // clear cache before update if by any chance, the person name changes, so will the person link + protected $afterInsert = ['clearCache']; + protected $beforeUpdate = ['clearCache']; + protected $beforeDelete = ['clearCache']; + + public function getPersonById($personId) + { + if (!($found = cache("person{$personId}"))) { + $found = $this->find($personId); + cache()->save("person{$personId}", $found, DECADE); + } + + return $found; + } + + public function getPerson($fullName) + { + return $this->where('full_name', $fullName)->first(); + } + + public function createPerson($fullName, $informationUrl, $image) + { + $person = new \App\Entities\Person([ + 'full_name' => $fullName, + 'unique_name' => slugify($fullName), + 'information_url' => $informationUrl, + 'image' => download_file($image), + 'created_by' => user()->id, + 'updated_by' => user()->id, + ]); + return $this->insert($person); + } + + public function getPersonOptions() + { + $options = []; + + if (!($options = cache('person_options'))) { + $options = array_reduce( + $this->select('`id`, `full_name`') + ->orderBy('`full_name`', 'ASC') + ->findAll(), + function ($result, $person) { + $result[$person->id] = $person->full_name; + return $result; + }, + [] + ); + cache()->save('person_options', $options, DECADE); + } + + return $options; + } + + public function getTaxonomyOptions() + { + $options = []; + $locale = service('request')->getLocale(); + if (!($options = cache("taxonomy_options_{$locale}"))) { + foreach (lang('PersonsTaxonomy.persons') as $group_key => $group) { + foreach ($group['roles'] as $role_key => $role) { + $options[ + "$group_key,$role_key" + ] = "{$group['label']} ▸ {$role['label']}"; + } + } + + cache()->save("taxonomy_options_{$locale}", $options, DECADE); + } + + return $options; + } + + protected function clearCache(array $data) + { + $person = (new PersonModel())->getPersonById( + is_array($data['id']) ? $data['id'][0] : $data['id'] + ); + + cache()->delete('person_options'); + cache()->delete("person{$person->id}"); + cache()->delete("user{$person->created_by}_persons"); + + $supportedLocales = config('App')->supportedLocales; + // clear cache for every credit page + foreach ($supportedLocales as $locale) { + cache()->delete("credit_{$locale}"); + } + + return $data; + } +} diff --git a/app/Models/PlatformModel.php b/app/Models/PlatformModel.php index f13586dc..827c4de1 100644 --- a/app/Models/PlatformModel.php +++ b/app/Models/PlatformModel.php @@ -16,14 +16,20 @@ use CodeIgniter\Model; class PlatformModel extends Model { protected $table = 'platforms'; - protected $primaryKey = 'id'; + protected $primaryKey = 'slug'; - protected $allowedFields = ['slug', 'label', 'home_url', 'submit_url']; + protected $allowedFields = [ + 'slug', + 'type', + 'label', + 'home_url', + 'submit_url', + ]; protected $returnType = \App\Entities\Platform::class; protected $useSoftDeletes = false; - protected $useTimestamps = true; + protected $useTimestamps = false; public function getPlatforms() { @@ -37,26 +43,32 @@ class PlatformModel extends Model return $found; } - public function getOrCreatePlatform($slug, $platformType) + public function getPlatform($slug) { - if (!($found = cache("platforms_$slug"))) { + if (!($found = cache("platform_$slug"))) { $found = $this->where('slug', $slug)->first(); - if (!$found) { - $data = [ - 'slug' => $slug, - 'type' => $platformType, - 'label' => $slug, - 'home_url' => '', - 'submit_url' => null, - ]; - $this->insert($data); - $found = $this->where('slug', $slug)->first(); - } - cache()->save("platforms_$slug", $found, DECADE); + cache()->save("platform_$slug", $found, DECADE); } return $found; } + public function createPlatform( + $slug, + $type, + $label, + $homeUrl, + $submitUrl = null + ) { + $data = [ + 'slug' => $slug, + 'type' => $type, + 'label' => $label, + 'home_url' => $homeUrl, + 'submit_url' => $submitUrl, + ]; + return $this->insert($data, false); + } + public function getPlatformsWithLinks($podcastId, $platformType) { if ( diff --git a/app/Models/PodcastModel.php b/app/Models/PodcastModel.php index 7e401fd1..4bfed81d 100644 --- a/app/Models/PodcastModel.php +++ b/app/Models/PodcastModel.php @@ -1,7 +1,7 @@ getPodcastById( is_array($data['id']) ? $data['id'][0] : $data['id'] @@ -195,6 +195,10 @@ class PodcastModel extends Model ); } } + // clear cache for every credit page + foreach ($supportedLocales as $locale) { + cache()->delete("credits_{$locale}"); + } // delete episode lists cache per year / season // and localized pages diff --git a/app/Models/PodcastPersonModel.php b/app/Models/PodcastPersonModel.php new file mode 100644 index 00000000..8268cf0e --- /dev/null +++ b/app/Models/PodcastPersonModel.php @@ -0,0 +1,119 @@ + 'required', + 'person_id' => 'required', + ]; + protected $validationMessages = []; + + protected $afterInsert = ['clearCache']; + protected $beforeDelete = ['clearCache']; + + public function getPersonsByPodcastId($podcastId) + { + if (!($found = cache("podcast{$podcastId}_persons"))) { + $found = $this->select('podcasts_persons.*') + ->where('podcast_id', $podcastId) + ->join( + 'persons', + 'person_id=persons.id' + ) + ->orderby('full_name') + ->findAll(); + + cache()->save("podcast{$podcastId}_persons", $found, DECADE); + } + return $found; + } + + /** + * Add persons to podcast + * + * @param int $podcastId + * @param array $persons + * @param array $groups_roles + * + * @return integer Number of rows inserted or FALSE on failure + */ + public function addPodcastPersons($podcastId, $persons, $groups_roles) + { + if (!empty($persons)) { + $this->clearCache(['id' => ['podcast_id' => $podcastId]]); + $data = []; + foreach ($persons as $person) { + if ($groups_roles) { + foreach ($groups_roles as $group_role) { + $group_role = explode(',', $group_role); + $data[] = [ + 'podcast_id' => $podcastId, + 'person_id' => $person, + 'person_group' => $group_role[0], + 'person_role' => $group_role[1], + ]; + } + } else { + $data[] = [ + 'podcast_id' => $podcastId, + 'person_id' => $person, + ]; + } + } + return $this->insertBatch($data); + } + return 0; + } + + public function removePodcastPersons($podcastId, $podcastPersonId) + { + return $this->delete([ + 'podcast_id' => $podcastId, + 'id' => $podcastPersonId, + ]); + } + + protected function clearCache(array $data) + { + $podcastId = null; + if (isset($data['id']['podcast_id'])) { + $podcastId = $data['id']['podcast_id']; + } else { + $person = (new PodcastPersonModel())->find( + is_array($data['id']) ? $data['id']['id'] : $data['id'] + ); + $podcastId = $person->podcast_id; + } + + cache()->delete("podcast{$podcastId}_persons"); + (new PodcastModel())->clearCache(['id' => $podcastId]); + + return $data; + } +} diff --git a/app/Views/_assets/icons/folder-user.svg b/app/Views/_assets/icons/folder-user.svg new file mode 100644 index 00000000..590e6aa1 --- /dev/null +++ b/app/Views/_assets/icons/folder-user.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/app/Views/_assets/images/platforms/podcasting/breaker.svg b/app/Views/_assets/images/platforms/podcasting/breaker.svg new file mode 100644 index 00000000..27eeadd7 --- /dev/null +++ b/app/Views/_assets/images/platforms/podcasting/breaker.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/app/Views/_layout.php b/app/Views/_layout.php index 831f2cb0..9f0e6a9d 100644 --- a/app/Views/_layout.php +++ b/app/Views/_layout.php @@ -11,8 +11,8 @@ - -
+ +
title @@ -22,11 +22,15 @@
renderSection('content') ?>
-
+ diff --git a/app/Views/admin/_sidebar.php b/app/Views/admin/_sidebar.php index b57cccfe..069dc742 100644 --- a/app/Views/admin/_sidebar.php +++ b/app/Views/admin/_sidebar.php @@ -5,6 +5,10 @@ $navigation = [ 'icon' => 'mic', 'items' => ['podcast-list', 'podcast-create', 'podcast-import'], ], + 'persons' => [ + 'icon' => 'folder-user', + 'items' => ['person-list', 'person-create'], + ], 'users' => ['icon' => 'group', 'items' => ['user-list', 'user-create']], 'pages' => ['icon' => 'pages', 'items' => ['page-list', 'page-create']], ]; ?> diff --git a/app/Views/admin/episode/list.php b/app/Views/admin/episode/list.php index 78f8f427..6bed3c35 100644 --- a/app/Views/admin/episode/list.php +++ b/app/Views/admin/episode/list.php @@ -61,6 +61,11 @@ $podcast->id, $episode->id ) ?>"> + ' . + 'person->image->thumbnail_url}\" alt=\"{$episodePerson->person->full_name}\" class=\"object-cover w-16 h-16 rounded-full\" />" . + '
' . + $episodePerson->person->full_name . + ($episodePerson->person_group && $episodePerson->person_role + ? '' . + lang( + "PersonsTaxonomy.persons.{$episodePerson->person_group}.label" + ) . + ' ▸ ' . + lang( + "PersonsTaxonomy.persons.{$episodePerson->person_group}.roles.{$episodePerson->person_role}.label" + ) . + '' + : '') . + (empty($episodePerson->person->information_url) + ? '' + : "person->information_url}\" target=\"_blank\" rel=\"noreferrer noopener\" class=\"text-sm text-blue-800 hover:underline\">" . + $episodePerson->person->information_url . + '') . + '
'; + }, + ], + [ + 'header' => lang('Common.actions'), + 'cell' => function ($episodePerson) { + return button( + lang('Person.episode_form.remove'), + route_to( + 'episode-person-remove', + $episodePerson->podcast_id, + $episodePerson->episode_id, + $episodePerson->id + ), + ['variant' => 'danger', 'size' => 'small'] + ); + }, + ], + ], + $episodePersons +) ?> + + + + + + + + + 'person', + 'class' => 'form-select mb-4', + 'required' => 'required', +]) ?> + + + 'person_group_role', 'class' => 'form-select mb-4'] +) ?> + + + + 'primary'], + ['type' => 'submit', 'class' => 'self-end'] +) ?> + + +endSection() ?> diff --git a/app/Views/admin/episode/view.php b/app/Views/admin/episode/view.php index a66c8d71..08956ec3 100644 --- a/app/Views/admin/episode/view.php +++ b/app/Views/admin/episode/view.php @@ -64,6 +64,12 @@ ['variant' => 'info', 'iconLeft' => 'edit'], ['class' => 'mb-4'] ) ?> + id, $episode->id), + ['variant' => 'info', 'iconLeft' => 'folder-user'], + ['class' => 'mb-4'] + ) ?> soundbites) > 0): ?> extend('admin/_layout') ?> + +section('title') ?> + +endSection() ?> + +section('pageTitle') ?> + +endSection() ?> + + +section('content') ?> + + 'post', + 'class' => 'flex flex-col', +]) ?> + + + + + + 'full_name', + 'name' => 'full_name', + 'class' => 'form-input mb-4', + 'value' => old('full_name'), + 'required' => 'required', + 'data-slugify' => 'title', +]) ?> + + + 'unique_name', + 'name' => 'unique_name', + 'class' => 'form-input mb-4', + 'value' => old('unique_name'), + 'required' => 'required', + 'data-slugify' => 'slug', +]) ?> + + + 'information_url', + 'name' => 'information_url', + 'class' => 'form-input mb-4', + 'value' => old('information_url'), +]) ?> + + + 'image', + 'name' => 'image', + 'class' => 'form-input', + 'required' => 'required', + 'type' => 'file', + 'accept' => '.jpg,.jpeg,.png', +]) ?> + + + + + 'primary'], + ['type' => 'submit', 'class' => 'self-end'] +) ?> + + + + + +endSection() ?> diff --git a/app/Views/admin/person/edit.php b/app/Views/admin/person/edit.php new file mode 100644 index 00000000..98a1d629 --- /dev/null +++ b/app/Views/admin/person/edit.php @@ -0,0 +1,95 @@ +extend('admin/_layout') ?> + +section('title') ?> + +endSection() ?> + +section('pageTitle') ?> + +endSection() ?> + + +section('content') ?> + +id), [ + 'method' => 'post', + 'class' => 'flex flex-col', +]) ?> + + +image->thumbnail_url}\" alt=\"{$person->full_name}\" class=\"object-cover w-32 h-32 mt-3 rounded\" />" +) ?> + + + 'full_name', + 'name' => 'full_name', + 'class' => 'form-input mb-4', + 'value' => old('full_name', $person->full_name), + 'required' => 'required', + 'data-slugify' => 'title', +]) ?> + + + 'unique_name', + 'name' => 'unique_name', + 'class' => 'form-input mb-4', + 'value' => old('unique_name', $person->unique_name), + 'required' => 'required', + 'data-slugify' => 'slug', +]) ?> + + + 'information_url', + 'name' => 'information_url', + 'class' => 'form-input mb-4', + 'value' => old('information_url', $person->information_url), +]) ?> + + + 'image', + 'name' => 'image', + 'class' => 'form-input', + 'type' => 'file', + 'accept' => '.jpg,.jpeg,.png', +]) ?> + + + + + 'primary'], + ['type' => 'submit', 'class' => 'self-end'] +) ?> + + + + + +endSection() ?> diff --git a/app/Views/admin/person/list.php b/app/Views/admin/person/list.php new file mode 100644 index 00000000..de4040fd --- /dev/null +++ b/app/Views/admin/person/list.php @@ -0,0 +1,65 @@ +extend('admin/_layout') ?> + +section('title') ?> + +endSection() ?> + +section('pageTitle') ?> + () +endSection() ?> + +section('headerRight') ?> + 'primary', 'iconLeft' => 'add'], + ['class' => 'mr-2'] +) ?> +endSection() ?> + +section('content') ?> + +
+ + + + + +

+ +
+ +endSection() ?> diff --git a/app/Views/admin/person/view.php b/app/Views/admin/person/view.php new file mode 100644 index 00000000..99446eb4 --- /dev/null +++ b/app/Views/admin/person/view.php @@ -0,0 +1,38 @@ +extend('admin/_layout') ?> + +section('title') ?> +full_name ?> +endSection() ?> + +section('pageTitle') ?> +full_name ?> + +endSection() ?> + +section('headerRight') ?> +id), + ['variant' => 'secondary', 'iconLeft' => 'edit'], + ['class' => 'mr-2'] +) ?> +endSection() ?> + +section('content') ?> + +
+
+ $person->full_name +
+ +
+ full_name ?>
+ information_url ?> +
+
+ +endSection() ?> diff --git a/app/Views/admin/podcast/_sidebar.php b/app/Views/admin/podcast/_sidebar.php index 7d525a25..ffa8fe3c 100644 --- a/app/Views/admin/podcast/_sidebar.php +++ b/app/Views/admin/podcast/_sidebar.php @@ -8,6 +8,10 @@ $podcastNavigation = [ 'icon' => 'mic', 'items' => ['episode-list', 'episode-create'], ], + 'persons' => [ + 'icon' => 'folder-user', + 'items' => ['podcast-person-manage'], + ], 'analytics' => [ 'icon' => 'line-chart', 'items' => [ diff --git a/app/Views/admin/podcast/latest_episodes.php b/app/Views/admin/podcast/latest_episodes.php index db304958..7c6f17db 100644 --- a/app/Views/admin/podcast/latest_episodes.php +++ b/app/Views/admin/podcast/latest_episodes.php @@ -58,6 +58,16 @@ $podcast->id, $episode->id ) ?>"> + + ' . + 'person->image->thumbnail_url}\" alt=\"{$podcastPerson->person->full_name}\" class=\"object-cover w-16 h-16 rounded-full\" />" . + '
' . + $podcastPerson->person->full_name . + ($podcastPerson->person_group && $podcastPerson->person_role + ? '' . + lang( + "PersonsTaxonomy.persons.{$podcastPerson->person_group}.label" + ) . + ' ▸ ' . + lang( + "PersonsTaxonomy.persons.{$podcastPerson->person_group}.roles.{$podcastPerson->person_role}.label" + ) . + '' + : '') . + (empty($podcastPerson->person->information_url) + ? '' + : "person->information_url}\" target=\"_blank\" rel=\"noreferrer noopener\" class=\"text-sm text-blue-800 hover:underline\">" . + $podcastPerson->person->information_url . + '') . + '
'; + }, + ], + [ + 'header' => lang('Common.actions'), + 'cell' => function ($podcastPerson) { + return button( + lang('Person.podcast_form.remove'), + route_to( + 'podcast-person-remove', + $podcastPerson->podcast_id, + $podcastPerson->id + ), + + ['variant' => 'danger', 'size' => 'small'] + ); + }, + ], + ], + $podcastPersons +) ?> + + + + + + + + + 'person', + 'class' => 'form-select mb-4', + 'required' => 'required', +]) ?> + + + 'person_group_role', 'class' => 'form-select mb-4'] +) ?> + + + + 'primary'], + ['type' => 'submit', 'class' => 'self-end'] +) ?> + + +endSection() ?> diff --git a/app/Views/credits.php b/app/Views/credits.php new file mode 100644 index 00000000..97152c0b --- /dev/null +++ b/app/Views/credits.php @@ -0,0 +1,49 @@ +extend('_layout') ?> + +section('title') ?> + +endSection() ?> + +section('content') ?> + +
+ $groups): ?> +
+ $persons): ?> +
+ <?= $persons[
+    'full_name'
+] ?> +
+
+
+
+ $role_array): ?> + + + + + + + +
+ + +
+endSection(); ?> diff --git a/app/Views/episode.php b/app/Views/episode.php index 6bdf4840..0b116d7b 100644 --- a/app/Views/episode.php +++ b/app/Views/episode.php @@ -100,11 +100,28 @@ enclosure_duration) ?> +
+ + + + + <?= $person[
+    'full_name'
+] ?> + + + + +
location_name, $episode->location_geo, $episode->location_osmid, - 'self-start mt-2' + 'self-start mt-2 mb-2' ) ?>