How to re-execute the query script of an App Maker Datasource - google-apps-script

Goal
Use App Maker to collect User Birthdays and display only the Birthdays this Month.
Issue
I have a data model, persons. In that model are two Datasources, the default persons and a second birthdaysThisMonth. The datasource query script in birthdaysThisMonth properly runs and returns only the birthdays this month from the persons model.
However, when I change a birthdate in the persons datasource, the birthdaysThisMonth datasource remains unchanged, e.g., the Query script in birthdaysThisMonth is not re-executed.
To change the birthdate, I select a new date from the date picker, and the new value is shown in a table. I am not using a submit button, I see the change when the date picker looses focus.
What I've Tried
This script is executed as a Query script in the birthdaysThisMonth datasource which is not set to Manual save mode. It returns the records I want.
function setBirthdays() {
var calcRecords = [];
var personsRecords = app.models.persons.newQuery().run();
for (i=0; i<personsRecords.length; i++) {
var id = app.models.persons.newQuery().filters.Id._equals = i;
if (personsRecords[i].birthdate.getMonth() == thisMonth()) {
var calcRecord = app.models.persons.newRecord();
calcRecord.emailSecondary = personsRecords[i].emailSecondary;
calcRecord.birthdate = personsRecords[I].birthdate;
calcRecord.Id = personsRecords[i].Id;
calcRecords.push(calcRecord);
}
}
return calcRecords;
}
Question
How do I re-execute the query script in birthdaysThisMonth when the data in persons has been updated. How do I trigger the query so it reevaluates the data in persons and filters accordingly.
Using Events and onAfterSave seems promising, but I haven't found an example of this.
BTW
I'd like this work to happen on the server side if possible.

See Markus Malessa's comment for the answer.
Unfortunately you can't use server events like onAfterSave to trigger reloading a datasource on the client, so your proposed solution won't work. It would seem that your only possible solution would be to make your persons datasource a manual save datasource, in the form where you change the birth date you will need a 'Save' button that calls widget.datasource.saveChanges() and in that function incorporate a callback that will reload your birthdaysThisMonth datasource
The way I'm attempting to push/pull data isn't great. Looks like Manual Mode provides the types of features I need.

Related

How do you track automated data inputs in Google Sheets over time?

I have a google sheet that runs a report on data that is automatically updated. What I would like to do is compare the new inputs with the old inputs to determine if the changes were positive or negative. How would you go about automating a sheet to track these changes?
Changes happen monthly
there would be a score 1-100; 100 being the best
would like to store this data over time for a historical view
Any advice would surely be appreciated
The numbers in each criteria change every month producing a score at the end of the table called Current Score
This score is then pulled into the historical tab as the "Current Score"
What I would like to see happen is that the Current score be saved every month and processed with a percentage change month over month
So I would need a function that stores a copy of the results before they change, processes a new score, and then calculates the difference between the two. Example here is the Dec score (stored values) compared to the most recent score.
Here is a link to the working example
https://docs.google.com/spreadsheets/d/1ImbRhWqGjvIx2CFRKapZ2wmxC9qpSKxxCbHr5tPOBOs/edit#gid=0
Solution
You can automate this process by using Google Apps Script. Open the script editor by clicking on Tools > Script Editor. It is based on JavaScript and allows you to create, access and modify Google Sheets files with a service called Spreadsheet Service.
In addition, you can use Time-driven triggers to run the script automatically once a month. To set it up, click Triggers in the left bar, then Add Trigger and select Time-driven in Select event source. You can now specify the month timer and the exact day and hour you want the script to run. However, I recommend that you do some testing before setting up the trigger to check that you get the desired results. You can test the code by clicking Run in the Editor.
Explanation of the code
There are three functions in the code. The main function is called updateScores and it does what you described in the question. It takes the current score, stores it in a new column and calculates the difference from the last month. Try this function and if you like the result, you can put the trigger in the main function. This way, the trigger calls main which its only responsibility is to call the other two functions. The first is updateScores, which I have already explained, and the second is clearScores, which clears all the values of Reports so you don't have to do it manually and you can start writing the new values for the new month.
I have added some comments so you can understand what each line does.
var lr = SpreadsheetApp.getActiveSpreadsheet().getSheetByName('report').getLastRow()
function updateScores() {
var ss = SpreadsheetApp.getActiveSpreadsheet().getSheetByName('Historical')
var currentValues = ss.getRange('B2:B'+lr).getDisplayValues() // get current score
ss.insertColumnsAfter(2,2) // insert two new columns (current score and percent difference)
ss.getRange('D2:D'+lr).setValues(currentValues) // paste stored score
ss.getRange('C2:C'+lr).setFormula('=if(D2=0,"N/A",B2/D2-1)') // apply formula for last stored scores
ss.getRange('E2:E'+lr).setFormula('=if(F2=0,"N/A",D2/F2-1)') // correct formula reference
ss.getRange('E2:E'+lr).copyFormatToRange(ss,3,3,2,lr) // copy format percent
ss.getRange('F2:F'+lr).copyFormatToRange(ss,4,4,2,lr) // copy format scores
var month = new Date().toString().split(' ')[1] // get current month
ss.getRange('D1').setValue(month + ' score') // write current month on last stored scores
var diff = ss.getRange('E1').getDisplayValue() // get diff symbol
ss.getRange('C1').setValue(diff) // write diff
}
function clearScores(){
var ss = SpreadsheetApp.getActiveSpreadsheet().getSheetByName('report')
ss.getRange('B2:G'+lr).clear()
}
function main(){
updateScores()
clearScores()
}

Can I trigger Google Sheets scripts in a particular sequence?

I've altered a script that gets data from MailChimp and then displays it in Google Sheets. However, the the data comes out in a random order, rather than ordered (e.g. by "campaign date") and also creates duplicates.
So I added two scripts that
Clear the previous data
Sort in date order
I want to run these scripts in a specific order so:
The cell ranges clear
The data imports from mailchimp
The data is rearranged in date order
Ideally I'd like to refresh this data every 15 minutes. What's the best way to do this? I can post my code but it's quite bloated and messy.
I have a similar problem which I have solved by creating a "parent" function that runs all the other functions in order. For robustness I have also included a simple logging function that logs this to a sheet so I can easily see that the functions have run. However, if you're running this every 15 mins that could quickly get out of hand, so you could either bin the logging, or use the built-in Logger (see reference here)
function runAllFunctions() {
logit("Clearing data");
clearCellRange();
logit("Data Cleared. Importing MailChimp Data");
importMailchimpData();
logit("MailChimp data imported. Reordering data.");
orderData();
logit("Data reordered. Update complete");
}
;
function logit(message) {
var logBook = SpreadsheetApp.openById("<insert ID here>")
var logSheet = logBook.getSheetByName("Log")
logSheet.appendRow([new Date(),message]);
};

What is the best way to update another table based on a certain condition ON UPDATE?

I'm having two tables subscription and subscription_event. A subscription_event can be one of the following types:
public enum SubscriptionEventType {
CREATED,
CANCELED,
CHARGED_SUCCESSFULLY,
CHARGED_UNSUCCESSFULLY,
EXPIRED,
TRIAL_STARTED,
TRIAL_ENDED,
WENT_ACTIVE, // Subscription went active and can be charged from now on.
WENT_PAST_DUE;
public Long getValue() {
return this.ordinal() + 1L;
}
}
What I want to do is to keep the state of subscription to the most recent event. The problem: Those events do not come in correct order. E.g. it is possible to get a CHARGED_SUCCESSFULLY event before a WENT_ACTIVE event.
So there are several way how I can accomplish what I need. First of all I can check the condition in my application layer and always set that "most recent" state based on the timestamp of the event.
Long subscriptionId = lastRecordedEvent.getSubscriptionId();
if(event.getTimestamp() > lastRecordedEvent.getTimestamp()) {
// Since the current event is more recent than all other events
// we also have to update the subscription state
subscriptionRepository.updateState(subscriptionId, event.getTimestamp());
}
However, I do not want to do this in my application layer. Another solution would be to use a TRIGGER on the subscription_event table and let that on decide whether to update the relevant subscription or not. The reason why I do not go for that just yet is because I know that triggers can be easily forgotten and also be a pain to maintain. Also I know one should take every other option into account before using a TRIGGER but since I am not a SQL/MySQL expert I'm not aware of all my options here.
So what would be the most practicable way to keep subscription up-to-date in this situation?
Insert your event as usual into the table and then execute the following
UPDATE subscriptions set state=events.state
FROM subscriptions inner join events on subscriptions.id = events.subscriptionID
Where events.SubscriptionId = ? and events.Timestamp =
(select max(timestamp) from events where events.SubscriptionId = ?)
You will need to pass parameters for the two ?s to be the subscription id of the event you just inserted
EDIT
An alternative approach is rather than have a status field in the database, create a view for your subscriptions and always query the view instead.
CREATE VIEW vw_subscriptions as
Select s.id, otherfields from subscription, coalesce(e.Status, 1) as status
from subscriptions s left outer join events e on s.id=e.subscriptionId
AND e.timestamp =
(select max(timestamp) from events where subscriptionId=s.id)
If you are worried about forgetting/maintaining the SQL or triggers, document them as comments in your repository functions and maintain all changes to the database as a change script that you store with your source code. That way your changes are all in your source control.

Dynamically edit multiple choice options in live Google Form using Apps Script

I'm a high school teacher in L.A. trying to create a course registration system using Apps Script. I need the Google Form I'm using for this registration to:
Question 1) Update the choices available in subsequent multiple choice questions on new pages based on a student's current response choices.
Question 2) Eliminate choices from the form when a multiple choice option has reached it's "cap".
Question 1 Example)
A student registers for “tie-tying” in workshop 1, and gets taken to a new page. The Script edits the available choices on that new page based on the student’s first choice, and removes “tie-tying” from the list of possible choices on that new page, so “etiquette” is their only remaining option.
Question 2 Example)
Students can either register for “tie-tying” or “etiquette”, both responses are initially available in the Google Form. 30 students take the survey, all 30 register for the “tie-tying” workshop. The Apps Script references the response spreadsheet, realizes the “tie-tying” workshop is full, then removes it from the Google Form's list of possible choices. Student 31 goes to register, and their only option is “etiquette”.
If my question has already been asked and answered (believe me, I did search!) I'd appreciate the redirection.
I believe we can achieve your second objective without too much difficulty and modify the form, based on the current state of response.
The approach is to
Create the form and associate it with a response spreadsheet
In that response spreadsheet, create a script with a function (updateForm for instance)
Bind that function with the onFormSubmit event, see Using Container-Specific Installable Triggers.
Analyse the response in the updateForm function and modify your form using the Form Service
For instance
function updateForm(e) {
if (e.values[1] == 'Yes') {
Logger.log('Yes');
var existingForm = FormApp.openById('1jYHXD0TBYoKoRUI1mhY4j....yLWGE2vAm_Ux7Twk61c');
Logger.log(existingForm);
var item = existingForm.addMultipleChoiceItem();
item.setTitle('Do you prefer cats or dogs?')
.setChoices([
item.createChoice('Cats'),
item.createChoice('Dogs')
])
.showOtherOption(true);
}
}
When it comes to achieving the goal in your first question, its more delicate, as the form will not submit mid way. What is possible is to go to different pages based on different responses to a Multiple Choice question, your use case may fit this method, although its not very dynamic.
Further its possible to use html Service to create completely dynamic experience.
Let me know if you need further information.
You are not able to create this type of dynamic form using the Google Forms Service, because there is no interaction between the service and scripts during form entry, except upon Form Submission. In the case of a multi-page form, a script has no way to know that a student has completed one page and gone on to another.
You could achieve this using the HtmlService or UiService, though. In either case, you'd rely on the client-side form interacting through server-side scripts to get updated lists of course options, then modifying the next 'page'. It will be complex.
The other answer to this question will keep adding a multichoice select each time for the form is submitted. Using similar approach of:
Create the form and associate it with a response spreadsheet
In that response spreadsheet, create a script with a function (updateForm for instance)
Bind that function with the onFormSubmit event, see Using Container-Specific Installable Triggers.
Analyse the response in the updateForm function and modify your form using the Form Service
I've used the following code to modify a list select which could be easiliy modified for a multiple choice.
function updateForm(){
var form = FormApp.openById('YOUR_FORM_ID'); // Base form
// need to read what dates are available and which are taken
var doc = SpreadsheetApp.getActiveSpreadsheet();
var dates = doc.getRange("dates!A1:A10").getValues(); //available options
var taken_dates = doc.getRange("responses!F2:F51").getValues(); //just getting first 50 responses
// joining the taken dates into one string instead of an array to compare easier
var taken_dates_string = taken_dates.join("|");
var choice = [];
// loop through our available dates
for (d in dates){
// test if date still available
if (dates[d][0] != "" && taken_dates_string.indexOf(dates[d][0]) === -1){
choice.push(dates[d][0]); // if so we add to temp array
}
}
var formItems = form.getItems(FormApp.ItemType.LIST); // our form list items
// assumption that first select list is the one you want to change
// and we just rewrite all the options to ones that are free
formItems[0].asListItem().setChoiceValues(choice);
}

Flex DateField and MySQL default date value 0000-00-00

I created a service and callResponder (Via Generate Service Call and Generate Form in Flash Builder 4) that query a MySQL database for a name and a birth date.
My problem is so simple I guess but I haven't been able to find the solution...
When I have an empty date in MySQL (0000-00-00), my binded DateField indicates 1899-11-30
I tried almost everything possible... A custom labelFunction, a custom function called straight after the call to my service to try to handle my data like this.. :
protected function parseDate(date:Date):void
{
if (date.getFullYear() == -1) {
friendbirthdate.selectedDate = null;
} else {
var df:DateFormatter = new DateFormatter;
df.formatString = 'YYYY-MM-DD';
friendbirthdate.selectedDate = date;
}
}
Unfortunately, this works only partially. When I try to update this same form in the database I get disconnected. I'm missing something here and would greatly appreciate a tip or two :-)
THANKS!
I recommend that you store NULL for unknown dates, as opposed to '0000-00-00'. NULL is a more accurate value in this case.
If you want to store it as NULL and display it as a different value you can do something like this:
SELECT COALESCE(date_column,'0000-00-00') as formatted_date
FROM your_table
Don't know if it will help somebody but I had trouble to insert values from a form, containing a DateField. That Form has been generated through the Flash Builder Generate Form tool.
The problem is that the service generated by Flash Builder expect a Date Object, no matter what... If you submit the form without selecting a date in the DateField, the service call crashes.
Let's have a look at a part of the generated sample service (Create and Update functions), we can see that PHP is expecting to transform a Date Object to a String like this :
mysqli_stmt_bind_param($stmt, 'sss', $item->friendname, $item->friendlastname, $item->friendbirth->toString('YYYY-MM-dd HH:mm:ss'));
First of all, be sure that your Date field in MySQL has as NULL default value.
Then alter the create and update functions like this :
if ($item->friendbirth == null)
$friendbirth = null;
else
$friendbirth = $item->friendbirth->toString('yyyy-MM-dd HH:mm:ss');
mysqli_stmt_bind_param($stmt, 'sss', $item->friendname, $item->friendnickname, $friendbirth);
This way, the service call script won't try to do kinda null->toString('...')