From e8980e7a79d8429557f58c48c1d8030b98d1e96f Mon Sep 17 00:00:00 2001 From: Duncan Sommerville Date: Thu, 22 Oct 2015 18:03:38 -0400 Subject: [PATCH] Bugfixes and more work on station podcast frontend --- .../application/common/PodcastManager.php | 5 +- .../services/PodcastEpisodeService.php | 8 +- .../application/services/PodcastService.php | 9 +- .../views/scripts/podcast/podcast.phtml | 2 +- .../scripts/podcast/station_podcast.phtml | 4 +- airtime_mvc/public/css/styles.css | 4 +- .../public/js/airtime/library/podcast.js | 98 +++++++++++-------- .../public/js/airtime/library/publish.js | 3 +- .../public/js/airtime/widgets/table.js | 5 +- 9 files changed, 82 insertions(+), 56 deletions(-) diff --git a/airtime_mvc/application/common/PodcastManager.php b/airtime_mvc/application/common/PodcastManager.php index 20c6f698d..ec076e5c7 100644 --- a/airtime_mvc/application/common/PodcastManager.php +++ b/airtime_mvc/application/common/PodcastManager.php @@ -50,12 +50,13 @@ class PodcastManager { $podcastArray = Application_Service_PodcastService::getPodcastById($podcast->getDbPodcastId()); $episodeList = $podcastArray["episodes"]; $episodes = array(); + // Sort the episodes by publication date to get the most recent + usort($episodeList, array(static::class, "_sortByEpisodePubDate")); for ($i = 0; $i < sizeof($episodeList); $i++) { $episodeData = $episodeList[$i]; - $ingestTimestamp = $podcast->getDbAutoIngestTimestamp(); // If the publication date of this episode is before the ingest timestamp, we don't need to ingest it // Since we're sorting by publication date, we can break - if ($episodeData["pub_date"] < $ingestTimestamp) continue; + if (strtotime($episodeData["pub_date"]) < strtotime($podcast->getDbAutoIngestTimestamp())) break; $episode = PodcastEpisodesQuery::create()->findOneByDbEpisodeGuid($episodeData["guid"]); // Make sure there's no existing episode placeholder or import, and that the data is non-empty if (empty($episode) && !empty($episodeData)) { diff --git a/airtime_mvc/application/services/PodcastEpisodeService.php b/airtime_mvc/application/services/PodcastEpisodeService.php index 527d41942..476115840 100644 --- a/airtime_mvc/application/services/PodcastEpisodeService.php +++ b/airtime_mvc/application/services/PodcastEpisodeService.php @@ -170,8 +170,12 @@ class Application_Service_PodcastEpisodeService extends Application_Service_Thir public function publish($fileId) { $id = Application_Model_Preference::getStationPodcastId(); $url = $guid = Application_Common_HTTPHelper::getStationUrl()."rest/media/$fileId/download"; - $e = $this->_buildEpisode($id, $url, $guid, date('r')); - $e->setDbFileId($fileId)->save(); + if (!PodcastEpisodesQuery::create() + ->filterByDbPodcastId($id) + ->findOneByDbFileId($fileId)) { // Don't allow duplicate episodes + $e = $this->_buildEpisode($id, $url, $guid, date('r')); + $e->setDbFileId($fileId)->save(); + } } /** diff --git a/airtime_mvc/application/services/PodcastService.php b/airtime_mvc/application/services/PodcastService.php index 57d5ae304..21cd9aab5 100644 --- a/airtime_mvc/application/services/PodcastService.php +++ b/airtime_mvc/application/services/PodcastService.php @@ -220,11 +220,14 @@ class Application_Service_PodcastService * @return array */ private static function _generatePodcastArray($podcast, $rss) { + $stationPodcast = StationPodcastQuery::create()->findOneByDbPodcastId($podcast->getDbId()); $ingestedEpisodes = PodcastEpisodesQuery::create() ->findByDbPodcastId($podcast->getDbId()); $episodeIds = array(); - foreach ($ingestedEpisodes as $e) { - array_push($episodeIds, $e->getDbEpisodeGuid()); + if (!$stationPodcast) { + foreach ($ingestedEpisodes as $e) { + array_push($episodeIds, $e->getDbEpisodeGuid()); + } } $podcastArray = $podcast->toArray(BasePeer::TYPE_FIELDNAME); @@ -239,7 +242,7 @@ class Application_Service_PodcastService // 'An item's author element provides the e-mail address of the person who wrote the item' "author" => $item->get_author()->get_email(), "description" => $item->get_description(), - "pub_date" => $item->get_date("Y-m-d H:i:s"), + "pub_date" => $item->get_gmdate(), "link" => $item->get_link(), "enclosure" => $item->get_enclosure() )); diff --git a/airtime_mvc/application/views/scripts/podcast/podcast.phtml b/airtime_mvc/application/views/scripts/podcast/podcast.phtml index 8ccf2412c..1d7492d8f 100644 --- a/airtime_mvc/application/views/scripts/podcast/podcast.phtml +++ b/airtime_mvc/application/views/scripts/podcast/podcast.phtml @@ -27,7 +27,7 @@ -
+
diff --git a/airtime_mvc/application/views/scripts/podcast/station_podcast.phtml b/airtime_mvc/application/views/scripts/podcast/station_podcast.phtml index 2c6fcc012..05ef238ac 100644 --- a/airtime_mvc/application/views/scripts/podcast/station_podcast.phtml +++ b/airtime_mvc/application/views/scripts/podcast/station_podcast.phtml @@ -27,7 +27,7 @@
-
+
@@ -36,7 +36,7 @@
-
diff --git a/airtime_mvc/public/css/styles.css b/airtime_mvc/public/css/styles.css index 33072375d..eeecf8e9e 100644 --- a/airtime_mvc/public/css/styles.css +++ b/airtime_mvc/public/css/styles.css @@ -3916,7 +3916,7 @@ li .ui-state-hover { /* Podcasts */ -#podcast_episodes_wrapper { +[id^="podcast_episodes"][id$="_wrapper"] { position: relative; height: 100%; float: left; @@ -3928,7 +3928,7 @@ li .ui-state-hover { box-sizing: border-box; } -#podcast_episodes_wrapper td { +[id^="podcast_episodes"][id$="_wrapper"] td { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; diff --git a/airtime_mvc/public/js/airtime/library/podcast.js b/airtime_mvc/public/js/airtime/library/podcast.js index 85501d425..264fcceeb 100644 --- a/airtime_mvc/public/js/airtime/library/podcast.js +++ b/airtime_mvc/public/js/airtime/library/podcast.js @@ -7,11 +7,11 @@ var AIRTIME = (function (AIRTIME) { mod = AIRTIME.podcast; - var endpoint = 'rest/podcast/'; + var endpoint = 'rest/podcast/', PodcastTable; //AngularJS app var podcastApp = angular.module('podcast', []) - .controller('RestController', function($scope, $http, podcast, tab, episodeTable) { + .controller('RestController', function($scope, $http, podcast, tab) { // We need to pass in the tab object and the episodes table object so we can reference them //We take a podcast object in as a parameter rather fetching the podcast by ID here because @@ -19,20 +19,31 @@ var AIRTIME = (function (AIRTIME) { //a roundtrip by not fetching it again here. $scope.podcast = podcast; tab.setName($scope.podcast.title); + $scope.csrf = jQuery("#csrf").val(); + tab.contents.find("table").attr("id", "podcast_episodes_" + podcast.id); + var episodeTable = AIRTIME.podcast.initPodcastEpisodeDatatable(podcast.episodes, tab); - $scope.savePodcast = function() { - var episodes = episodeTable.getSelectedRows(), - csrf = jQuery("#csrf").val(); - // TODO: Should we implement a batch endpoint for this instead? - jQuery.each(episodes, function() { - $http.post(endpoint + $scope.podcast.id + '/episodes', { csrf_token: csrf, episode: this }); - }); - $http.put(endpoint + $scope.podcast.id, { csrf_token: csrf, podcast: $scope.podcast }) + function updatePodcast() { + $http.put(endpoint + $scope.podcast.id, { csrf_token: $scope.csrf, podcast: $scope.podcast }) .success(function() { episodeTable.reload($scope.podcast.id); AIRTIME.library.podcastDataTable.fnDraw(); tab.setName($scope.podcast.title); }); + } + + $scope.savePodcast = function() { + var episodes = episodeTable.getSelectedRows(); + // TODO: Should we implement a batch endpoint for this instead? + jQuery.each(episodes, function() { + $http.post(endpoint + $scope.podcast.id + '/episodes', { csrf_token: $scope.csrf, episode: this }); + }); + updatePodcast(); + }; + + $scope.saveStationPodcast = function() { + // TODO: We still need a way to delete episodes from the station podcast + updatePodcast(); }; $scope.discard = function() { @@ -63,10 +74,9 @@ var AIRTIME = (function (AIRTIME) { } } - function _bootstrapAngularApp(podcast, tab, table) { + function _bootstrapAngularApp(podcast, tab) { podcastApp.value('podcast', podcast); podcastApp.value('tab', tab); - podcastApp.value('episodeTable', table); var wrapper = tab.contents.find(".editor_pane_wrapper"); wrapper.attr("ng-controller", "RestController"); angular.bootstrap(wrapper.get(0), ["podcast"]); @@ -75,9 +85,34 @@ var AIRTIME = (function (AIRTIME) { function _initAppFromResponse(data) { var podcast = JSON.parse(data.podcast), uid = AIRTIME.library.MediaTypeStringEnum.PODCAST+"_"+podcast.id, - tab = AIRTIME.tabs.openTab(data.html, uid, null), - table = mod.initPodcastEpisodeDatatable(podcast.episodes); - _bootstrapAngularApp(podcast, tab, table); + tab = AIRTIME.tabs.openTab(data.html, uid, null); + _bootstrapAngularApp(podcast, tab); + } + + function _initPodcastTable() { + PodcastTable = function(wrapperDOMNode, bItemSelection, toolbarButtons, dataTablesOptions) { + // Just call the superconstructor. For clarity/extensibility + return AIRTIME.widgets.Table.call(this, wrapperDOMNode, bItemSelection, toolbarButtons, dataTablesOptions); + }; // Subclass AIRTIME.widgets.Table + PodcastTable.prototype = Object.create(AIRTIME.widgets.Table.prototype); + PodcastTable.prototype.constructor = PodcastTable; + PodcastTable.prototype._SELECTORS = Object.freeze({ + SELECTION_CHECKBOX: ".airtime_table_checkbox:has(input)", + SELECTION_TABLE_ROW: "tr:has(td.airtime_table_checkbox > input)" + }); + PodcastTable.prototype._datatablesCheckboxDataDelegate = function(rowData, callType, dataToSave) { + if (rowData.ingested) return null; // Don't create checkboxes for ingested items + return AIRTIME.widgets.Table.prototype._datatablesCheckboxDataDelegate.call(this, rowData, callType, dataToSave); + }; + // Since we're using a static source, define a separate function to fetch and 'reload' the table data + // We use this when we save the Podcast because we need to flag rows the user is ingesting + PodcastTable.prototype.reload = function(id) { + var dt = this._datatable; + $.get(endpoint + id, function(json) { + dt.fnClearTable(); + dt.fnAddData(JSON.parse(json).episodes); + }); + }; } mod.createUrlDialog = function() { @@ -116,7 +151,7 @@ var AIRTIME = (function (AIRTIME) { } }; - mod.initPodcastEpisodeDatatable = function(episodes) { + mod.initPodcastEpisodeDatatable = function(episodes, tab) { var aoColumns = [ /* GUID */ { "sTitle" : "" , "mDataProp" : "guid" , "sClass" : "podcast_episodes_guid" , "bVisible" : false }, /* Title */ { "sTitle" : $.i18n._("Title") , "mDataProp" : "title" , "sClass" : "podcast_episodes_title" , "sWidth" : "170px" }, @@ -126,35 +161,18 @@ var AIRTIME = (function (AIRTIME) { /* Publication Date */ { "sTitle" : $.i18n._("Publication Date") , "mDataProp" : "pub_date" , "sClass" : "podcast_episodes_pub_date" , "sWidth" : "170px" } ]; - var PodcastTable = function(wrapperDOMNode, bItemSelection, toolbarButtons, dataTablesOptions) { - // Just call the superconstructor. For clarity/extensibility - return AIRTIME.widgets.Table.call(this, wrapperDOMNode, bItemSelection, toolbarButtons, dataTablesOptions); - }; // Subclass AIRTIME.widgets.Table - PodcastTable.prototype = Object.create(AIRTIME.widgets.Table.prototype); - PodcastTable.prototype.constructor = PodcastTable; - PodcastTable.prototype._SELECTORS = Object.freeze({ - SELECTION_CHECKBOX: ".airtime_table_checkbox:has(input)", - SELECTION_TABLE_ROW: "tr:has(td.airtime_table_checkbox > input)" - }); - PodcastTable.prototype._datatablesCheckboxDataDelegate = function(rowData, callType, dataToSave) { - if (rowData.ingested) return null; // Don't create checkboxes for ingested items - return AIRTIME.widgets.Table.prototype._datatablesCheckboxDataDelegate.call(this, rowData, callType, dataToSave); - }; - // Since we're using a static source, define a separate function to fetch and 'reload' the table data - // We use this when we save the Podcast because we need to flag rows the user is ingesting - PodcastTable.prototype.reload = function(id) { - var dt = this._datatable; - $.get(endpoint + id, function(json) { - dt.fnClearTable(); - dt.fnAddData(JSON.parse(json).episodes); - }); - }; + if (typeof PodcastTable === 'undefined') { + _initPodcastTable(); + } var podcastToolbarButtons = AIRTIME.widgets.Table.getStandardToolbarButtons(); + podcastToolbarButtons[AIRTIME.widgets.Table.TOOLBAR_BUTTON_ROLES.DELETE].eventHandlers.click = function(e) { + // TODO: add {this} reference to event handlers and implement deletion for station podcasts + }; // Set up the div with id "podcast_table" as a datatable. var podcastEpisodesTableWidget = new PodcastTable( - AIRTIME.tabs.getActiveTab().contents.find('#podcast_episodes'), // DOM node to create the table inside. + tab.contents.find('.podcast_episodes'), // DOM node to create the table inside. true, // Enable item selection podcastToolbarButtons, // Toolbar buttons { // Datatables overrides. diff --git a/airtime_mvc/public/js/airtime/library/publish.js b/airtime_mvc/public/js/airtime/library/publish.js index 00c5156b4..4b50abc57 100644 --- a/airtime_mvc/public/js/airtime/library/publish.js +++ b/airtime_mvc/public/js/airtime/library/publish.js @@ -30,7 +30,6 @@ var AIRTIME = (function (AIRTIME) { $scope.publish = function() { var sources = {}; - console.log($scope.publishSources); $.each($scope.publishSources, function(k, v) { if (v) sources[k] = 'publish'; // Tentative TODO: decide on a robust implementation }); @@ -41,7 +40,7 @@ var AIRTIME = (function (AIRTIME) { }; $scope.discard = function() { - AIRTIME.tabs.getActiveTab().close(); + tab.close(); $scope.media = {}; }; }); diff --git a/airtime_mvc/public/js/airtime/widgets/table.js b/airtime_mvc/public/js/airtime/widgets/table.js index 5fe15b5a2..4bcd8c180 100644 --- a/airtime_mvc/public/js/airtime/widgets/table.js +++ b/airtime_mvc/public/js/airtime/widgets/table.js @@ -75,8 +75,8 @@ var AIRTIME = (function(AIRTIME) { // z = ColResize, R = ColReorder, C = ColVis "sDom": 'Rf<"dt-process-rel"r><"H"<"table_toolbar"C>><"dataTables_scrolling"t<"#library_empty"<"#library_empty_image"><"#library_empty_text">>><"F"lip>>', - "fnServerData": self._fetchData, - "fnInitComplete" : function() { self._setupEventHandlers(bItemSelection) } + "fnServerData": self._fetchData + //"fnInitComplete" : function() { self._setupEventHandlers(bItemSelection) } //"fnDrawCallback" : self._tableDrawCallback }; @@ -88,6 +88,7 @@ var AIRTIME = (function(AIRTIME) { self._datatable = self._$wrapperDOMNode.dataTable(options); self._datatable.fnDraw(); //Load the AJAX data now that our event handlers have been bound. + self._setupEventHandlers(bItemSelection); //return self._datatable; return self;