Google Sheets Print All IDs in one click - google-apps-script

I'm working on a student information template and I'm wondering if it's possible to print all of the data for each student in one go? I used data validation on my spreadsheet to modify the student ID so that their data will be easily view and print. Because I have to print it one by one for each pupil, this is a time-consuming process so I come up with this kind of flow to save time. Is it possible?
Please see the sample spreadsheet.
Upon printing, there should be a different page per student.

I believe your goal is as follows.
You want to reduce the cost of printing the data from the Spreadsheet.
I thought that when the Spreadsheet is printed out using Google Apps Script, the Google Cloud Print can achieve this. But I thought that in this case, the settings might be a bit complicated. So, in this case, I would like to propose a workaround. How about the following flow?
Retrieve the values from the source sheet and create the output values as the array.
Create a new Spreadsheet and put each value on each page.
Print out the Spreadsheet.
By this flow, you can print out all pages by one manual process.
The sample script is as follows.
Sample script:
Please copy and paste the following script to the script editor of Google Spreadsheet. And run myFunction.
function myFunction() {
const rowHeader = ["STUD ID", "LAST NAME", "FIRST NAME", "MIDDLE NAME", "NAME EXTENSION"];
const sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName("DATA");
const [header, ...values] = sheet.getDataRange().getValues();
const ar = values.map(r => header.reduce((o, h, i) => Object.assign(o, { [h.toUpperCase()]: r[i] }), {}));
const newValues = ar.map(e => [rowHeader, ...[rowHeader.map(f => e[f])]]).map(e => e[0].map((_, c) => e.map(r => r[c])));
const ss = SpreadsheetApp.create("tempSpreadsheet");
newValues.forEach((v, i) => (i == 0 ? ss.getSheets()[0] : ss.insertSheet()).getRange(1, 1, v.length, v[0].length).setValues(v));
}
When you run this script, a new Spreadsheet of tempSpreadsheet is created to the root folder. When you open it, you can see the expected values for each worksheet. By this, you can print out them.
Note:
This sample script is prepared from your sample Spreadsheet. So when your sample Spreadsheet is different from your actual situation, this sample script might not be able to be used. Please be careful about this.
References:
reduce()
map()
create(name)

Related

How to set a named range for a data validation programmatically (in Google apps script) in a Google spreadsheet?

Use Case
Example. I have a named range Apples (address "Sheet10!B2:B"), which in use for data validation for plenty of sheet cells. The data range for Apples can be changed (in a script), e.g. to "Sheet10!D2:D".
It works from UI
I can set manually a named range as a data source of data validation.
In this case, the data validation of a cell will always refer to the named range Apples with updated the data range.
How to make it in Google Apps Script?
GAS Limits
The code, for setting data validation, should look like this, if you have a namedRange object:
mySheet.getRange('F5')
.setDataValidation(
SpreadsheetApp.newDataValidation()
.requireValueInRange(
namedRange.getRange()
)
.setAllowInvalid(false)
.build()
);
DataValidationBuilder.requireValueInRange() does not work here as it requires only class Range (it cannot get NamedRange), and no reference to a named range will be used.
Is there a workaround or so?
UPD1 - Spreadsheet.getRangeByName() does not work
Getting range by name does not help, the data validation will get actual range address.
SpreadsheetApp.getActive().getRangeByName("Apples")
UPD2 No way to make it so far in GAS
As #TheMaster posted, it's not possible at this moment.
Please set +1 for posts:
https://issuetracker.google.com/issues/143913035
https://issuetracker.google.com/issues/203557342
P.S. It looks like the only solution will work is Google Sheets API.
I thought that in your situation, I thought that when Sheets API is used, your goal might be able to be used.
Workaround 1:
This workaround uses Sheets API.
Usage:
1. Prepare a Google Spreadsheet.
Please create a new Google Spreadsheet.
From Example. I have a named range Apples (address "Sheet10!B2:B"), which in use for data validation for plenty of sheet cells. The data range for Apples can be changed (in a script), e.g. to "Sheet10!D2:D"., please insert a sheet of "Sheet10" and put sample values to the cells "B2:B" and "D2:D".
Please set the named range Sheet10!B2:B as Apple.
2. Sample script.
Please copy and paste the following script to the script editor of Spreadsheet and save the script. And, please enable Sheets API at Advanced Google services.
function myFunction() {
const namedRangeName = "Apple"; // Please set the name of the named range.
const ss = SpreadsheetApp.getActiveSpreadsheet();
const sheet = ss.getSheetByName("Sheet10");
const requests = [{ updateCells: { range: { sheetId: sheet.getSheetId(), startRowIndex: 0, endRowIndex: 1, startColumnIndex: 0, endColumnIndex: 1 }, rows: [{ values: [{ dataValidation: { condition: { values: [{ userEnteredValue: "=" + namedRangeName }], type: "ONE_OF_RANGE" }, showCustomUi: true } }] }], fields: "dataValidation" } }];
Sheets.Spreadsheets.batchUpdate({ requests }, ss.getId());
}
In this request, the name of the named range is directly put to userEnteredValue.
3. Testing.
When this script is run to the above sample Spreadsheet, the following result is obtained.
When this demonstration is seen, first, you can see the named range of "Apple" which has the cells "B1:B1000". When a script is run, data validation is put to the cell "A1" with the named range of "Apple". In this case, the values of data validation indicate "B1:B1000". When the range named range "Apple" is changed from "B1:B1000" to "D1:D1000" and the data validation of "A1" is confirmed, it is found that the values are changed from "B1:B1000" to "D1:D1000".
Workaround 2:
This workaround uses the Google Spreadsheet service (SpreadsheetApp). In the current stage, it seems that the Google Spreadsheet service (SpreadsheetApp) cannot directly achieve your goal. This has already been mentioned in the discussions in the comment and TheMaster's answer. When you want to achieve this, how about checking whether the range of the named range is changed using OnChange as following workaround 2?
Usage:
1. Prepare a Google Spreadsheet.
Please create a new Google Spreadsheet.
From Example. I have a named range Apples (address "Sheet10!B2:B"), which in use for data validation for plenty of sheet cells. The data range for Apples can be changed (in a script), e.g. to "Sheet10!D2:D"., please insert a sheet of "Sheet10" and put sample values to the cells "B2:B" and "D2:D".
Please set the named range Sheet10!B2:B as Apple.
2. Sample script.
Please copy and paste the following script to the script editor of Spreadsheet and save the script. And, please install OnChange trigger to the function onChange.
First, please run createDataValidation. By this, data validation is put to the cell "A1" of "Sheet10". In this case, the set range is the range retrieved from the named range "Apple". So, in this case, the range is Sheet10!B2:B1000.
As the next step, please change the range of the named range from Sheet10!B2:B1000 to Sheet10!D2:D1000. By this, onChange` function is automatically run by the installed OnChange trigger. By this, the data validation of "A2" is updated. By this, the values of data validation are changed.
const namedRangeName = "Apple"; // Please set the name of the named range.
const datavalidationCell = "Sheet10!A2"; // As a sample. data validation is put to this cell.
function onChange(e) {
if (e.changeType != "OTHER") return;
const range = e.source.getRangeByName(namedRangeName);
const a1Notation = `'${range.getSheet().getSheetName()}'!${range.getA1Notation()}`;
const prop = PropertiesService.getScriptProperties();
const previousRange = prop.getProperty("previousRange");
if (previousRange != a1Notation) {
const rule = SpreadsheetApp.newDataValidation().requireValueInRange(e.source.getRangeByName(namedRangeName)).setAllowInvalid(false).build();
e.source.getRange(datavalidationCell).setDataValidation(rule);
}
prop.setProperty("previousRange", a1Notation);
}
// First, please run this function.
function createDataValidation() {
const ss = SpreadsheetApp.getActiveSpreadsheet();
const rule = SpreadsheetApp.newDataValidation().requireValueInRange(ss.getRangeByName(namedRangeName)).setAllowInvalid(false).build();
ss.getRange(datavalidationCell).setDataValidation(rule);
const prop = PropertiesService.getScriptProperties();
const range = ss.getRangeByName(namedRangeName);
const a1Notation = `'${range.getSheet().getSheetName()}'!${range.getA1Notation()}`;
prop.setProperty("previousRange", a1Notation);
}
References:
Method: spreadsheets.batchUpdate
UpdateCellsRequest
DataValidationRule
Currently, This seems to be impossible. This is however a known issue. +1 this feature request, if you want this implemented.
https://issuetracker.google.com/issues/143913035
Workarounds from the tracker issue creator:
If a validation rule is manually created with a NamedRange via the Sheets GUI, it can then be copied programmatically using Range.getDataValidations(), and subsequently used to programmatically create new DataValidations. DataValidations created this way maintain their connection to the NamedRange, and behave like their manually created counterparts. This demonstrates that the functionality to 'use' NamedRanges for data validation rules is already possible with Apps Scripts, but not the option to 'create' them.
As a half-answer, if you want just validation and can live without the drop-down list of valid values, you can programmatically set a custom formula that references the named range. This reference to the named range will not get expanded in the AppsScript, so future changes to the Named Range's actual range will percolate to the validator. Like so:
mySheet.getRange('F5')
.setDataValidation(
SpreadsheetApp.newDataValidation()
.requireFormulaSatisfied(
'=EQ(F5, VLOOKUP(F5, ' + namedRange.getName() + ', 1))'
)
.setAllowInvalid(false)
.build()
);
(The formula just checks that the value in the cell being tested is equal to what VLOOKUP finds for that cell, in the first column -- I'm assuming the named range content is sorted.)
Use getRangeByName()
function lfunko() {
const ss = SpreadsheetApp.getActive();
const sh = ss.getSheetByName("Sheet0");
var cell = sh.getRange(1, 10);//location where datavalidation is applied
var rule = SpreadsheetApp.newDataValidation().requireValueInRange(ss.getRangeByName("MyList")).build();
cell.setDataValidation(rule);
}

Need to make simple read and update webapp using appscript

I have a google sheet like this
I Need to make a google appscript webapp like below with the above sheet.
only the price field need to be editable.
When it is edited and save is clicked, it should be updated in the sheet.
How i can do this in a easy way?
PS:I know html and css part well. I just need the script part.
Thanks in Advance
A script to get your data
function lfunko() {
const ss = SpreadsheetApp.getActive();
const sh = ss.getSheetByName("Sheet Name");
const [h,...vs] = sh.getDataRange().getValues();
Logger.log(JSON.stringify(vs));//view 2d array that stores your data
return vs
}
You can call it in a window.onload using google.script run or use templated html to load the data server side prior to rendering.
There are many examples of this on SO.
I think the easiest approach is using Templated HTML. It allows you to easily use your Google Apps Script functions inside your HTML, like ejs or Jinja2.
Code.gs
/* Sheet where the data lives */
const sS = SpreadsheetApp.openById('<SS_ID>').getSheetByName('StoreData')
/* Function to extract the data */
const getStoreData = () => sS.getDataRange().getValues().slice(1)
/* HTTP/GET function (For using template HTML you need to evaluate your file) */
const doGet = () => HtmlService.createTemplateFromFile('Index').evaluate()
And from using the getStoreData inside the HTML use the <? ?>, syntax.
Index.html
<div class="store-container">
<? getStoreData().forEach((item) => { ?>
<div>
<img src="<?= item[1] ?>">
<span>Price: <?= item[2] ?></span>
<span>Id: <?= item[0] ?> </span>
</div>
<? }) ?>
</div>
After this, you only need to style it as you need.
Documentation:
SpreadsheetApp
openById
getDataRange()
getValues()
I believe your goal is as follows.
You want to retrieve the values from the Spreadsheet and create the situation you showing with Web Apps.
When the value of the price is changed on Web Apps and click the save button, you want to update the column "C" of the Spreadsheet.
In this case, how about the following sample script?
Usage:
1. Sample script:
Google Apps Script side: Code.gs
Please copy and paste the following script to the script editor of Google Spreadsheet as a script. And, please set the sheet name.
const sheetName = "Sheet1"; // Please set the sheet name.
function doGet() {
const split = 4; // When this value is changed, the number of columns can be changed.
const sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName(sheetName);
const values = sheet.getRange("A2:C" + sheet.getLastRow()).getValues();
let res = [];
do {
const temp = values.splice(0, split).reduce((s, [a, b, c]) =>
s += `<td><img src="${b}"><br>${a}<br><input type="number" name="saveValue" value="${c}"><input type="button" value="save" onclick="save();"></td>`
, "");
res.push(`<tr>${temp}</tr>`);
} while (values.length > 0);
const html = HtmlService.createTemplateFromFile("index");
html.table = res.join("");
return html.evaluate();
}
function saveValues(e) {
const sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName(sheetName);
sheet.getRange(2, 3, e.length, 1).setValues(e);
}
HTML & Javascript side: index.html
Please copy and paste the following script to the script editor of Google Spreadsheet as a HTML.
<table><?!= table ?></table>
<script>
function save() {
const obj = document.getElementsByName("saveValue");
const values = [...obj].map(e => [e.value]);
google.script.run.saveValues(values);
}
</script>
2. Deploy Web Apps.
The detailed information can be seen at the official document. In this case, it supposes that you are using new IDE. Please be careful this.
On the script editor, at the top right of the script editor, please click "click Deploy" -> "New deployment".
Please click "Select type" -> "Web App".
Please input the information about the Web App in the fields under "Deployment configuration".
Please select "Me" for "Execute as".
Please select "Anyone" for "Who has access".
For testing this script, I thought that this setting might be suitable.
Please click "Deploy" button.
Copy the URL of the Web App. It's like https://script.google.com/macros/s/###/exec.
When you modified the Google Apps Script, please modify the deployment as a new version. By this, the modified script is reflected in Web Apps. Please be careful this.
You can see the detail of this in the report of "Redeploying Web Apps without Changing URL of Web Apps for new IDE".
3. Testing.
Please access to the Web Apps using the browser. By this, the following result is obtained.
Sample Spreadsheet:
Sample Web Apps:
For example, when you change the price from 100 to 101 and click the save button, the column "C" of Spreadsheet is updated.
Note:
This is a simple sample script for answering your question. So, please modify this for your actual situation.
References:
Web Apps
HtmlFormApp
Taking advantage of Web Apps with Google Apps Script

Automate Costs in Google Sheets from Google Form Checkbox Input

I'm trying to use Google Forms to collect some information on my clients. The application here is a clinical setting, so the form asks what treatments they would like in the form of a checkbox option. I would then like to automate the sum total of the treatment costs using the forms output.
The issue I'm having is that Google Forms outputs a list of strings in a single cell for this response. I'll add more detail below, but I don't know how to split the string into individual values, lookup that value in a separate column, get the cost, and display only the sum in a separate cell.
I've made a minimal working example in the form of a GSheet, you can find it here.
In that master sheet, you'll find three other sheets; Costs, Form Responses, and Overview.
The Costs sheet is static and only contains a list of items and their costs. This sheet will change on occasion (price updates, removal/addition of items)
The Form Responses sheet will contain the raw output from a Google Form. The column of note here is the Choose Things from the List column, which contains a list of responses.
The Overview sheet will house some redundant info, but it's meant to be a cleaned-up sheet with information. You'll notice a Cost of Things ($) column. In this column, I would like the total sum of all items the response list from the Form Responses sheet.
I can do this in Python easy. I would do it something like this:
costs = {'a': 1, 'b': 2, 'c': 3}
input_items = ['a', 'b', 'c']
x = []
for item in input_items:
x.append(costs[item])
total_sum = sum(x)
How would I do this with Google Sheets? I want to
split a list embedded in a cell
check each list item for its cost in a separate sheet
sum the costs of each item
Please let me know if I need to clarify, I'm not quite sure how to pose the problem using Google Sheets language.
==============
EDIT: Sorry, I just updated the GSheet permissions. It should be viewable to everyone now.
In your situation, how about the following sample script?
Sample script:
function myFunction() {
const ss = SpreadsheetApp.getActiveSpreadsheet();
const sheet1 = ss.getSheetByName("Costs");
const sheet2 = ss.getSheetByName("Forms Responses");
const sheet3 = ss.getSheetByName("Overview");
// 1. Retrieve values from "Costs" sheet and create an object.
const values1 = sheet1.getRange("A2:B" + sheet1.getLastRow()).getValues();
const obj1 = values1.reduce((o, [a, b]) => (o[a.trim()] = b, o), {});
// 2. Retrieve values from "Forms Responses" sheet and create an object.
const values2 = sheet2.getRange("A2:C" + sheet2.getLastRow()).getValues();
const obj2 = values2.reduce((o, [a, b, c]) => (o[a.trim() + b.trim()] = c.split(",").reduce((n, e) => (n += obj1[e.trim()] || 0, n), 0), o), {});
// 3. Retrieve values from "Overview" sheet and create an array.
const values3 = sheet3.getRange("A2:B" + sheet3.getLastRow()).getValues();
const res = values3.map(([a, b]) => [obj2[a.trim() + b.trim()] || null]);
// 4. Put array to "Overview" sheet.
sheet3.getRange(2, 3, res.length, 1).setValues(res);
}
In this sample script, the following flow is run.
Retrieve values from "Costs" sheet and create an object.
Retrieve values from "Forms Responses" sheet and create an object.
Retrieve values from "Overview" sheet and create an array.
Put array to "Overview" sheet.
In this case, the result values are put to the column "C".
Note:
This sample script is for your sample Spreadsheet. So, when you change the Spreadsheet, the script might not be able to be used. Please be careful about this.
References:
getValues()
setValues(values)
reduce()
map()

Print multiple data per ID in Google Sheet

In this spreadsheet, I have a sheet called Form Responses 1 that contains the students' data and a sheet called TEMPLATE that contains the template that needs to be printed per student. The problem is that I can only print the spreadsheet one at a time by changing the data in the C2 cell. Is there any way for me to print it quickly and easily to save time and effort?
In Google Cloud Print, I want it to say Page 1 = Student 1, Page 2 = Student 2, Page 3 = Student 3, and so on. Is it possible?
In your situation, how about creating a new Google Spreadsheet including each page? When this is reflected in a sample script, it becomes as follows.
Sample script:
function myFunction() {
const srcSs = SpreadsheetApp.getActiveSpreadsheet();
const sheet = srcSs.getSheetByName("TEMPLATE");
const values = sheet.getRange("C2").getDataValidation().getCriteriaValues()[0].getValues().flat().filter(String);
const dstSs = SpreadsheetApp.create("tempSpreadsheet");
values.forEach(v => {
sheet.getRange("C2").setValue(v);
SpreadsheetApp.flush();
const tempSheet = sheet.copyTo(srcSs);
const range = tempSheet.getDataRange();
range.copyTo(range, {contentsOnly: true});
tempSheet.getRange("B2:2").clear().clearDataValidations();
tempSheet.getDrawings().forEach(e => e.remove());
tempSheet.copyTo(dstSs);
srcSs.deleteSheet(tempSheet);
});
dstSs.deleteSheet(dstSs.getSheets()[0]);
}
When this script is run, a new Google Spreadsheet of tempSpreadsheet is created to the root folder. When you see it, each tab has each sheet without using the formulas. By this, you can print out the Spreadsheet by one manual process.
Note:
When I saw your sample Spreadsheet, /** #OnlyCurrentDoc */ is used in your script. When you use my proposed script, please remove /** #OnlyCurrentDoc */. By this, the scope of https://www.googleapis.com/auth/spreadsheets is automatically detected and used it. Please be careful about this.
This sample script is for your sample Spreadsheet. So when the spreadsheet is changed, this script might not be able to be used. Please be careful about this.
References:
create(name)
copyTo(spreadsheet) of Class Sheet
copyTo(destination) Class Range

Google Spreadsheet Query using Dynamically defined Sheets

i was strugling with something unusual or not...
I am doing an google spreadsheet, that will have a lot of sheets, and their names are dates as MM/YYYY, and i need to get all the data from a range A1:B100 for example, and do QUERY stuff on an master sheet with that data, the problem is, i have done a function with javascript to get the Range of that sheets, but i can´t use them on =QUERY() function, tryed a lot, with different aproaches from internet, but nothing successfull yet..
My sample spreadsheet:
https://docs.google.com/spreadsheets/d/1W5W7y16yvOUoqNZCZ59ol8D2nbelJHNqtgaxrtb1jC8/edit?usp=sharing
Also the app script i have done to manage the sheets data dynamically:
let sheetsData = () => SpreadsheetApp.getActiveSpreadsheet().getSheets().filter(sheet => sheet.getName().match(/^[\d]/)).map(sheet => sheet.getRange("A1:B20"));
Logger.log( sheetsData() );
My sample usage on the spreadsheet is =QUERY(sheetsData();"SELECT *"), i can´t even list the data... lul
Any help will be most welcome, tnx.
Instead of returning the Range objects, you need to return the values contained within the ranges. You also have to make sure that the data you return is structured as a two-dimensional array.
function GET_DATA() {
const dataSheets = SpreadsheetApp.getActiveSpreadsheet()
.getSheets()
.filter(s => s.getName().match(/^[\d]/));
const ranges = dataSheets
.map(s => s.getRange("A1:B20"));
return ranges
.reduce((result, range) => result.concat(range.getValues()), []);
}
The above implementation works with a formula such as =QUERY(GET_DATA(), "SELECT *").
However, there's a fundamental problem with this approach. Since the range location is hardcoded on the Apps Script side and not specified as part of the formula, recalculations are not triggered as data in the sheets change.