I'm testing a simple sample CRUD app. Everything works well...until I perform a DELETE (omodel.remove()) operation. If the next operation I do is an Insert (create entry, bind to view and submit changes) the app will perform MERGE (with the data of the deleted record) instead of POST and fail. Everything will work afterwards until I repeat this DELETE-INSERT sequence. If following the deletion, try to update an existing record, everything will work as well. Adding an omodel.refresh() at the beginning didn't work. Any ideas?
Starting with empty recordset, adding first record, model before submit changes
Record added, post performed
Ready to add 2nd record, all good in model, one existing and one new entry
2nd record added, another POST, all well
Ready to delete 1st record, situation right before the model.remove() operation, 2 recs, first one pending deletion
Record deleted, DELETE action triggered as expected
Last step, about to enter another record, see the existing and the pending new entry
BAMMM! Record NOT added, app instead of adding the new entry, performed a MERGE with the data of the deleted record!
Code for creating the new entry and passing it to the object page
onActionAdd: function() {
var oModel = this.getView().getModel();
var oParamModel = this.getView().getModel("Params");
oParamModel.setProperty("/ObjectMode", "Add");
oModel.refresh();
//var oNewObject = "{\"Curr\": \"GBP\"}";
var oNewObject = "{\"Pernr\": \"1023912\",\"Begda\":\"" + new Date('2021', '05', '01').toString()
+ "\",\"Endda\":\"" + new Date('2021', '06', '01').toString()
//+ "\",\"ActionDate\":\"" + new Date('2021', '07', '01').toString()
+ "\"}";
oNewObject = JSON.parse(oNewObject);
var oEntry = oModel.createEntry("/Industrial_ActionSet", {
properties: oNewObject
});
oParamModel.setProperty("/EntryId", oEntry.sPath.toString());
this.getRouter().navTo("object", {
objectId: oNewObject.Pernr,
dateFromId: oNewObject.Begda,
dateToId: oNewObject.Endda
});
}
Code for save (insert/update)
onActionSave: function() {
var oModel = this.getView().getModel();
var oParamModel = this.getView().getModel("Params");
var objectMode = oParamModel.getProperty("/ObjectMode");
var self = this;
// abort if the model has not been changed
if (!oModel.hasPendingChanges()) {
MessageBox.information(
this.getResourceBundle().getText("noChangesMessage"), {
id: "noChangesInfoMessageBox",
styleClass: self.getOwnerComponent().getContentDensityClass()
}
);
return;
}
if (objectMode === "Add") {
var sDateFrom = new Date(this.getView().byId("idDateFrom").getDateValue());
var sObjectPath = oParamModel.getProperty("/EntryId") + "/Begda";
oModel.setProperty(sObjectPath, sDateFrom);
var sDateTo = new Date(this.getView().byId("idDateTo").getDateValue());
sObjectPath = oParamModel.getProperty("/EntryId") + "/Endda";
oModel.setProperty(sObjectPath, sDateTo);
var sActionDate = new Date(this.getView().byId("idActionDate").getDateValue());
sObjectPath = oParamModel.getProperty("/EntryId") + "/ActionDate";
oModel.setProperty(sObjectPath, sActionDate);
var sMethod = "POST";
} else {
sMethod = "PUT";
}
oModel.submitChanges({
method: sMethod,
success: function(oData, sResponse) {
MessageToast.show("Record Updated");
self.onNavBack();
},
error: function(oError) {
jQuery.sap.log.error("Action Save oData Failure", oError);
}
});
},
Code for delete
onActionDelete: function() {
var oModel = this.getView().getModel();
var msgText = this.getModel("i18n").getResourceBundle().getText("confirmDelete");
var sPath = this.getView().getBindingContext().sPath;
var self = this;
// Opens the confirmation dialog
MessageBox.confirm(msgText, {
title: "Exit Confirmation",
initialFocus: sap.m.MessageBox.Action.CANCEL,
onClose: function(sButton) {
if (sButton === MessageBox.Action.OK) {
oModel.remove(sPath, {
method: "DELETE",
success: function(data) {
MessageToast.show("Record Deleted");
self.onNavBack();
},
error: function(e) {
jQuery.sap.log.error("Delete Action oData Failure", e);
}
});
} else if (sButton === MessageBox.Action.CANCEL) {
MessageToast.show("Deletion aborted");
return;
}
}
});
},
Thanks, cheers!
This is not the most elaborate way to solve this but problem is related with dates as keys. There are 3 date fields in the entity, 2 of them keys. As soon as I converted the entity date keys to string (still DATS on the backend) everything started working fine...
I have an issue while making an SAPUI5 odata V2 batch request :
var that = this;
var oServiceModel = that.getModel("oServiceModel");
odataMod = this.getModel("Service");
odataMod.setUseBatch(true);
var aData = oServiceModel.getData();
var stupidService = _.filter(aData, function (ae) {
return ae.Info === "-N/A";
});
var i = 0 ;
_.forEach(stupidService, function (sap) {
oGlobalBusyDialog.setText("Deleting service :" + sap.ObjectID);
oGlobalBusyDialog.setTitle("Deleting Service");
oGlobalBusyDialog.open();
that.removeService(sap).then(function () {
if (i === 615) {
oGlobalBusyDialog.close();
}
}).catch(function () {});
});
my Delete function is like this:
removeService: function (service) {
var that = this;
return new Promise(
function (resolve, reject) {
odataMod.remove('/ProjectTaskServiceCollection(\'' + service.ObjectID + '\')/', {
success: function (oData) {
resolve(oData);
},
error: function (oResult) {
that.handleError(oResult);
oGlobalBusyDialog.close();
reject(oResult);
}
});
});
What's happening ,is that if I'm trying to delete 500 entry, and if 200 entry cannot be deleted, the error message gets displayed 200 times
How to make it in a way to only display the error message once ?
Also, I want to turn off the batch request once everything is done odataMod.setUseBatch(false); how to do it ?
*EDIT: *
I've manage to do :
var aDeffGroup = odataMod.getDeferredGroups();
//add your deffered group
aDeffGroup.push("deletionGroup");
for (var s = 0; s < 5; s++) {
odataMod.remove('/ProjectTaskServiceCollection(\'' + stupidService[s].ObjectID + '\')/', {
//pass groupid to remove method.
groupId: "deletionGroup"
});
}
odataMod.submitChanges({
// your deffered group id
groupId: "deletionGroup",
success: function() {
//Get message model data from Core and it contains all errors
// Use this data to show in dialog or in a popover or set this to your local model see below code
var aErrorData = sap.ui.getCore().getMessageManager().getMessageModel();
console.log(aErrorData);
}
});
yet stills my console.log(aErrorData); still prints multiple error message
Instead of doing individual deletion odata calls. Add these all remove methods in a single group, then call odatamod.submitChanges() method.
Example:
//get all deffered groups
var aDeffGroup = odataMod.getDeferredGroups();
//add your deffered group
aDeffGroup.push("deletionGroup");
//set it back again to odatamodel
odataMod.setDeferredGroups(aDeffGroup);
odataMod.remove('/ProjectTaskServiceCollection(\'' + service.ObjectID + '\')/', {
//pass groupid to remove method.
groupId: "deletionGroup"});
odataMod.submitChanges({
// your deffered group id
groupId:"deletionGroup",
success: function() {
//Get message model data from Core and it contains all errors
// Use this data to show in dialog or in a popover or set this to your local model see below code
var aErrorData = sap.ui.getCore().getMessageManager().getMessageModel();
});
I am not able to send batch records. But I am able to add single entity each time. I used the following function on submit.
// creating single entry each time.
onSubmitChanges: function() {
var oSelectedVal = this.getView().byId("plmSelect"),
oSelectedVal = oSelectedVal.getSelectedItem().getKey(),
oModel = this.getView().getModel(),
oEntry = {};
oEntry.MyKeyField1 = oSelectedVal;
oEntry.MyEntry1 = globalVariable1; // global variable declared to get values
oEntry.MyEntry2 = globalVariable2;
oEntry.MyEntry3 = globalVariable3;
oEntry.MyEntry4 = globalVariable4;
if (oEntry.MyKeyField1 !== "" && oEntry.MyEntry1 !== "" && oEntry.MyEntry2 !== "") {
var oContext = oModel.createEntry('/MyEntitySet', {
properties: oEntry,
success: function() {
MessageToast.show("Create successfuly");
// not able to delete/remove after created successfully used the following
//oModel.setBindingContext(oContext);
//oModel.resetChanges();
//aModel.destroyBindingContext();
/*oModel.updateBindings({
bForceUpdate: true
});*/
// oModel.refresh();
//oModel.deleteCreatedEntry();
},
error: function() {
MessageToast.show("Create failed");
}
});
oModel.submitChanges();
//oModel.refresh();
} else {
MessageToast.show("Store Area and Store Description are madatory.");
}
this.onUpdateFinished();
},
Batch is not allowed. You must use deep entity if you wanna send a table.
I've followed this API reference :
https://developers.google.com/youtube/analytics/v1/sample-application
And created "index.html","index.css","index.js" files. I've inserted a proper CLIENT_ID but when I run "index.html" file it shows the following o/p :
I am new to web development and trying to understand API's. Can anyone please tell me how to make this code work?
Code for index.js is like this :
(function() {
// Retrieve your client ID from the Google APIs Console at
// https://code.google.com/apis/console.
var OAUTH2_CLIENT_ID = 'YOUR_CLIENT_ID';
var OAUTH2_SCOPES = [
'https://www.googleapis.com/auth/yt-analytics.readonly',
'https://www.googleapis.com/auth/youtube.readonly'
];
var ONE_MONTH_IN_MILLISECONDS = 1000 * 60 * 60 * 24 * 30;
// Keeps track of the currently authenticated user's YouTube user ID.
var channelId;
// For information about the Google Chart Tools API, see
// https://developers.google.com/chart/interactive/docs/quick_start
google.load('visualization', '1.0', {'packages': ['corechart']});
// The Google APIs JS client invokes this callback automatically after loading.
// See http://code.google.com/p/google-api-javascript-client/wiki/Authentication
window.onJSClientLoad = function() {
gapi.auth.init(function() {
window.setTimeout(checkAuth, 1);
});
};
// Attempt the immediate OAuth 2 client flow as soon as the page loads.
// If the currently logged-in Google Account has previously authorized
// OAUTH2_CLIENT_ID, then it will succeed with no user intervention.
// Otherwise, it will fail and the user interface that prompts for
// authorization will need to be displayed.
function checkAuth() {
gapi.auth.authorize({
client_id: OAUTH2_CLIENT_ID,
scope: OAUTH2_SCOPES,
immediate: true
}, handleAuthResult);
}
// Handle the result of a gapi.auth.authorize() call.
function handleAuthResult(authResult) {
if (authResult) {
// Auth was successful. Hide auth prompts and show things
// that should be visible after auth succeeds.
$('.pre-auth').hide();
$('.post-auth').show();
loadAPIClientInterfaces();
} else {
// Auth was unsuccessful. Show things related to prompting for auth
// and hide the things that should be visible after auth succeeds.
$('.post-auth').hide();
$('.pre-auth').show();
// Make the #login-link clickable. Attempt a non-immediate OAuth 2 client
// flow. The current function will be called when that flow completes.
$('#login-link').click(function() {
gapi.auth.authorize({
client_id: OAUTH2_CLIENT_ID,
scope: OAUTH2_SCOPES,
immediate: false
}, handleAuthResult);
});
}
}
// Load the client interface for the YouTube Analytics and Data APIs, which
// is required to use the Google APIs JS client. More info is available at
// http://code.google.com/p/google-api-javascript-client/wiki/GettingStarted#Loading_the_Client
function loadAPIClientInterfaces() {
gapi.client.load('youtube', 'v3', function() {
gapi.client.load('youtubeAnalytics', 'v1', function() {
// After both client interfaces load, use the Data API to request
// information about the authenticated user's channel.
getUserChannel();
});
});
}
// Calls the Data API to retrieve info about the currently authenticated
// user's YouTube channel.
function getUserChannel() {
// https://developers.google.com/youtube/v3/docs/channels/list
var request = gapi.client.youtube.channels.list({
// "mine: true" indicates that you want to retrieve the authenticated user's channel.
mine: true,
part: 'id,contentDetails'
});
request.execute(function(response) {
if ('error' in response) {
displayMessage(response.error.message);
} else {
// We will need the channel's channel ID to make calls to the
// Analytics API. The channel ID looks like "UCdLFeWKpkLhkguiMZUp8lWA".
channelId = response.items[0].id;
// This string, of the form "UUdLFeWKpkLhkguiMZUp8lWA", is a unique ID
// for a playlist of videos uploaded to the authenticated user's channel.
var uploadsListId = response.items[0].contentDetails.relatedPlaylists.uploads;
// Use the uploads playlist ID to retrieve the list of uploaded videos.
getPlaylistItems(uploadsListId);
}
});
}
// Calls the Data API to retrieve the items in a particular playlist. In this
// example, we are retrieving a playlist of the currently authenticated user's
// uploaded videos. By default, the list returns the most recent videos first.
function getPlaylistItems(listId) {
// https://developers.google.com/youtube/v3/docs/playlistItems/list
var request = gapi.client.youtube.playlistItems.list({
playlistId: listId,
part: 'snippet'
});
request.execute(function(response) {
if ('error' in response) {
displayMessage(response.error.message);
} else {
if ('items' in response) {
// jQuery.map() iterates through all of the items in the response and
// creates a new array that only contains the specific property we're
// looking for: videoId.
var videoIds = $.map(response.items, function(item) {
return item.snippet.resourceId.videoId;
});
// Now that we know the IDs of all the videos in the uploads list,
// we can retrieve info about each video.
getVideoMetadata(videoIds);
} else {
displayMessage('There are no videos in your channel.');
}
}
});
}
// Given an array of video ids, obtains metadata about each video and then
// uses that metadata to display a list of videos to the user.
function getVideoMetadata(videoIds) {
// https://developers.google.com/youtube/v3/docs/videos/list
var request = gapi.client.youtube.videos.list({
// The 'id' property value is a comma-separated string of video IDs.
id: videoIds.join(','),
part: 'id,snippet,statistics'
});
request.execute(function(response) {
if ('error' in response) {
displayMessage(response.error.message);
} else {
// Get the jQuery wrapper for #video-list once outside the loop.
var videoList = $('#video-list');
$.each(response.items, function() {
// Exclude videos that don't have any views, since those videos
// will not have any interesting viewcount analytics data.
if (this.statistics.viewCount == 0) {
return;
}
var title = this.snippet.title;
var videoId = this.id;
// Create a new <li> element that contains an <a> element.
// Set the <a> element's text content to the video's title, and
// add a click handler that will display Analytics data when invoked.
var liElement = $('<li>');
var aElement = $('<a>');
// The dummy href value of '#' ensures that the browser renders the
// <a> element as a clickable link.
aElement.attr('href', '#');
aElement.text(title);
aElement.click(function() {
displayVideoAnalytics(videoId);
});
// Call the jQuery.append() method to add the new <a> element to
// the <li> element, and the <li> element to the parent
// list, which is identified by the 'videoList' variable.
liElement.append(aElement);
videoList.append(liElement);
});
if (videoList.children().length == 0) {
displayMessage('Your channel does not have any videos that have been viewed.');
}
}
});
}
// Requests YouTube Analytics for a video, and displays results in a chart.
function displayVideoAnalytics(videoId) {
if (channelId) {
// To use a different date range, modify the ONE_MONTH_IN_MILLISECONDS
// variable to a different millisecond delta as desired.
var today = new Date();
var lastMonth = new Date(today.getTime() - ONE_MONTH_IN_MILLISECONDS);
var request = gapi.client.youtubeAnalytics.reports.query({
// The start-date and end-date parameters need to be YYYY-MM-DD strings.
'start-date': formatDateString(lastMonth),
'end-date': formatDateString(today),
// A future YouTube Analytics API release should support channel==default.
// In the meantime, you need to explicitly specify channel==channelId.
// See https://devsite.googleplex.com/youtube/analytics/v1/#ids
ids: 'channel==' + channelId,
dimensions: 'day',
// See https://developers.google.com/youtube/analytics/v1/available_reports for details
// on different filters and metrics you can request when dimensions=day.
metrics: 'views',
filters: 'video==' + videoId
});
request.execute(function(response) {
// This function is called regardless of whether the request succeeds.
// The response either has valid analytics data or an error message.
if ('error' in response) {
displayMessage(response.error.message);
} else {
displayChart(videoId, response);
}
});
} else {
displayMessage('The YouTube user id for the current user is not available.');
}
}
// Boilerplate code to take a Date object and return a YYYY-MM-DD string.
function formatDateString(date) {
var yyyy = date.getFullYear().toString();
var mm = padToTwoCharacters(date.getMonth() + 1);
var dd = padToTwoCharacters(date.getDate());
return yyyy + '-' + mm + '-' + dd;
}
// If number is a single digit, prepend a '0'. Otherwise, return it as a string.
function padToTwoCharacters(number) {
if (number < 10) {
return '0' + number;
} else {
return number.toString();
}
}
// Calls the Google Chart Tools API to generate a chart of analytics data.
function displayChart(videoId, response) {
if ('rows' in response) {
hideMessage();
// The columnHeaders property contains an array of objects representing
// each column's title – e.g.: [{name:"day"},{name:"views"}]
// We need these column titles as a simple array, so we call jQuery.map()
// to get each element's "name" property and create a new array that only
// contains those values.
var columns = $.map(response.columnHeaders, function(item) {
return item.name;
});
// The google.visualization.arrayToDataTable() wants an array of arrays.
// The first element is an array of column titles, calculated above as
// "columns". The remaining elements are arrays that each represent
// a row of data. Fortunately, response.rows is already in this format,
// so it can just be concatenated.
// See https://developers.google.com/chart/interactive/docs/datatables_dataviews#arraytodatatable
var chartDataArray = [columns].concat(response.rows);
var chartDataTable = google.visualization.arrayToDataTable(chartDataArray);
var chart = new google.visualization.LineChart(document.getElementById('chart'));
chart.draw(chartDataTable, {
// Additional options can be set if desired.
// See https://developers.google.com/chart/interactive/docs/reference#visdraw
title: 'Views per Day of Video ' + videoId
});
} else {
displayMessage('No data available for video ' + videoId);
}
}
// Helper method to display a message on the page.
function displayMessage(message) {
$('#message').text(message).show();
}
// Helper method to hide a previously displayed message on the page.
function hideMessage() {
$('#message').hide();
}
})();
I've got the same issue and found a possible solution at stackoverflow:
before: gapi.auth.authorize
put: gapi.client.setApiKey(API_KEY);
For me the error message dissappears then but now I get a blank website. Does anyone know how to solve this issue?
Thanks,
Martin
EDIT: this solved my problem: Youtube Analytics API 403 error
I have already modified some metrics but basically this should work:
(function() {
// Retrieve your client ID from the Google APIs Console at
// https://code.google.com/apis/console.
var OAUTH2_CLIENT_ID = 'CLIENT_ID';
var OAUTH2_SCOPES = [
'https://www.googleapis.com/auth/yt-analytics.readonly',
'https://www.googleapis.com/auth/youtube.readonly'
];
var ONE_MONTH_IN_MILLISECONDS = 1000 * 60 * 60 * 24 * 30;
// Keeps track of the currently authenticated user's YouTube user ID.
var channelId;
// For information about the Google Chart Tools API, see
// https://developers.google.com/chart/interactive/docs/quick_start
google.load('visualization', '1.0', {'packages': ['corechart']});
// The Google APIs JS client invokes this callback automatically after loading.
// See http://code.google.com/p/google-api-javascript-client/wiki/Authentication
window.onJSClientLoad = function() {
gapi.auth.init(function() {
window.setTimeout(checkAuth, 1);
});
};
// Attempt the immediate OAuth 2 client flow as soon as the page loads.
// If the currently logged-in Google Account has previously authorized
// OAUTH2_CLIENT_ID, then it will succeed with no user intervention.
// Otherwise, it will fail and the user interface that prompts for
// authorization will need to be displayed.
function checkAuth() {
gapi.auth.authorize({
client_id: OAUTH2_CLIENT_ID,
scope: OAUTH2_SCOPES,
immediate: false
}, handleAuthResult);
}
// Handle the result of a gapi.auth.authorize() call.
function handleAuthResult(authResult) {
if (authResult) {
// Auth was successful. Hide auth prompts and show things
// that should be visible after auth succeeds.
$('.pre-auth').hide();
$('.post-auth').show();
loadAPIClientInterfaces();
} else {
// Auth was unsuccessful. Show things related to prompting for auth
// and hide the things that should be visible after auth succeeds.
$('.post-auth').hide();
$('.pre-auth').show();
// Make the #login-link clickable. Attempt a non-immediate OAuth 2 client
// flow. The current function will be called when that flow completes.
$('#login-link').click(function() {
gapi.auth.authorize({
client_id: OAUTH2_CLIENT_ID,
scope: OAUTH2_SCOPES,
immediate: false
}, handleAuthResult);
});
}
}
// Load the client interface for the YouTube Analytics and Data APIs, which
// is required to use the Google APIs JS client. More info is available at
// http://code.google.com/p/google-api-javascript-client/wiki/GettingStarted#Loading_the_Client
function loadAPIClientInterfaces() {
gapi.client.load('youtube', 'v3', function() {
gapi.client.load('youtubeAnalytics', 'v1', function() {
// After both client interfaces load, use the Data API to request
// information about the authenticated user's channel.
getUserChannel();
});
});
}
// Calls the Data API to retrieve info about the currently authenticated
// user's YouTube channel.
function getUserChannel() {
// https://developers.google.com/youtube/v3/docs/channels/list
var request = gapi.client.youtube.channels.list({
// "mine: true" indicates that you want to retrieve the authenticated user's channel.
mine: true,
part: 'id,contentDetails'
});
request.execute(function(response) {
if ('error' in response) {
displayMessage(response.error.message);
} else {
// We will need the channel's channel ID to make calls to the
// Analytics API. The channel ID looks like "UCdLFeWKpkLhkguiMZUp8lWA".
channelId = response.items[0].id;
// This string, of the form "UUdLFeWKpkLhkguiMZUp8lWA", is a unique ID
// for a playlist of videos uploaded to the authenticated user's channel.
var uploadsListId = response.items[0].contentDetails.relatedPlaylists.uploads;
// Use the uploads playlist ID to retrieve the list of uploaded videos.
getPlaylistItems(uploadsListId);
}
});
}
// Calls the Data API to retrieve the items in a particular playlist. In this
// example, we are retrieving a playlist of the currently authenticated user's
// uploaded videos. By default, the list returns the most recent videos first.
function getPlaylistItems(listId) {
// https://developers.google.com/youtube/v3/docs/playlistItems/list
var request = gapi.client.youtube.playlistItems.list({
playlistId: listId,
part: 'snippet',
maxResults: 30
});
request.execute(function(response) {
if ('error' in response) {
displayMessage(response.error.message);
} else {
if ('items' in response) {
// jQuery.map() iterates through all of the items in the response and
// creates a new array that only contains the specific property we're
// looking for: videoId.
var videoIds = $.map(response.items, function(item) {
return item.snippet.resourceId.videoId;
});
// Now that we know the IDs of all the videos in the uploads list,
// we can retrieve info about each video.
getVideoMetadata(videoIds);
} else {
displayMessage('There are no videos in your channel.');
}
}
});
}
// Given an array of video ids, obtains metadata about each video and then
// uses that metadata to display a list of videos to the user.
function getVideoMetadata(videoIds) {
// https://developers.google.com/youtube/v3/docs/videos/list
var request = gapi.client.youtube.videos.list({
// The 'id' property value is a comma-separated string of video IDs.
id: videoIds.join(','),
part: 'id,snippet,statistics'
});
request.execute(function(response) {
if ('error' in response) {
displayMessage(response.error.message);
} else {
// Get the jQuery wrapper for #video-list once outside the loop.
var videoList = $('#video-list');
$.each(response.items, function() {
// Exclude videos that don't have any views, since those videos
// will not have any interesting viewcount analytics data.
if (this.statistics.viewCount == 0) {
return;
}
var title = this.snippet.title;
var videoId = this.id;
// Create a new <li> element that contains an <a> element.
// Set the <a> element's text content to the video's title, and
// add a click handler that will display Analytics data when invoked.
var liElement = $('<li>');
var aElement = $('<a>');
// The dummy href value of '#' ensures that the browser renders the
// <a> element as a clickable link.
aElement.attr('href', '#');
aElement.text(title);
aElement.click(function() {
displayVideoAnalytics(videoId);
});
// Call the jQuery.append() method to add the new <a> element to
// the <li> element, and the <li> element to the parent
// list, which is identified by the 'videoList' variable.
liElement.append(aElement);
videoList.append(liElement);
});
if (videoList.children().length == 0) {
displayMessage('Your channel does not have any videos that have been viewed.');
}
}
});
}
// Requests YouTube Analytics for a video, and displays results in a chart.
function displayVideoAnalytics(videoId) {
if (channelId) {
// To use a different date range, modify the ONE_MONTH_IN_MILLISECONDS
// variable to a different millisecond delta as desired.
var today = new Date();
var lastMonth = new Date(today.getTime() - ONE_MONTH_IN_MILLISECONDS);
var request = gapi.client.youtubeAnalytics.reports.query({
// The start-date and end-date parameters need to be YYYY-MM-DD strings.
'start-date': '2014-11-01',
'end-date': formatDateString(today),
// A future YouTube Analytics API release should support channel==default.
// In the meantime, you need to explicitly specify channel==channelId.
// See https://devsite.googleplex.com/youtube/analytics/v1/#ids
ids: 'channel==' + channelId,
dimensions: 'elapsedVideoTimeRatio',
// See https://developers.google.com/youtube/analytics/v1/available_reports for details
// on different filters and metrics you can request when dimensions=day.
metrics: 'audienceWatchRatio',
filters: 'video==' + videoId
});
request.execute(function(response) {
// This function is called regardless of whether the request succeeds.
// The response either has valid analytics data or an error message.
if ('error' in response) {
displayMessage(response.error.message);
} else {
displayChart(videoId, response);
}
});
} else {
displayMessage('The YouTube user id for the current user is not available.');
}
}
// Boilerplate code to take a Date object and return a YYYY-MM-DD string.
function formatDateString(date) {
var yyyy = date.getFullYear().toString();
var mm = padToTwoCharacters(date.getMonth() + 1);
var dd = padToTwoCharacters(date.getDate());
return yyyy + '-' + mm + '-' + dd;
}
// If number is a single digit, prepend a '0'. Otherwise, return it as a string.
function padToTwoCharacters(number) {
if (number < 10) {
return '0' + number;
} else {
return number.toString();
}
}
// Calls the Google Chart Tools API to generate a chart of analytics data.
function displayChart(videoId, response) {
if ('rows' in response) {
hideMessage();
// The columnHeaders property contains an array of objects representing
// each column's title – e.g.: [{name:"day"},{name:"views"}]
// We need these column titles as a simple array, so we call jQuery.map()
// to get each element's "name" property and create a new array that only
// contains those values.
var columns = $.map(response.columnHeaders, function(item) {
return item.name;
});
// The google.visualization.arrayToDataTable() wants an array of arrays.
// The first element is an array of column titles, calculated above as
// "columns". The remaining elements are arrays that each represent
// a row of data. Fortunately, response.rows is already in this format,
// so it can just be concatenated.
// See https://developers.google.com/chart/interactive/docs/datatables_dataviews#arraytodatatable
var chartDataArray = [columns].concat(response.rows);
var chartDataTable = google.visualization.arrayToDataTable(chartDataArray);
var chart = new google.visualization.LineChart(document.getElementById('chart'));
chart.draw(chartDataTable, {
// Additional options can be set if desired.
// See https://developers.google.com/chart/interactive/docs/reference#visdraw
width: 800, height: 300, is3D: false,
title: 'Audience Retention per Video ' + videoId
});
} else {
displayMessage('No data available for video ' + videoId);
}
}
// Helper method to display a message on the page.
function displayMessage(message) {
$('#message').text(message).show();
}
// Helper method to hide a previously displayed message on the page.
function hideMessage() {
$('#message').hide();
}
})();
I hope this helps.
Martin
Put your Api key in url like
"https://www.googleapis.com/qpxExpress/v1/trips/search?key=Your_Api_Key"
instead in header
try and run, this work in my case.....
I'm trying to create a loading spinner that will be displayed when breeze is communicating with the server. Is there some property in Breeze that is 'true' only when breeze is sending data to the server, receiving data, or waiting for a response (e.g. after an async call has been made but no response yet)? I thought of binding this data to a knockout observable and binding the spinner to this observable,
Thanks,
Elior
Use spin.js
http://fgnass.github.io/spin.js/
Its so simple..make it visible before you execute the query and disable it after the query succeeds or fails.
I don't see any property that is set or observable while Breeze is querying, but if you are using a datacontext, or some JavaScript module for your data calls, this is what you can do -
EDIT
Taking John's comments into account, I added a token'd way of tracking each query.
var activeQueries = ko.observableArray();
var isQuerying = ko.computed(function () {
return activeQueries().length !== 0;
});
var toggleQuery = function (token) {
if (activeQueries.indexOf(token) === -1)
{ activeQueries.push(token); }
else { activeQueries.remove(token); }
};
var getProducts = function (productsObservable, forceRemote) {
// Don't toggle if you aren't getting it remotely since this is synchronous
if (!forceRemote) {
var p = getLocal('Products', 'Product','product_id');
if (p.length > 0) {
productsObservable(p);
return Q.resolve();
}
}
// Create a token and toggle it
var token = 'products' + new Date().getTime();
toggleQuery(token);
var query = breeze.EntityQuery
.from("Products");
return manager.executeQuery(query).then(querySucceeded).fail(queryFailed);
function querySucceeded(data) {
var s = data.results;
log('Retrieved [Products] from remote data source', s, true);
// Toggle it off
toggleQuery(token);
return productsObservable(s);
}
};
You will need to make sure all of your fail logic toggles the query as well.
Then in your view where you want to place the spinner
var spinnerState = ko.computed(function () {
datacontext.isQuerying();
};