How to get Course Id from Classroom URL? - google-apps-script

Goal: I want to quickly connect to a Google Classroom using a Google Classroom URL via Google Apps Script.
Problem: Need help filtering Map of courses by URL.
Background:
Classroom API has little documentation for GAS. Furthermore, COURSE_ID is used for nearly all connections. I can map the active courses, but I cannot filter the map. The code below originated from Yagisanatode with modifications in an attempt to map active courses by URL. Changing the Logger to (courseData) reveals the creation of the double array.
function findCourseByUrl() {
const courseList = Classroom.Courses.list({"courseStates":["ACTIVE"]}).courses;
const courseData = courseList.map(course => {
let ownerName = Classroom
.Courses
.Teachers
.get(course.id, course.ownerId)
.profile
.name
.fullName;
return `[${course.name}, ${course.id}, ${ownerName}, ${course.alternateLink}]`;
});
const link = 'https://classroom.google.com/c/YOUCLASSROOMURL'; //change this
const data = courseData.filter(function(item){return item[4] === link;});
Logger.log(data);
};
Any help would be appreciated. I'm stuck.

Answer:
link is not defined since it is outside of the courseData.filter(function(item){}). The solution is to call a global variable or create a conditional with the declared variable within the function(item).
The toString is looking for an exact match for the URL text, which is naturally unique.
Video reference: https://youtu.be/PT_TDhMhWsE
Code:
function findCourseByUrl() {
const courseList = Classroom.Courses.list({"courseStates":["ACTIVE"]}).courses;
const courseData = courseList.map(course => {
let ownerName = Classroom
.Courses
.Teachers
.get(course.id, course.ownerId)
.profile
.name
.fullName;
return `[${course.name}, ${course.id}, ${ownerName}, ${course.alternateLink}]`;
});
const filterCourse = function(item){
let link = 'https://classroom.google.com/c/YOURCOURSEURL' ///Change this or replace with a global variable
if(item.toString().indexOf(link) === -1){
return false;
} else {
return true
}
};
let theCourse = courseData.filter(filterCourse); //this could be a return if called by function in Test.gs
Logger.log(theCourse); //remove if using a function with console.log in Test.gs
};

Related

Google Docs API - complete documentation (hyperlink issue)

I hope everyone is in good health. This post is my continue of my previous post
My main goal
So main goal was to get the hyperlink and change it the text linked with it. I initially used code from this post and modified it to change the text of first hyperlink. Here is my modified code to change the text of first hyperlink.
function onOpen() {
const ui = DocumentApp.getUi();
ui.createMenu('What to do?')
.addItem('HyperLink Modifier', 'findAndReplacetext')
.addToUi();
}
/**
* Get an array of all LinkUrls in the document. The function is
* recursive, and if no element is provided, it will default to
* the active document's Body element.
*
* #param element The document element to operate on.
* .
* #returns {Array} Array of objects, vis
* {element,
* startOffset,
* endOffsetInclusive,
* url}
*/
function getAllLinks(element) {
var links = [];
element = element || DocumentApp.getActiveDocument().getBody();
if (element.getType() === DocumentApp.ElementType.TEXT) {
var textObj = element.editAsText();
var text = element.getText();
var inUrl = false;
for (var ch=0; ch < text.length; ch++) {
var url = textObj.getLinkUrl(ch);
if (url != null) {
if (!inUrl) {
// We are now!
inUrl = true;
var curUrl = {};
curUrl.element = element;
curUrl.url = String( url ); // grab a copy
curUrl.startOffset = ch;
}
else {
curUrl.endOffsetInclusive = ch;
}
}
else {
if (inUrl) {
// Not any more, we're not.
inUrl = false;
links.push(curUrl); // add to links
curUrl = {};
}
}
}
if (inUrl) {
// in case the link ends on the same char that the element does
links.push(curUrl);
}
}
else {
var numChildren = element.getNumChildren();
for (var i=0; i<numChildren; i++) {
links = links.concat(getAllLinks(element.getChild(i)));
}
}
return links;
}
/**
* Replace all or part of UrlLinks in the document.
*
* #param {String} searchPattern the regex pattern to search for
* #param {String} replacement the text to use as replacement
*
* #returns {Number} number of Urls changed
*/
function findAndReplacetext() {
var links = getAllLinks();
while(links.length > 0){
var link = links[0];
var paragraph = link.element.getText();
var linkText = paragraph.substring(link.startOffset, link.endOffsetInclusive+1);
var newlinkText = `(${linkText})[${link.url}]`
link.element.deleteText(link.startOffset, link.endOffsetInclusive);
link.element.insertText(link.startOffset, newlinkText);
links = getAllLinks();
}
}
String.prototype.betterReplace = function(search, replace, position) {
if (this.length > position) {
return this.slice(0, position) + this.slice(position).replace(search, replace);
}
return this;
}
Note: I used insertText and deleteText functions to update the text value of hyperlink.
My problem with above code
Now the problem was that this code was running too slow. I thought may be it was because I was running the script every-time I needed to search for next hyperlink, So maybe I can break the loop and only get the first hyperlink each time. Then from my previous post the guy gave me a solution to break loop and only get the first hyperlink but when I tried the new code unfortunately it was still slow. In that post he also proposed me a new method by using Google Docs API, I tried using that it was was super fast. Here is the code using Google Docs API
function myFunction() {
const doc = DocumentApp.getActiveDocument();
const res = Docs.Documents.get(doc.getId()).body.content.reduce((ar, {paragraph}) => {
if (paragraph && paragraph.elements) {
paragraph.elements.forEach(({textRun}) => {
if (textRun && textRun.textStyle && textRun.textStyle.link) {
ar.push({text: textRun.content, url: textRun.textStyle.link.url});
}
});
}
return ar;
}, []);
console.log(res) // You can retrieve 1st link and test by console.log(res[0]).
}
My new problem
I liked the new code but I am stuck again at this point as I am unable to find how can I change the text associated with the hyperlink. I tried using the functions setContent and setUrl but they don't seem to work. Also I am unable to find the documentation for these functions on main documentation of this API. I did find I reference for previously mentioned functions here but they are not available for appscript. Here is the sample document I am working on
https://docs.google.com/document/d/1eRvnR2NCdsO94C5nqly4nRXCttNziGhwgR99jElcJ_I/edit?usp=sharing
End note:
I hope I was able to completly convey my message and all the details assosiated with it. If not kindly don't be mad at me, I am still in learning process and my English skills are pretty weak. Anyway if you want any other data let me know in the comments and Thanks for giving your time I really appreciate that.
In order to remove all the hyperlink from your document, you can do the following:
First, retrieve the start and end indexes of these hyperlinks. This can be done by calling documents.get, iterate through all elements in the body content, checking which ones are paragraphs, iterating through the corresponding TextRun, and checking which TextRuns contain a TextStyle with a link property. All this is already done in the code you provided in your question.
Next, for all TextRuns that include a link, retrieve their startIndex and endIndex.
Using these retrieved indexes, call batchUpdate to make an UpdateTextStyleRequest. You want to remove the link property between each pair of indexes, and for that you would just need to set fields to link (in order to specify which properties you want to update) and don't set a link property in the textStyle property you provide in the request since, as the docs for TextStyle say:
link: If unset, there is no link.
Code sample:
function removeHyperlinks() {
const doc = DocumentApp.getActiveDocument();
const hyperlinkIndexes = Docs.Documents.get(doc.getId()).body.content.reduce((ar, {paragraph}) => {
if (paragraph && paragraph.elements) {
paragraph.elements.forEach(element => {
const textRun = element.textRun;
if (textRun && textRun.textStyle && textRun.textStyle.link) {
ar.push({startIndex: element.startIndex, endIndex: element.endIndex });
}
});
}
return ar;
}, []);
hyperlinkIndexes.forEach(hyperlinkIndex => {
const resourceUpdateStyle = {
requests: [
{
updateTextStyle: {
textStyle: {},
fields: "link",
range: {
startIndex: hyperlinkIndex.startIndex,
endIndex: hyperlinkIndex.endIndex
}
}
}
]
}
Docs.Documents.batchUpdate(resourceUpdateStyle, doc.getId());
});
}

How to deal with if statement when one Form Response has no response in Google Script?

I'm trying to create a script that sends an email when someone submits a google form. The form includes an optional file upload that the script will then attach to the email as a pdf.
The issue I'm facing is how to ignore the process that creates the attachment if the response is empty.
Sample code below
function getIdFrom(url) {
var id = '';
var parts = url.split(
/^(([^:\/?#]+):)?(\/\/([^\/?#]*))?([^?#]*)(\?([^#]*))?(#(.*))?/
);
if (url.indexOf('?id=') >= 0) {
id = parts[6].split('=')[1].replace('&usp', '');
return id;
} else {
id = parts[5].split('/');
var sortArr = id.sort(function (a, b) {
return b.length - a.length;
});
id = sortArr[0];
return id; //returns google doc id.
}
}
function onFormSubmit(response) {
var link = response.namedValues['Upload file'];
if (typeof link !== "undefined" && link.length > 0) { // I think it's here that's the issue
var uploadFileId = getIdFrom(link[0]);
var uploadFile = DriveApp.getFileById(uploadFileId);
var uploadFileType = (function () {
if (uploadFile.getMimeType().includes('image')) {
return uploadFile.getMimeType();
} else {
return 'application/pdf';
}
};
var attachArr = [uploadFile.getAs(uploadFileType)];
}
// etc etc send email.
}
Works fine if the user submits a form with an uploaded file.
However if the form is submitted without entering anything in the "Upload File" question, I'm getting a "TypeError: Cannot read property 'split' of undefined" at the getIdFrom(url) function I assume because it's still trying to pass link through getIdFrom() even though it shouldn't because it's undefined.
Weirdly it works perfectly fine when I use the two test inputs I have, one of which 'Upload File' exists but is empty and the other it doesn't exist at all.
I'm not sure what I'm missing here.
Also I have no doubt it's a messy way to do things but I'm getting there.
response.namedValues['Upload file'] is an object
even if it's empty it will have at least the length of >0
Workaround
Modify your if statement to
if (link[0].length > 0) {
...
}

How to check for URL redirects in Google Sheets with Google Apps Script

I have been trying to run some URL redirect testing using Google Apps Script in Google Sheets, I've been successful by getting a response code and also the final redirect URL for some of them but most of the links are not working.
Examples of the links I would like to check:
https://www.airbnb.com/rooms/4606613
https://www.airbnb.com/rooms/4661522
https://www.airbnb.com/rooms/6014647
https://www.airbnb.com/rooms/14452305
https://www.airbnb.com/rooms/15910617
Pretty much I need to check if those links will redirect to https://www.airbnb.com/s/homes
Using the script below, I get the following list, which is not correct since all of them will redirect to https://www.airbnb.com/s/homes:
https://www.airbnb.com/rooms/4606613
https://www.airbnb.com/s/homes
https://www.airbnb.com/s/homes
https://www.airbnb.com/rooms/14452305
https://www.airbnb.com/rooms/15910617
It seems that the website is taking 1 second to do the redirect and probably that could be the issue.
Below the code:
function urlProtocol(url){
return URI(url).protocol()
}
function urlHostname(url){
return URI(url).hostname()
}
function getRedirects(url) {
eval(UrlFetchApp.fetch('https://rawgit.com/medialize/URI.js/gh-pages/src/URI.js').getContentText());
var params = {
'followRedirects': false,
'muteHttpExceptions': true
};
var baseUrl = urlProtocol(url) + "://" + urlHostname(url),
response = UrlFetchApp.fetch(url, params),
responseCode = response.getResponseCode();
if(response.getHeaders()['Location']){
var redirectedUrl = getRedirects(baseUrl + response.getHeaders()['Location']);
return redirectedUrl;
} else {
return url;
}
}
Seems like the final redirect on some of the URLs happens after the page is loaded. Most likely there is a client-side script that initiates the change of window.location. Therefore, your correct logic fails to catch such pages.
To make matters worse, after-load redirect seem to be inconsistent as sometimes the pages you provided are not redirected to https://www.airbnb.com/s/homes. I was able to stop this redirect from happening, so the theory is confirmed - will update with what exactly causes it.
Apart from that, there are several optimizations you can apply to your script:
Get rid of eval and, actually, of the whole library unless you really need it (see how to do the same in just two lines). Improved security is the main benefit: no eval() of external scripts means less possibilities for breach.
Check for status code in 3xx range before looking through the Location header (as a precaucion).
/**
*
* #param {string} target
*/
const getRedirects = (target) =>
/**
* #param {string}
* #returns {boolean}
*/
(url) => {
if(url === target) {
return false;
}
const response = UrlFetchApp.fetch(url, {
'followRedirects': false,
'muteHttpExceptions': true
});
const code = response.getResponseCode();
let { Location } = response.getHeaders();
if (code < 300 || code >= 400) {
return true;
}
if (!Location) {
return false;
}
if (/^\/\w+/.test(Location)) {
const [protocol, , base] = url.split("/");
Location = `${protocol}//${base}${Location}`;
}
console.log(Location);
return getRedirects(target)(Location);
};
const testRedirects = () => {
const redirectsToHome = getRedirects("https://www.airbnb.com/s/homes");
const accessible = [
"https://www.airbnb.com/rooms/23861670",
"https://www.airbnb.com/rooms/4606613",
"https://www.airbnb.com/rooms/4661522",
"https://www.airbnb.com/rooms/6014647",
"https://www.airbnb.com/rooms/14452305",
"https://www.airbnb.com/rooms/15910617"
].filter(redirectsToHome);
console.log(accessible);
};
Since the clarification that the function is a custom function, you can add a wrapper function that will serve as public API that you can reference in a cell that will call the utility, something like this:
const checkIfRedirects = (source, target = "https://www.airbnb.com/s/homes") => getRedirects(target)(source);
You can then use it like you would do a formula:
=checkIfRedirects(A20)

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;
}
},

Safe getElementById or try to determine if ID exists in GUI

Method UiInstance.getElementById(ID) always returns GenericWidget object, even if ID does not exist.
Is there some way how to find out that returned object does not exist in my app, or check whether UI contains object with given ID?
Solution for UI created with GUI builder:
function getSafeElement(app, txtID) {
var elem = app.getElementById(txtID);
var bExists = elem != null && Object.keys(elem).length < 100;
return bExists ? elem : null;
}
It returns null if ID does not exist. I didn't test all widgets for keys length boundary, so be careful and test it with your GUI.
EDIT: This solution works only within doGet() function. It does not work in server handlers, so in this case use it in combination with #corey-g answer.
This will only work in the same execution that you created the widget in, and not in a subsequent event handler where you retrieve the widget, because in that case everything is a GenericWidget whether or not it exists.
You can see for yourself that the solution fails:
function doGet() {
var app = UiApp.createApplication();
app.add(app.createButton().setId("control").addClickHandler(
app.createServerHandler("clicked")));
app.add(app.createLabel(exists(app)));
return app;
}
function clicked() {
var app = UiApp.getActiveApplication();
app.add(app.createLabel(exists(app)));
return app;
}
function exists(app) {
var control = app.getElementById("control");
return control != null && Object.keys(control).length < 100;
}
The app will first print 'true', but on the click handler it will print 'false' for the same widget.
This is by design; a GenericWidget is a "pointer" of sorts to a widget in the browser. We don't keep track of what widgets you have created, to reduce data transfer and latency between the browser and your script (otherwise we'd have to send up a long list of what widgets exist on every event handler). You are supposed to keep track of what you've created and only "ask" for widgets that you already know exist (and that you already know the "real" type of).
If you really want to keep track of what widgets exist, you have two main options. The first is to log entries into ScriptDb as you create widgets, and then look them up afterwards. Something like this:
function doGet() {
var app = UiApp.createApplication();
var db = ScriptDb.getMyDb();
// You'd need to clear out old entries here... ignoring that for now
app.add(app.createButton().setId('foo')
.addClickHandler(app.createServerHandler("clicked")));
db.save({id: 'foo', type: 'button'});
app.add(app.createButton().setId('bar'));
db.save({id: 'bar', type: 'button'});
return app
}
Then in a handler you can look up what's there:
function clicked() {
var db = ScriptDb.getMyDb();
var widgets = db.query({}); // all widgets
var button = db.query({type: 'button'}); // all buttons
var foo = db.query({id: 'foo'}); // widget with id foo
}
Alternatively, you can do this purely in UiApp by making use of tags
function doGet() {
var app = UiApp.createApplication();
var root = app.createFlowPanel(); // need a root panel
// tag just needs to exist; value is irrelevant.
var button1 = app.createButton().setId('button1').setTag("");
var button2 = app.createButton().setId('button2').setTag("");
// Add root as a callback element to any server handler
// that needs to know if widgets exist
button1.addClickHandler(app.createServerHandler("clicked")
.addCallbackElement(root));
root.add(button1).add(button2);
app.add(root);
return app;
}
function clicked(e) {
throw "\n" +
"button1 " + (e.parameter["button1_tag"] === "") + "\n" +
"button2 " + (e.parameter["button2_tag"] === "") + "\n" +
"button3 " + (e.parameter["button3_tag"] === "");
}
This will throw:
button1 true
button2 true
button3 false
because buttons 1 and 2 exist but 3 doesn't. You can get fancier by storing the type in the tag, but this suffices to check for widget existence. It works because all children of the root get added as callback elements, and the tags for all callback elements are sent up with the handler. Note that this is as expensive as it sounds and for an app with a huge amount of widgets could potentially impact performance, although it's probably ok in many cases especially if you only add the root as a callback element to handlers that actually need to verify the existence of arbitrary widgets.
My initial solution is wrong, because it returns false exist controls.
A solution, based on Corey's answer, is to add the setTag("") method and here is ready to use code. It is suitable for event handlers only, because uses tags.
function doGet() {
var app = UiApp.createApplication();
var btn01 = app.createButton("control01").setId("control01").setTag("");
var btn02 = app.createButton("control02").setId("control02").setTag("");
var handler = app.createServerHandler("clicked");
handler.addCallbackElement(btn01);
handler.addCallbackElement(btn02);
btn01.addClickHandler(handler);
btn02.addClickHandler(handler);
app.add(btn01);
app.add(btn02);
return app;
}
function clicked(e) {
var app = UiApp.getActiveApplication();
app.add(app.createLabel("control01 - " + controlExists(e, "control01")));
app.add(app.createLabel("control02 - " + controlExists(e, "control02")));
app.add(app.createLabel("fake - " + controlExists(e, "fake")));
return app;
}
function controlExists(e, controlName) {
return e.parameter[controlName + "_tag"] != null;
}