remove selected items from google form dropdown list - google-apps-script

I have been trying to get this to work for a couple of days now and I give up.
I want to create a Google form with a drop down list populated form a spreadsheet. I don't what anyone to choose the same as any one else. (like in a potluck situation)
example:
I am giving away :
a comb
a brush
a bowl full of mush
I tell Thomas, Richard and Henry that they can each have one and send them a link to a Google form I created. Tom is quick and opens the form 1st. He enters his name and chooses a comb out of a three item drop down list. Dick opens the form link and in the same drop down question he chooses out of the two remaining items. He chooses the brush. Harry is a bit of a slow poke, so when he gets home he opens my link, but alas, he can only have a bowl full of mush.
How can I get this done?
Based on my research so far I will be needing to use the if function on the responses spread sheet to see if there has been a take for an item (see if the cell is vacant) and maybe VLOOKUP, but I can't get a clear picture of how to make it all work together.
Thank you,
Good night
EDIT:
Based on gssi's answer, I wanted to post the code and describe the way I did it.
function updateListChoices(item){
var inventory = (SpreadsheetApp.openById(theIdOfTheResponceSpreadsheet)
.getSheetByName("inventory")
.getDataRange()
.getValues());
var selected = (SpreadsheetApp.openById("0Al-3LXunCqgodHB5RGNpR0RyQ0pERmVnek1JeUJKS0E")
.getSheetByName("responses")
.getDataRange()
.getValues());
var choices = [];
var selectedReal = [];
for (var i = 0; i< selected.length; i+=1){
selectedReal.push(selected[i][2]) }
for (var i = 1; i< inventory.length; i+=1){
if(selectedReal.indexOf(inventory[i][0])=== -1){
choices.push(item.createChoice(inventory[i][0]));}
}
item.setChoices(choices);
}
var LIST_DATA = [{title:"the title of the question", sheet:"inventory"}]
function updateLists() {
var form = FormApp.getActiveForm();
var items = form.getItems();
for (var i = 0; i < items.length; i += 1){
for (var j = 0; j < LIST_DATA.length; j+=1) {
var item = items[i]
if (item.getIndex() === 1){
updateListChoices(item.asListItem(), "inventory");
break;
}
}
}
}
In the building of the form, click the tools menu, then click script editor. Copy the code from here (with changes to fit your needs) to the script editor and hit save. Click the Resources menu and hit the project triggers (the 1st option). Click Add trigger. Choose updateLists from form do this once with when sending and once when opening (you should end up with 2 lines.)
It isn't very elegant, but this is what I am capable of. Good Luck.

I tried to accomplish exactly the same (list with products to select from), but I couldn't make it work with your final code example. Here's mine, with detailed instructions. Just for anybody who is landing on this page and is looking for a working code.
(Menu names might differ from yours, because I'm using a non-English Google Forms, and I'm just guessing the translations here.)
1) Create a new form, and create a new radio button based question (multiple choice) (In this example, I use the question name: "Select a product"). Don't add any options to it. Save it.
2) Open the spreadsheet where the responses are going to be stored, and add a new sheet in it (name: "inventory")
3) Fix the first row (A) of the inventory sheet, and put in A1: "Select a product"
4) Put in column A all the products you want to appear in the form
5) Open the form editor again, and go to tools > script editor
6) Paste this code in the editor, put in your form and spreadsheet ID's (3x) and save it.
var LIST_DATA = [{title:"Select a product", sheet:"inventory"}];
function updateLists() {
//var form = FormApp.getActiveForm();
var form = FormApp.openById("paste_ID_of_your_FORM_here");
var items = form.getItems();
for (var i = 0; i < items.length; i += 1){
for (var j = 0; j < LIST_DATA.length; j+=1) {
var item = items[i];
if (item.getTitle() === LIST_DATA[0].title){
updateListChoices(item.asMultipleChoiceItem(), LIST_DATA[0].sheet);
break;
}
}
}
}
function updateListChoices(item, sheetName){
var inventory = (SpreadsheetApp.openById("paste_ID_of_your_RESPONSE_SHEET_here")
.getSheetByName("inventory")
.getDataRange()
.getValues());
var selected = (SpreadsheetApp.openById("paste_ID_of_your_RESPONSE_SHEET_here")
.getSheetByName("responses")
.getDataRange()
.getValues());
var choices = [];
var selectedReal = [];
for (var i = 0; i< selected.length; i+=1){
selectedReal.push(selected[i][1])
}
for (var i = 1; i< inventory.length; i+=1){
if(selectedReal.indexOf(inventory[i][0])=== -1){
choices.push(item.createChoice(inventory[i][0]));}
}
if (choices.length < 1) {
var form = FormApp.getActiveForm();
form.setAcceptingResponses(false);
} else {
item.setChoices(choices);
}
}
7) With the code editor open, go to Resources > Create triggers and create these two triggers. They need to appear in this order:
updateLists - from form - sending
updateLists - from form - opening
Now you're good to go. If you open the form editor, the products added in the inventory sheet will appear as options.
Every time a product is chosen, it will disappear from the form. To reset all the chosen products, go to the form editor, and choose Responses > Remove all responses. You might need to remove all responses from the responses sheet manually as well (Don't know why, but that happened to me). After that, you need to manually run the updateLists script in the code editor.

Here's a 'How-To' that describes how to construct the spreadsheet.
Column A : named 'Inventory', contains the names of the items initially available.
Column B : named 'Indices', in cell B1 contains the formula =if(isnumber(match(Inventory,Selected,0)),"",if(row(B1)=1,1,max(offset(Indices,0,0,row(B1)-1,1))+1)). Copy the formula in B1 into all cells below it in column B.
Column C : named 'Selected', contains the names of items currently selected starting in row 1 and continuing down contiguously.
Column D : named 'Available', cell D1 contains the formula =if(isnumber(match(row(D1),Indices,0)),index(Inventory,match(row(D1),Indices,0),1),"") which is then copied into all cells below it in column D.
The Available column will always contain a contiguous list of the 'as-yet-unselected' inventory items.

Related

Google Apps Script adds extra blank questions to Form Responses sheet when importing from Google Sheets

I am using Google Apps Script to add questions from a Google Sheets into a Google Form. The lists are read from two separate arrays and added as individual Grid type questions. I also set the Sheet as the destination for the Form results.
When I run the script, the Form gets updated perfectly. If there are 10 items to be added, 10 questions are added to the Form. However, in the Form Responses sheet that is linked, there are often times additional columns titled " [Row 1]" that are added. The number of additional columns and their position change what seems like every other time I run the script. I haven't been able to pick up on any patterns.
I do know that "Row 1" appears as the default first item in a Grid type question when creating the question in the Form's UI. I'm not sure if that has anything to do with it. FYI - grid items allow multiple rows of questions to be added but I am only adding one question. I know there is a multiple choice grid type but I do not like the formatting of it.
As a workaround, I've created a script to delete all of these additional columns but I would really like to figure out what is actually happening.
Any ideas on what is happening?
Here is part of my code:
function editForm()
{
var setupSheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName('Setup'); //Create variable for the Setup sheet
var metricSheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName('Final Metric Statements'); //Create variable for the Metric Statements sheet
var form = FormApp.openById(setupSheet.getRange("D1").getValue()); //Open form using the form ID from the setup sheet
var pageFour = form.addPageBreakItem().setTitle('Questions');
var emotLen = 0; //Counter for sub-emotion items
var featLen = 0; //Counter for sub-feature items
//Count number of items for first list
while (((metricSheet.getRange("C"+ (emotLen+2)).getValue()) != "") && ((metricSheet.getRange("C"+ (emotLen+2)).getValue()) != "#N/A")) //Go through sub-emotion column until a blank cell
{
emotLen++; //Increase the counter
}
//Count number of items for second list
while (((metricSheet.getRange("D"+ (featLen+2)).getValue()) != "") && ((metricSheet.getRange("D"+ (featLen+2)).getValue()) != "#N/A")) //Go through sub-feature column until a blank cell
{
featLen++; //Increase the counter
}
if (emotLen > 0)
{
var emotRng = metricSheet.getRange(2,3,emotLen); //Create a range for sub-emotions based on number of items and column
var emotArray = emotRng.getValues(); //Copy the items into an array
for (i=0; i <= (emotLen-1); i++)
{
var emotItem = form.addGridItem();
emotItem.setRows(emotArray[i]);
emotItem.setColumns(['Strongly Disagree', 'Disagree','Slightly Disagree', 'Neither Disagree or Agree', 'Slightly Agree', 'Agree', 'Strongly Agree', 'N/A']);//Add column header
emotItem.setRequired(true);
}
}
if (featLen > 0 )
{
var featRng = metricSheet.getRange(2,4,featLen); //Create a range for features based on number of items
var featArray = featRng.getValues(); //Copy the items into an array
for (i = 0; i <=(featLen-1); i++) //Go through all items in the array
{
var featItem = form.addGridItem();//Add item to survey
featItem.setRows(featArray[i]); //Add row item
featItem.setColumns(['Strongly Disagree', 'Disagree','Slightly Disagree', 'Neither Disagree or Agree', 'Slightly Agree', 'Agree', 'Strongly Agree', 'N/A']);//Add column header
featItem.setRequired(true);
}
}
//deleteRowColumns();
//Logger.log(emotLen, emotArray, featLen, featArray); //Logger used for debugging
}
And here is a screenshot of the Form Responses sheet with the additional column:
This appears to be a bug!
I have taken the liberty of reporting this on Google's Issue Tracker for you, detailing the behaviour:
Adding GridItems to a form, that has a Sheet attached to it, from an Apps Script function, intermittently adds question columns that do not exist.
You can hit the ☆ next to the issue number in the top left on the page which lets Google know more people are encountering this and so it is more likely to be seen to faster.

Copying Notes from one sheet to another. Importrange?

OK, still rookie coder but getting better. Able to modify codes but not write most of them myself yet. Here is my current problem.
I run a small business and we use google sheets as our CRM (its faster and easier for us to do it this way) I have a master sheet that I bring in with =importrange everyone else jobs. It works perfect, makes my life real easy but there is one thing I can not get to come over. That is the notes stored in a cell. I have to actually open their sheet to view the notes. So I am trying to get a script that would update the notes down the importedrange. Then each day when I go over their info I push a button and it would write the notes from the other persons sheet onto my sheet, can just write over the last note and replace it.
I made an example with 3 sheets (all made editable for everyone) since I can't post our actual business sheets to work with. I should be able to modify it and transfer to my actual sheets after some help.
(Master Sheet) https://docs.google.com/spreadsheets/d/1TMNyohd5Vtn3p9cpLebmZASt2TzbWL80fIesCu89-ig/edit?usp=sharing
(Employee 1)
https://docs.google.com/spreadsheets/d/1n4iFXGuC7yG1XC-UIbuT9VrQ7rJWngPkDCv0vsvDed4/edit?usp=sharing
(Employee 2)
https://docs.google.com/spreadsheets/d/1EJVa5TgF6UkLhiLtfQ6o7BzpdzXDGcVhkibLCYwlfAU/edit?usp=sharing
Links are provided to each other sheet on the top of the master sheet. This is above my head so I won't try to butcher the code below lol. Here is a function I found but don't know how to implement it to use with import range.
function getNotes(rangeAddress) {
// returns notes inserted with Insert > Note from one cell or a range of cells
// usage:
// =getNotes("A1"; GoogleClock())
// =getNotes("A1:B5"; GoogleClock())
// see /docs/forum/AAAABuH1jm0xgeLRpFPzqc/discussion
var ss = SpreadsheetApp.getActiveSpreadsheet();
var range = ss.getRangeByName(rangeAddress);
if (!range) return '#N/A: invalid range. Usage: =getNotes("A1:B5"; GoogleClock())';
var notesArray = new Array();
for (var i = 0; i < range.getHeight(); i++) {
notesArray[i] = new Array();
for (var j = 0; j < range.getWidth(); j++) {
notesArray[i][j] = range.getCell(i + 1, j + 1).getComment();
}
}
return notesArray;
}
So The code I would want it to read the note from "Employee 1" sheet and write it onto the "Master" sheet in the correct cell. Since it is an =importrange the orientation of the cells will always be the same on both sheets, just needs to pick the starting cell and go down the list. I want to make it work with the button I put on the top of the master sheet on each tab.
This script uses outdated methods. For example, GoogleClock() is gone.
Please see my other response to your similar question for a possible solution.

Conditional formatting - Google spreadsheets

I'm using the current Google Spreadsheets. NOT the "new" version of Sheets that's in Beta at the moment. I want to do 2 things:
1 - Receive an email reminder when a certain date is reached in a cell. There is a script for this in the Scripts Gallery called "Add Reminder" but it seems to have problems (you can't set a different reminder for each tab in a spreadsheet and this is crucial functionality)
2 - Change the color of an 'entire row' automatically triggered by a cell's content. I already used the conditional formatting to try to do this, but it can only change the color of the single cell, not the entire row
I don't know how to write scripts and I know there are lots of scripts people have posted online that address these issues, but I'd looking for reliable and fast. Hoping that someone with experience can guide me to the right source for this.
Thanks!
Scott
you can set a different reminder for each tab by just repeating the code...
function remindMe() {
var sheet = SpreadsheetApp.getActiveSpreadsheet().getSheets()[0];
var data = sheet.getDataRange().getValues();
for(var i = 1; i < data.length; i++){
if(data[i][2] > new Date()){
MailApp.sendEmail(message);
}
}
// Now for tab index [1]
var sheet = SpreadsheetApp.getActiveSpreadsheet().getSheets()[1];
var data = sheet.getDataRange().getValues();
for(var i = 1; i < data.length; i++){
if(data[i][2] > new Date()){
MailApp.sendEmail(message);
}
}
}
To set an entire row color in the old Sheets use some thing like...
sheet.getRange('1:1').setBackground('yellow');

handler design for dynamically created listboxes

I'm trying to make a UI that allow users to select an action for each agenda item in a spreadsheet. After a user select an action, I would like to update the spreadsheet with the selection. Since the data in the spreadsheet are not static, the UI was written dynamically.
This is how I created the listboxes :
// add labels and a drop down box of actions for each agenda item mark for today
for (i = 0; i < labels.length; i++) {
// labels is an array of objects
var topic = labels[i]['topic'];
// add label to grid
myGrid.setWidget(i, 0, myApp.createLabel(topic));
// the id of each listbox is the content of its corresponding label
var id = ObjApp.camelString(topic)
var lboxActions = myApp.createListBox().setId(id);
//add items to listbox
lboxActions.addItem('Select');
lboxActions.addItem('Add to agenda');
lboxActions.addItem('Move to another meeting');
lboxActions.addItem('Move to a special meetin');
lboxActions.addItem('Move to email');
//add drop down list to grid
myGrid.setWidget(i, 1, lboxActions);
}
I have 3 questions:
1) Which is a better design?
a) Design 1: a save button next to each listbox.
b) Design 2: one submit button at the bottom to save every entry
2) How would I collect information on what the user select? How would I write such handlers? I added the following code for each design but I don't think I'm doing it right.
a) Design 1: the following lines of code were added to the for loop described above
var buttonSave = myApp.createButton('Save');
myGrid.setWidget(i, 2, buttonSave);
var handlerSelection = myApp.createServerHandler('selectAction');
handlerSelection.addCallbackElement(mainPanel);
buttonSave.addClickHandler(handlerSelection);
b) Design 2: the following lines of code were added outside the for loop
//update spreadsheet when user click "submit"
var handlerUpdate = myApp.createServerHandler('responseToSubmit');
handlerUpdate.addCallbackElement(mainPanel);
buttonSubmit.addClickHandler(handlerUpdate);
mainPanel.add(myGrid);
mainPanel.add(buttonSubmit);
myApp.add(mainPanel);
3) How do I write functions for the handlers? These are not correct because I wasn't able to extract the information from the list boxes.
a) Design 1
function responseToSave(e) {
var name = e.parameter.source;
var selection = e.parameter.name;
var selectionObj = new Object();
selectionObj['status'] = selection;
selectionObj['name'] = name;
choicesMade.push(selectionObj)
Logger.log(choicesMade);
return choicesMade;
}
b) Design 2
function responseToSubmit(e) {
var myApp = UiApp.getActiveApplication();
for (i=0; i < labels.length; i++) {
var lboxId = ObjApp.camelString(labels[i]['topic']);
//[EDIT] e.parameter.lboxId would not work because lboxId is a string
var selection = e.parameter[lboxId];
choicesMade[labels[i]] = selection;
}
Logger.log(choicesMade);
return choicesMade;
}
Thanks
Q1:
The design purely depends on what your application is intended to do and how your users use it. There is no 'better' design - each of them has its own pros and cons and the choice would be based on how your app is used.
However, do also consider Design 3 which is saving the changes when the dropdown box is changed. It will be one click less for the user
Q2 and Q3:
You should use the setName on the listBox
var lboxActions = myApp.createListBox().setId(id).setName(id);
I generally use the same string for the id and the name to avoid confusion, but you should use setName
After that you can access the item selected in the handler function as
e.parameter.id
Finally, on buttonSave, you should use the addClickHandler instead of addChangeHandler.

A script for google spreadsheet to provide multiple hyperlink choice for one cell

I have a google spreadsheet. In some cells, it has multiple names(strings) that I would like to associate with individual hyperlinks.
E.g. if I have a cell such as "Charles Darwin", it's easy for me to create a hyperlink out of this name by doing something like
=Hyperlink(VLOOKUP("Charles Darwin", People!$A$1:$B$738, 2, false), "Charles Darwin")
(note that I have a "People" sheet from which I grab the hyperlink)
But if I happen to have multiple entries in that cell, say ";" or newline separated, e.g., "Charles Darwin; George Washington", I can't do that. I'd like to give the user an ability to click on the cell, have the contents of the cell be sent (as argument) to some kind of script, and for that script to find the hyperlinks in my "People" sheet for those strings, and then to present the user with a little "pop-up" right next to that cell, where the desired hyperlink could be clicked on.
I tried to find something along those lines on this site, but nothing similar seemed to come up. Might someone have a link or two for me (or basic example code) that I could start with to try to solve this? (I am assuming this is possible).
It's not possible to have two hyperlinks on the same cell.
It is possible to write scripts to Google Spreadsheets, but I'm not sure it's going to suit your use case well. The solution I see would be like this:
The user click on the desired cell, selecting it.
Then he clicks on a custom menu and picks an entry there, e.g. show links
A popup will show up (not besides the cell, but centered on the screen) with the links.
Do you think this is fine? The code would look like this (open the menu Tools > Script Editor)
function onOpen() {
SpreadsheetApp.getActive().
addMenu("Test", [{name: 'Show Links', functionName:'showLinks'}]);
}
function showLinks() {
var values = SpreadsheetApp.getActiveRange().getValue().split(';');
var app = UiApp.createApplication().setTitle('Links');
var grid = app.createGrid(values.length, 2);
for( var i = 0; i < values.length; ++i ) {
var url = findLink(values[i]);
grid.setWidget(
i, 0, app.createLabel(values[i])).setWidget(
i, 1, url ? app.createAnchor(url, url) : app.createLabel('Not Found'));
}
app.add(grid);
SpreadsheetApp.getActive().show(app);
}
var mapName2Url = null;
function findLink(name) {
if( mapName2Url == null ) { //lazy load
mapName2Url = {};
var data = SpreadsheetApp.getActive().getSheetByName('People').getDataRange().getValues();
for( var i = 1; i < data.length; ++i ) //skipping the header
mapName2Url[data[i][0]] = data[i][1];
}
return mapName2Url[name];
}
After you paste it on the script editor, run the onOpen function twice to authorize it and have the menu created for you. Next time you open the spreadsheet, the menu should be created automatically.
By the way, I have not tested this code, so it might contain dumb mistakes.