SAAS-1165, SAAS-1155 - add constraints to Table buttons, implement podcast + podcast episode constraints; finish left-hand podcast episode view functionality

This commit is contained in:
Duncan Sommerville 2015-11-06 14:40:22 -05:00
parent 7072762dd9
commit 729360e1a1
6 changed files with 235 additions and 46 deletions

View File

@ -2,6 +2,8 @@
class PodcastEpisodeNotFoundException extends Exception {}
class DuplicatePodcastEpisodeException extends Exception {}
class Application_Service_PodcastEpisodeService extends Application_Service_ThirdPartyCeleryService implements Publish
{
/**
@ -57,7 +59,12 @@ class Application_Service_PodcastEpisodeService extends Application_Service_Thir
public function addPodcastEpisodePlaceholders($podcastId, $episodes) {
$storedEpisodes = array();
foreach ($episodes as $episode) {
$e = $this->addPlaceholder($podcastId, $episode);
try {
$e = $this->addPlaceholder($podcastId, $episode);
} catch(DuplicatePodcastEpisodeException $ex) {
Logging::warn($ex->getMessage());
continue;
}
array_push($storedEpisodes, $e);
}
return $storedEpisodes;
@ -71,8 +78,14 @@ class Application_Service_PodcastEpisodeService extends Application_Service_Thir
* @param array $episode array of podcast episode data
*
* @return PodcastEpisodes the stored PodcastEpisodes object
*
* @throws DuplicatePodcastEpisodeException
*/
public function addPlaceholder($podcastId, $episode) {
$existingEpisode = PodcastEpisodesQuery::create()->findOneByDbEpisodeGuid($episode["guid"]);
if (!empty($existingEpisode)) {
throw new DuplicatePodcastEpisodeException("Episode already exists: \n" . var_export($episode, true));
}
// We need to check whether the array is parsed directly from the SimplePie
// feed object, or whether it's passed in as json
$enclosure = $episode["enclosure"];
@ -288,7 +301,7 @@ class Application_Service_PodcastEpisodeService extends Application_Service_Thir
// From the RSS spec best practices:
// '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(),
"description" => htmlspecialchars($item->get_description()),
"pub_date" => $item->get_gmdate(),
"link" => $item->get_link(),
"enclosure" => $item->get_enclosure(),

View File

@ -31,13 +31,11 @@
}
@media screen and (max-width: 1600px) {
#library_display_wrapper button:not(.dropdown-toggle):not(.btn-new) > span,
#show_builder_table_wrapper button:not(.dropdown-toggle):not(.btn-new) > span,
.content-pane button:not(.dropdown-toggle):not(.btn-new) > span,
#show_builder_table_wrapper #sb_submit > span {
display: none;
}
#library_display_wrapper button:not(.dropdown-toggle):not(.btn-new) > i,
#show_builder_table_wrapper button:not(.dropdown-toggle):not(.btn-new) > i,
.content-pane button:not(.dropdown-toggle):not(.btn-new) > i,
#show_builder_table_wrapper #sb_submit > i {
margin-right: 0 !important;
}

View File

@ -4053,6 +4053,10 @@ li .ui-state-hover {
display: block;
}
#podcast_episode_dialog {
max-height: 50%;
}
/* UI Revamp Video */
#whatsnew {

View File

@ -1249,9 +1249,13 @@ var AIRTIME = (function(AIRTIME) {
};
/**
* Show the given table in the left-hand pane of the dashboard and give it internal focus
*
* @param table the table to show
*/
mod.setCurrentTable = function (table) {
// FIXME: This is hacky...
mod.podcastEpisodeDataTable.fnClearTable();
if (oTable) oTable.fnClearTable();
var dt = $datatables[table],
wrapper = $(dt).closest(".dataTables_wrapper");
$("#library_content").find(".dataTables_wrapper").hide();
@ -1260,6 +1264,23 @@ var AIRTIME = (function(AIRTIME) {
oTable.fnDraw();
};
mod.openPodcastEpisodeDialog = function () {
var episode = mod.podcastEpisodeTableWidget.getSelectedRows()[0];
$("body").append("<div id='podcast_episode_dialog'></div>");
var dialog = $("#podcast_episode_dialog").html(episode.description);
dialog.html(dialog.text());
dialog.dialog({
title: $.i18n._(episode.title),
width: "auto",
height: "auto",
modal: true,
resizable: false,
close: function() {
$(this).dialog('destroy').remove();
}
});
};
/**
* Create the podcast datatable widget
*
@ -1278,11 +1299,32 @@ var AIRTIME = (function(AIRTIME) {
var ajaxSourceURL = baseUrl+"rest/podcast";
var podcastToolbarButtons = AIRTIME.widgets.Table.getStandardToolbarButtons();
podcastToolbarButtons[AIRTIME.widgets.Table.TOOLBAR_BUTTON_ROLES.NEW].title = $.i18n._('Add'); //"New" Podcast is misleading
podcastToolbarButtons[AIRTIME.widgets.Table.TOOLBAR_BUTTON_ROLES.NEW].eventHandlers.click = AIRTIME.podcast.createUrlDialog;
podcastToolbarButtons[AIRTIME.widgets.Table.TOOLBAR_BUTTON_ROLES.EDIT].eventHandlers.click = AIRTIME.podcast.editSelectedPodcasts;
podcastToolbarButtons[AIRTIME.widgets.Table.TOOLBAR_BUTTON_ROLES.DELETE].eventHandlers.click = AIRTIME.podcast.deleteSelectedPodcasts;
// TODO: only enable this if user has exactly one podcast selected
$.extend(true, podcastToolbarButtons[AIRTIME.widgets.Table.TOOLBAR_BUTTON_ROLES.NEW],
{
title: $.i18n._('Add'), //"New" Podcast is misleading
eventHandlers: {
click: AIRTIME.podcast.createUrlDialog
},
validateConstraints: function () { return true; }
});
$.extend(true, podcastToolbarButtons[AIRTIME.widgets.Table.TOOLBAR_BUTTON_ROLES.EDIT],
{
eventHandlers: {
click: AIRTIME.podcast.editSelectedPodcasts
},
validateConstraints: function () {
return this.getSelectedRows().length >= 1;
}
});
$.extend(true, podcastToolbarButtons[AIRTIME.widgets.Table.TOOLBAR_BUTTON_ROLES.DELETE],
{
eventHandlers: {
click: AIRTIME.podcast.deleteSelectedPodcasts
},
validateConstraints: function () {
return this.getSelectedRows().length >= 1;
}
});
podcastToolbarButtons["ViewPodcast"] = {
title : $.i18n._("View Podcast"),
iconClass : "icon-chevron-right",
@ -1295,8 +1337,12 @@ var AIRTIME = (function(AIRTIME) {
mod.podcastTableWidget._clearSelection();
mod.setCurrentTable(mod.DataTableTypeEnum.PODCAST_EPISODES);
}
},
validateConstraints: function () {
return this.getSelectedRows().length == 1;
}
};
// Add a button to view the station podcast
podcastToolbarButtons["StationPodcast"] = {
title : $.i18n._("Station Podcast"),
@ -1305,7 +1351,8 @@ var AIRTIME = (function(AIRTIME) {
elementId : "",
eventHandlers : {
click: AIRTIME.podcast.openStationPodcast
}
},
validateConstraints: function () { return true; }
};
//Set up the div with id "podcast_table" as a datatable.
@ -1322,6 +1369,8 @@ var AIRTIME = (function(AIRTIME) {
});
mod._initPodcastEpisodeDatatable();
// On double click, open a table showing the selected podcast's episodes
// in the left-hand pane.
mod.podcastTableWidget.assignDblClickHandler(function () {
var podcast = mod.podcastDataTable.fnGetData($(this).index());
mod.podcastEpisodeTableWidget.reload(podcast.id);
@ -1346,28 +1395,81 @@ var AIRTIME = (function(AIRTIME) {
extraBtnClass : 'btn-small',
elementId : '',
eventHandlers : {
click: function (e) {
click: function () {
mod.setCurrentTable(mod.DataTableTypeEnum.PODCAST);
}
}
},
validateConstraints: function () { return true; }
}
},
defaults = AIRTIME.widgets.Table.getStandardToolbarButtons();
defaults[AIRTIME.widgets.Table.TOOLBAR_BUTTON_ROLES.NEW].title = "Import";
defaults[AIRTIME.widgets.Table.TOOLBAR_BUTTON_ROLES.NEW].eventHandlers.click = function () {
var episodes = mod.podcastEpisodeTableWidget.getSelectedRows();
AIRTIME.podcast.importSelectedEpisodes(episodes, mod.podcastEpisodeTableWidget);
/**
* Check the import statuses of each selected episode to see which
* buttons should be enabled or disabled.
*
* @param shouldBeImported whether or not the selected item(s)
* should be imported to obtain a valid result.
*
* @returns {boolean} true if all selected episodes are valid and
* the button should be enabled, otherwise false.
*/
var checkSelectedEpisodeImportStatus = function (shouldBeImported) {
var selected = this.getSelectedRows(), isValid = true;
if (selected.length == 0) return false;
$.each(selected, function () {
var isImported = !$.isEmptyObject(this.file);
if ((!shouldBeImported && isImported) || (shouldBeImported && !isImported)) {
isValid = false;
}
});
return isValid;
};
defaults[AIRTIME.widgets.Table.TOOLBAR_BUTTON_ROLES.DELETE].eventHandlers.click = function () {
var podcastId, data = [], episodes = mod.podcastEpisodeTableWidget.getSelectedRows();
$.each(episodes, function () {
data.push({id: this.file.id, type: this.file.ftype});
// Setup the default buttons (new, edit, delete)
$.extend(true, defaults[AIRTIME.widgets.Table.TOOLBAR_BUTTON_ROLES.NEW],
{
title: "Import",
eventHandlers: {
click: function () {
var episodes = mod.podcastEpisodeTableWidget.getSelectedRows();
AIRTIME.podcast.importSelectedEpisodes(episodes, mod.podcastEpisodeTableWidget);
}
},
validateConstraints: function () {
return checkSelectedEpisodeImportStatus.call(this, false);
}
});
$.extend(true, defaults[AIRTIME.widgets.Table.TOOLBAR_BUTTON_ROLES.EDIT],
{
eventHandlers: {
click: function () {
var episodes = mod.podcastEpisodeTableWidget.getSelectedRows();
AIRTIME.podcast.editSelectedEpisodes(episodes);
}
},
validateConstraints: function () {
return checkSelectedEpisodeImportStatus.call(this, true);
}
});
$.extend(true, defaults[AIRTIME.widgets.Table.TOOLBAR_BUTTON_ROLES.DELETE],
{
eventHandlers: {
click: function () {
var data = [], episodes = mod.podcastEpisodeTableWidget.getSelectedRows();
$.each(episodes, function () {
data.push({id: this.file.id, type: this.file.ftype});
});
mod.fnDeleteItems(data);
}
},
validateConstraints: function () {
return checkSelectedEpisodeImportStatus.call(this, true);
}
});
mod.fnDeleteItems(data);
};
// Reassign these because integer keys take precedence in iteration order - we want to order based on insertion
// FIXME: this is a pretty flimsy way to try to set up iteration order (possibly not xbrowser compatible?)
defaults = {
newBtn : defaults[AIRTIME.widgets.Table.TOOLBAR_BUTTON_ROLES.NEW],
editBtn: defaults[AIRTIME.widgets.Table.TOOLBAR_BUTTON_ROLES.EDIT],
@ -1376,7 +1478,6 @@ var AIRTIME = (function(AIRTIME) {
$.extend(true, buttons, defaults, {
addToScheduleBtn: {
// TODO: compatibility with checkAddButton function
title : $.i18n._('Add to Schedule'),
iconClass : '',
extraBtnClass : 'btn-small',
@ -1387,6 +1488,21 @@ var AIRTIME = (function(AIRTIME) {
$.each(selected, function () { data.push(this.file); });
mod.addToSchedule(data);
}
},
validateConstraints: function () {
return checkSelectedEpisodeImportStatus.call(this, true);
}
},
viewDescBtn: {
title : $.i18n._("View"),
iconClass : "icon-globe",
extraBtnClass : "btn-small",
elementId : "",
eventHandlers : {
click: mod.openPodcastEpisodeDialog
},
validateConstraints: function () {
return this.getSelectedRows().length == 1;
}
}
});
@ -1422,7 +1538,7 @@ var AIRTIME = (function(AIRTIME) {
mod.podcastEpisodeDataTable = $datatables[mod.DataTableTypeEnum.PODCAST_EPISODES] = mod.podcastEpisodeTableWidget.getDatatable();
mod.podcastEpisodeTableWidget.assignDblClickHandler(function () {
var data = mod.podcastEpisodeDataTable.fnGetData($(this).index());
if (data.file.length > 0) {
if (!$.isEmptyObject(data.file)) {
mod.dblClickAdd(data.file, data.file.ftype);
} else {
AIRTIME.podcast.importSelectedEpisodes([data], mod.podcastEpisodeTableWidget);

View File

@ -110,6 +110,10 @@ var AIRTIME = (function (AIRTIME) {
click: function () {
mod.importSelectedEpisodes(self.episodeTable.getSelectedRows(), self.episodeTable);
}
},
validateConstraints: function () {
// Only importable rows can be selected
return this.getSelectedRows().length >= 1;
}
}
};
@ -199,14 +203,7 @@ var AIRTIME = (function (AIRTIME) {
* Open metadata editor tabs for each of the selected episodes.
*/
StationPodcastController.prototype.openSelectedTabEditors = function () {
var self = this,
episodes = self.episodeTable.getSelectedRows();
jQuery.each(episodes, function () {
var uid = AIRTIME.library.MediaTypeStringEnum.FILE + "_" + this.file_id;
jQuery.get(baseUrl + "library/edit-file-md/id/" + this.file_id, {format: "json"}, function (json) {
AIRTIME.playlist.fileMdEdit(json, uid);
});
});
mod.editSelectedEpisodes(this.episodeTable.getSelectedRows());
};
/**
@ -224,6 +221,9 @@ var AIRTIME = (function (AIRTIME) {
elementId : '',
eventHandlers : {
click: self.openSelectedTabEditors.bind(self)
},
validateConstraints: function () {
return this.getSelectedRows().length >= 1;
}
},
deleteBtn: {
@ -233,6 +233,9 @@ var AIRTIME = (function (AIRTIME) {
elementId : '',
eventHandlers : {
click: self.unpublishSelectedEpisodes.bind(self)
},
validateConstraints: function () {
return this.getSelectedRows().length >= 1;
}
},
slideToggle: {}
@ -425,7 +428,7 @@ var AIRTIME = (function (AIRTIME) {
* Setup an interval that checks for any pending imports and reloads
* the table once imports are finished.
*
* TODO: remember selection
* TODO: remember selection; make this more elegant?
*
* @private
*/
@ -441,14 +444,15 @@ var AIRTIME = (function (AIRTIME) {
pendingRows.push(this.guid);
}
});
console.log(pendingRows);
if (pendingRows.length > 0) {
// Fetch the table data if there are pending rows,
// then check if any of the pending rows have
// succeeded or failed before reloading the table.
$.get(endpoint + podcastId + '/episodes', function (json) {
data = JSON.parse(json);
var delta = false;
$.each(data, function () {
var idx = pendingRows.indexOf(this.guid);
console.log(idx);
if (idx > -1 && this.ingested != -1) {
delta = true;
pendingRows.slice(idx, 0);
@ -462,7 +466,7 @@ var AIRTIME = (function (AIRTIME) {
}
});
}
}, 15000); // Run every 15 seconds
}, 10000); // Run every 10 seconds
};
}
@ -530,6 +534,21 @@ var AIRTIME = (function (AIRTIME) {
}
};
/**
* Open metadata editor tabs for each of the selected episodes.
*
* @param {Array} episodes the array of selected episodes
*/
mod.editSelectedEpisodes = function (episodes) {
$.each(episodes, function () {
if (!Object.keys(this.file).length > 0) return false;
var uid = AIRTIME.library.MediaTypeStringEnum.FILE + "_" + this.file.id;
$.get(baseUrl + "library/edit-file-md/id/" + this.file.id, {format: "json"}, function (json) {
AIRTIME.playlist.fileMdEdit(json, uid);
});
});
};
/**
* Import one or more podcast episodes.
*
@ -538,6 +557,7 @@ var AIRTIME = (function (AIRTIME) {
*/
mod.importSelectedEpisodes = function (episodes, dt) {
$.each(episodes, function () {
if (Object.keys(this.file).length > 0) return false;
var podcastId = this.podcast_id;
$.post(endpoint + podcastId + '/episodes', JSON.stringify({
csrf_token: $("#csrf").val(),
@ -569,7 +589,8 @@ var AIRTIME = (function (AIRTIME) {
iconClass: 'spl-no-r-margin icon-chevron-up',
extraBtnClass: 'toggle-editor-form',
elementId: '',
eventHandlers: {}
eventHandlers: {},
validateConstraints: function () { return true; }
}
}, buttons);
}

View File

@ -199,6 +199,21 @@ var AIRTIME = (function(AIRTIME) {
$(buttonElement).on(eventName, eventCallback);
});
});
self._checkToolbarButtons();
};
/**
* Check each of the toolbar buttons for the table and disable them if their constraints are invalid.
*
* Passes current Table object context to function calls.
*/
Table.prototype._checkToolbarButtons = function () {
var self = this;
$.each(self._toolbarButtons, function (idx, btn) {
var btnNode = $(btn.element).find("button").get(0);
btnNode.disabled = btn.disabled = !btn.validateConstraints.call(self);
});
};
/** Create the DOM element for a toolbar button and return it. */
@ -242,6 +257,7 @@ var AIRTIME = (function(AIRTIME) {
this._selectedRowVisualIdxMax = -1;
this._$wrapperDOMNode.find('.selected').removeClass('selected');
this._$wrapperDOMNode.find(this._SELECTORS.SELECTION_CHECKBOX).find('input').attr('checked', false);
this._checkToolbarButtons();
};
/** @param nRow is a tr DOM node (non-jQuery)
@ -333,11 +349,12 @@ var AIRTIME = (function(AIRTIME) {
console.log("Unimplemented selection mode");
}
self._checkToolbarButtons();
};
Table.prototype.getSelectedRows = function() {
return this._selectedRows;
}
};
Table.prototype._handleAjaxError = function(r) {
// If the request was denied due to permissioning
@ -443,15 +460,35 @@ var AIRTIME = (function(AIRTIME) {
//Set of standard buttons. Use getStandardToolbarButtons() to grab these and pass them to the init() function.
Table._STANDARD_TOOLBAR_BUTTONS = {};
Table._STANDARD_TOOLBAR_BUTTONS[Table.TOOLBAR_BUTTON_ROLES.NEW] = { 'title' : $.i18n._('New'), 'iconClass' : "icon-plus", extraBtnClass : "btn-small btn-new", elementId : '', eventHandlers : {} };
Table._STANDARD_TOOLBAR_BUTTONS[Table.TOOLBAR_BUTTON_ROLES.EDIT] = { 'title' : $.i18n._('Edit'), 'iconClass' : "icon-pencil", extraBtnClass : "btn-small", elementId : '', eventHandlers : {} };
Table._STANDARD_TOOLBAR_BUTTONS[Table.TOOLBAR_BUTTON_ROLES.DELETE] = { 'title' : $.i18n._('Delete'), 'iconClass' : "icon-trash", extraBtnClass : "btn-small btn-danger", elementId : '', eventHandlers : {} };
Table._STANDARD_TOOLBAR_BUTTONS[Table.TOOLBAR_BUTTON_ROLES.NEW] = {
title : $.i18n._('New'),
iconClass : "icon-plus",
extraBtnClass : "btn-small btn-new",
elementId : '',
eventHandlers : {},
validateConstraints: function () { return true; }
};
Table._STANDARD_TOOLBAR_BUTTONS[Table.TOOLBAR_BUTTON_ROLES.EDIT] = {
title : $.i18n._('Edit'),
iconClass : "icon-pencil",
extraBtnClass : "btn-small",
elementId : '',
eventHandlers : {},
validateConstraints: function () { return true; }
};
Table._STANDARD_TOOLBAR_BUTTONS[Table.TOOLBAR_BUTTON_ROLES.DELETE] = {
title : $.i18n._('Delete'),
iconClass : "icon-trash",
extraBtnClass : "btn-small btn-danger",
elementId : '',
eventHandlers : {},
validateConstraints: function () { return true; }
};
Object.freeze(Table._STANDARD_TOOLBAR_BUTTONS);
//Static method
Table.getStandardToolbarButtons = function() {
//Return a deep copy
return jQuery.extend(true, {}, Table._STANDARD_TOOLBAR_BUTTONS);
};