2

Update #2: Okay, I'm pretty sure my error in update #1 was because of indexing out of bounds over the array (I'm still not used to JS indexing at 0). But here is the new problem... if I write out the different combinations of the loop manually, setting the page index to 1 in moveItem() like so:

newForm.moveItem(itemsArray[0][0], 1);
newForm.moveItem(itemsArray[0][1], 1);
newForm.moveItem(itemsArray[0][2], 1);
newForm.moveItem(itemsArray[1][0], 1);
newForm.moveItem(itemsArray[1][1], 1);
newForm.moveItem(itemsArray[1][2], 1);
newForm.moveItem(itemsArray[2][0], 1);
...

...I don't get any errors but the items end up on different pages! What is going on?


Update #1:: Using Sandy Good's answer as well as a script I found at this WordPress blog, I have managed to get closer to what I needed. I believe Sandy Good misinterpreted what I wanted to do because I wasn't specific enough in my question.

I would like to:

  1. Get all items from a page (section header, images, question etc)
  2. Put them into an array
  3. Do this for all pages, adding these arrays to an array (i.e: [[all items from page 1][all items from page 2][all items from page 3]...])
  4. Shuffle the elements of this array
  5. Repopulate a new form with each element of this array. In this way, page order will be randomized.

My JavaScript skills are poor (this is the first time I've used it). There is a step that produces null entries and I don't know why... I had to remove them manually. I am not able to complete step 5 as I get the following error:

Cannot convert Item,Item,Item to (class).

"Item,Item,Item" is the array element containing all the items from a particular page. So it seems that I can't add three items to a page at a time? Or is something else going on here?

Here is my code:

function shuffleForms() {
  var itemsArray,shuffleQuestionsInNewForm,fncGetQuestionID,
      newFormFile,newForm,newID,shuffle, sections;

  // Copy template form by ID, set a new name
  newFormFile = DriveApp.getFileById('1prfcl-RhaD4gn0b2oP4sbcKaRcZT5XoCAQCbLm1PR7I')
               .makeCopy();
  newFormFile.setName('AAAAA_Shuffled_Form');

  // Get ID of new form and open it
  newID = newFormFile.getId();
  newForm = FormApp.openById(newID);

  // Initialize array to put IDs in
  itemsArray = [];

  function getPageItems(thisPageNum) {
    Logger.log("Getting items for page number: " + thisPageNum );
    var thisPageItems = []; // Used for result
    var thisPageBreakIndex = getPageItem(thisPageNum).getIndex();
    Logger.log( "This is index num : " + thisPageBreakIndex );

    // Get all items from page
    var allItems = newForm.getItems();
    thisPageItems.push(allItems[thisPageBreakIndex]);
    Logger.log( "Added pagebreak item: " + allItems[thisPageBreakIndex].getIndex() );
    for( var i = thisPageBreakIndex+1; ( i < allItems.length ) && ( allItems[i].getType() != FormApp.ItemType.PAGE_BREAK ); ++i ) {
      thisPageItems.push(allItems[i]);
      Logger.log( "Added non-pagebreak item: " + allItems[i].getIndex() );
    }
    return thisPageItems;
  }

  function shuffle(array) {
    var currentIndex = array.length, temporaryValue, randomIndex;

    Logger.log('shuffle ran')

    // While there remain elements to shuffle...
    while (0 !== currentIndex) {

      // Pick a remaining element...
      randomIndex = Math.floor(Math.random() * currentIndex);
      currentIndex -= 1;

      // And swap it with the current element.
      temporaryValue = array[currentIndex];
      array[currentIndex] = array[randomIndex];
      array[randomIndex] = temporaryValue;
  }

  return array;
  }

  function shuffleAndMove() {

    // Get page items for all pages into an array
    for(i = 2; i <= 5; i++) {
      itemsArray[i] = getPageItems(i);
    }

    // Removes null values from array
    itemsArray = itemsArray.filter(function(x){return x});

    // Shuffle page items
    itemsArray = shuffle(itemsArray);

    // Move page items to the new form
    for(i = 2; i <= 5; ++i) {
      newForm.moveItem(itemsArray[i], i);
    }
  }

  shuffleAndMove();
} 

Original post: I have used Google forms to create a questionnaire. For my purposes, each question needs to be on a separate page but I need the pages to be randomized. A quick Google search shows this feature has not been added yet.

I see that the Form class in the Google apps script has a number of methods that alter/give access to various properties of Google Forms. Since I do not know Javascript and am not too familiar with Google apps/API I would like to know if what I am trying to do is even possible before diving in and figuring it all out.

If it is possible, I would appreciate any insight on what methods would be relevant for this task just to give me some direction to get started.

Based on comments from Sandy Good and two SE questions found here and here, this is the code I have so far:

// Script to shuffle question in a Google Form when the questions are in separate sections

function shuffleFormSections() {
  getQuestionID();
  createNewShuffledForm();
}

// Get question IDs
  function getQuestionID() {
    var form = FormApp.getActiveForm();
    var items = form.getItems();
    arrayID = [];
    for (var i in items) { 
      arrayID[i] = items[i].getId();
    }
    // Logger.log(arrayID);
    return(arrayID);
}

// Shuffle function
  function shuffle(a) {
    var j, x, i;
    for (i = a.length; i; i--) {
        j = Math.floor(Math.random() * i);
        x = a[i - 1];
        a[i - 1] = a[j];
        a[j] = x;
    }
}

// Shuffle IDs and create new form with new question order
function createNewShuffledForm() {
    shuffle(arrayID);
    // Logger.log(arrayID);
    var newForm = FormApp.create('Shuffled Form');
    for (var i in arrayID) {
      arrayID[i].getItemsbyId();
  }
}
syntonicC
  • 371
  • 2
  • 11
  • The problem is, that the Form can not be changed once it is open is View mode. You can write code that will change the order of questions in a Form before it is opened in View mode, but how are you going to get that code to run right before the user opens the Form in View mode? It's possible, but it would be a multi-step process. The user would need to do something like click a link or a button that first ran some code, and then opened the changed Form. And then what if someone uses the same Form link before the Form is randomized again? So there are some challenges. – Alan Wells Jun 30 '17 at 21:56
  • And might not work exactly as you would like. You could have all the users visit a Google Site, then click a button, that created a new Form just for that user, and then displayed that Form. You'd have a new Form for every user. That's possible. Then you could have each of those Forms all write their data to the same spreadsheet, so that all the data is saved in one place. – Alan Wells Jun 30 '17 at 21:59
  • In the settings, there is a setting to "**Shuffle question order**." It is under the "Presentation" tab. – Alan Wells Jun 30 '17 at 22:03
  • I think the option to have a new form for every user is the best option right now. However, I would like to automate the generation of those forms. What we do now, by hand, is have one master form and then create a new copy where we change the "after section X, continue to" and use that to shuffle which page will come next in the form. Is there a way to automate this action so I could feed a random page number in when I generate a new form for a new user? I don't see a method which would control this. – syntonicC Jul 05 '17 at 18:45
  • I think you will need to write custom code to do that. You can do this in JavaScript. See this [Link to Stack Overflow answer](https://stackoverflow.com/a/6274381/2946873) You could have an array of the questions ID's, and then randomly shuffle the ID's in the array, and then use the array to place the questions in the Form. So, you would get all the questions out of the template Form, get the ID's, put them in an array, shuffle the array, then get each question by it's ID, and put it into the new Form. – Alan Wells Jul 05 '17 at 23:59
  • Almost there! I'm just trying to get the shuffled ID's back to questions. Can I use `getItemsbyId()` to do this? I assume I need to use a for loop to get the items by ID and then add to the Form in each iteration of the loop. The only issue I am having is that I don't see an "add question" or "add item" method. All the add methods in the Forms class are about adding a specific kind of question. One last hint and I should be good to go. Thanks for your help! – syntonicC Jul 06 '17 at 16:00
  • Well, can you post the code that you have? That would help. – Alan Wells Jul 06 '17 at 17:13
  • I've updated the question with my code. – syntonicC Jul 06 '17 at 17:16

2 Answers2

1

I tested this code. It created a new Form, and then shuffled the questions in the new Form. It excludes page breaks, images and section headers. You need to provide a source file ID for the original template Form. This function has 3 inner sub-functions. The inner functions are at the top, and they are called at the bottom of the outer function. The arrayOfIDs variable does not need to be returned or passed to another function because it is available in the outer scope.

function shuffleFormSections() {
  var arrayOfIDs,shuffleQuestionsInNewForm,fncGetQuestionID,
      newFormFile,newForm,newID,items,shuffle;

  newFormFile = DriveApp.getFileById('Put the source file ID here')
               .makeCopy();
  newFormFile.setName('AAAAA_Shuffled_Form');

  newID = newFormFile.getId();

  newForm = FormApp.openById(newID);

  arrayOfIDs = [];

  fncGetQuestionID = function() {
    var i,L,thisID,thisItem,thisType;

    items = newForm.getItems();
    L = items.length;

    for (i=0;i<L;i++) {
      thisItem = items[i];
      thisType = thisItem.getType();

      if (thisType === FormApp.ItemType.PAGE_BREAK || 
      thisType === FormApp.ItemType.SECTION_HEADER || 
      thisType === FormApp.ItemType.IMAGE) {
       continue; 
      }

      thisID = thisItem.getId();
      arrayOfIDs.push(thisID);
    }

    Logger.log('arrayOfIDs: ' + arrayOfIDs);
    //the array arrayOfIDs does not need to be returned since it is available
    //in the outermost scope
  }// End of fncGetQuestionID function

  shuffle = function() {// Shuffle function
    var j, x, i;
    Logger.log('shuffle ran')

    for (i = arrayOfIDs.length; i; i--) {
      j = Math.floor(Math.random() * i);
      Logger.log('j: ' + j)

      x = arrayOfIDs[i - 1];
      Logger.log('x: ' + x)

      arrayOfIDs[i - 1] = arrayOfIDs[j];

      arrayOfIDs[j] = x;
    }

    Logger.log('arrayOfIDs: ' + arrayOfIDs)
  }

  shuffleQuestionsInNewForm = function() {
    var i,L,thisID,thisItem,thisQuestion,questionType;

    L = arrayOfIDs.length;

    for (i=0;i<L;i++) {
      thisID = arrayOfIDs[i];
      Logger.log('thisID: ' + thisID)
      thisItem = newForm.getItemById(thisID);

      newForm.moveItem(thisItem, i)
    }
  }

  fncGetQuestionID();//Get all the question ID's and put them into an array
  shuffle();

  shuffleQuestionsInNewForm();
}
Alan Wells
  • 27,268
  • 14
  • 81
  • 128
  • I would like to thank you very much for taking the time to write out this script, I really appreciate it. However, I do not think I described what I needed correctly. When I use it, it seems that the questions are shuffled and then placed into one section together leaving the sections they came from empty. Instead, the questions should be shuffled and placed new sections. So, it is really sections that are being shuffled not the questions. Sorry if this wasn't clear. Lastly, how could the code be modified so images and sections header transfer as well? – syntonicC Jul 13 '17 at 20:57
1

Try this. There's a few "constants" to be set at the top of the function, check the comments. Form file copying and opening borrowed from Sandy Good's answer, thanks!

// This is the function to run, all the others here are helper functions
// You'll need to set your source file id and your destination file name in the
// constants at the top of this function here.
// It appears that the "Title" page does not count as a page, so you don't need
// to include it in the PAGES_AT_BEGINNING_TO_NOT_SHUFFLE count. 
function shuffleFormPages() {   
  // UPDATE THESE CONSTANTS AS NEEDED
  var PAGES_AT_BEGINNING_TO_NOT_SHUFFLE = 2; // preserve X intro pages; shuffle everything after page X
  var SOURCE_FILE_ID = 'YOUR_SOURCE_FILE_ID_HERE'; 
  var DESTINATION_FILE_NAME = 'YOUR_DESTINATION_FILE_NAME_HERE';

  // Copy template form by ID, set a new name
  var newFormFile = DriveApp.getFileById(SOURCE_FILE_ID).makeCopy();
  newFormFile.setName(DESTINATION_FILE_NAME);

  // Open the duplicated form file as a form
  var newForm = FormApp.openById(newFormFile.getId());

  var pages = extractPages(newForm);
  shuffleEndOfPages(pages, PAGES_AT_BEGINNING_TO_NOT_SHUFFLE); 
  var shuffledFormItems = flatten(pages);

  setFormItems(newForm, shuffledFormItems);  
}

// Builds an array of "page" arrays. Each page array starts with a page break
// and continues until the next page break.
function extractPages(form) {
  var formItems = form.getItems();
  var currentPage = [];
  var allPages = [];

  formItems.forEach(function(item) { 
    if (item.getType() == FormApp.ItemType.PAGE_BREAK && currentPage.length > 0) {
      // found a page break (and it isn't the first one)
      allPages.push(currentPage); // push what we've built for this page onto the output array
      currentPage = [item]; // reset the current page to just this most recent item
    } else {
      currentPage.push(item);
    }
  });
  // We've got the last page dangling, so add it
  allPages.push(currentPage);
  return allPages;
};

// startIndex is the array index to start shuffling from. E.g. to start
// shuffling on page 5, startIndex should be 4. startIndex could also be thought
// of as the number of pages to keep unshuffled.
// This function has no return value, it just mutates pages
function shuffleEndOfPages(pages, startIndex) {
  var currentIndex = pages.length;

  // While there remain elements to shuffle...
  while (currentIndex > startIndex) {
    // Pick an element between startIndex and currentIndex (inclusive)
    var randomIndex = Math.floor(Math.random() * (currentIndex - startIndex)) + startIndex;

    currentIndex -= 1;

    // And swap it with the current element.
    var temporaryValue = pages[currentIndex];
    pages[currentIndex] = pages[randomIndex];
    pages[randomIndex] = temporaryValue;
  }
};

// Sourced from elsewhere on SO:
// https://stackoverflow.com/a/15030117/4280232
function flatten(array) {
  return array.reduce(
    function (flattenedArray, toFlatten) {
      return flattenedArray.concat(Array.isArray(toFlatten) ? flatten(toFlatten) : toFlatten);
    }, 
    []
  );
};

// No safety checks around items being the same as the form length or whatever.
// This mutates form.
function setFormItems(form, items) {
  items.forEach(function(item, index) {
    form.moveItem(item, index);
  });
};
alexcavalli
  • 486
  • 7
  • 10