Google Slides: newly inserted table not found - google-apps-script

I´m wondering what is going on. I have two functions which both are working good when called one after one:
function createTable() {
var slidesPage = SlidesApp.openById('1QWRV4eQzGNNBz4SkR3WPurTL3O60oGYxQpBu63KrUoI').getSlides()[0];
var table = slidesPage.insertTable(7, 4);
}
function changeColumnWidth() {
var slidesPage = SlidesApp.openById('1QWRV4eQzGNNBz4SkR3WPurTL3O60oGYxQpBu63KrUoI').getSlides()[0];
var tableId = slidesPage.getTables()[0].getObjectId();
var requests = [{
updateTableColumnProperties: {
objectId: tableId,
"columnIndices": [ 1, 3],
"tableColumnProperties": {
"columnWidth": {
"magnitude": 80,
"unit": "PT"
}
},
"fields": "columnWidth"
}
}];
var createSlideResponse = Slides.Presentations.batchUpdate({
requests: requests
}, '1QWRV4eQzGNNBz4SkR3WPurTL3O60oGYxQpBu63KrUoI');
}
But trying to combine these two functions like:
function combined() {
createTable();
changeColumnWidth();
}
I´m getting Error:
Invalid requests[0].updateTableColumnProperties: The object (SLIDES_API456304911_0) could not be found.
Wondering if the insertTable method is asynchronous and therefore the created table is not ready?
Thanks for any help.

How about this modification? Please think of this as one of several workarounds. In my workaround, I used saveAndClose() for your situation. Using this, I thought to separate the process of SlidesApp and Slides API.
Modification points :
Save and close the slide using saveAndClose() after the table was inserted.
Return an object ID of inserted table to use at changeColumnWidth().
At changeColumnWidth(), the table is modified by Slides API using the received object ID.
Modified script :
function combined() {
var tableId = createTable(); // Modified
changeColumnWidth(tableId); // Modified
}
function createTable() {
var slide = SlidesApp.openById('1QWRV4eQzGNNBz4SkR3WPurTL3O60oGYxQpBu63KrUoI'); // Modified
var slidesPage = slide.getSlides()[9]; // Modified
var table = slidesPage.insertTable(7, 4);
slide.saveAndClose(); // Added
return table.getObjectId();
}
function changeColumnWidth(tableId) { // Modified
// var slidesPage = SlidesApp.openById('1QWRV4eQzGNNBz4SkR3WPurTL3O60oGYxQpBu63KrUoI').getSlides()[0]; // This line is not used.
// var tableId = slidesPage.getTables()[0].getObjectId(); // This line is not used because slidesPage.getTables().length becomes 0.
var requests = [{
updateTableColumnProperties: {
objectId: tableId,
"columnIndices": [ 1, 3],
"tableColumnProperties": {
"columnWidth": {
"magnitude": 80,
"unit": "PT"
}
},
"fields": "columnWidth"
}
}];
var createSlideResponse = Slides.Presentations.batchUpdate({
requests: requests
}, '1QWRV4eQzGNNBz4SkR3WPurTL3O60oGYxQpBu63KrUoI');
}
Note :
For the slide which is saved and closed by saveAndClose(), when the slide is reopened, the inserted table cannot be retrieved. When the table is tried to be retrieved using getTables() again, the length becomes 0. But at Slides API, the object ID of table can be retrieved. So I thought that the issue might be able to be solved by returning the object ID of table after the table was inserted.
But I couldn't understand about the reason that the values retrieved by getTables() from the reopened Slide become "0" yet. I'm sorry.
Reference :
saveAndClose()
If this workaround was not what you want, I'm sorry.

To achieve your goal - create a table with a specified layout and specific column sizes in one function - you should use the Slides API for the entire task. The Slides API lets you both create and modify the same element in the same batch request, if you provided a unique object ID for it. Otherwise, you have to first create the element, then send the modification request using the objectId found in the response to the first request. This second approach is essentially the behavior you were experiencing when the function calls were done separately.
There are restrictions on user-supplied IDs, naturally:
objectId string: A user-supplied object ID.If you specify an ID, it must be unique among all pages and page elements in the presentation. The ID must start with an alphanumeric character or an underscore (matches regex [a-zA-Z0-9_] ); remaining characters may include those as well as a hyphen or colon (matches regex [a-zA-Z0-9_-:] ). The length of the ID must not be less than 5 or greater than 50.If you don't specify an ID, a unique one is generated.
Given that hyphens are allowed, we can use the Utilites.getUuid() method to help supply our own unique object IDs.
When mixing SlidesApp and Slides, it is very likely that internal Google optimizations (e.g. write-caching) change the operation order. By restricting to a single service for related task operations, we can ensure that the objects we need are available when needed.
This example uses two methods that make Request objects for batchUpdate and ultimately creates a presentation, adds a blank slide, adds a table and modifies it, and then creates another blank slide.
function makeCreateTableRequest_(slideId, rows, columns, shouldSupplyID) {
const tablerq = {
rows: rows,
columns: columns,
elementProperties: {
pageObjectId: slideId,
/** size: {
height: {...},
width: {...}
},
transform: { ... } */
}
};
// If asked to use a custom ID (e.g. also going to modify this table), use a unique one.
if (shouldSupplyID)
tablerq.objectId = ("table" + Utilities.getUuid()).slice(0, 50);
return {createTable: tablerq};
}
function makeModifyTableColumnPropsRequest_(tableId, newWidthDimension, indicesArray) {
const rq = {
objectId: tableId,
fields: "columnWidth" // There are no other fields for this request as of 2018-07
};
if (newWidthDimension && newWidthDimension.magnitude !== undefined && newWidthDimension.unit)
rq.tableColumnProperties = { columnWidth: newWidthDimension };
if (indicesArray && indicesArray.length)
rq.columnIndices = indicesArray;
return {updateTableColumnProperties: rq};
}
function createPresentation_() {
const newPres = { title: "API-created Presentation" };
// Presentations are huge... limit the metadata sent back to us.
const fields = "presentationId,pageSize,title"
+ ",slides(objectId,pageType,pageElements(objectId,size,title,description))"
+ ",masters(objectId,pageType,pageElements(objectId,size,title,description))"
+ ",layouts(objectId,pageType,pageElements(objectId,size,title,description))";
const createdMetadata = Slides.Presentations.create(newPres, {fields: fields});
console.log({message:"Created a Presentation", response: createdMetadata});
return createdMetadata;
}
function addSlide_(pId) {
const response = Slides.Presentations.batchUpdate({ requests: [{ createSlide: {} }] }, pId);
return response.replies[0].createSlide.objectId;
}
function foo() {
const pres = createPresentation_();
const newSlideId = addSlide_(pres.presentationId);
// Get requests to add and to modify tables.
const openingTableRq = makeCreateTableRequest_(pres.slides[0].objectId, 2, 4);
const newTableRq = makeCreateTableRequest_(newSlideId, 7, 4, true);
const changeWidthRq = makeModifyTableColumnPropsRequest_(newTableRq.createTable.objectId, {magnitude: 80, unit: "PT"}, [0]);
// Add and update the desired table, then create a new slide.
var response = Slides.Presentations.batchUpdate({
requests: [
openingTableRq, // will have reply
newTableRq, // will have reply
changeWidthRq, // no reply
{ createSlide: {} } // will have reply
]
}, pres.presentationId);
console.log({message: "Performed updates to the created presentation", response: response});
}

Related

google appsScript wait for batchUpdate results to take affect on slide

I'm using the Slides API in my appsScript to change a tables column size, when later I query the tables column width I still get the old values.
When the script ends I can see in slides the table width has changed & next time I'll run the script it will return the updated width value.
Is there a way to wait for the request to finish? Something else?
To reproduce, open an empty google slides and place a table in the first (and only slide).
/**
* #OnlyCurrentDoc Limits the script to only accessing the current presentation.
*/
function getColumnsWidth(table) {
colsWidth = []
for (var c=0; c<table.getNumColumns(); c++) {
col = table.getColumn(c)
colsWidth.push(col.getWidth());
}
return colsWidth
}
function changeColumnWidth(table, columnsInd, W){
Logger.log('Called changeColumnWidth with columnsInd=%s, W=%s', columnsInd, W)
var presentation = SlidesApp.getActivePresentation();
var tableObjectId = table.getObjectId();
var resource = {"requests": [
{"updateTableColumnProperties": {
"objectId": tableObjectId,
"columnIndices": columnsInd,
"tableColumnProperties": {"columnWidth": {"unit": "PT", "magnitude": W}}, "fields": "*"}
}
]};
Slides.Presentations.batchUpdate(resource, presentation.getId());
}
function getTableInSlide(selection) {
var currentPage = selection.getCurrentPage();
pageElements = currentPage.getPageElements();
for (var i=0; i<pageElements.length; i++){
var elementType = pageElements[i].getPageElementType()
if (elementType == 'TABLE'){
return pageElements[i].asTable()
}
}
return Null
}
function myFunction() {
var presentation = SlidesApp.getActivePresentation();
var selection = presentation.getSelection();
table = getTableInSlide(selection)
var colsW = getColumnsWidth(table)
Logger.log('columns width (before update): %s', colsW);
changeColumnWidth(table, [0], colsW[0]*2)
colsW = getColumnsWidth(table)
Logger.log('columns width (after update): %s', colsW);
}
Output:
columns width (before update): [43.303149606299215, 181.63385826771653, 149.73228346456693]
Called changeColumnWidth with columnsInd=[0.0], W=86.60629921259843
columns width (after update): [**43.303149606299215**, 181.63385826771653, 149.73228346456693]
Next run output:
columns width (before update): [**86.60629921259843**, 181.63385826771653, 149.73228346456693]
Called changeColumnWidth with columnsInd=[0.0], W=173.21259842519686
columns width (after update): [86.60629921259843, 181.63385826771653, 149.73228346456693]
P.S
I'm using the slides API as I didn't find any better way to change a tables column width (if there is - I'd love to learn that too :)
I recommend giving it some time before checking for the updated value. The function to try is Utilities.sleep(numberOfSeconds), see docs.
Basically, before you prepend it to this section as shown here:
Utilities.sleep(1000)
colsW = getColumnsWidth(table)
Logger.log('columns width (after update): %s', colsW);
If that does not work then I suggest you explicitly load the updated file again, e.g.
const originalTable = getTableInSlide()
changeColumnWidth(originalTable, 1, 2,)
const updatedTable = getTableInSlide()
const newWidth = getColumnsWidth(updatedTable)
Not sure I fully got the example, but I had a similar problem. I solved it by using the following command:
saveAndClose()
Then reopening the Presentation. It forces execution completion.

How to add a profile photo to a contact as you create it via google contact People API?

Update/Solution
There isn't a way to add a photo to a person object on creating a contact via People API. However you can updated the photo directly after using People.People.updateContactPhoto. updateContactPhoto has a limit of 60 queries per minute per user. If your script needs to update more than 60 contact photos, then you will need to add a delay in your code so that query limit isn't reached
Question:
I am creating a large list of contacts from a set of data, including contact photos for each contact. When I create contacts via people.createContact, if I have the photos field there it states "person.photos is a read only field." How I can I create the contact with a profile photo link as well?
Example Code
const contact = {names:..., addresses:..., emailAddressess:..., phoneNumbers:...,
photos: {"url":..., "primary": true}
}
People.People.createContact(contact)
this gives the error message
Update 1:
Currently this is what I'm doing and it works. The main problem now is I don't want to use the Utilities.Sleep function. I'm looping over this function hundreds of times, and that adds up quickly. If I don't use the sleep though around the 40th or 50th contact it throws an "Internal error encountered". The contact it errors on is different each time, so the error is not tied to a specific person. My assumption is the error comes from running too many requests too quickly, but how can I run this without forcing it to sleep?
function importPerson(person){
const photoData = encodePhotoData(person.photos[0].url)
delete person.photos
const newContact = People.People.createContact(person)
People.People.updateContactPhoto({
photoBytes: photoData,
},newContact.resourceName)
//This stops it from throwing an error
Utilities.sleep(1000)
}
You need to use people.updateContactPhoto to update a contact's person. You need to convert your photo's url into a raw photo bytes in base64-encoded string.
Sample Code:
var resource = {
names: [
{
givenName: "Sample1"
}
],
emailAddresses: [
{
value: "sample1#example.com"
}
]
};
//create contact
var response = People.People.createContact(resource);
//Get raw photo bytes in base64-encoded string
var url = "https://lh3.googleusercontent.com/-T_wVWLlmg7w/AAAAAAAAAAI/AAAAAAAABa8/00gzXvDBYqw/s100/photo.jpg"
var photoBlob = UrlFetchApp.fetch(url);
var base64EncodedBytes = Utilities.base64Encode(photoBlob.getBlob().getBytes());
var photoResource = {
photoBytes: base64EncodedBytes
}
//update photo
People.People.updateContactPhoto(photoResource,response.resourceName)
Output:
(UPDATE)
Here is the sample code where I tried to create 100 contacts:
function testContact(){
for(var i=0;i<100;i++){
var resource = {
names: [
{
givenName: "Sample1"
}
],
emailAddresses: [
{
value: "sample1#example.com"
}
],
photos:[
{
url: "https://lh3.googleusercontent.com/-T_wVWLlmg7w/AAAAAAAAAAI/AAAAAAAABa8/00gzXvDBYqw/s100/photo.jpg"
}
]
};
Logger.log(i);
importPerson(resource);
}
}
function importPerson(person){
const photoData = encodePhotoData(person.photos[0].url)
delete person.photos
const newContact = People.People.createContact(person)
People.People.updateContactPhoto({
photoBytes: photoData,
},newContact.resourceName)
//This stops it from throwing an error
//Utilities.sleep(1000)
}

Insert YouTube top level comment using Google Apps Script

I am trying to create a program using Google Apps Script that inserts a comment when a certain YouTube channel uploads. I have been able to get the latest YouTube video ID from the channel but when I try to insert a comment, it throws an error, "Parse Error (line 19, file 'Code')".
Line 19: YouTube.CommentThreads.insert("snippet", {
Here's my code:
function getVideo() {
// MrBeast Channel ID: UCX6OQ3DkcsbYNE6H8uQQuVA
var channel = "UCX6OQ3DkcsbYNE6H8uQQuVA";
var fttx = "FIRST!";
var results = YouTube.Channels.list("contentDetails", {"id": channel});
for (var i in results.items) {
var item = results.items[i];
var playlistId = item.contentDetails.relatedPlaylists.uploads;
// Uploads Playlist ID: UUX6OQ3DkcsbYNE6H8uQQuVA
var playlistResponse = YouTube.PlaylistItems.list("snippet", {"playlistId": playlistId, "maxResults": 1});
for (var j = 0; j < playlistResponse.items.length; j++) {
var playlistItem = playlistResponse.items[j];
var latvid = playlistItem.snippet.resourceId.videoId;
comment(latvid, channel, fttx);
}
}
}
function comment(vid, ytch, fc) {
YouTube.CommentThreads.insert("snippet", {
"snippet.channelId": ytch,
"snippet.videoId": vid,
"snippet.topLevelComment.snippet.textOriginal": fc
});
}
Per Apps Script advanced services documentation, when specifying resources (such as a CommentThread) they are the first parameter to a method. If you use the Apps Script editor's autocomplete, it is very clear about the required order:
Also note that you have incorrectly created your resource body - you have various sub-properties. For example, the snippet property is a required member of the CommentThread resource. Three "snippet.___" properties are not equivalent to one snippet property with 3 sub-properties.
Thus the solution to resolve the parse error in YouTube.CommentThreads.insert is to use the required method signature, with the required resource format:
function startCommentThread(vid, ytch, fc) {
const resource = {
snippet: {
channelId: ytch,
videoId: vid,
topLevelComment: {
snippet: {
textOriginal: fc
}
}
}
};
YouTube.CommentThreads.insert(resource, "snippet");
}
According to docs the {}, is missing and single quotes are used. I'm unable to test this right now but hopefully it will solve your problem.
commentThreadsInsert('snippet',
{},
{'snippet.channelId': '',
'snippet.videoId': '',
'snippet.topLevelComment.snippet.textOriginal': ''
});

Functions being returned as keys

Newbie question here...I'm building a simple stack using a functional pattern and returning the push and pop functions as keys(I ofcourse don't want this)...I really am not sure why. The function is operational, just returning those two extra keys...
This is what the return looks like...
{ size: 2,
storage: { '1': 'test0', '2': 'test1' },
push: [Function], <== don't want
pop: [Function] } <== don't want
[Finished in 0.1s]
function Stack () {
var obj = {};
obj.size = 0;
obj.storage = {};
obj.push = function(data) {
var newSize = ++obj.size;
obj.storage[newSize] = data;
};
obj.pop = function() {
var newSize = obj.size;
var deletedData;
if (newSize) {
deletedData = obj.storage[newSize];
delete obj.storage[newSize];
obj.size--;
return deletedData;
}
};
return obj;
};
var stack = new Stack();
stack.push('test0')
stack.push('test1')
stack.push('test2')
stack.pop()
console.log(stack)
You say "obviously" you don't want the object to include keys for the functions, but I guess to me it's not so obvious... So what is it you do want to end up with? In JavaScript a function reference is just another piece of data, and a method is just a function reference stored as the value for some key on an object.
If you want the user to be able to say obj.push(...) then you do want a push key on obj (so that obj.push means something), and you want its value to be a function (so that the () operator can be applied to it).
Now I am curious because your output block says the keys are xPush and xPop but those aren't the values you show in the code block. Is that because of editing in the question? If not I don't see how that could be the way you've shown it.

Read records from firebase based on a previously saved value

I took an angularjs + firebase example and modified it for an app where I can register some kids for a small cross-country race.
I'm able to register kids (participants), races, locations, clubs etc. using a basic structure:
FIREBASE_URL/races
FIREBASE_URL/clubs
and so forth. When the active race is selected, I save the raceId and race json-object and can add participants to the active race.
Example:
FIREBASE_URL/active_race/-JI6H9VQewd444na_CQY
FIREBASE_URL/active_race/json-object
What I'd like to do is to get all the participants, if any, based on raceId:
FIREBASE_URL/races/-JI6H9VQewd444na_CQY/participants
I tried the following
'use strict';
app.factory('Race', function ($firebase, FIREBASE_URL, User) {
var ref = new Firebase(FIREBASE_URL + 'races');
var races = $firebase(ref);
var Race = {
all: races,
getParticipantsInRace: function () {
var fb = new Firebase(FIREBASE_URL);
fb.child('active_race/raceId').once('value', function (activeSnap) {
races.$child('/' + activeSnap.val() + '/participants');
});
}
};
return Race;
But I believe I'm doing it wrong. I tried to prepend return before races.$child and fb.child but it did not solve my problem.
I tried to hardcode the following json-array and this is shown on the webpage:
return [{name: 'Claus', born: '1967'}, {name: 'John', born: '1968'}];
How do I get all the participants into $scope.participantsInRace?
I believe I have a solution, but I'm not sure if it's wise to do it this way. But it may be that simple. Prepending $rootScope.participantsInRace = to put it into rootScope:
$rootScope.participantsInRace = races.$child('/' + activeSnap.val() + '/participants');
The code is already synchronizing all data in all races when it declares $firebase(URL+'races');. Additionally, you never assigned your races.$child(...) to anything, so it's not possible to reference that data later.
app.factory('Race', function ($firebase, FIREBASE_URL, User) {
var ref = new Firebase(FIREBASE_URL + 'races');
var races = $firebase(ref);
var Race = {
all: races,
getParticipantsInRace: function (raceId) {
return races[raceId]? races[raceId].participants || {};
}
};
return Race;
});
Keep in mind that the race data won't be available until races.$on('loaded') is invoked (when the data returns from the server).
Thank you for the input. I know a bit more about angularjs and javascript now so I did some refactoring and cleanup. Hardcoding raceId works:
getParticipantsInRace: function () {
return races.$child('-JIecmbdDa4kUT2L51iS').$child('participants');
}
When I wrap it in a call to Firebase I can't seem to return the desired data, probably due to my somewhat limited knowledge of javascript on how to return data. Example:
getParticipantsInRace: function () {
ref.child('activeRace').child('raceId').once('value', function (activeSnap) {
return races.$child(activeSnap.val()).$child('participants');
});
}
My idea is to get the raceId and then return all participants. I tried to prepend return to ref.child() but still no data was returned. So not really an answer.
Regards
Claus
This works. I changed $rootScope.participantsInRace to $scope.participantsInRace and the following:
getParticipantsInRace: function () {
if (User.signedIn()) {
var t = [];
var user = User.getCurrent();
var fb = new Firebase(FIREBASE_URL + 'users');
fb.child(user.username).child('activeRace/raceId').once('value', function (userSnap) {
t = races.$child(userSnap.val()).$child('participants');
});
return t;
}
},