├── Code.gs ├── LICENSE.md ├── README.md ├── dashboard.html ├── img ├── overview.png ├── restart.png ├── step1-1.png ├── step2-1.png ├── step2-2.png ├── step2-3.png ├── step3-1.png ├── step3-2.png ├── step4-1.png ├── step4-2.png ├── step5-1.png ├── step5-2.png ├── stop.png ├── view-1.png └── view.png ├── moment-timezone.gs ├── moment.gs ├── tests └── test.gs └── tscron.gs /Code.gs: -------------------------------------------------------------------------------- 1 | /* 2 | * This function will be called every time the cron scheduler runs - see TSCron https://github.com/techstreams/TSCron for documentation 3 | * 4 | * @param {Object} e - time-driven event object (see 'time-driven events' https://developers.google.com/apps-script/guides/triggers/events) 5 | * @param {Object} params - an array of user defined ItemResponses (see https://developers.google.com/apps-script/reference/forms/item-response) 6 | */ 7 | function cronJob(e, params) { 8 | 9 | // ADD YOUR CRON IMPLEMENTATION CODE HERE 10 | 11 | } 12 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | **TSCron License** 2 | 3 | © Laura Taylor ([github.com/techstreams](https://github.com/techstreams)). Licensed under an MIT license. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | **3rd Party Licenses** 24 | 25 | | File | From | Copyright | License | 26 | | :--: | :--: | :-------: | :-----: | 27 | | [moment.gs](moment.gs) | [Github](https://github.com/moment/moment) | Copyright (c) JS Foundation and other contributors | [MIT License](https://github.com/moment/moment/blob/develop/LICENSE) | 28 | | [moment-timezone.gs](moment-timezone.gs) | [Github](https://github.com/moment/moment-timezone) | Copyright (c) JS Foundation and other contributors | [MIT License](https://github.com/moment/moment-timezone/blob/develop/LICENSE) | 29 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Overview 2 | 3 | *If you enjoy my [Google Workspace Apps Script](https://developers.google.com/apps-script) work, please consider buying me a cup of coffee!* 4 | 5 | 6 | [![](https://techstreams.github.io/images/bmac.svg)](https://www.buymeacoffee.com/techstreams) 7 | 8 | --- 9 | 10 | ![](img/overview.png) 11 | 12 | **TSCron** is a [Google Forms](https://www.google.com/forms/about/) based [Cron](https://en.wikipedia.org/wiki/Cron) scheduler powered by [Google Apps Script](https://www.google.com/script/start/). 13 | 14 | Cron scheduling is available for: 15 | 16 | * **Minutes** 17 | * **Hours** 18 | * **Days** 19 | * **Weeks** 20 | * **Months** 21 | * **Years** 22 | * **Custom date/time** *(runs once)* 23 | 24 | 25 | Potential uses for TSCron: 26 | 27 | * Send a [Google Docs](https://www.google.com/docs/about/) newsletter to clients on the `1st of every month` 28 | * Populate a [Google Sheet](https://www.google.com/sheets/about/) with [Google Analytics](https://analytics.google.com/) data `every 4 weeks` 29 | * Email event attendees a link to [Google Slides](https://www.google.com/slides/about/) `1 hour after` the presentation 30 | * Notify warehouse managers of new regulations `every 3 months on the 1st of the month` 31 | * Send an email to a colleague `every 3 days` 32 | * Send yourself a list of goals every `January 1st` 33 | * Add new todos to a [Trello](https://trello.com/) board every `Sunday at 9:00 PM` 34 | * Send invoices to customers on the `"last day" of every month` 35 | * Post a message to a [Slack](https://slack.com/) channel `daily at 8:00 AM` 36 | * Automatically create a Sales Forecast presentation with [Google Slides](https://www.google.com/slides/about/) and share it with your manager `every 6 months` 37 | * Send school activity updates to parents `every Monday at 8:00 AM` 38 | * Run a backend [microservice](https://en.wikipedia.org/wiki/Microservices) `every 3 hours` 39 | * ... 40 | 41 | 42 | --- 43 | 44 | ## How It Works 45 | 46 | A submission to a **TSCron** enabled Google Form initiates a **new [cron](https://en.wikipedia.org/wiki/Cron) job** *(any previously scheduled cron job is removed)*. 47 | 48 | Each form submission contains responses for both: 49 | 50 | * pre-existing **TSCron configuration form elements** *(i.e. cron interval configuration, start date and optional end date)* 51 | 52 | * any **user defined form elements** which are needed to implement the cron job 53 | 54 | TSCron uses the configuration to **"schedule"** the new cron job at the selected *start date/time* and to **"reschedule"** the cron job at the desired cron interval. If an *optional* **end date** is specified, TSCron will stop the cron job at that date/time. 55 | 56 | *NOTE: Actual start, end and cron execution times may [vary +/- 15 minutes](https://developers.google.com/apps-script/reference/script/clock-trigger-builder) from the original selected dates/times.* 57 | 58 | The **form owner** *(or a form collaborator)* **implements the cron job** in a provided `cronJob()` function using [Google Apps Script](https://www.google.com/script/start/). *(TSCron executes the implemented `cronJob()` function on the start date/time and on each repeating cron interval.)* 59 | 60 | Cron scheduling occurs in the **form owner's default time zone** and takes into account: 61 | 62 | * *short months (e.g. Feburary, April, June, September, November)* 63 | * *leap years* 64 | * *daylight savings time* 65 | 66 | --- 67 | 68 | 69 | ## Getting Started 70 | 71 | #### 1) Install TSCron in Google Drive. 72 | 73 | * Login to [Google Drive](https://drive.google.com/). 74 | 75 | * Access the **[TSCron form](https://docs.google.com/forms/d/1puyShNiWHuBy2ZZ6MADI_bcnmbbgqqsFmtDxai0r9Qs/template/preview)**. 76 | 77 | * Click the ***Use Template*** button. This will copy the form to Google Drive. 78 | 79 | ![](img/step1-1.png) 80 | 81 |
82 | 83 | #### 2) Add any *user defined* form elements needed to implement the `cronJob()` function 84 | 85 | 86 | Locate and open the newly copied TSCron form and add any ***optional user defined*** form elements needed to implement the `cronJob()` function. 87 | 88 | ![](img/step2-1.png) 89 | 90 | Be sure to add these optional form elements to form section(s) **BEFORE** the pre-existing TSCron configuration form elements. 91 | 92 | ![](img/step2-2.png) 93 | 94 | Any form submission responses for **user defined form elements** will be passed to the `cronJob()` function as an **array of [ItemResponses](https://developers.google.com/apps-script/reference/forms/item-response) each time** the cron job executes. 95 | 96 | Be sure to set the ***Continue to next section*** option on the TSCron ***form section preceding*** the pre-existing TSCron configuration section. 97 | 98 | ![](img/step2-3.png) 99 | 100 | 101 | **IMPORTANT:** 102 | 103 | * **Do not add, delete or modify** any form element or form section ***ON OR AFTER*** the pre-existing TSCron configuration form elements or the cron scheduler will cease to function properly. 104 | 105 | * **Do not add any user defined elements** with the title `Run TSCron`. 106 | 107 |
108 | 109 | #### 3) Implement the `cronJob()` function 110 | 111 | 112 | Open the TSCron form's script editor. 113 | 114 | ![](img/step3-1.png) 115 | 116 | Implement the `cronJob()` function in the supplied `Code.gs` file using [Google Apps Script](https://www.google.com/script/start/). *This file can be found within the TSCron form's Script Editor.* 117 | 118 | ![](img/step3-2.png) 119 | 120 | The `cronJob()` function will be called ***each time*** the cron job executes *(i.e. first on the cron start date/time and then on each repeating cron interval)* and will be passed **two parameters**: 121 | 122 | * **e** - time-driven event object which contains information for the current cron execution *(see 'time-driven events' in the [Google Apps Script documentation](https://developers.google.com/apps-script/guides/triggers/events) for more information)*. The event object is represented in [Coordinated Universal Time (UTC)](https://en.wikipedia.org/wiki/Coordinated_Universal_Time). 123 | 124 | * **params** - an array of any ***user defined [ItemResponses](https://developers.google.com/apps-script/reference/forms/item-response)*** for the form elements defined in [Step #2](https://github.com/techstreams/TSCron#2-add-any-user-defined-form-elements-needed-to-implement-the-cronjob-function) *(NOTE: If no user defined form elements exist this parameter will be `null`)* 125 | 126 | Best implementation practices: 127 | 128 | * Test for a `null` value in the passed `params` function parameter 129 | 130 | * Use the [Lock Service](https://developers.google.com/apps-script/reference/lock/lock-service) to prevent any concurrent access to sections of code within the `cronJob()` function which might modify a shared resource. This is particularly important when implementing longer running processes on a short `Minutes` cron interval where multiple `cronJob()` executions could occur simultaneously. 131 | 132 | * Wrap the `cronJob()` implementation code in a [try/catch](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/try...catch) block and send an email on any error conditions 133 | 134 | *NOTE: Both the [moment.js](https://github.com/moment/moment) and [moment-timezone.js](https://github.com/moment/moment-timezone) libraries used by TSCron are available for use within the `cronJob()` function.* 135 | 136 | 137 | Here's an simple example implementation of a `cronJob()` function which is passed a user defined text form element [ItemResponse](https://developers.google.com/apps-script/reference/forms/item-response) which contains an email address. The cron function sends a message to the specified email address *(or to the form owner if no email address is supplied through the form submission)*. 138 | 139 | ```js 140 | function cronJob(e, params) { 141 | 142 | try { 143 | var email = params ? params[0].getResponse() : Session.getEffectiveUser().getEmail(); 144 | var emailMsg = 'Cron Job Successfully Executed On: '+ e.year + '-' + e.month + '-' + 145 | e['day-of-month'] + ' At: ' + (e.hour-6) + ':' + e.minute + ':' + 146 | e.second + ' with message:
'; 147 | GmailApp.sendEmail(email, 'Cron Job Execution Success', '', {htmlBody: emailMsg}); 148 | } catch(err) { 149 | GmailApp.sendEmail(email, 'Cron Job Execution Failure', '', {htmlBody: 'Error: ' + err.message }); 150 | } 151 | 152 | } 153 | ``` 154 | 155 |
156 | 157 | **Important:** 158 | 159 | When implementing the `cronJob()` function: 160 | 161 | * **Do not create** a "Script" [PropertiesService](https://developers.google.com/apps-script/reference/properties/properties-service) value with the key `tscron` or the cron scheduler will cease to function properly. 162 | 163 | * **Do not create** any [form submit triggers](https://developers.google.com/apps-script/guides/triggers/#available_types_of_triggers) using the existing TSCron function `newTSCron` or the cron scheduler will cease to function properly. 164 | 165 | * **Do not create** any [time-based triggers](https://developers.google.com/apps-script/guides/triggers/#available_types_of_triggers) using the existing TSCron functions `startTSCron`, `runTSCron` or `endTScron` or the cron scheduler will cease to function properly. 166 | 167 | 168 |
169 | 170 | #### 4) Run `Configure` option from the TSCron menu 171 | 172 | 173 | Run the `TSCron > Configure` option from the form's add-on menu. 174 | 175 | ![](img/step4-1.png) 176 | 177 | The first time the `Configure` option is run, the script **will prompt for authorization**. Complete the authorization process by following the Google authorization prompts. 178 | 179 | **Important:** 180 | 181 | * **Be sure to run the `Configure` option BEFORE** submitting to the form or the cron scheduler will not work. 182 | 183 | * **If code requiring additional user authorization** is added to the `cronJob()` function, the authorization process must be repeated for TSCron to continue to operate correctly. *Re-run the `Configure` menu option to re-authorize.* 184 | 185 |
186 | 187 | #### 5) Start cron scheduler 188 | 189 | Start the cron scheduler by submitting a response to the form. 190 | 191 | ![](img/step5-1.png) 192 | 193 | 194 | ![](img/step5-2.png) 195 | 196 | 197 | **Special Notes About "Month" and "Year" Cron Scheduling** 198 | 199 | ***Month Scheduling:*** 200 | 201 | If a cron execution is scheduled in a month *(e.g. February, April, June, September, November)* which does not contain the original schedule date of the month *(e.g. 29th, 30th, 31st - determined from the cron start date)*, the cron will execute on the "last day" of that short month near the selected time. 202 | 203 | For months which contain the original schedule date, the cron will execute on that date of the month near the selected time. 204 | 205 | *Example: A cron job scheduled every `1` months starting on `January 31st` will next execute on `February 28th` (or `February 29th` if a leap year) and then again on `March 31st`, etc.* 206 | 207 | If the cron job will be started on the "last day" of a short month *(e.g. February, April, June, September, November)* and you would like it to run on the "last day" of subsequent longer months *(e.g. January, March, May, July, August, October, December)* select `Yes` on the `Short Months?` configuration element. For all other cases select `No`. 208 | 209 | 210 | ***Year Scheduling:*** 211 | 212 | If the cron start date is `February 29th` during a leap year, the cron job will be scheduled to run on `February 28th` in non-leap years. 213 | 214 | If the cron job is started on `February 28th` of a non-leap year and you would like it to run on `February 29th` in subsequent leap years, select `Yes` on the `Leap Years?` configuration element. For all other cases select `No`. 215 | 216 | **Important:** 217 | 218 | * **When scheduling a new cron** the *start date/time* must be `at least 15+ minutes in the future` of the form submission and the *optional* *end date/time* must be scheduled `at least 1 hour after the start date/time`. *NOTE: The cron job will not be scheduled and an __error message email will be sent__ to the form owner if these conditions are not met.* 219 | 220 | * **Actual cron execution times** may [vary +/- 15 minutes](https://developers.google.com/apps-script/reference/script/clock-trigger-builder) from the original schedule dates/times. 221 | 222 | * **If a failure occurs during the initial or subsequent cron scheduling**, TSCron will be disabled and an error message email will be sent to the form owner. *Restart the cron job by submitting another form request.* 223 | 224 | * **Cron scheduling uses the Google Form owner's default time zone** *(determined by the form's associated Google Apps Script project properties - see `File -> Project properties -> Time zone` in the form's Script Editor)*. 225 | 226 | * **If the TSCron default time zone is modified to a new time zone** the cron schedule ***will remain on the previous time zone***. You must ***submit a new response*** to the form to get an updated cron schedule in the new time zone. 227 | 228 | * **Do not delete form responses** while there is a running cron job or the cron scheduler will cease to function properly. 229 | 230 | * **Do not delete any TSCron related [triggers](https://developers.google.com/apps-script/guides/triggers/#available_types_of_triggers)** from the form while there is a running cron job or the cron scheduler will cease to function properly. 231 | 232 | * **Because cron schedule times can vary +/- 15 minutes**, the cron job ***could potentially execute after*** the original end date/time but before the cron job is stopped. 233 | 234 | --- 235 | 236 | ## Additional Options 237 | 238 | ### Stop Cron Scheduler 239 | 240 | To stop the cron scheduler, run the `TSCron > Stop` option from the form's add-on menu. *(This will remove all cron scheduling configuration for the current cron job)* 241 | 242 | ![](img/stop.png) 243 | 244 |
245 | 246 | ### Re-Start Cron Scheduler 247 | 248 | To re-start the cron scheduler after an error condition or manual stop, submit a new response to the form. 249 | 250 | ![](img/restart.png) 251 | 252 |
253 | 254 | ### View Cron Status 255 | 256 | To view the cron scheduler status, select the `TSCron > Status` option from the form's add-on menu. 257 | 258 | ![](img/view.png) 259 | 260 | The TSCron status sidebar will open. 261 | 262 | ![](img/view-1.png) 263 | 264 | --- 265 | 266 | 267 | ## FAQ 268 | 269 |
270 | Will the status sidebar automatically refresh? 271 | No. Reopen the sidebar to see any changes to the cron status. 272 |
273 |
274 |
275 | I submitted a form response but TSCron did not start. What should I do? 276 |
277 | If TSCron is not working properly, perform the following steps:

278 | 283 |
284 |
285 |
286 | Is TSCron Internationalized? 287 | No. TSCron currently exists in English only. 288 |
289 |
290 |
291 | Why am I receiving a quota error when the cronJob() function runs? 292 | Google Apps Script is subject to daily quotas based upon the type of account accessing and running the script. See the "Quota Limits" tab on the Google Apps Script Dashboard for more information. 293 |
294 |
295 |
296 | How do I ask general questions about TSCron? 297 | For general questions, submit an issue through the issue tracker. 298 |
299 |
300 |
301 | How do I submit bug reports for TSCron? 302 | For bug reports, fork this repository, create a test which demonstrates the problem following the procedures outlined in the "Tests" section below and submit a pull request. If possible, please submit a solution along with the pull request. 303 |
304 | 305 | --- 306 | 307 | ## Tests 308 | 309 | TSCron unit tests can be found in [test.gs](/tests/test.gs). 310 | 311 | To perform TSCron unit tests: 312 | 313 | * Add the [test.gs](/tests/test.gs) file to the TSCron script editor. 314 | 315 | * Install [QUnit for Google Apps Script](https://github.com/simula-innovation/qunit/tree/gas/gas) by adding the project library *(see the project documentation for proper install instructions)*. 316 | 317 | * [Deploy the script](https://developers.google.com/apps-script/guides/web#deploying_a_script_as_a_web_app) as a web app *(execute script as self)*. 318 | 319 | * Set the desired unit test configuration entry in the `testConfig` object to `true` *(see the [test.gs](/tests/test.gs) file for information on where to modify these values to enable tests)*. NOTE: Some tests can take longer to run so best to run them individually. 320 | 321 | * Access the deployed web app for test results. 322 | 323 | --- 324 | 325 | ## License 326 | 327 | **TSCron License** 328 | 329 | © Laura Taylor ([github.com/techstreams](https://github.com/techstreams)). Licensed under an MIT license. 330 | 331 | Permission is hereby granted, free of charge, to any person obtaining a copy 332 | of this software and associated documentation files (the "Software"), to deal 333 | in the Software without restriction, including without limitation the rights 334 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 335 | copies of the Software, and to permit persons to whom the Software is 336 | furnished to do so, subject to the following conditions: 337 | 338 | The above copyright notice and this permission notice shall be included in all 339 | copies or substantial portions of the Software. 340 | 341 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 342 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 343 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 344 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 345 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 346 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 347 | SOFTWARE. 348 | 349 | **3rd Party Licenses** 350 | 351 | | File | From | Copyright | License | 352 | | :--: | :--: | :-------: | :-----: | 353 | | [moment.gs](moment.gs) | [Github](https://github.com/moment/moment) | Copyright (c) JS Foundation and other contributors | [MIT License](https://github.com/moment/moment/blob/develop/LICENSE) | 354 | | [moment-timezone.gs](moment-timezone.gs) | [Github](https://github.com/moment/moment-timezone) | Copyright (c) JS Foundation and other contributors | [MIT License](https://github.com/moment/moment-timezone/blob/develop/LICENSE) | 355 | -------------------------------------------------------------------------------- /dashboard.html: -------------------------------------------------------------------------------- 1 | 25 | 26 | 27 | 28 | 29 | 30 | TSCron 31 | 32 | 33 | 34 | 35 | 48 | 49 | 50 |
51 |
52 |
53 |
54 |
55 |
56 |

Cron Scheduler

57 |
58 |
59 |
    60 |
  • 61 | 62 |

    Mode:   Enabled

    63 | 64 |

    Status:   Scheduled

    65 | 66 |

    Status:   Running

    67 | 68 |

    Status:   Not Running

    69 |
    70 |

    Submit a response to the form to start the Cron Scheduler.

    If you've recently submitted a form response and the Cron Scheduler Status does not show 'Scheduled' or 'Running', please wait a couple minutes before reopening this sidebar.

    71 | 72 | 73 |

    Run:  

    74 | 75 |

    Near:  

    76 | 77 |

    78 | 79 | 80 |

    Mode:   Disabled

    81 |
    82 |

    Enable Cron Scheduler using the form's
    TSCron > Configure
    add-ons menu

    83 | 84 |
  • 85 | 86 | 87 |
  • 88 |

    Last Run On:

    89 |

    90 | 91 |

    92 |
  • 93 | 94 | 95 |
  • 96 |

    Next Scheduled On:

    97 |

    98 | 99 |
    100 | Actual time may vary +/- 15 minutes 101 |

    102 |
  • 103 | 104 | 105 |
  • 106 |

    Start Date:

    107 |

    108 | 109 |
    110 | Actual time may vary +/- 15 minutes 111 |

    112 |
  • 113 | 114 | 115 |
  • 116 |

    Start Date:

    117 |

    118 | 119 |

    120 |
  • 121 | 122 | 123 | 124 |
  • 125 |

    End Date:

    126 |

    127 | 128 |
    129 | Actual time may vary +/- 15 minutes 130 |

    131 |
  • 132 | 133 | 134 |
  • 135 |

    Created On:

    136 |

    137 |
  • 138 | 139 | 140 |
141 |
142 | 145 |
146 |
147 |
148 |
149 |
150 | 151 | 152 | -------------------------------------------------------------------------------- /img/overview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/techstreams/TSCron/9a8ddd7571af311faa8b5a2c4c93f3bad7031f7c/img/overview.png -------------------------------------------------------------------------------- /img/restart.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/techstreams/TSCron/9a8ddd7571af311faa8b5a2c4c93f3bad7031f7c/img/restart.png -------------------------------------------------------------------------------- /img/step1-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/techstreams/TSCron/9a8ddd7571af311faa8b5a2c4c93f3bad7031f7c/img/step1-1.png -------------------------------------------------------------------------------- /img/step2-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/techstreams/TSCron/9a8ddd7571af311faa8b5a2c4c93f3bad7031f7c/img/step2-1.png -------------------------------------------------------------------------------- /img/step2-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/techstreams/TSCron/9a8ddd7571af311faa8b5a2c4c93f3bad7031f7c/img/step2-2.png -------------------------------------------------------------------------------- /img/step2-3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/techstreams/TSCron/9a8ddd7571af311faa8b5a2c4c93f3bad7031f7c/img/step2-3.png -------------------------------------------------------------------------------- /img/step3-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/techstreams/TSCron/9a8ddd7571af311faa8b5a2c4c93f3bad7031f7c/img/step3-1.png -------------------------------------------------------------------------------- /img/step3-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/techstreams/TSCron/9a8ddd7571af311faa8b5a2c4c93f3bad7031f7c/img/step3-2.png -------------------------------------------------------------------------------- /img/step4-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/techstreams/TSCron/9a8ddd7571af311faa8b5a2c4c93f3bad7031f7c/img/step4-1.png -------------------------------------------------------------------------------- /img/step4-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/techstreams/TSCron/9a8ddd7571af311faa8b5a2c4c93f3bad7031f7c/img/step4-2.png -------------------------------------------------------------------------------- /img/step5-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/techstreams/TSCron/9a8ddd7571af311faa8b5a2c4c93f3bad7031f7c/img/step5-1.png -------------------------------------------------------------------------------- /img/step5-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/techstreams/TSCron/9a8ddd7571af311faa8b5a2c4c93f3bad7031f7c/img/step5-2.png -------------------------------------------------------------------------------- /img/stop.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/techstreams/TSCron/9a8ddd7571af311faa8b5a2c4c93f3bad7031f7c/img/stop.png -------------------------------------------------------------------------------- /img/view-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/techstreams/TSCron/9a8ddd7571af311faa8b5a2c4c93f3bad7031f7c/img/view-1.png -------------------------------------------------------------------------------- /img/view.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/techstreams/TSCron/9a8ddd7571af311faa8b5a2c4c93f3bad7031f7c/img/view.png -------------------------------------------------------------------------------- /tests/test.gs: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Laura Taylor 3 | * (https://github.com/techstreams/TSCron) 4 | * 5 | * Permission is hereby granted, free of charge, to any person obtaining a copy 6 | * of this software and associated documentation files (the "Software"), to deal 7 | * in the Software without restriction, including without limitation the rights 8 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | * copies of the Software, and to permit persons to whom the Software is 10 | * furnished to do so, subject to the following conditions: 11 | * 12 | * The above copyright notice and this permission notice shall be included in all 13 | * copies or substantial portions of the Software. 14 | * 15 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | * SOFTWARE. 22 | */ 23 | 24 | // Setup QUnit helpers with global object 25 | QUnit.helpers(this); 26 | 27 | 28 | /* 29 | * Create QUnit Testing Dashboard for Google Apps Script 30 | * See https://github.com/simula-innovation/qunit/tree/gas/gas for more information and setup instructions 31 | * 32 | * @param {Object} e - event object passed to doGet() method 33 | * @return {string} html of testing output 34 | */ 35 | function doGet(e) { 36 | QUnit.urlParams(e.parameter); 37 | QUnit.config({ title: 'Unit tests for TSCron' }); 38 | QUnit.load(cases_); 39 | return QUnit.getHtml(); 40 | }; 41 | 42 | 43 | /* 44 | * TSCron Unit Tests Main Function 45 | * 46 | * To perform TSCRon unit testing, change the desired unit test configuration = "true" in the "testConfig" object below 47 | */ 48 | var cases_ = function() { 49 | 50 | // To perform TSCRon unit testing, change the desired unit test configuration = "true" in the "testConfig" object below 51 | var testConfig = { 52 | configuration: false, // Test 'Configuration' Menu 53 | stop: false, // Test 'Stop' Menu 54 | invalidstartend: false, // Test Invalid Start & End Dates 55 | everyminutes: false, // Test 'Every Minutes' Cron Schedule 56 | everyhours: false, // Test 'Every Hours' Cron Schedule 57 | everydays: false, // Test 'Every Days' Cron Schedule 58 | everyweeks: false, // Test 'Every Weeks' Cron Schedule 59 | everymonths: { 60 | standard: false, // Test 'Every Months' Cron Schedule 61 | longno: false, // Test 'Every Months' Cron Schedule - Start Day in a Long Month (Start Date Month Has 31 Days in Month) and "Short Months?" = "No" 62 | longyes: false, // Test 'Every Months' Cron Schedule - Start Day in a Long Month (Start Date Month Has 31 Days in Month) and "Short Months?" = "Yes" 63 | shortno: false, // Test 'Every Months' Cron Schedule - Start Day in a Short Month (Start Date Month Has < 31 days) and "Short Months?" = "No" 64 | shortyes: false, // Test 'Every Months' Cron Schedule - Start Day in a Short Month (Start Date Month Has < 31 days) and "Short Months?" = "Yes" 65 | leapyear: false // Test 'Every Months' Cron Schedule - Next Schedule Month is February in a Leap Year and Scheduled is Last Day of Month 66 | }, 67 | everyyears: { 68 | standard: false, // Test 'Every Years' Cron Schedule 69 | leapyear: false, // Test 'Every Years' Cron Schedule - Schedule Date is February 29th in a Leap Year and Next Schedule Date is February 28th in non Leap Year 70 | leapyes: false, // Test 'Every Years' Cron Schedule - Schedule Date is February 28th in a non Leap Year and "Leap Years?" = "Yes" 71 | leapno: false // Test 'Every Years' Cron Schedule - Schedule Date is February 28th in a non Leap Year and "Leap Years?" = "No" 72 | }, 73 | custom: { 74 | valid: false, // Test 'Custom' Cron Schedule 75 | invalid: false // Test Invalid 'Custom' Cron Schedule 76 | }, 77 | runcron: false, // Test running the cron job function 78 | initialCronJobFunction: 'newTSCron', 79 | additionalCronJobFunction: 'runTSCron', 80 | startCronJobFunction: 'startTSCron', 81 | endCronJobFunction: 'endTSCron', 82 | propsKey: 'tscron' 83 | 84 | } 85 | 86 | 87 | module('TSCron'); 88 | 89 | 90 | // Test Configuration Menu 91 | if (testConfig.configuration === true) { 92 | test('Configuration Menu', function() { 93 | expect(2); 94 | 95 | var tscron = null; 96 | 97 | tscron = new TSCron(moment(), FormApp.getActiveForm()); 98 | 99 | // Remove All Existing Submit Triggers 100 | TestUtil.deleteTriggers(ScriptApp.EventType.ON_FORM_SUBMIT, testConfig.initialCronJobFunction); 101 | 102 | // Test 'Configuration' Menu 103 | tscron.enableCron(); 104 | equal(TestUtil.getTriggers(ScriptApp.EventType.ON_FORM_SUBMIT,testConfig.initialCronJobFunction).length, 1, '1 tscron form submit trigger exists after running configuration menu option'); 105 | 106 | // Test creating mulitple form submit triggers 107 | ScriptApp.newTrigger(testConfig.initialCronJobFunction).forForm(FormApp.getActiveForm()).onFormSubmit().create(); 108 | tscron.enableCron(); 109 | equal(TestUtil.getTriggers(ScriptApp.EventType.ON_FORM_SUBMIT,testConfig.initialCronJobFunction).length, 1, '1 tscron form submit trigger exists after adding a second tscron form submit trigger'); 110 | 111 | }); 112 | } 113 | 114 | // Test Stop Cron Menu 115 | if (testConfig.stop === true) { 116 | test('Stop Menu', function() { 117 | expect(4); 118 | 119 | var tscron = null; 120 | 121 | tscron = new TSCron(moment(), FormApp.getActiveForm()); 122 | 123 | // Remove All Existing Submit Triggers 124 | tscron.stopCron(); 125 | TestUtil.deleteTriggers(ScriptApp.EventType.CLOCK, testConfig.additionalCronJobFunction); 126 | 127 | // Test 'Stop' Menu 128 | ScriptApp.newTrigger(testConfig.additionalCronJobFunction).timeBased().everyHours(1).create(); 129 | tscron.stopCron(); 130 | equal(TestUtil.getTriggers(ScriptApp.EventType.CLOCK, testConfig.additionalCronJobFunction).length, 0, 'tscron hourly trigger deleted after running stop menu option'); 131 | equal(PropertiesService.getScriptProperties().getProperty(testConfig.propsKey), null, 'no "tscron" properties key exists after deleting hourly time trigger'); 132 | 133 | // Test 'Stop' Menu for multiple time-based triggers 134 | ScriptApp.newTrigger(testConfig.additionalCronJobFunction).timeBased().everyHours(1).create(); 135 | ScriptApp.newTrigger(testConfig.additionalCronJobFunction).timeBased().everyDays(1).atHour(8).nearMinute(10).create(); 136 | tscron.stopCron(); 137 | equal(TestUtil.getTriggers(ScriptApp.EventType.CLOCK, testConfig.additionalCronJobFunction).length, 0, 'all tscron time-based triggers deleted after running stop menu option'); 138 | equal(PropertiesService.getScriptProperties().getProperty(testConfig.propsKey), null, 'no "tscron" properties key exists after deleting multiple time trigger'); 139 | 140 | }); 141 | } 142 | 143 | 144 | // Test invalidstartendInvalid Start/End Dates Cron Form Submission 145 | if (testConfig.invalidstartend === true) { 146 | test('Schedule Cron "Every Minutes" With Invalid Start/End Dates', function() { 147 | 148 | expect(7); 149 | 150 | var form = FormApp.getActiveForm(), 151 | now = moment(), 152 | response = null, 153 | tscron = null; 154 | 155 | // Make sure tscron enabled 156 | tscron = new TSCron(now, form); 157 | tscron.enableCron(); 158 | equal(TestUtil.getTriggers(ScriptApp.EventType.ON_FORM_SUBMIT,testConfig.initialCronJobFunction).length, 1, 'tscron is enabled'); 159 | tscron.stopCron(); 160 | 161 | // Test Invalid Start Date with no End Date - Created from Form Submit 162 | TestUtil.createEveryMinute(form, ['Every Minutes', '30', now]); 163 | Utilities.sleep(20000); // To make sure trigger gets setup before testing 164 | equal(TestUtil.getTriggers(ScriptApp.EventType.CLOCK, testConfig.startCronJobFunction).length, 0, '0 "startTSCron" time-based triggers exist after form submit with invalid start date and no end date'); 165 | equal(TestUtil.getTriggers(ScriptApp.EventType.CLOCK, testConfig.endCronJobFunction).length, 0, '0 "endTSCron" time-based triggers exist after form submit with invalid start date and no end date'); 166 | 167 | // Test Valid Start Date with Invalid End Date - Created from Form Submit 168 | TestUtil.createEveryMinute(form, ['Every Minutes', '30', now.clone().add(30, 'm'), now.clone().add(30, 'm')]); 169 | Utilities.sleep(20000); // To make sure trigger gets setup before testing 170 | equal(TestUtil.getTriggers(ScriptApp.EventType.CLOCK, testConfig.startCronJobFunction).length, 0, '0 "startTSCron" time-based triggers exist after form submit with valid start date and invalid end date'); 171 | equal(TestUtil.getTriggers(ScriptApp.EventType.CLOCK, testConfig.endCronJobFunction).length, 0, '0 "endTSCron" time-based triggers exist after form submit with valid start date and invalid end date'); 172 | 173 | // Test Invalid Start Date with Invalid End Date - Created from Form Submit 174 | TestUtil.createEveryMinute(form, ['Every Minutes', '30', now, now.clone().add(45, 'm')]); 175 | Utilities.sleep(20000); // To make sure trigger gets setup before testing 176 | equal(TestUtil.getTriggers(ScriptApp.EventType.CLOCK, testConfig.startCronJobFunction).length, 0, '0 "startTSCron" time-based triggers exist after form submit with invalid start date and invalid end date'); 177 | equal(TestUtil.getTriggers(ScriptApp.EventType.CLOCK, testConfig.endCronJobFunction).length, 0, '0 "endTSCron" time-based triggers exist after form submit with invalid start date and invalid end date'); 178 | 179 | // Cleanup up any cron triggers 180 | tscron.stopCron(); 181 | 182 | }); 183 | } 184 | 185 | 186 | // Test "Every Minutes" Cron Form Submission 187 | if (testConfig.everyminutes === true) { 188 | test('Schedule Cron "Every Minutes"', function() { 189 | expect(19); 190 | 191 | var event = null, 192 | expectedProps = null, 193 | form = FormApp.getActiveForm(), 194 | now = moment(), 195 | props = null, 196 | response = null, 197 | responses = null, 198 | tscron = null; 199 | 200 | // Make sure tscron enabled 201 | tscron = new TSCron(now, form); 202 | tscron.enableCron(); 203 | equal(TestUtil.getTriggers(ScriptApp.EventType.ON_FORM_SUBMIT,testConfig.initialCronJobFunction).length, 1, 'tscron is enabled'); 204 | 205 | // Test Valid Start Date with no End Date - Created from Form Submit 206 | TestUtil.createEveryMinute(form, ['Every Minutes', '30', now.clone().add(30,'m')]); 207 | Utilities.sleep(20000); // To make sure trigger gets setup before testing 208 | equal(TestUtil.getTriggers(ScriptApp.EventType.CLOCK, testConfig.startCronJobFunction).length, 1, '1 "startTSCron" time-based triggers exist after form submit with valid start date and no end date'); 209 | equal(TestUtil.getTriggers(ScriptApp.EventType.CLOCK, testConfig.additionalCronJobFunction).length, 0, '0 "runTSCron" time-based triggers exist after form submit with valid start date and no end date'); 210 | equal(TestUtil.getTriggers(ScriptApp.EventType.CLOCK, testConfig.endCronJobFunction).length, 0, '0 "endTSCron" time-based triggers exist after form submit with valid start date and no end date'); 211 | 212 | // Test Valid Start Date and End Date - Created from Form Submit 213 | TestUtil.createEveryMinute(form, ['Every Minutes', '30', now.clone().add(30,'m'), now.clone().add(2, 'h')]); 214 | Utilities.sleep(20000); // To make sure trigger gets setup before testing 215 | equal(TestUtil.getTriggers(ScriptApp.EventType.CLOCK, testConfig.startCronJobFunction).length, 1, '1 "startTSCron" time-based triggers exist after form submit with valid start date and end date'); 216 | equal(TestUtil.getTriggers(ScriptApp.EventType.CLOCK, testConfig.additionalCronJobFunction).length, 0, '0 "runTSCron" time-based triggers exist after form submit with valid start date and end date'); 217 | equal(TestUtil.getTriggers(ScriptApp.EventType.CLOCK, testConfig.endCronJobFunction).length, 1, '1 "endTSCron" time-based triggers exist after form submit with valid start date and end date'); 218 | 219 | // Test Script Properties with Valid Start Date and End Date 220 | TestUtil.createEveryMinute(form, ['Every Minutes', '30', now.clone().add(30,'m'), now.clone().add(2, 'h')]); 221 | Utilities.sleep(20000); // To make sure trigger gets setup before testing 222 | responses = form.getResponses(); 223 | expectedProps = { 224 | action: "Every Minutes", 225 | params:["30", now.clone().add(30,'m').format('YYYY-MM-DD kk:mm'), now.clone().add(2, 'h').format('YYYY-MM-DD kk:mm')], 226 | id: responses[responses.length-1].getId(), 227 | created: moment(responses[responses.length-1].getTimestamp()).utc().valueOf() 228 | } 229 | props = JSON.parse(PropertiesService.getScriptProperties().getProperty(testConfig.propsKey)); 230 | deepEqual(props, expectedProps, 'tscron script properties matches for "Every Minutes" initial configuration with valid start date and end date' ); 231 | 232 | // Test Schedule Cron "Every 30 Minutes" 233 | event = TestUtil.getUTCEvent(now.clone().add(30,'m').utc()); 234 | tscron.startTSCron(event); 235 | Utilities.sleep(20000); // To make sure trigger gets setup before testing 236 | equal(TestUtil.getTriggers(ScriptApp.EventType.CLOCK, testConfig.startCronJobFunction).length, 0, '0 "startTSCron" time-based triggers exist after start date trigger'); 237 | equal(TestUtil.getTriggers(ScriptApp.EventType.CLOCK, testConfig.additionalCronJobFunction).length, 1, '1 "runTSCron" time-based triggers exist after start date trigger'); 238 | equal(TestUtil.getTriggers(ScriptApp.EventType.CLOCK, testConfig.endCronJobFunction).length, 1, '1 "endTSCron" time-based triggers exist after form start date trigger'); 239 | props = JSON.parse(PropertiesService.getScriptProperties().getProperty(testConfig.propsKey)); 240 | expectedProps.last = moment.utc({y:event.year, M:event.month-1, d:event['day-of-month'], h:event.hour, m:event.minute}).valueOf(); 241 | equal(props.last, expectedProps.last, 'tscron "last" script properties correct after cron runs first time'); 242 | 243 | // Test Reschedule Cron "Every 30 Minutes" 244 | event = TestUtil.getUTCEvent(now.clone().add(60,'m').utc()); 245 | tscron.runTSCron(event); 246 | Utilities.sleep(20000); // To make sure trigger gets setup before testing 247 | equal(TestUtil.getTriggers(ScriptApp.EventType.CLOCK, testConfig.startCronJobFunction).length, 0, '0 "startTSCron" time-based triggers exist after reschedule trigger'); 248 | equal(TestUtil.getTriggers(ScriptApp.EventType.CLOCK, testConfig.additionalCronJobFunction).length, 1, '1 "runTSCron" time-based triggers exist after reschedule trigger'); 249 | equal(TestUtil.getTriggers(ScriptApp.EventType.CLOCK, testConfig.endCronJobFunction).length, 1, '1 "endTSCron" time-based triggers exist after reschedule trigger'); 250 | props = JSON.parse(PropertiesService.getScriptProperties().getProperty(testConfig.propsKey)); 251 | expectedProps.last = moment.utc({y:event.year, M:event.month-1, d:event['day-of-month'], h:event.hour, m:event.minute}).valueOf(); 252 | equal(props.last, expectedProps.last, 'tscron "last" script properties correct after reschedule'); 253 | 254 | // Test End Cron 255 | event = TestUtil.getUTCEvent(now.clone().add(2,'h').utc()); 256 | tscron.endTSCron(event); 257 | Utilities.sleep(20000); 258 | equal(TestUtil.getTriggers(ScriptApp.EventType.CLOCK, testConfig.startCronJobFunction).length, 0, '0 "startTSCron" time-based triggers exist after end date trigger'); 259 | equal(TestUtil.getTriggers(ScriptApp.EventType.CLOCK, testConfig.additionalCronJobFunction).length, 0, '0 "runTSCron" time-based triggers exist after end date trigger'); 260 | equal(TestUtil.getTriggers(ScriptApp.EventType.CLOCK, testConfig.endCronJobFunction).length, 0, '0 "endTSCron" time-based triggers exist after form end date trigger'); 261 | 262 | // Cleanup up cron triggers 263 | tscron.stopCron(); 264 | 265 | 266 | }); 267 | } 268 | 269 | 270 | // Test "Every Hours" Cron Form Submission 271 | if (testConfig.everyhours === true) { 272 | test('Schedule Cron "Every Hours"', function() { 273 | expect(19); 274 | 275 | var event = null, 276 | expectedProps = null, 277 | form = FormApp.getActiveForm(), 278 | now = moment(), 279 | props = null, 280 | response = null, 281 | responses = null, 282 | tscron = null; 283 | 284 | // Make sure tscron enabled 285 | tscron = new TSCron(now, form); 286 | tscron.enableCron(); 287 | equal(TestUtil.getTriggers(ScriptApp.EventType.ON_FORM_SUBMIT,testConfig.initialCronJobFunction).length, 1, 'tscron is enabled'); 288 | 289 | // Test Valid Start Date with no End Date - Created from Form Submit 290 | TestUtil.createEveryHour(form, ['Every Hours', '3', now.clone().add(30,'m')]); 291 | Utilities.sleep(20000); // To make sure trigger gets setup before testing 292 | equal(TestUtil.getTriggers(ScriptApp.EventType.CLOCK, testConfig.startCronJobFunction).length, 1, '1 "startTSCron" time-based triggers exist after form submit with valid start date and no end date'); 293 | equal(TestUtil.getTriggers(ScriptApp.EventType.CLOCK, testConfig.additionalCronJobFunction).length, 0, '0 "runTSCron" time-based triggers exist after form submit with valid start date and no end date'); 294 | equal(TestUtil.getTriggers(ScriptApp.EventType.CLOCK, testConfig.endCronJobFunction).length, 0, '0 "endTSCron" time-based triggers exist after form submit with valid start date and no end date'); 295 | 296 | // Test Valid Start Date and End Date - Created from Form Submit 297 | TestUtil.createEveryHour(form, ['Every Minutes', '30', now.clone().add(30,'m'), now.clone().add(2, 'd')]); 298 | Utilities.sleep(20000); // To make sure trigger gets setup before testing 299 | equal(TestUtil.getTriggers(ScriptApp.EventType.CLOCK, testConfig.startCronJobFunction).length, 1, '1 "startTSCron" time-based triggers exist after form submit with valid start date and end date'); 300 | equal(TestUtil.getTriggers(ScriptApp.EventType.CLOCK, testConfig.additionalCronJobFunction).length, 0, '0 "runTSCron" time-based triggers exist after form submit with valid start date and end date'); 301 | equal(TestUtil.getTriggers(ScriptApp.EventType.CLOCK, testConfig.endCronJobFunction).length, 1, '1 "endTSCron" time-based triggers exist after form submit with valid start date and end date'); 302 | 303 | // Test Script Properties with Valid Start Date and End Date 304 | TestUtil.createEveryHour(form, ['Every Hours', '3', now.clone().add(30,'m'), now.clone().add(2, 'd')]); 305 | Utilities.sleep(20000); // To make sure trigger gets setup before testing 306 | responses = form.getResponses(); 307 | expectedProps = { 308 | action: "Every Hours", 309 | params:["3", now.clone().add(30,'m').format('YYYY-MM-DD kk:mm'), now.clone().add(2, 'd').format('YYYY-MM-DD kk:mm')], 310 | id: responses[responses.length-1].getId(), 311 | created: moment(responses[responses.length-1].getTimestamp()).utc().valueOf() 312 | } 313 | props = JSON.parse(PropertiesService.getScriptProperties().getProperty(testConfig.propsKey)); 314 | deepEqual(props, expectedProps, 'tscron script properties matches for "Every Hours" initial configuration with valid start date and end date' ); 315 | 316 | // Test Schedule Cron "Every 3 Hours" 317 | event = TestUtil.getUTCEvent(now.clone().add(30,'m').utc()); 318 | tscron.startTSCron(event); 319 | Utilities.sleep(20000); // To make sure trigger gets setup before testing 320 | equal(TestUtil.getTriggers(ScriptApp.EventType.CLOCK, testConfig.startCronJobFunction).length, 0, '0 "startTSCron" time-based triggers exist after start date trigger'); 321 | equal(TestUtil.getTriggers(ScriptApp.EventType.CLOCK, testConfig.additionalCronJobFunction).length, 1, '1 "runTSCron" time-based triggers exist after start date trigger'); 322 | equal(TestUtil.getTriggers(ScriptApp.EventType.CLOCK, testConfig.endCronJobFunction).length, 1, '1 "endTSCron" time-based triggers exist" after form start date trigger'); 323 | props = JSON.parse(PropertiesService.getScriptProperties().getProperty(testConfig.propsKey)); 324 | expectedProps.last = moment.utc({y:event.year, M:event.month-1, d:event['day-of-month'], h:event.hour, m:event.minute}).valueOf(); 325 | equal(props.last, expectedProps.last, 'tscron "last" script properties correct after cron runs first time'); 326 | 327 | // Test Reschedule Cron "Every 3 Hours" 328 | event = TestUtil.getUTCEvent(now.clone().add(30, 'm').add(3, 'h').utc()); 329 | tscron.runTSCron(event); 330 | Utilities.sleep(20000); // To make sure trigger gets setup before testing 331 | equal(TestUtil.getTriggers(ScriptApp.EventType.CLOCK, testConfig.startCronJobFunction).length, 0, '0 "startTSCron" time-based triggers exist after reschedule trigger'); 332 | equal(TestUtil.getTriggers(ScriptApp.EventType.CLOCK, testConfig.additionalCronJobFunction).length, 1, '1 "runTSCron" time-based triggers exist after reschedule trigger'); 333 | equal(TestUtil.getTriggers(ScriptApp.EventType.CLOCK, testConfig.endCronJobFunction).length, 1, '1 "endTSCron" time-based triggers exist after reschedule trigger'); 334 | props = JSON.parse(PropertiesService.getScriptProperties().getProperty(testConfig.propsKey)); 335 | expectedProps.last = moment.utc({y:event.year, M:event.month-1, d:event['day-of-month'], h:event.hour, m:event.minute}).valueOf(); 336 | equal(props.last, expectedProps.last, 'tscron "last" script properties correct after reschedule'); 337 | 338 | // Test End Cron 339 | event = TestUtil.getUTCEvent(now.clone().add(2,'d').utc()); 340 | tscron.endTSCron(event); 341 | Utilities.sleep(20000); 342 | equal(TestUtil.getTriggers(ScriptApp.EventType.CLOCK, testConfig.startCronJobFunction).length, 0, '0 "startTSCron" time-based triggers exist after end date trigger'); 343 | equal(TestUtil.getTriggers(ScriptApp.EventType.CLOCK, testConfig.additionalCronJobFunction).length, 0, '0 "runTSCron" time-based triggers exist after end date trigger'); 344 | equal(TestUtil.getTriggers(ScriptApp.EventType.CLOCK, testConfig.endCronJobFunction).length, 0, '0 "endTSCron" time-based triggers exist after form end date trigger'); 345 | 346 | // Cleanup up cron triggers 347 | tscron.stopCron(); 348 | 349 | }); 350 | } 351 | 352 | 353 | // Test "Every Days" Cron Form Submission 354 | if (testConfig.everydays === true) { 355 | test('Schedule Cron "Every Days"', function() { 356 | expect(19); 357 | 358 | var event = null, 359 | expectedProps = null, 360 | form = FormApp.getActiveForm(), 361 | now = moment(), 362 | props = null, 363 | response = null, 364 | responses = null, 365 | tscron = null; 366 | 367 | // Make sure tscron enabled 368 | tscron = new TSCron(now, form); 369 | tscron.enableCron(); 370 | equal(TestUtil.getTriggers(ScriptApp.EventType.ON_FORM_SUBMIT,testConfig.initialCronJobFunction).length, 1, 'tscron is enabled'); 371 | 372 | // Test Valid Start Date with no End Date - Created from Form Submit 373 | TestUtil.createEveryDay(form, ['Every Days', '3', now.clone().add(30,'m')]); 374 | Utilities.sleep(20000); // To make sure trigger gets setup before testing 375 | equal(TestUtil.getTriggers(ScriptApp.EventType.CLOCK, testConfig.startCronJobFunction).length, 1, '1 "startTSCron" time-based triggers exist after form submit with valid start date and no end date'); 376 | equal(TestUtil.getTriggers(ScriptApp.EventType.CLOCK, testConfig.additionalCronJobFunction).length, 0, '0 "runTSCron" time-based triggers exist after form submit with valid start date and no end date'); 377 | equal(TestUtil.getTriggers(ScriptApp.EventType.CLOCK, testConfig.endCronJobFunction).length, 0, '0 "endTSCron" time-based triggers exist after form submit with valid start date and no end date'); 378 | 379 | // Test Valid Start Date and End Date - Created from Form Submit 380 | TestUtil.createEveryDay(form, ['Every Days', '3', now.clone().add(30,'m'), now.clone().add(1, 'w')]); 381 | Utilities.sleep(20000); // To make sure trigger gets setup before testing 382 | equal(TestUtil.getTriggers(ScriptApp.EventType.CLOCK, testConfig.startCronJobFunction).length, 1, '1 "startTSCron" time-based triggers exist after form submit with valid start date and end date'); 383 | equal(TestUtil.getTriggers(ScriptApp.EventType.CLOCK, testConfig.additionalCronJobFunction).length, 0, '0 "runTSCron" time-based triggers exist after form submit with valid start date and end date'); 384 | equal(TestUtil.getTriggers(ScriptApp.EventType.CLOCK, testConfig.endCronJobFunction).length, 1, '1 "endTSCron" time-based triggers exist after form submit with valid start date and end date'); 385 | 386 | // Test Script Properties with Valid Start Date and End Date 387 | TestUtil.createEveryDay(form, ['Every Days', '3', now.clone().add(30,'m'), now.clone().add(1, 'w')]); 388 | Utilities.sleep(20000); // To make sure trigger gets setup before testing 389 | responses = form.getResponses(); 390 | expectedProps = { 391 | action: "Every Days", 392 | params:["3", now.clone().add(30,'m').format('YYYY-MM-DD kk:mm'), now.clone().add(1, 'w').format('YYYY-MM-DD kk:mm')], 393 | id: responses[responses.length-1].getId(), 394 | created: moment(responses[responses.length-1].getTimestamp()).utc().valueOf() 395 | } 396 | props = JSON.parse(PropertiesService.getScriptProperties().getProperty(testConfig.propsKey)); 397 | deepEqual(props, expectedProps, 'tscron script properties matches for "Every Days" initial configuration with valid start date and end date' ); 398 | 399 | // Test Schedule Cron "Every 3 Days" 400 | event = TestUtil.getUTCEvent(now.clone().add(30,'m').utc()); 401 | tscron.startTSCron(event); 402 | Utilities.sleep(20000); // To make sure trigger gets setup before testing 403 | equal(TestUtil.getTriggers(ScriptApp.EventType.CLOCK, testConfig.startCronJobFunction).length, 0, '0 "startTSCron" time-based triggers exist after start date trigger'); 404 | equal(TestUtil.getTriggers(ScriptApp.EventType.CLOCK, testConfig.additionalCronJobFunction).length, 1, '1 "runTSCron" time-based triggers exist after start date trigger'); 405 | equal(TestUtil.getTriggers(ScriptApp.EventType.CLOCK, testConfig.endCronJobFunction).length, 1, '1 "endTSCron" time-based triggers exist after form start date trigger'); 406 | props = JSON.parse(PropertiesService.getScriptProperties().getProperty(testConfig.propsKey)); 407 | expectedProps.last = moment.utc({y:event.year, M:event.month-1, d:event['day-of-month'], h:event.hour, m:event.minute}).valueOf(); 408 | equal(props.last, expectedProps.last, 'tscron "last" script properties correct after cron runs first time'); 409 | 410 | 411 | // Test Reschedule Cron "Every 3 Days" 412 | event = TestUtil.getUTCEvent(now.clone().add(30, 'm').add(3, 'd').utc()); 413 | tscron.runTSCron(event); 414 | Utilities.sleep(20000); // To make sure trigger gets setup before testing 415 | equal(TestUtil.getTriggers(ScriptApp.EventType.CLOCK, testConfig.startCronJobFunction).length, 0, '0 "startTSCron" time-based triggers exist after reschedule trigger'); 416 | equal(TestUtil.getTriggers(ScriptApp.EventType.CLOCK, testConfig.additionalCronJobFunction).length, 1, '1 "runTSCron" time-based triggers exist after reschedule trigger'); 417 | equal(TestUtil.getTriggers(ScriptApp.EventType.CLOCK, testConfig.endCronJobFunction).length, 1, '1 "endTSCron" time-based triggers exist after reschedule trigger'); 418 | props = JSON.parse(PropertiesService.getScriptProperties().getProperty(testConfig.propsKey)); 419 | expectedProps.last = moment.utc({y:event.year, M:event.month-1, d:event['day-of-month'], h:event.hour, m:event.minute}).valueOf(); 420 | equal(props.last, expectedProps.last, 'tscron "last" script properties correct after reschedule'); 421 | 422 | // Test End Cron 423 | event = TestUtil.getUTCEvent(now.clone().add(1,'w').utc()); 424 | tscron.endTSCron(event); 425 | Utilities.sleep(20000); 426 | equal(TestUtil.getTriggers(ScriptApp.EventType.CLOCK, testConfig.startCronJobFunction).length, 0, '0 "startTSCron" time-based triggers exist after end date trigger'); 427 | equal(TestUtil.getTriggers(ScriptApp.EventType.CLOCK, testConfig.additionalCronJobFunction).length, 0, '0 "runTSCron" time-based triggers exist after end date trigger'); 428 | equal(TestUtil.getTriggers(ScriptApp.EventType.CLOCK, testConfig.endCronJobFunction).length, 0, '0 "endTSCron" time-based triggers exist after form end date trigger'); 429 | 430 | // Cleanup up cron triggers 431 | tscron.stopCron(); 432 | 433 | }); 434 | } 435 | 436 | 437 | // Test "Every Weeks" Cron Form Submission 438 | if (testConfig.everyweeks === true) { 439 | test('Schedule Cron "Every Weeks"', function() { 440 | expect(16); 441 | 442 | var event = null, 443 | expectedProps = null, 444 | form = FormApp.getActiveForm(), 445 | now = moment(), 446 | props = null, 447 | response = null, 448 | responses = null, 449 | tscron = null; 450 | 451 | // Make sure tscron enabled 452 | tscron = new TSCron(now, form); 453 | tscron.enableCron(); 454 | equal(TestUtil.getTriggers(ScriptApp.EventType.ON_FORM_SUBMIT,testConfig.initialCronJobFunction).length, 1, 'tscron is enabled'); 455 | 456 | // Test Valid Start Date with no End Date - Created from Form Submit 457 | TestUtil.createEveryWeeks(form, ['Every Weeks', '3', now.clone().add(30,'m')]); 458 | Utilities.sleep(20000); // To make sure trigger gets setup before testing 459 | equal(TestUtil.getTriggers(ScriptApp.EventType.CLOCK, testConfig.startCronJobFunction).length, 1, '1 "startTSCron" time-based triggers exist after form submit with valid start date and no end date'); 460 | equal(TestUtil.getTriggers(ScriptApp.EventType.CLOCK, testConfig.additionalCronJobFunction).length, 0, '0 "runTSCron" time-based triggers exist after form submit with valid start date and no end date'); 461 | equal(TestUtil.getTriggers(ScriptApp.EventType.CLOCK, testConfig.endCronJobFunction).length, 0, '0 "endTSCron" time-based triggers exist after form start date trigger'); 462 | 463 | // Test Script Properties with Valid Start Date and End Date 464 | TestUtil.createEveryWeeks(form, ['Every Weeks', '3', now.clone().add(30,'m'), now.clone().add(2, 'M')]); 465 | Utilities.sleep(20000); // To make sure trigger gets setup before testing 466 | responses = form.getResponses(); 467 | expectedProps = { 468 | action: "Every Weeks", 469 | params:["3", now.clone().add(30,'m').format('YYYY-MM-DD kk:mm'), now.clone().add(2, 'M').format('YYYY-MM-DD kk:mm')], 470 | id: responses[responses.length-1].getId(), 471 | created: moment(responses[responses.length-1].getTimestamp()).utc().valueOf() 472 | } 473 | props = JSON.parse(PropertiesService.getScriptProperties().getProperty(testConfig.propsKey)); 474 | deepEqual(props, expectedProps, 'tscron script properties matches for "Every Weeks" initial configuration with valid start date and end date' ); 475 | 476 | // Test Schedule Cron "Every 3 Weeks" 477 | event = TestUtil.getUTCEvent(now.clone().add(30,'m').utc()); 478 | tscron.startTSCron(event); 479 | Utilities.sleep(20000); // To make sure trigger gets setup before testing 480 | equal(TestUtil.getTriggers(ScriptApp.EventType.CLOCK, testConfig.startCronJobFunction).length, 0, '0 "startTSCron" time-based triggers exist after start date trigger'); 481 | equal(TestUtil.getTriggers(ScriptApp.EventType.CLOCK, testConfig.additionalCronJobFunction).length, 1, '1 "runTSCron" time-based triggers exist after start date trigger'); 482 | equal(TestUtil.getTriggers(ScriptApp.EventType.CLOCK, testConfig.endCronJobFunction).length, 1, '1 "endTSCron" time-based triggers exist after form start date trigger'); 483 | props = JSON.parse(PropertiesService.getScriptProperties().getProperty(testConfig.propsKey)); 484 | expectedProps.last = moment.utc({y:event.year, M:event.month-1, d:event['day-of-month'], h:event.hour, m:event.minute}).valueOf(); 485 | equal(props.last, expectedProps.last, 'tscron "last" script properties correct after cron runs first time'); 486 | 487 | // Test Reschedule Cron "Every 3 Weeks" 488 | event = TestUtil.getUTCEvent(now.clone().add(30, 'm').add(3, 'w').utc()); 489 | tscron.runTSCron(event); 490 | Utilities.sleep(20000); // To make sure trigger gets setup before testing 491 | equal(TestUtil.getTriggers(ScriptApp.EventType.CLOCK, testConfig.startCronJobFunction).length, 0, '0 "startTSCron" time-based triggers exist after reschedule trigger'); 492 | equal(TestUtil.getTriggers(ScriptApp.EventType.CLOCK, testConfig.additionalCronJobFunction).length, 1, '1 "runTSCron" time-based triggers exist after reschedule trigger'); 493 | equal(TestUtil.getTriggers(ScriptApp.EventType.CLOCK, testConfig.endCronJobFunction).length, 1, '1 "endTSCron" time-based triggers exist after reschedule trigger'); 494 | props = JSON.parse(PropertiesService.getScriptProperties().getProperty(testConfig.propsKey)); 495 | expectedProps.last = moment.utc({y:event.year, M:event.month-1, d:event['day-of-month'], h:event.hour, m:event.minute}).valueOf(); 496 | equal(props.last, expectedProps.last, 'tscron "last" script properties correct after reschedule'); 497 | 498 | // Test End Cron 499 | event = TestUtil.getUTCEvent(now.clone().add(2,'M').utc()); 500 | tscron.endTSCron(event); 501 | Utilities.sleep(20000); 502 | equal(TestUtil.getTriggers(ScriptApp.EventType.CLOCK, testConfig.startCronJobFunction).length, 0, '0 "startTSCron" time-based triggers exist after end date trigger'); 503 | equal(TestUtil.getTriggers(ScriptApp.EventType.CLOCK, testConfig.additionalCronJobFunction).length, 0, '0 "runTSCron" time-based triggers exist after end date trigger'); 504 | equal(TestUtil.getTriggers(ScriptApp.EventType.CLOCK, testConfig.endCronJobFunction).length, 0, '0 "endTSCron" time-based triggers exist after form end date trigger'); 505 | 506 | // Cleanup up cron triggers 507 | tscron.stopCron(); 508 | 509 | 510 | }); 511 | } 512 | 513 | 514 | // Test "Every Months" Cron Form Submission 515 | if (testConfig.everymonths.standard === true) { 516 | test('Schedule Cron "Every Months"', function() { 517 | expect(16); 518 | 519 | var event = null, 520 | expectedProps = null, 521 | form = FormApp.getActiveForm(), 522 | now = moment(), 523 | props = null, 524 | response = null, 525 | responses = null, 526 | tscron = null; 527 | 528 | // Make sure tscron enabled 529 | tscron = new TSCron(now, form); 530 | tscron.enableCron(); 531 | equal(TestUtil.getTriggers(ScriptApp.EventType.ON_FORM_SUBMIT,testConfig.initialCronJobFunction).length, 1, 'tscron is enabled'); 532 | 533 | // Test Valid Start Date with no End Date - Created from Form Submit 534 | TestUtil.createEveryMonths(form, ['Every Months', '3', 'No', now.clone().add(30,'m')]); 535 | Utilities.sleep(20000); // To make sure trigger gets setup before testing 536 | equal(TestUtil.getTriggers(ScriptApp.EventType.CLOCK, testConfig.startCronJobFunction).length, 1, '1 "startTSCron" time-based triggers exist after form submit with valid start date and no end date'); 537 | equal(TestUtil.getTriggers(ScriptApp.EventType.CLOCK, testConfig.additionalCronJobFunction).length, 0, '0 "runTSCron" time-based triggers exist after form submit with valid start date and no end date'); 538 | equal(TestUtil.getTriggers(ScriptApp.EventType.CLOCK, testConfig.endCronJobFunction).length, 0, '0 "endTSCron" time-based triggers exist after form start date trigger'); 539 | 540 | // Test Script Properties with Valid Start Date and End Date 541 | TestUtil.createEveryMonths(form, ['Every Months', '3', 'No', now.clone().add(30,'m'), now.clone().add(2, 'y')]); 542 | Utilities.sleep(20000); // To make sure trigger gets setup before testing 543 | responses = form.getResponses(); 544 | expectedProps = { 545 | action: "Every Months", 546 | params:["3", "No", now.clone().add(30,'m').format('YYYY-MM-DD kk:mm'), now.clone().add(2, 'y').format('YYYY-MM-DD kk:mm')], 547 | id: responses[responses.length-1].getId(), 548 | created: moment(responses[responses.length-1].getTimestamp()).utc().valueOf() 549 | } 550 | props = JSON.parse(PropertiesService.getScriptProperties().getProperty(testConfig.propsKey)); 551 | deepEqual(props, expectedProps, 'tscron script properties matches for "Every Months" initial configuration with valid start date and end date' ); 552 | 553 | // Test Schedule Cron "Every 3 Months" 554 | event = TestUtil.getUTCEvent(now.clone().add(30,'m').utc()); 555 | tscron.startTSCron(event); 556 | Utilities.sleep(20000); // To make sure trigger gets setup before testing 557 | equal(TestUtil.getTriggers(ScriptApp.EventType.CLOCK, testConfig.startCronJobFunction).length, 0, '0 "startTSCron" time-based triggers exist after start date trigger'); 558 | equal(TestUtil.getTriggers(ScriptApp.EventType.CLOCK, testConfig.additionalCronJobFunction).length, 1, '1 "runTSCron" time-based triggers exist after start date trigger'); 559 | equal(TestUtil.getTriggers(ScriptApp.EventType.CLOCK, testConfig.endCronJobFunction).length, 1, '1 "endTSCron" time-based triggers exist after form start date trigger'); 560 | props = JSON.parse(PropertiesService.getScriptProperties().getProperty(testConfig.propsKey)); 561 | expectedProps.last = moment.utc({y:event.year, M:event.month-1, d:event['day-of-month'], h:event.hour, m:event.minute}).valueOf(); 562 | equal(props.last, expectedProps.last, 'tscron "last" script properties correct after cron runs first time'); 563 | 564 | // Test Reschedule Cron "Every 3 Months" 565 | event = TestUtil.getUTCEvent(now.clone().add(30, 'm').add(3, 'M').utc()); 566 | tscron.runTSCron(event); 567 | Utilities.sleep(20000); // To make sure trigger gets setup before testing 568 | equal(TestUtil.getTriggers(ScriptApp.EventType.CLOCK, testConfig.startCronJobFunction).length, 0, '0 "startTSCron" time-based triggers exist after reschedule trigger'); 569 | equal(TestUtil.getTriggers(ScriptApp.EventType.CLOCK, testConfig.additionalCronJobFunction).length, 1, '1 "runTSCron" time-based triggers exist after reschedule trigger'); 570 | equal(TestUtil.getTriggers(ScriptApp.EventType.CLOCK, testConfig.endCronJobFunction).length, 1, '1 "endTSCron" time-based triggers exist after reschedule trigger'); 571 | props = JSON.parse(PropertiesService.getScriptProperties().getProperty(testConfig.propsKey)); 572 | expectedProps.last = moment.utc({y:event.year, M:event.month-1, d:event['day-of-month'], h:event.hour, m:event.minute}).valueOf(); 573 | equal(props.last, expectedProps.last, 'tscron "last" script properties correct after reschedule'); 574 | 575 | // Test End Cron 576 | event = TestUtil.getUTCEvent(now.clone().add(2,'y').utc()); 577 | tscron.endTSCron(event); 578 | Utilities.sleep(20000); 579 | equal(TestUtil.getTriggers(ScriptApp.EventType.CLOCK, testConfig.startCronJobFunction).length, 0, '0 "startTSCron" time-based triggers exist after end date trigger'); 580 | equal(TestUtil.getTriggers(ScriptApp.EventType.CLOCK, testConfig.additionalCronJobFunction).length, 0, '0 "runTSCron" time-based triggers exist" after end date trigger'); 581 | equal(TestUtil.getTriggers(ScriptApp.EventType.CLOCK, testConfig.endCronJobFunction).length, 0, '0 "endTSCron" time-based triggers exist" after form end date trigger'); 582 | 583 | // Cleanup up cron triggers 584 | tscron.stopCron(); 585 | 586 | }); 587 | } 588 | 589 | 590 | // Test "Every Months" Where Start Date is in a Long Month (Long Month Has 31 Days) and "Short Months?" = "No" 591 | if (testConfig.everymonths.longno === true) { 592 | test('Schedule Cron Job "Every Months" Where Start Date is in Long Month (Has 31 Days) and "Short Months?" Form Response = "No"', function() { 593 | expect(12); 594 | 595 | var event = null, 596 | expectedProps = null, 597 | form = FormApp.getActiveForm(), 598 | now = moment(), 599 | props = null, 600 | response = null, 601 | responses = null, 602 | tscron = null; 603 | 604 | // Make sure tscron enabled 605 | tscron = new TSCron(now, form); 606 | tscron.enableCron(); 607 | equal(TestUtil.getTriggers(ScriptApp.EventType.ON_FORM_SUBMIT,testConfig.initialCronJobFunction).length, 1, 'tscron is enabled'); 608 | 609 | // Test Script Properties with Valid Start Date and End Date 610 | TestUtil.createEveryMonths(form, ['Every Months', '1', 'No', now.clone().add(1,'y').startOf('year').date(31).hour(13), now.clone().add(1,'y').endOf('year').hour(13).minute(0)]); 611 | Utilities.sleep(20000); // To make sure trigger gets setup before testing 612 | responses = form.getResponses(); 613 | expectedProps = { 614 | action: "Every Months", 615 | params:["1", "No", now.clone().add(1,'y').startOf('year').date(31).hour(13).format('YYYY-MM-DD kk:mm'), now.clone().add(1,'y').endOf('year').hour(13).minute(0).format('YYYY-MM-DD kk:mm')], 616 | id: responses[responses.length-1].getId(), 617 | created: moment(responses[responses.length-1].getTimestamp()).utc().valueOf() 618 | } 619 | props = JSON.parse(PropertiesService.getScriptProperties().getProperty(testConfig.propsKey)); 620 | deepEqual(props, expectedProps, 'tscron script properties matches for "Every Months" initial configuration with valid start date and end date' ); 621 | 622 | // Test Schedule Cron "Every 1 Months" Starting on January 31st of Next Year 623 | event = TestUtil.getUTCEvent(now.clone().add(1,'y').startOf('year').endOf('M').hour(13).minute(0).utc()); 624 | tscron.startTSCron(event); 625 | Utilities.sleep(20000); // To make sure trigger gets setup before testing 626 | equal(TestUtil.getTriggers(ScriptApp.EventType.CLOCK, testConfig.startCronJobFunction).length, 0, '0 "startTSCron" time-based triggers exist after start date trigger'); 627 | equal(TestUtil.getTriggers(ScriptApp.EventType.CLOCK, testConfig.additionalCronJobFunction).length, 1, '1 "runTSCron" time-based triggers exist after start date trigger'); 628 | equal(TestUtil.getTriggers(ScriptApp.EventType.CLOCK, testConfig.endCronJobFunction).length, 1, '1 "endTSCron" time-based triggers exist after form start date trigger'); 629 | props = JSON.parse(PropertiesService.getScriptProperties().getProperty(testConfig.propsKey)); 630 | expectedProps.last = moment.utc({y:event.year, M:event.month-1, d:event['day-of-month'], h:event.hour, m:event.minute}).valueOf(); 631 | equal(props.last, expectedProps.last, 'tscron "last" script properties correct after cron runs first time'); 632 | equal(moment.utc(props.next).format('MMMM D, YYYY h:mm A'), moment.utc(now.clone().add(1,'y').startOf('year').add(1, 'M').endOf('M').hour(13).minute(0)).format('MMMM D, YYYY h:mm A'), 'tscron "next" script properties correct after cron runs first time'); 633 | 634 | // Test Reschedule Cron "Every 1 Months" Starting on last day of Feb of Next Year 635 | event = TestUtil.getUTCEvent(now.clone().add(1,'y').startOf('year').add(1, 'M').endOf('M').hour(13).minute(0).utc()); 636 | tscron.runTSCron(event); 637 | Utilities.sleep(20000); // To make sure trigger gets setup before testing 638 | equal(TestUtil.getTriggers(ScriptApp.EventType.CLOCK, testConfig.startCronJobFunction).length, 0, '0 "startTSCron" time-based triggers exist after reschedule trigger'); 639 | equal(TestUtil.getTriggers(ScriptApp.EventType.CLOCK, testConfig.additionalCronJobFunction).length, 1, '1 "runTSCron" time-based triggers exist after reschedule trigger'); 640 | equal(TestUtil.getTriggers(ScriptApp.EventType.CLOCK, testConfig.endCronJobFunction).length, 1, '1 "endTSCron" time-based triggers exist after reschedule trigger'); 641 | props = JSON.parse(PropertiesService.getScriptProperties().getProperty(testConfig.propsKey)); 642 | expectedProps.last = moment.utc({y:event.year, M:event.month-1, d:event['day-of-month'], h:event.hour, m:event.minute}).valueOf(); 643 | equal(props.last, expectedProps.last, 'tscron "last" script properties correct after reschedule'); 644 | equal(moment.utc(props.next).format('MMMM D, YYYY h:mm A'), moment.utc(now.clone().add(1,'y').startOf('year').add(2, 'M').endOf('M').hour(13).minute(0)).format('MMMM D, YYYY h:mm A'), 'tscron "next" script properties correct after cron runs first time'); 645 | 646 | // Cleanup up cron triggers 647 | tscron.stopCron(); 648 | 649 | }); 650 | } 651 | 652 | // Test "Every Months" Where Start Date is in a Long Month (Long Month Has 31 Days) and "Short Months?" = "Yes" 653 | if (testConfig.everymonths.longyes === true) { 654 | test('Schedule Cron Job "Every Months" Where Start Date is in Long Month (Has 31 Days) and "Short Months?" Form Response = "Yes"', function() { 655 | expect(12); 656 | 657 | var event = null, 658 | expectedProps = null, 659 | form = FormApp.getActiveForm(), 660 | now = moment(), 661 | props = null, 662 | response = null, 663 | responses = null, 664 | tscron = null; 665 | 666 | // Make sure tscron enabled 667 | tscron = new TSCron(now, form); 668 | tscron.enableCron(); 669 | equal(TestUtil.getTriggers(ScriptApp.EventType.ON_FORM_SUBMIT,testConfig.initialCronJobFunction).length, 1, 'tscron is enabled'); 670 | 671 | // Test Script Properties with Valid Start Date and End Date 672 | TestUtil.createEveryMonths(form, ['Every Months', '1', 'Yes', now.clone().add(1,'y').startOf('year').date(31).hour(13), now.clone().add(1,'y').endOf('year').hour(13).minute(0)]); 673 | Utilities.sleep(20000); // To make sure trigger gets setup before testing 674 | responses = form.getResponses(); 675 | expectedProps = { 676 | action: "Every Months", 677 | params:["1", "Yes", now.clone().add(1,'y').startOf('year').date(31).hour(13).format('YYYY-MM-DD kk:mm'), now.clone().add(1,'y').endOf('year').hour(13).minute(0).format('YYYY-MM-DD kk:mm')], 678 | id: responses[responses.length-1].getId(), 679 | created: moment(responses[responses.length-1].getTimestamp()).utc().valueOf() 680 | } 681 | props = JSON.parse(PropertiesService.getScriptProperties().getProperty(testConfig.propsKey)); 682 | deepEqual(props, expectedProps, 'tscron script properties matches for "Every Months" initial configuration with valid start date and end date' ); 683 | 684 | // Test Schedule Cron "Every 1 Months" Starting on January 31st of Next Year 685 | event = TestUtil.getUTCEvent(now.clone().add(1,'y').startOf('year').endOf('M').hour(13).minute(0).utc()); 686 | tscron.startTSCron(event); 687 | Utilities.sleep(20000); // To make sure trigger gets setup before testing 688 | equal(TestUtil.getTriggers(ScriptApp.EventType.CLOCK, testConfig.startCronJobFunction).length, 0, '0 "startTSCron" time-based triggers exist after start date trigger'); 689 | equal(TestUtil.getTriggers(ScriptApp.EventType.CLOCK, testConfig.additionalCronJobFunction).length, 1, '1 "runTSCron" time-based triggers exist after start date trigger'); 690 | equal(TestUtil.getTriggers(ScriptApp.EventType.CLOCK, testConfig.endCronJobFunction).length, 1, '1 "endTSCron" time-based triggers exist after form start date trigger'); 691 | props = JSON.parse(PropertiesService.getScriptProperties().getProperty(testConfig.propsKey)); 692 | expectedProps.last = moment.utc({y:event.year, M:event.month-1, d:event['day-of-month'], h:event.hour, m:event.minute}).valueOf(); 693 | equal(props.last, expectedProps.last, 'tscron "last" script properties correct after cron runs first time'); 694 | equal(moment.utc(props.next).format('MMMM D, YYYY h:mm A'), moment.utc(now.clone().add(1,'y').startOf('year').add(1, 'M').endOf('M').hour(13).minute(0)).format('MMMM D, YYYY h:mm A'), 'tscron "next" script properties correct after cron runs first time'); 695 | 696 | // Test Reschedule Cron "Every 1 Months" Starting on last day of Feb (28 or 29) of Next Year 697 | event = TestUtil.getUTCEvent(now.clone().add(1,'y').startOf('year').add(1, 'M').endOf('M').hour(13).minute(0).utc()); 698 | tscron.runTSCron(event); 699 | Utilities.sleep(20000); // To make sure trigger gets setup before testing 700 | equal(TestUtil.getTriggers(ScriptApp.EventType.CLOCK, testConfig.startCronJobFunction).length, 0, '0 "startTSCron" time-based triggers exist after reschedule trigger'); 701 | equal(TestUtil.getTriggers(ScriptApp.EventType.CLOCK, testConfig.additionalCronJobFunction).length, 1, '1 "runTSCron" time-based triggers exist after reschedule trigger'); 702 | equal(TestUtil.getTriggers(ScriptApp.EventType.CLOCK, testConfig.endCronJobFunction).length, 1, '1 "endTSCron" time-based triggers exist after reschedule trigger'); 703 | props = JSON.parse(PropertiesService.getScriptProperties().getProperty(testConfig.propsKey)); 704 | expectedProps.last = moment.utc({y:event.year, M:event.month-1, d:event['day-of-month'], h:event.hour, m:event.minute}).valueOf(); 705 | equal(props.last, expectedProps.last, 'tscron "last" script properties correct after reschedule'); 706 | equal(moment.utc(props.next).format('MMMM D, YYYY h:mm A'), moment.utc(now.clone().add(1,'y').startOf('year').add(2, 'M').endOf('M').hour(13).minute(0)).format('MMMM D, YYYY h:mm A'), 'tscron "next" script properties correct after reschedule'); 707 | 708 | // Cleanup up cron triggers 709 | tscron.stopCron(); 710 | 711 | }); 712 | } 713 | 714 | 715 | // Test "Every Months" Where Start Day in a Short Month (Start Date Month Has < 31 days) and "Short Months?" = "No" 716 | if (testConfig.everymonths.shortno === true) { 717 | test('Schedule Cron Job "Every Months" Where Start Day in a Short Month (Start Date Month Has < 31 days) and "Short Months?" Form Response = "No"', function() { 718 | expect(12); 719 | 720 | var event = null, 721 | expectedProps = null, 722 | form = FormApp.getActiveForm(), 723 | now = moment(), 724 | props = null, 725 | response = null, 726 | responses = null, 727 | tscron = null; 728 | 729 | // Make sure tscron enabled 730 | tscron = new TSCron(now, form); 731 | tscron.enableCron(); 732 | equal(TestUtil.getTriggers(ScriptApp.EventType.ON_FORM_SUBMIT,testConfig.initialCronJobFunction).length, 1, 'tscron is enabled'); 733 | 734 | // Test Script Properties with Valid Start Date and End Date - Start Date is Feb 28th 735 | TestUtil.createEveryMonths(form, ['Every Months', '1', 'No', now.clone().add(1,'y').startOf('year').add(1, 'M').endOf('M').hour(13).minute(0), now.clone().add(1,'y').endOf('year').hour(13).minute(0)]); 736 | Utilities.sleep(20000); // To make sure trigger gets setup before testing 737 | responses = form.getResponses(); 738 | expectedProps = { 739 | action: "Every Months", 740 | params:["1", "No", now.clone().add(1,'y').startOf('year').add(1,'M').date(28).hour(13).format('YYYY-MM-DD kk:mm'), now.clone().add(1,'y').endOf('year').hour(13).minute(0).format('YYYY-MM-DD kk:mm')], 741 | id: responses[responses.length-1].getId(), 742 | created: moment(responses[responses.length-1].getTimestamp()).utc().valueOf() 743 | } 744 | props = JSON.parse(PropertiesService.getScriptProperties().getProperty(testConfig.propsKey)); 745 | deepEqual(props, expectedProps, 'tscron script properties matches for "Every Months" initial configuration with valid start date and end date' ); 746 | 747 | // Test Schedule Cron "Every 1 Months" Starting on last day of Feb next year 748 | event = TestUtil.getUTCEvent(now.clone().add(1,'y').startOf('year').add(1, 'M').endOf('M').hour(13).minute(0).utc()); 749 | tscron.startTSCron(event); 750 | Utilities.sleep(20000); // To make sure trigger gets setup before testing 751 | equal(TestUtil.getTriggers(ScriptApp.EventType.CLOCK, testConfig.startCronJobFunction).length, 0, '0 "startTSCron" time-based triggers exist after start date trigger'); 752 | equal(TestUtil.getTriggers(ScriptApp.EventType.CLOCK, testConfig.additionalCronJobFunction).length, 1, '1 "runTSCron" time-based triggers exist after start date trigger'); 753 | equal(TestUtil.getTriggers(ScriptApp.EventType.CLOCK, testConfig.endCronJobFunction).length, 1, '1 "endTSCron" time-based triggers exist after form start date trigger'); 754 | props = JSON.parse(PropertiesService.getScriptProperties().getProperty(testConfig.propsKey)); 755 | expectedProps.last = moment.utc({y:event.year, M:event.month-1, d:event['day-of-month'], h:event.hour, m:event.minute}).valueOf(); 756 | equal(props.last, expectedProps.last, 'tscron "last" script properties correct after cron runs first time'); 757 | equal(moment.utc(props.next).format('MMMM D, YYYY h:mm A'), moment.utc(now.clone().add(1,'y').startOf('year').add(1, 'M').endOf('M').add(1,'M').hour(13).minute(0)).format('MMMM D, YYYY h:mm A'), 'tscron "next" script properties correct after cron runs first time'); 758 | 759 | // Test Reschedule Cron "Every 1 Months" on Schedule Date in Mar of Next Year (Based on Start Date from Feb) 760 | event = TestUtil.getUTCEvent(now.clone().add(1,'y').startOf('year').add(1, 'M').endOf('M').add(1,'M').hour(13).minute(0).utc()); 761 | tscron.runTSCron(event); 762 | Utilities.sleep(20000); // To make sure trigger gets setup before testing 763 | equal(TestUtil.getTriggers(ScriptApp.EventType.CLOCK, testConfig.startCronJobFunction).length, 0, '0 "startTSCron" time-based triggers exist after reschedule trigger'); 764 | equal(TestUtil.getTriggers(ScriptApp.EventType.CLOCK, testConfig.additionalCronJobFunction).length, 1, '1 "runTSCron" time-based triggers exist after reschedule trigger'); 765 | equal(TestUtil.getTriggers(ScriptApp.EventType.CLOCK, testConfig.endCronJobFunction).length, 1, '1 "endTSCron" time-based triggers exist after reschedule trigger'); 766 | props = JSON.parse(PropertiesService.getScriptProperties().getProperty(testConfig.propsKey)); 767 | expectedProps.last = moment.utc({y:event.year, M:event.month-1, d:event['day-of-month'], h:event.hour, m:event.minute}).valueOf(); 768 | equal(props.last, expectedProps.last, 'tscron "last" script properties correct after reschedule'); 769 | equal(moment.utc(props.next).format('MMMM D, YYYY h:mm A'), moment.utc(now.clone().add(1,'y').startOf('year').add(1, 'M').endOf('M').add(2, 'M').hour(13).minute(0)).format('MMMM D, YYYY h:mm A'), 'tscron "next" script properties correct after reschedule'); 770 | 771 | // Cleanup up cron triggers 772 | tscron.stopCron(); 773 | 774 | }); 775 | } 776 | 777 | 778 | 779 | // Test "Every Months" Where Start Day in a Short Month (Start Date Month Has < 31 days) and "Short Months?" = "Yes" 780 | if (testConfig.everymonths.shortyes === true) { 781 | test('Schedule Cron Job "Every Months" Where Start Day in a Short Month (Start Date Month Has < 31 days) and "Short Months?" Form Response = "Yes"', function() { 782 | expect(12); 783 | 784 | var event = null, 785 | expectedProps = null, 786 | form = FormApp.getActiveForm(), 787 | now = moment(), 788 | props = null, 789 | response = null, 790 | responses = null, 791 | tscron = null; 792 | 793 | // Make sure tscron enabled 794 | tscron = new TSCron(now, form); 795 | tscron.enableCron(); 796 | equal(TestUtil.getTriggers(ScriptApp.EventType.ON_FORM_SUBMIT,testConfig.initialCronJobFunction).length, 1, 'tscron is enabled'); 797 | 798 | // Test Script Properties with Valid Start Date and End Date - Start Date is Feb 28th 799 | TestUtil.createEveryMonths(form, ['Every Months', '1', 'Yes', now.clone().add(1,'y').startOf('year').add(1, 'M').endOf('M').hour(11).minute(0), now.clone().add(1,'y').endOf('year').hour(11).minute(0)]); 800 | Utilities.sleep(20000); // To make sure trigger gets setup before testing 801 | responses = form.getResponses(); 802 | expectedProps = { 803 | action: "Every Months", 804 | params:["1", "Yes", now.clone().add(1,'y').startOf('year').add(1,'M').endOf('M').hour(11).minute(0).format('YYYY-MM-DD kk:mm'), now.clone().add(1,'y').endOf('year').hour(11).minute(0).format('YYYY-MM-DD kk:mm')], 805 | id: responses[responses.length-1].getId(), 806 | created: moment(responses[responses.length-1].getTimestamp()).utc().valueOf() 807 | } 808 | props = JSON.parse(PropertiesService.getScriptProperties().getProperty(testConfig.propsKey)); 809 | deepEqual(props, expectedProps, 'tscron script properties matches for "Every Months" initial configuration with valid start date and end date' ); 810 | 811 | // Test Schedule Cron "Every 1 Months" Starting on last day of Feb next year 812 | event = TestUtil.getUTCEvent(now.clone().add(1,'y').startOf('year').add(1,'M').endOf('M').hour(11).minute(0).utc()); 813 | tscron.startTSCron(event); 814 | Utilities.sleep(20000); // To make sure trigger gets setup before testing 815 | equal(TestUtil.getTriggers(ScriptApp.EventType.CLOCK, testConfig.startCronJobFunction).length, 0, '0 "startTSCron" time-based triggers exist after start date trigger'); 816 | equal(TestUtil.getTriggers(ScriptApp.EventType.CLOCK, testConfig.additionalCronJobFunction).length, 1, '1 "runTSCron" time-based triggers exist after start date trigger'); 817 | equal(TestUtil.getTriggers(ScriptApp.EventType.CLOCK, testConfig.endCronJobFunction).length, 1, '1 "endTSCron" time-based triggers exist after form start date trigger'); 818 | props = JSON.parse(PropertiesService.getScriptProperties().getProperty(testConfig.propsKey)); 819 | expectedProps.last = moment.utc({y:event.year, M:event.month-1, d:event['day-of-month'], h:event.hour, m:event.minute}).valueOf(); 820 | equal(props.last, expectedProps.last, 'tscron "last" script properties correct after cron runs first time'); 821 | equal(moment.utc(props.next).format('MMMM D, YYYY h:mm A'), moment.utc(now.clone().add(1,'y').startOf('year').add(2,'M').endOf('M').hour(11).minute(0)).format('MMMM D, YYYY h:mm A'), 'tscron "next" script properties correct after cron runs first time'); 822 | 823 | // Test Reschedule Cron "Every 1 Months" on Schedule Date in Mar of Next Year (Based on Start Date from Feb) 824 | event = TestUtil.getUTCEvent(now.clone().add(1,'y').startOf('year').add(1,'M').endOf('M').add(1,'M').hour(11).minute(0).utc()); 825 | tscron.runTSCron(event); 826 | Utilities.sleep(20000); // To make sure trigger gets setup before testing 827 | equal(TestUtil.getTriggers(ScriptApp.EventType.CLOCK, testConfig.startCronJobFunction).length, 0, '0 "startTSCron" time-based triggers exist after reschedule trigger'); 828 | equal(TestUtil.getTriggers(ScriptApp.EventType.CLOCK, testConfig.additionalCronJobFunction).length, 1, '1 "runTSCron" time-based triggers exist after reschedule trigger'); 829 | equal(TestUtil.getTriggers(ScriptApp.EventType.CLOCK, testConfig.endCronJobFunction).length, 1, '1 "endTSCron" time-based triggers exist after reschedule trigger'); 830 | props = JSON.parse(PropertiesService.getScriptProperties().getProperty(testConfig.propsKey)); 831 | expectedProps.last = moment.utc({y:event.year, M:event.month-1, d:event['day-of-month'], h:event.hour, m:event.minute}).valueOf(); 832 | equal(props.last, expectedProps.last, 'tscron "last" script properties correct after reschedule'); 833 | equal(moment.utc(props.next).format('MMMM D, YYYY h:mm A'), moment.utc(now.clone().add(1,'y').startOf('year').add(3, 'M').endOf('M').hour(11).minute(0)).format('MMMM D, YYYY h:mm A'), 'tscron "next" script properties correct after reschedule'); 834 | 835 | // Cleanup up cron triggers 836 | tscron.stopCron(); 837 | 838 | }); 839 | } 840 | 841 | // Test "Every Months" Where Next Schedule Month is February in a Leap Year and Scheduled is Last Day of Month 842 | if (testConfig.everymonths.leapyear === true) { 843 | test('Schedule Cron Job "Every Months" Where Next Schedule Month is February in a Leap Year and Scheduled is Last Day of Month', function() { 844 | expect(7); 845 | 846 | var event = null, 847 | expectedProps = null, 848 | form = FormApp.getActiveForm(), 849 | now = moment(), 850 | props = null, 851 | response = null, 852 | responses = null, 853 | tscron = null; 854 | 855 | // Make sure tscron enabled 856 | tscron = new TSCron(now, form); 857 | tscron.enableCron(); 858 | equal(TestUtil.getTriggers(ScriptApp.EventType.ON_FORM_SUBMIT,testConfig.initialCronJobFunction).length, 1, 'tscron is enabled'); 859 | 860 | // Test Script Properties with Valid Start Date and End Date 861 | TestUtil.createEveryMonths(form, ['Every Months', '1', 'No', TestUtil.getLeapYear(now).startOf('year').date(31).hour(13), TestUtil.getLeapYear(now).endOf('year').hour(13).minute(0)]); 862 | Utilities.sleep(20000); // To make sure trigger gets setup before testing 863 | responses = form.getResponses(); 864 | expectedProps = { 865 | action: "Every Months", 866 | params:["1", "No", TestUtil.getLeapYear(now).startOf('year').date(31).hour(13).format('YYYY-MM-DD kk:mm'), TestUtil.getLeapYear(now).endOf('year').hour(13).minute(0).format('YYYY-MM-DD kk:mm')], 867 | id: responses[responses.length-1].getId(), 868 | created: moment(responses[responses.length-1].getTimestamp()).utc().valueOf() 869 | } 870 | props = JSON.parse(PropertiesService.getScriptProperties().getProperty(testConfig.propsKey)); 871 | deepEqual(props, expectedProps, 'tscron script properties matches for "Every Months" initial configuration with valid start date and end date' ); 872 | 873 | // Test Schedule Cron "Every 1 Months" Starting on January 31st of Next Leap Year 874 | event = TestUtil.getUTCEvent(TestUtil.getLeapYear(now).startOf('year').endOf('M').hour(13).minute(0).utc()); 875 | tscron.startTSCron(event); 876 | Utilities.sleep(20000); // To make sure trigger gets setup before testing 877 | equal(TestUtil.getTriggers(ScriptApp.EventType.CLOCK, testConfig.startCronJobFunction).length, 0, '0 "startTSCron" time-based triggers exist after start date trigger'); 878 | equal(TestUtil.getTriggers(ScriptApp.EventType.CLOCK, testConfig.additionalCronJobFunction).length, 1, '1 "runTSCron" time-based triggers exist after start date trigger'); 879 | equal(TestUtil.getTriggers(ScriptApp.EventType.CLOCK, testConfig.endCronJobFunction).length, 1, '1 "endTSCron" time-based triggers exist after form start date trigger'); 880 | props = JSON.parse(PropertiesService.getScriptProperties().getProperty(testConfig.propsKey)); 881 | expectedProps.last = moment.utc({y:event.year, M:event.month-1, d:event['day-of-month'], h:event.hour, m:event.minute}).valueOf(); 882 | equal(props.last, expectedProps.last, 'tscron "last" script properties correct after cron runs first time'); 883 | equal(moment.utc(props.next).format('MMMM D, YYYY h:mm A'), moment.utc(TestUtil.getLeapYear(now).startOf('year').add(1, 'M').endOf('M').hour(13).minute(0)).format('MMMM D, YYYY h:mm A'), 'tscron "next" script properties correct after cron runs first time'); 884 | 885 | // Cleanup up cron triggers 886 | tscron.stopCron(); 887 | 888 | }); 889 | } 890 | 891 | 892 | // Test "Every Years" Cron Schedule 893 | if (testConfig.everyyears.standard === true) { 894 | test('Schedule Cron "Every Years"', function() { 895 | expect(10); 896 | 897 | var event = null, 898 | expectedProps = null, 899 | form = FormApp.getActiveForm(), 900 | now = moment(), 901 | props = null, 902 | response = null, 903 | responses = null, 904 | tscron = null; 905 | 906 | // Make sure tscron enabled 907 | tscron = new TSCron(now, form); 908 | tscron.enableCron(); 909 | equal(TestUtil.getTriggers(ScriptApp.EventType.ON_FORM_SUBMIT,testConfig.initialCronJobFunction).length, 1, 'tscron is enabled'); 910 | 911 | // Test Script Properties with Valid Start Date and End Date 912 | TestUtil.createEveryYears(form, ['Every Years', '1', 'No', now.clone().add(1,'y').startOf('y').date(31).hour(13).minute(0), now.clone().add(5,'y').endOf('y').hour(13).minute(0)]); 913 | Utilities.sleep(20000); // To make sure trigger gets setup before testing 914 | responses = form.getResponses(); 915 | expectedProps = { 916 | action: "Every Years", 917 | params:["1", "No", now.clone().add(1,'y').startOf('year').date(31).hour(13).format('YYYY-MM-DD kk:mm'), now.clone().add(5,'y').endOf('year').hour(13).minute(0).format('YYYY-MM-DD kk:mm')], 918 | id: responses[responses.length-1].getId(), 919 | created: moment(responses[responses.length-1].getTimestamp()).utc().valueOf() 920 | } 921 | props = JSON.parse(PropertiesService.getScriptProperties().getProperty(testConfig.propsKey)); 922 | deepEqual(props, expectedProps, 'tscron script properties matches for "Every Years" initial configuration with valid start date and end date' ); 923 | 924 | // Test Schedule Cron "Every 1 Years" Starting on January 31st of Next Year 925 | event = TestUtil.getUTCEvent(now.clone().add(1,'y').startOf('y').date(31).hour(13).minute(0).utc()); 926 | tscron.startTSCron(event); 927 | Utilities.sleep(20000); // To make sure trigger gets setup before testing 928 | equal(TestUtil.getTriggers(ScriptApp.EventType.CLOCK, testConfig.startCronJobFunction).length, 0, '0 "startTSCron" time-based triggers exist after start date trigger'); 929 | equal(TestUtil.getTriggers(ScriptApp.EventType.CLOCK, testConfig.additionalCronJobFunction).length, 1, '1 "runTSCron" time-based triggers exist after start date trigger'); 930 | equal(TestUtil.getTriggers(ScriptApp.EventType.CLOCK, testConfig.endCronJobFunction).length, 1, '1 "endTSCron" time-based triggers exist after form start date trigger'); 931 | props = JSON.parse(PropertiesService.getScriptProperties().getProperty(testConfig.propsKey)); 932 | expectedProps.last = moment.utc({y:event.year, M:event.month-1, d:event['day-of-month'], h:event.hour, m:event.minute}).valueOf(); 933 | equal(props.last, expectedProps.last, 'tscron "last" script properties correct after cron runs first time'); 934 | 935 | // Test Schedule Cron "Every 1 Years" Starting on January 31st in 2 Years 936 | event = TestUtil.getUTCEvent(now.clone().add(2,'y').startOf('y').date(31).hour(13).minute(0).utc()); 937 | tscron.startTSCron(event); 938 | Utilities.sleep(20000); // To make sure trigger gets setup before testing 939 | equal(TestUtil.getTriggers(ScriptApp.EventType.CLOCK, testConfig.startCronJobFunction).length, 0, '0 "startTSCron" time-based triggers exist after start date trigger'); 940 | equal(TestUtil.getTriggers(ScriptApp.EventType.CLOCK, testConfig.additionalCronJobFunction).length, 1, '1 "runTSCron" time-based triggers exist after start date trigger'); 941 | equal(TestUtil.getTriggers(ScriptApp.EventType.CLOCK, testConfig.endCronJobFunction).length, 1, '1 "endTSCron" time-based triggers exist after form start date trigger'); 942 | props = JSON.parse(PropertiesService.getScriptProperties().getProperty(testConfig.propsKey)); 943 | expectedProps.last = moment.utc({y:event.year, M:event.month-1, d:event['day-of-month'], h:event.hour, m:event.minute}).valueOf(); 944 | equal(props.last, expectedProps.last, 'tscron "last" script properties correct after cron runs first time'); 945 | 946 | // Cleanup up cron triggers 947 | tscron.stopCron(); 948 | 949 | }); 950 | } 951 | 952 | 953 | 954 | // Test "Every Years" Cron Schedule Test Where Start Date is February 29th in a Leap Year and Next Schedule Date is February 28th in non Leap Year 955 | if (testConfig.everyyears.leapyear === true) { 956 | test('Schedule Cron "Every Years" Where Start Date is February 29th in a Leap Year and Next Schedule Date is February 28th in non Leap Year', function() { 957 | expect(7); 958 | 959 | var event = null, 960 | expectedProps = null, 961 | form = FormApp.getActiveForm(), 962 | now = moment(), 963 | props = null, 964 | response = null, 965 | responses = null, 966 | tscron = null; 967 | 968 | // Make sure tscron enabled 969 | tscron = new TSCron(now, form); 970 | tscron.enableCron(); 971 | equal(TestUtil.getTriggers(ScriptApp.EventType.ON_FORM_SUBMIT,testConfig.initialCronJobFunction).length, 1, 'tscron is enabled'); 972 | 973 | // Test Script Properties with Valid Start Date and End Date 974 | TestUtil.createEveryYears(form, ['Every Years', '1', 'Yes', TestUtil.getLeapYear(now).startOf('year').add(1,'M').endOf('M').hour(11).minute(0), TestUtil.getLeapYear(now).add(5,'y').endOf('y').hour(11).minute(0)]); 975 | Utilities.sleep(20000); // To make sure trigger gets setup before testing 976 | responses = form.getResponses(); 977 | expectedProps = { 978 | action: "Every Years", 979 | params:["1", "Yes", TestUtil.getLeapYear(now).startOf('year').add(1,'M').endOf('M').hour(11).minute(0).format('YYYY-MM-DD kk:mm'), TestUtil.getLeapYear(now).add(5,'y').endOf('year').hour(11).minute(0).format('YYYY-MM-DD kk:mm')], 980 | id: responses[responses.length-1].getId(), 981 | created: moment(responses[responses.length-1].getTimestamp()).utc().valueOf() 982 | } 983 | props = JSON.parse(PropertiesService.getScriptProperties().getProperty(testConfig.propsKey)); 984 | deepEqual(props, expectedProps, 'tscron script properties matches for "Every Years" initial configuration with valid start date and end date' ); 985 | 986 | // Test Schedule Cron "Every 1 Years" Starting on Feb 29th in Leap Year 987 | event = TestUtil.getUTCEvent(TestUtil.getLeapYear(now).startOf('year').add(1,'M').endOf('M').hour(11).minute(0).utc()); 988 | tscron.startTSCron(event); 989 | Utilities.sleep(20000); // To make sure trigger gets setup before testing 990 | equal(TestUtil.getTriggers(ScriptApp.EventType.CLOCK, testConfig.startCronJobFunction).length, 0, '0 "startTSCron" time-based triggers exist after start date trigger'); 991 | equal(TestUtil.getTriggers(ScriptApp.EventType.CLOCK, testConfig.additionalCronJobFunction).length, 1, '1 "runTSCron" time-based triggers exist after start date trigger'); 992 | equal(TestUtil.getTriggers(ScriptApp.EventType.CLOCK, testConfig.endCronJobFunction).length, 1, '1 "endTSCron" time-based triggers exist after form start date trigger'); 993 | props = JSON.parse(PropertiesService.getScriptProperties().getProperty(testConfig.propsKey)); 994 | expectedProps.last = moment.utc({y:event.year, M:event.month-1, d:event['day-of-month'], h:event.hour, m:event.minute}).valueOf(); 995 | equal(props.last, expectedProps.last, 'tscron "last" script properties correct after cron runs first time'); 996 | equal(moment.utc(props.next).format('MMMM D, YYYY h:mm A'), moment.utc(TestUtil.getLeapYear(now).add(1,'y').startOf('year').add(1,'M').endOf('M').hour(11).minute(0)).format('MMMM D, YYYY h:mm A'), 'tscron "next" script properties correct after cron runs first time'); 997 | 998 | // Cleanup up cron triggers 999 | tscron.stopCron(); 1000 | 1001 | }); 1002 | } 1003 | 1004 | // Test 'Every Years' Cron Schedule Where Schedule Date is February 28th in a non Leap Year and "Leap Years?" = "Yes" 1005 | if (testConfig.everyyears.leapyes === true) { 1006 | test('Schedule Cron "Every Years" Where Schedule Date is February 28th in a non Leap Year and "Leap Years?" Form Response = "Yes"', function() { 1007 | expect(7); 1008 | 1009 | var event = null, 1010 | expectedProps = null, 1011 | form = FormApp.getActiveForm(), 1012 | now = moment(), 1013 | props = null, 1014 | response = null, 1015 | responses = null, 1016 | tscron = null; 1017 | 1018 | // Make sure tscron enabled 1019 | tscron = new TSCron(now, form); 1020 | tscron.enableCron(); 1021 | equal(TestUtil.getTriggers(ScriptApp.EventType.ON_FORM_SUBMIT,testConfig.initialCronJobFunction).length, 1, 'tscron is enabled'); 1022 | 1023 | // Test Script Properties with Valid Start Date and End Date 1024 | TestUtil.createEveryYears(form, ['Every Years', '1', 'Yes', TestUtil.getLeapYear(now).add(3,'y').startOf('year').add(1,'M').endOf('M').hour(11).minute(0), TestUtil.getLeapYear(now).add(5,'y').endOf('y').hour(11).minute(0)]); 1025 | Utilities.sleep(20000); // To make sure trigger gets setup before testing 1026 | responses = form.getResponses(); 1027 | expectedProps = { 1028 | action: "Every Years", 1029 | params:["1", "Yes", TestUtil.getLeapYear(now).add(3,'y').startOf('year').add(1,'M').endOf('M').hour(11).minute(0).format('YYYY-MM-DD kk:mm'), TestUtil.getLeapYear(now).add(5,'y').endOf('year').hour(11).minute(0).format('YYYY-MM-DD kk:mm')], 1030 | id: responses[responses.length-1].getId(), 1031 | created: moment(responses[responses.length-1].getTimestamp()).utc().valueOf() 1032 | } 1033 | props = JSON.parse(PropertiesService.getScriptProperties().getProperty(testConfig.propsKey)); 1034 | deepEqual(props, expectedProps, 'tscron script properties matches for "Every Years" initial configuration with valid start date and end date' ); 1035 | 1036 | // Test Schedule Cron "Every 1 Years" Starting on Feb 28th in non Leap Year with "Leap Years?" = "Yes" 1037 | event = TestUtil.getUTCEvent(TestUtil.getLeapYear(now).add(3,'y').startOf('year').add(1,'M').endOf('M').hour(11).minute(0).utc()); 1038 | tscron.startTSCron(event); 1039 | Utilities.sleep(20000); // To make sure trigger gets setup before testing 1040 | equal(TestUtil.getTriggers(ScriptApp.EventType.CLOCK, testConfig.startCronJobFunction).length, 0, '0 "startTSCron" time-based triggers exist after start date trigger'); 1041 | equal(TestUtil.getTriggers(ScriptApp.EventType.CLOCK, testConfig.additionalCronJobFunction).length, 1, '1 "runTSCron" time-based triggers exist after start date trigger'); 1042 | equal(TestUtil.getTriggers(ScriptApp.EventType.CLOCK, testConfig.endCronJobFunction).length, 1, '1 "endTSCron" time-based triggers exist after form start date trigger'); 1043 | props = JSON.parse(PropertiesService.getScriptProperties().getProperty(testConfig.propsKey)); 1044 | expectedProps.last = moment.utc({y:event.year, M:event.month-1, d:event['day-of-month'], h:event.hour, m:event.minute}).valueOf(); 1045 | equal(props.last, expectedProps.last, 'tscron "last" script properties correct after cron runs first time'); 1046 | equal(moment.utc(props.next).format('MMMM D, YYYY h:mm A'), moment.utc(TestUtil.getLeapYear(now).add(4,'y').startOf('year').add(1,'M').endOf('M').hour(11).minute(0)).format('MMMM D, YYYY h:mm A'), 'tscron "next" script properties correct after cron runs first time'); 1047 | 1048 | // Cleanup up cron triggers 1049 | tscron.stopCron(); 1050 | 1051 | }); 1052 | } 1053 | 1054 | 1055 | // Test 'Every Years' Cron Schedule Where Schedule Date is February 28th in a non Leap Year and "Leap Years?" = "No" 1056 | if (testConfig.everyyears.leapno === true) { 1057 | test('Schedule Cron "Every Years" Where Schedule Date is February 28th in a non Leap Year and "Leap Years?" Form Response = "No"', function() { 1058 | expect(7); 1059 | 1060 | var event = null, 1061 | expectedProps = null, 1062 | form = FormApp.getActiveForm(), 1063 | now = moment(), 1064 | props = null, 1065 | response = null, 1066 | responses = null, 1067 | tscron = null; 1068 | 1069 | // Make sure tscron enabled 1070 | tscron = new TSCron(now, form); 1071 | tscron.enableCron(); 1072 | equal(TestUtil.getTriggers(ScriptApp.EventType.ON_FORM_SUBMIT,testConfig.initialCronJobFunction).length, 1, 'tscron is enabled'); 1073 | 1074 | // Test Script Properties with Valid Start Date and End Date 1075 | TestUtil.createEveryYears(form, ['Every Years', '1', 'No', TestUtil.getLeapYear(now).add(3,'y').startOf('year').add(1,'M').endOf('M').hour(11).minute(0), TestUtil.getLeapYear(now).add(5,'y').endOf('y').hour(11).minute(0)]); 1076 | Utilities.sleep(20000); // To make sure trigger gets setup before testing 1077 | responses = form.getResponses(); 1078 | expectedProps = { 1079 | action: "Every Years", 1080 | params:["1", "No", TestUtil.getLeapYear(now).add(3,'y').startOf('year').add(1,'M').endOf('M').hour(11).minute(0).format('YYYY-MM-DD kk:mm'), TestUtil.getLeapYear(now).add(5,'y').endOf('year').hour(11).minute(0).format('YYYY-MM-DD kk:mm')], 1081 | id: responses[responses.length-1].getId(), 1082 | created: moment(responses[responses.length-1].getTimestamp()).utc().valueOf() 1083 | } 1084 | props = JSON.parse(PropertiesService.getScriptProperties().getProperty(testConfig.propsKey)); 1085 | deepEqual(props, expectedProps, 'tscron script properties matches for "Every Years" initial configuration with valid start date and end date' ); 1086 | 1087 | // Test Schedule Cron "Every 1 Years" Starting on Feb 28th in non Leap Year with "Leap Years?" = "No" 1088 | event = TestUtil.getUTCEvent(TestUtil.getLeapYear(now).add(3,'y').startOf('y').add(1,'M').endOf('M').hour(11).minute(0).utc()); 1089 | tscron.startTSCron(event); 1090 | Utilities.sleep(20000); // To make sure trigger gets setup before testing 1091 | equal(TestUtil.getTriggers(ScriptApp.EventType.CLOCK, testConfig.startCronJobFunction).length, 0, '0 "startTSCron" time-based triggers exist after start date trigger'); 1092 | equal(TestUtil.getTriggers(ScriptApp.EventType.CLOCK, testConfig.additionalCronJobFunction).length, 1, '1 "runTSCron" time-based triggers exist after start date trigger'); 1093 | equal(TestUtil.getTriggers(ScriptApp.EventType.CLOCK, testConfig.endCronJobFunction).length, 1, '1 "endTSCron" time-based triggers exist after form start date trigger'); 1094 | props = JSON.parse(PropertiesService.getScriptProperties().getProperty(testConfig.propsKey)); 1095 | expectedProps.last = moment.utc({y:event.year, M:event.month-1, d:event['day-of-month'], h:event.hour, m:event.minute}).valueOf(); 1096 | expectedProps.next = moment.utc(TestUtil.getLeapYear(now).add(4,'y').startOf('y').add(1,'M').date(28).hour(11).minute(0)).valueOf(); 1097 | equal(props.last, expectedProps.last, 'tscron "last" script properties correct after cron runs first time'); 1098 | equal(moment.utc(props.next).format('MMMM D, YYYY h:mm A'), moment.utc(TestUtil.getLeapYear(now).add(4,'y').startOf('y').add(1,'M').date(28).hour(11).minute(0)).format('MMMM D, YYYY h:mm A'), 'tscron "next" script properties correct after cron runs first time'); 1099 | 1100 | // Cleanup up cron triggers 1101 | tscron.stopCron(); 1102 | 1103 | }); 1104 | } 1105 | 1106 | 1107 | // Test "Custom" Valid Date Cron Form Submission 1108 | if (testConfig.custom.valid === true) { 1109 | test('Schedule Cron for "Custom" Valid Date', function() { 1110 | expect(5); 1111 | 1112 | var event = null, 1113 | expectedProps = null, 1114 | d = moment().add(1, 'd'), 1115 | form = FormApp.getActiveForm(), 1116 | props = null, 1117 | response = null, 1118 | responses = null, 1119 | tscron = null; 1120 | 1121 | // Make sure tscron enabled 1122 | tscron = new TSCron(moment(), form); 1123 | tscron.enableCron(); 1124 | equal(TestUtil.getTriggers(ScriptApp.EventType.ON_FORM_SUBMIT,testConfig.initialCronJobFunction).length, 1, 'tscron is enabled'); 1125 | 1126 | // Test Script Properties with Valid Custom Date 1127 | TestUtil.createCustom(form, ['Custom', d]); 1128 | Utilities.sleep(20000); // To make sure trigger gets setup before testing 1129 | responses = form.getResponses(); 1130 | expectedProps = { 1131 | action: "Custom", 1132 | params:[d.format('YYYY-MM-DD kk:mm')], 1133 | id: responses[responses.length-1].getId(), 1134 | created: moment(responses[responses.length-1].getTimestamp()).utc().valueOf() 1135 | } 1136 | props = JSON.parse(PropertiesService.getScriptProperties().getProperty(testConfig.propsKey)); 1137 | deepEqual(props, expectedProps, 'tscron script properties matches for "Custom" initial configuration with valid date' ); 1138 | 1139 | // Test Cron at Schedule Date 1140 | event = TestUtil.getUTCEvent(d.utc()); 1141 | tscron.startTSCron(event); 1142 | Utilities.sleep(20000); // To make sure trigger gets setup before testing 1143 | equal(TestUtil.getTriggers(ScriptApp.EventType.CLOCK, testConfig.startCronJobFunction).length, 0, '0 "startTSCron" time-based triggers exist after "Custom" Schedule Date'); 1144 | equal(TestUtil.getTriggers(ScriptApp.EventType.CLOCK, testConfig.additionalCronJobFunction).length, 0, '0 "runTSCron" time-based triggers exist after "Custom" Schedule Date'); 1145 | equal(TestUtil.getTriggers(ScriptApp.EventType.CLOCK, testConfig.endCronJobFunction).length, 0, '0 "endTSCron" time-based triggers exist after "Custom" Schedule Date'); 1146 | 1147 | // Cleanup up cron triggers 1148 | tscron.stopCron(); 1149 | 1150 | }); 1151 | 1152 | } 1153 | 1154 | 1155 | // Test "Custom" Invalid Date Cron Form Submission 1156 | if (testConfig.custom.invalid === true) { 1157 | test('Schedule Cron for "Custom" Invalid Date', function() { 1158 | expect(4); 1159 | 1160 | var event = null, 1161 | expectedProps = null, 1162 | d = moment(), 1163 | form = FormApp.getActiveForm(), 1164 | props = null, 1165 | response = null, 1166 | responses = null, 1167 | tscron = null; 1168 | 1169 | // Make sure tscron enabled 1170 | tscron = new TSCron(moment(), form); 1171 | tscron.enableCron(); 1172 | equal(TestUtil.getTriggers(ScriptApp.EventType.ON_FORM_SUBMIT,testConfig.initialCronJobFunction).length, 1, 'tscron is enabled'); 1173 | 1174 | // Test Script Properties with Invalid Date 1175 | TestUtil.createCustom(form, ['Custom', d]); 1176 | equal(TestUtil.getTriggers(ScriptApp.EventType.CLOCK, testConfig.startCronJobFunction).length, 0, '0 "startTSCron" time-based triggers "Custom" Schedule Date'); 1177 | equal(TestUtil.getTriggers(ScriptApp.EventType.CLOCK, testConfig.additionalCronJobFunction).length, 0, '0 "runTSCron" time-based triggers exist after "Custom" Schedule Date'); 1178 | equal(TestUtil.getTriggers(ScriptApp.EventType.CLOCK, testConfig.endCronJobFunction).length, 0, '0 "endTSCron" time-based triggers exist after "Custom" Schedule Date'); 1179 | 1180 | // Cleanup up cron triggers 1181 | tscron.stopCron(); 1182 | 1183 | }); 1184 | 1185 | } 1186 | 1187 | 1188 | 1189 | // Test Running the Cron Job Function 1190 | // Assumes form contains one text user defined parameter with title 'Phrase' 1191 | // Assumes cronJob() function contains - return(params.length); 1192 | if (testConfig.runcron === true) { 1193 | test('Test Cron Job Function Call', function() { 1194 | expect(5); 1195 | 1196 | var event = null, 1197 | expectedProps = null, 1198 | form = FormApp.getActiveForm(), 1199 | now = moment(), 1200 | props = null, 1201 | response = null, 1202 | responses = null, 1203 | tscron = null; 1204 | 1205 | // Make sure tscron enabled 1206 | tscron = new TSCron(moment(), form); 1207 | tscron.enableCron(); 1208 | equal(TestUtil.getTriggers(ScriptApp.EventType.ON_FORM_SUBMIT,testConfig.initialCronJobFunction).length, 1, 'tscron is enabled'); 1209 | 1210 | // Test Valid Start Date with no End Date - Created from Form Submit 1211 | TestUtil.createUserDefined(form, ['Hello World!', 'Every Weeks', '3', now.clone().add(30,'m')]); 1212 | Utilities.sleep(20000); // To make sure trigger gets setup before testing 1213 | equal(TestUtil.getTriggers(ScriptApp.EventType.CLOCK, testConfig.startCronJobFunction).length, 1, '1 "startTSCron" time-based triggers exist after form submit with valid start date and no end date'); 1214 | equal(TestUtil.getTriggers(ScriptApp.EventType.CLOCK, testConfig.additionalCronJobFunction).length, 0, '0 "runTSCron" time-based triggers exist after form submit with valid start date and no end date'); 1215 | equal(TestUtil.getTriggers(ScriptApp.EventType.CLOCK, testConfig.endCronJobFunction).length, 0, '0 "endTSCron" time-based triggers exist after form start date trigger'); 1216 | 1217 | event = TestUtil.getUTCEvent(now.clone().add(30,'m').utc()); 1218 | tscron.startTSCron(event); 1219 | equal(cronJob(event, tscron.getUserDefinedItemResponses()), 1, 'Correct number of user defined params sent to cronJob(e,params) function'); 1220 | 1221 | 1222 | // Cleanup up cron triggers 1223 | tscron.stopCron(); 1224 | 1225 | }); 1226 | 1227 | } 1228 | 1229 | 1230 | 1231 | } 1232 | 1233 | 1234 | /* 1235 | * Various TSCron Unit Test Utility Helper Functions 1236 | */ 1237 | var TestUtil = { 1238 | createCustom: function(form, params) { 1239 | var response = form.createResponse(); 1240 | response.withItemResponse(form.getItemById(TestUtil.getItemId(form, FormApp.ItemType.LIST, 'Run TSCron')).asListItem().createResponse(params[0])); 1241 | response.withItemResponse(form.getItemById(TestUtil.getItemId(form, FormApp.ItemType.DATETIME, 'Custom')).asDateTimeItem().createResponse(TestUtil.getScheduleDate(params[1]))); 1242 | response.submit(); 1243 | }, 1244 | createEveryDay: function(form, params) { 1245 | var response = form.createResponse(); 1246 | response.withItemResponse(form.getItemById(TestUtil.getItemId(form, FormApp.ItemType.LIST, 'Run TSCron')).asListItem().createResponse(params[0])); 1247 | response.withItemResponse(form.getItemById(TestUtil.getItemId(form, FormApp.ItemType.TEXT, 'Every Days')).asTextItem().createResponse(params[1])); 1248 | response.withItemResponse(form.getItemById(TestUtil.getItemId(form, FormApp.ItemType.DATETIME, 'Start On')).asDateTimeItem().createResponse(TestUtil.getScheduleDate(params[2]))); 1249 | if (params[3]) { 1250 | response.withItemResponse(form.getItemById(TestUtil.getItemId(form, FormApp.ItemType.DATETIME, 'End On')).asDateTimeItem().createResponse(TestUtil.getScheduleDate(params[3]))); 1251 | } 1252 | response.submit(); 1253 | }, 1254 | createEveryHour: function(form, params) { 1255 | var response = form.createResponse(); 1256 | response.withItemResponse(form.getItemById(TestUtil.getItemId(form, FormApp.ItemType.LIST, 'Run TSCron')).asListItem().createResponse(params[0])); 1257 | response.withItemResponse(form.getItemById(TestUtil.getItemId(form, FormApp.ItemType.TEXT, 'Every Hours')).asTextItem().createResponse(params[1])); 1258 | response.withItemResponse(form.getItemById(TestUtil.getItemId(form, FormApp.ItemType.DATETIME, 'Start On')).asDateTimeItem().createResponse(TestUtil.getScheduleDate(params[2]))); 1259 | if (params[3]) { 1260 | response.withItemResponse(form.getItemById(TestUtil.getItemId(form, FormApp.ItemType.DATETIME, 'End On')).asDateTimeItem().createResponse(TestUtil.getScheduleDate(params[3]))); 1261 | } 1262 | response.submit(); 1263 | }, 1264 | createEveryMinute: function(form, params) { 1265 | var response = form.createResponse(); 1266 | response.withItemResponse(form.getItemById(TestUtil.getItemId(form, FormApp.ItemType.LIST, 'Run TSCron')).asListItem().createResponse(params[0])); 1267 | response.withItemResponse(form.getItemById(TestUtil.getItemId(form, FormApp.ItemType.LIST, 'Every Minutes')).asListItem().createResponse(params[1])); 1268 | response.withItemResponse(form.getItemById(TestUtil.getItemId(form, FormApp.ItemType.DATETIME, 'Start On')).asDateTimeItem().createResponse(TestUtil.getScheduleDate(params[2]))); 1269 | if (params[3]) { 1270 | response.withItemResponse(form.getItemById(TestUtil.getItemId(form, FormApp.ItemType.DATETIME, 'End On')).asDateTimeItem().createResponse(TestUtil.getScheduleDate(params[3]))); 1271 | } 1272 | response.submit(); 1273 | }, 1274 | createEveryMonths: function(form, params) { 1275 | var response = form.createResponse(); 1276 | response.withItemResponse(form.getItemById(TestUtil.getItemId(form, FormApp.ItemType.LIST, 'Run TSCron')).asListItem().createResponse(params[0])); 1277 | response.withItemResponse(form.getItemById(TestUtil.getItemId(form, FormApp.ItemType.TEXT, 'Every Months')).asTextItem().createResponse(params[1])); 1278 | response.withItemResponse(form.getItemById(TestUtil.getItemId(form, FormApp.ItemType.MULTIPLE_CHOICE, 'Short Months?')).asMultipleChoiceItem().createResponse(params[2])); 1279 | response.withItemResponse(form.getItemById(TestUtil.getItemId(form, FormApp.ItemType.DATETIME, 'Start On')).asDateTimeItem().createResponse(TestUtil.getScheduleDate(params[3]))); 1280 | if (params[4]) { 1281 | response.withItemResponse(form.getItemById(TestUtil.getItemId(form, FormApp.ItemType.DATETIME, 'End On')).asDateTimeItem().createResponse(TestUtil.getScheduleDate(params[4]))); 1282 | } 1283 | response.submit(); 1284 | }, 1285 | createEveryWeeks: function(form, params) { 1286 | var response = form.createResponse(); 1287 | response.withItemResponse(form.getItemById(TestUtil.getItemId(form, FormApp.ItemType.LIST, 'Run TSCron')).asListItem().createResponse(params[0])); 1288 | response.withItemResponse(form.getItemById(TestUtil.getItemId(form, FormApp.ItemType.TEXT, 'Every Weeks')).asTextItem().createResponse(params[1])); 1289 | response.withItemResponse(form.getItemById(TestUtil.getItemId(form, FormApp.ItemType.DATETIME, 'Start On')).asDateTimeItem().createResponse(TestUtil.getScheduleDate(params[2]))); 1290 | if (params[3]) { 1291 | response.withItemResponse(form.getItemById(TestUtil.getItemId(form, FormApp.ItemType.DATETIME, 'End On')).asDateTimeItem().createResponse(TestUtil.getScheduleDate(params[3]))); 1292 | } 1293 | response.submit(); 1294 | }, 1295 | createEveryYears: function(form, params) { 1296 | var response = form.createResponse(); 1297 | response.withItemResponse(form.getItemById(TestUtil.getItemId(form, FormApp.ItemType.LIST, 'Run TSCron')).asListItem().createResponse(params[0])); 1298 | response.withItemResponse(form.getItemById(TestUtil.getItemId(form, FormApp.ItemType.TEXT, 'Every Years')).asTextItem().createResponse(params[1])); 1299 | response.withItemResponse(form.getItemById(TestUtil.getItemId(form, FormApp.ItemType.MULTIPLE_CHOICE, 'Leap Years?')).asMultipleChoiceItem().createResponse(params[2])); 1300 | response.withItemResponse(form.getItemById(TestUtil.getItemId(form, FormApp.ItemType.DATETIME, 'Start On')).asDateTimeItem().createResponse(TestUtil.getScheduleDate(params[3]))); 1301 | if (params[4]) { 1302 | response.withItemResponse(form.getItemById(TestUtil.getItemId(form, FormApp.ItemType.DATETIME, 'End On')).asDateTimeItem().createResponse(TestUtil.getScheduleDate(params[4]))); 1303 | } 1304 | response.submit(); 1305 | }, 1306 | createUserDefined: function(form, params) { 1307 | var response = form.createResponse(); 1308 | response.withItemResponse(form.getItemById(TestUtil.getItemId(form, FormApp.ItemType.TEXT, 'Phrase')).asTextItem().createResponse(params[0])); 1309 | response.withItemResponse(form.getItemById(TestUtil.getItemId(form, FormApp.ItemType.LIST, 'Run TSCron')).asListItem().createResponse(params[1])); 1310 | response.withItemResponse(form.getItemById(TestUtil.getItemId(form, FormApp.ItemType.TEXT, 'Every Weeks')).asTextItem().createResponse(params[2])); 1311 | response.withItemResponse(form.getItemById(TestUtil.getItemId(form, FormApp.ItemType.DATETIME, 'Start On')).asDateTimeItem().createResponse(TestUtil.getScheduleDate(params[3]))); 1312 | if (params[4]) { 1313 | response.withItemResponse(form.getItemById(TestUtil.getItemId(form, FormApp.ItemType.DATETIME, 'End On')).asDateTimeItem().createResponse(TestUtil.getScheduleDate(params[4]))); 1314 | } 1315 | response.submit(); 1316 | }, 1317 | deleteTriggers: function(type, functionName) { 1318 | ScriptApp.getProjectTriggers().forEach(function(trigger) { 1319 | if ((trigger.getEventType() === type && trigger.getHandlerFunction() === functionName )) { 1320 | ScriptApp.deleteTrigger(trigger); 1321 | } 1322 | }); 1323 | }, 1324 | getItemArrayByName: function(form, name) { 1325 | var itemArray = []; 1326 | form.getItems().forEach(function(item) { 1327 | if (item.getTitle() === name) { 1328 | itemArray.push(item); 1329 | } 1330 | }); 1331 | return itemArray; 1332 | }, 1333 | getItemId: function(form, type, name) { 1334 | var id = null; 1335 | form.getItems(type).forEach(function(item) { 1336 | if (item.getTitle() === name) { 1337 | id = item.getId(); 1338 | } 1339 | }) 1340 | return id; 1341 | }, 1342 | getLeapYear: function(now) { 1343 | var i = 1, 1344 | year = now.clone(); 1345 | if (year.isLeapYear()) { 1346 | return year; 1347 | } else { 1348 | do { 1349 | year = now.clone().add(i,'y'); 1350 | i++; 1351 | } while(!year.isLeapYear()) 1352 | } 1353 | return year; 1354 | }, 1355 | getScheduleDate: function(date) { 1356 | var seconds = 0; 1357 | return new Date(Date.UTC(date.year(), date.month(), date.date(), date.hour(), date.minute(), seconds)); 1358 | }, 1359 | getTriggers: function(type, functionName) { 1360 | return ScriptApp.getProjectTriggers().filter(function(trigger) { 1361 | if ((trigger.getEventType() === type && trigger.getHandlerFunction() === functionName )) { 1362 | return trigger; 1363 | } 1364 | }); 1365 | }, 1366 | getUTCEvent: function(utc) { 1367 | return { 1368 | "year": utc.year(), 1369 | "month": utc.month() + 1, 1370 | "day-of-month": utc.date(), 1371 | "day-of-week": utc.day(), 1372 | "hour": utc.hour(), 1373 | "minute": utc.minute(), 1374 | "timezone": 'UTC' 1375 | 1376 | } 1377 | } 1378 | } 1379 | -------------------------------------------------------------------------------- /tscron.gs: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Laura Taylor 3 | * (https://github.com/techstreams/TSCron) 4 | * 5 | * Permission is hereby granted, free of charge, to any person obtaining a copy 6 | * of this software and associated documentation files (the "Software"), to deal 7 | * in the Software without restriction, including without limitation the rights 8 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | * copies of the Software, and to permit persons to whom the Software is 10 | * furnished to do so, subject to the following conditions: 11 | * 12 | * The above copyright notice and this permission notice shall be included in all 13 | * copies or substantial portions of the Software. 14 | * 15 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | * SOFTWARE. 22 | */ 23 | 24 | /* 25 | * Add a custom menu to the active form 26 | */ 27 | function onOpen() { 28 | FormApp.getUi().createMenu('TSCron') 29 | .addItem('🕜 Configure', 'enableCron') 30 | .addItem('❌ Stop', 'stopCron') 31 | .addItem('👓 Status', 'showStatus') 32 | .addToUi(); 33 | }; 34 | 35 | /* 36 | * Enable form submit trigger 37 | */ 38 | function enableCron() { 39 | var tscron = new TSCron(moment(), FormApp.getActiveForm()); 40 | tscron.enableCron(); 41 | FormApp.getUi().alert('TSCron Configuration Complete.\nSubmit a response to the form to start TSCron.\n\nClick "Ok" to continue.'); 42 | }; 43 | 44 | /* 45 | * Stop cron scheduler 46 | */ 47 | function stopCron() { 48 | var response, tscron, ui; 49 | ui = FormApp.getUi(); 50 | response = ui.alert(' ', 'TSCron will stop any currently running cron jobs.\n\nWould you like to continue?', FormApp.getUi().ButtonSet.YES_NO); 51 | if (response == ui.Button.YES) { 52 | tscron = new TSCron(moment(), FormApp.getActiveForm()); 53 | tscron.stopCron(); 54 | ui.alert('TSCron stopped all currently running cron jobs.\n\nClick "Ok" to continue'); 55 | } else { 56 | ui.alert('TSCron "Stop" action was canceled.\n\nClick "Ok" to continue'); 57 | } 58 | }; 59 | 60 | /* 61 | * Schedule a new cron when a new form response is submitted 62 | * @param {Object} trigger event object 63 | */ 64 | function newTSCron(e) { 65 | var resp = e.response; 66 | var form = e.source; 67 | var tscron = new TSCron(moment(), form, resp); 68 | tscron.newTSCron(); 69 | }; 70 | 71 | /* 72 | * Start a scheduled cron on its associated start date 73 | * @param {Object} trigger event object 74 | */ 75 | function startTSCron(e) { 76 | var form = FormApp.getActiveForm(); 77 | var tscron = new TSCron(moment(), form); 78 | tscron.startTSCron(e); 79 | cronJob(e, tscron.getUserDefinedItemResponses()); 80 | }; 81 | 82 | /* 83 | * Reschedule cron on its associated time schedule and call user defined "cronJob()" function 84 | * @param {Object} trigger event object 85 | */ 86 | function runTSCron(e) { 87 | var form = FormApp.getActiveForm(); 88 | var tscron = new TSCron(moment(), form); 89 | tscron.runTSCron(e); 90 | cronJob(e, tscron.getUserDefinedItemResponses()); 91 | }; 92 | 93 | /* 94 | * End cron on its associated end date 95 | * @param {Object} trigger event object 96 | */ 97 | function endTSCron(e) { 98 | var form = FormApp.getActiveForm(); 99 | var tscron = new TSCron(moment(), form); 100 | tscron.endTSCron(); 101 | }; 102 | 103 | /* 104 | * Show cron scheduler status 105 | */ 106 | function showStatus() { 107 | var tscron = new TSCron(moment(), FormApp.getActiveForm()); 108 | tscron.showStatus(); 109 | }; 110 | 111 | /* 112 | * TSCron 113 | * Assumes moment.js global object available 114 | */ 115 | (function() { 116 | 117 | /* 118 | * TSCron 119 | * @class 120 | */ 121 | return this.TSCron = (function() { 122 | 123 | /* 124 | * @constructor 125 | * @param {Moment} now - Moment which represents the time the constructor is called 126 | * @param {Form} form - current Form object 127 | * @param {FormResponse} response - current Form Response object 128 | * @return {TSCron} this object for chaining 129 | */ 130 | function TSCron(now, form, response1) { 131 | this.now = now; 132 | this.form = form; 133 | this.response = response1 != null ? response1 : null; 134 | this.config = null; 135 | this.firstFormCronElement = 'Run TSCron'; 136 | this.formSubmitFunction = 'newTSCron'; 137 | this.cronFunction = 'runTSCron'; 138 | this.startFunction = 'startTSCron'; 139 | this.endFunction = 'endTSCron'; 140 | this.propertiesKey = 'tscron'; 141 | this; 142 | } 143 | 144 | 145 | /* 146 | * Schedule a new cron when a new form response is submitted 147 | * @return {TSCron} this object for chaining 148 | */ 149 | 150 | TSCron.prototype.newTSCron = function() { 151 | var err; 152 | try { 153 | this.stopCron(); 154 | this.config = this.getCronConfigFromForm_(); 155 | if (this.config) { 156 | this.determineStartEndDates_(); 157 | this.setScriptProperties_(); 158 | } else { 159 | Logger.log('TSCron.configureNewCronJob(): Unable to get TSCron configuration from form submission.'); 160 | throw new Error('TSCron.configureNewCronJob(): Unable to get TSCron configuration from form submission.'); 161 | } 162 | } catch (error) { 163 | err = error; 164 | this.stopCron(); 165 | this.sendErrorMsg_(err); 166 | } 167 | return this; 168 | }; 169 | 170 | 171 | /* 172 | * Enable form submit trigger 173 | * @return {TSCron} this object for chaining 174 | */ 175 | 176 | TSCron.prototype.enableCron = function() { 177 | var cronSubmitTriggers; 178 | cronSubmitTriggers = ScriptApp.getProjectTriggers().filter((function(_this) { 179 | return function(trigger) { 180 | return trigger.getEventType() === ScriptApp.EventType.ON_FORM_SUBMIT && trigger.getHandlerFunction() === _this.formSubmitFunction; 181 | }; 182 | })(this)); 183 | if (cronSubmitTriggers.length < 1) { 184 | cronSubmitTriggers.push(ScriptApp.newTrigger(this.formSubmitFunction).forForm(this.form).onFormSubmit().create()); 185 | } 186 | if (cronSubmitTriggers.length > 1) { 187 | cronSubmitTriggers.forEach((function(_this) { 188 | return function(trigger, index) { 189 | if (index > 0) { 190 | return ScriptApp.deleteTrigger(trigger); 191 | } 192 | }; 193 | })(this)); 194 | } 195 | return this; 196 | }; 197 | 198 | 199 | /* 200 | * End Cron Job 201 | * @param {Object} e - trigger event object 202 | * @return {TSCron} this object for chaining 203 | */ 204 | 205 | TSCron.prototype.endTSCron = function(e) { 206 | this.deleteTriggers_(ScriptApp.EventType.CLOCK, this.cronFunction); 207 | this.deleteTriggers_(ScriptApp.EventType.CLOCK, this.endFunction); 208 | return this; 209 | }; 210 | 211 | 212 | /* 213 | * Get User Defined Item Responses 214 | * @return {Array} array of user defined ItemResponses from last form submit 215 | */ 216 | 217 | TSCron.prototype.getUserDefinedItemResponses = function() { 218 | var err, getStartIndex_, response, startIndex, userItemResponses; 219 | try { 220 | getStartIndex_ = (function(_this) { 221 | return function(itemResponses) { 222 | var startIndex; 223 | startIndex = 0; 224 | itemResponses.forEach(function(itemResponse, index) { 225 | if (itemResponse.getItem().getTitle() === _this.firstFormCronElement) { 226 | return startIndex = index; 227 | } 228 | }); 229 | return startIndex; 230 | }; 231 | })(this); 232 | userItemResponses = []; 233 | response = this.form.getResponse(this.config.id); 234 | if (response) { 235 | startIndex = getStartIndex_(response.getItemResponses()); 236 | response.getItemResponses().forEach((function(_this) { 237 | return function(itemResponse, index) { 238 | if (index < startIndex) { 239 | return userItemResponses.push(itemResponse); 240 | } 241 | }; 242 | })(this)); 243 | if (userItemResponses.length > 0) { 244 | return userItemResponses; 245 | } else { 246 | return null; 247 | } 248 | } else { 249 | Logger.log('TSCron.getUserDefinedItemResponses(): No Form Response Exists'); 250 | throw new Error('TSCron.getUserDefinedItemResponses(): No Form Response Exists'); 251 | } 252 | } catch (error) { 253 | err = error; 254 | this.stopCron(); 255 | this.sendErrorMsg_(err); 256 | return null; 257 | } 258 | }; 259 | 260 | 261 | /* 262 | * Run Cron Job 263 | * @param {Object} e - trigger event object 264 | * @return {TSCron} this object for chaining 265 | */ 266 | 267 | TSCron.prototype.runTSCron = function(e) { 268 | this.scheduleTSCron_(e); 269 | return this; 270 | }; 271 | 272 | 273 | /* 274 | * Start Cron Job 275 | * @param {Object} e - trigger event object 276 | * @return {TSCron} this object for chaining 277 | */ 278 | 279 | TSCron.prototype.startTSCron = function(e) { 280 | this.deleteTriggers_(ScriptApp.EventType.CLOCK, this.startFunction); 281 | this.scheduleTSCron_(e); 282 | return this; 283 | }; 284 | 285 | 286 | /* 287 | * Show cron scheduler status dashboard 288 | * @return {TSCron} this object for chaining 289 | */ 290 | 291 | TSCron.prototype.showStatus = function() { 292 | var endTriggers, runTriggers, scheduleTriggers, scriptProperties, statusConfig, submitTriggers, template, ui; 293 | statusConfig = new Object(); 294 | scriptProperties = this.getScriptProperties_(); 295 | submitTriggers = ScriptApp.getProjectTriggers().filter((function(_this) { 296 | return function(trigger) { 297 | return trigger.getEventType() === ScriptApp.EventType.ON_FORM_SUBMIT && trigger.getHandlerFunction() === _this.formSubmitFunction; 298 | }; 299 | })(this)); 300 | scheduleTriggers = ScriptApp.getProjectTriggers().filter((function(_this) { 301 | return function(trigger) { 302 | return trigger.getEventType() === ScriptApp.EventType.CLOCK && trigger.getHandlerFunction() === _this.startFunction && scriptProperties; 303 | }; 304 | })(this)); 305 | runTriggers = ScriptApp.getProjectTriggers().filter((function(_this) { 306 | return function(trigger) { 307 | return trigger.getEventType() === ScriptApp.EventType.CLOCK && trigger.getHandlerFunction() === _this.cronFunction && scriptProperties; 308 | }; 309 | })(this)); 310 | endTriggers = ScriptApp.getProjectTriggers().filter((function(_this) { 311 | return function(trigger) { 312 | return trigger.getEventType() === ScriptApp.EventType.CLOCK && trigger.getHandlerFunction() === _this.endFunction; 313 | }; 314 | })(this)); 315 | statusConfig.enabled = submitTriggers.length >= 1 ? true : false; 316 | statusConfig.scheduled = scheduleTriggers.length >= 1 ? true : false; 317 | statusConfig.running = runTriggers.length >= 1 ? true : false; 318 | statusConfig.end = endTriggers.length >= 1 ? true : false; 319 | if (statusConfig.scheduled || statusConfig.running) { 320 | statusConfig.schedule = this.getDashboardSchedule_(scriptProperties); 321 | statusConfig.last = scriptProperties.last ? moment.utc(scriptProperties.last).tz(Session.getScriptTimeZone()).format('MMMM D, YYYY h:mm A (z)') : null; 322 | statusConfig.next = scriptProperties.next ? moment.utc(scriptProperties.next).tz(Session.getScriptTimeZone()).format('MMMM D, YYYY h:mm A (z)') : null; 323 | statusConfig.created = moment.utc(scriptProperties.created).tz(Session.getScriptTimeZone()).format('MMMM D, YYYY h:mm A (z)'); 324 | } else { 325 | statusConfig.schedule = null; 326 | statusConfig.last = null; 327 | statusConfig.next = null; 328 | statusConfig.created = null; 329 | } 330 | template = HtmlService.createTemplateFromFile('dashboard'); 331 | template.display = statusConfig; 332 | ui = template.evaluate().setSandboxMode(HtmlService.SandboxMode.IFRAME).setTitle('TSCron Status'); 333 | FormApp.getUi().showSidebar(ui); 334 | return this; 335 | }; 336 | 337 | 338 | /* 339 | * Stop scheduled cron by deleting all cron time-based triggers 340 | * @return {TSCron} this object for chaining 341 | */ 342 | 343 | TSCron.prototype.stopCron = function() { 344 | ScriptApp.getProjectTriggers().forEach((function(_this) { 345 | return function(trigger) { 346 | if (trigger.getEventType() === ScriptApp.EventType.CLOCK && trigger.getHandlerFunction().lastIndexOf('TSCron') >= 0) { 347 | return ScriptApp.deleteTrigger(trigger); 348 | } 349 | }; 350 | })(this)); 351 | PropertiesService.getScriptProperties().deleteProperty(this.propertiesKey); 352 | return this; 353 | }; 354 | 355 | 356 | /* 357 | * Delete triggers by type and name of handler function 358 | * @param {ScriptApp.EventType} type - type of trigger 359 | * @param {string} functionName - name of trigger handler function 360 | * @return {TSCron} this object for chaining 361 | * @private 362 | */ 363 | 364 | TSCron.prototype.deleteTriggers_ = function(type, functionName) { 365 | ScriptApp.getProjectTriggers().forEach((function(_this) { 366 | return function(trigger) { 367 | if (trigger.getEventType() === type && trigger.getHandlerFunction() === functionName) { 368 | return ScriptApp.deleteTrigger(trigger); 369 | } 370 | }; 371 | })(this)); 372 | return this; 373 | }; 374 | 375 | 376 | /* 377 | * Determine type of cron scheduling and schedule cron 378 | * @param {Object} e - event object 379 | * @return {TSCron} this object for chaining 380 | * @private 381 | */ 382 | 383 | TSCron.prototype.detemineCronAction_ = function(e) { 384 | var createTimeTrigger_, duration, endOfNextMonth, lastScheduled, next, startDate, startOfNextMonth; 385 | createTimeTrigger_ = (function(_this) { 386 | return function(duration, param) { 387 | var next; 388 | if (_this.config.next) { 389 | next = moment.tz(_this.config.next, Session.getScriptTimeZone()).add(duration); 390 | } else { 391 | next = moment.tz(param, Session.getScriptTimeZone()).add(duration); 392 | } 393 | ScriptApp.newTrigger(_this.cronFunction).timeBased().at(next.toDate()).create(); 394 | return _this.config.next = next.utc().valueOf(); 395 | }; 396 | })(this); 397 | this.deleteTriggers_(ScriptApp.EventType.CLOCK, this.cronFunction); 398 | switch (this.config.action) { 399 | case 'Every Minutes': 400 | duration = moment.duration(parseInt(this.config.params[0], 10), 'm'); 401 | ScriptApp.newTrigger(this.cronFunction).timeBased().after(duration).create(); 402 | this.config.next = moment.utc({ 403 | y: e.year, 404 | M: e.month - 1, 405 | d: e['day-of-month'], 406 | h: e.hour, 407 | m: e.minute 408 | }).add(duration).valueOf(); 409 | break; 410 | case 'Every Hours': 411 | duration = moment.duration(Math.round(this.config.params[0]), 'h'); 412 | createTimeTrigger_(duration, this.config.params[1]); 413 | break; 414 | case 'Every Days': 415 | duration = moment.duration(Math.round(this.config.params[0]), 'd'); 416 | createTimeTrigger_(duration, this.config.params[1]); 417 | break; 418 | case 'Every Weeks': 419 | duration = moment.duration(Math.round(this.config.params[0]), 'w'); 420 | createTimeTrigger_(duration, this.config.params[1]); 421 | break; 422 | case 'Every Months': 423 | duration = moment.duration(Math.round(this.config.params[0]), 'M'); 424 | startDate = moment.tz(this.config.params[2], Session.getScriptTimeZone()); 425 | if (this.config.next) { 426 | lastScheduled = moment.tz(this.config.next, Session.getScriptTimeZone()); 427 | } else { 428 | lastScheduled = moment.tz(this.config.params[2], Session.getScriptTimeZone()); 429 | } 430 | startOfNextMonth = lastScheduled.clone().startOf('M').add(duration); 431 | endOfNextMonth = startOfNextMonth.clone().endOf('M'); 432 | if ((startDate.date() < 28) || ((startDate.date() <= endOfNextMonth.date()) && !(this.isShortMonth_(startDate) && this.isLastDayOfMonth_(startDate) && this.config.params[1] === 'Yes'))) { 433 | next = startOfNextMonth.clone().date(startDate.date()).hour(lastScheduled.hour()).minute(lastScheduled.minute()); 434 | } else { 435 | next = endOfNextMonth.clone().hour(lastScheduled.hour()).minute(lastScheduled.minute()); 436 | } 437 | ScriptApp.newTrigger(this.cronFunction).timeBased().at(next.toDate()).create(); 438 | this.config.next = next.utc().valueOf(); 439 | break; 440 | case 'Every Years': 441 | duration = moment.duration(Math.round(this.config.params[0]), 'y'); 442 | startDate = moment.tz(this.config.params[2], Session.getScriptTimeZone()); 443 | if (this.config.next) { 444 | lastScheduled = moment.tz(this.config.next, Session.getScriptTimeZone()); 445 | } else { 446 | lastScheduled = moment.tz(this.config.params[2], Session.getScriptTimeZone()); 447 | } 448 | if (startDate.month() === 1 && startDate.date() >= 28 && this.config.params[1] === 'Yes') { 449 | next = lastScheduled.clone().add(duration).startOf('y').add(1, 'M').endOf('M').hour(lastScheduled.hour()).minute(lastScheduled.minute()); 450 | } else { 451 | next = lastScheduled.clone().add(duration).hour(lastScheduled.hour()).minute(lastScheduled.minute()); 452 | } 453 | ScriptApp.newTrigger(this.cronFunction).timeBased().at(next.toDate()).create(); 454 | this.config.next = next.utc().valueOf(); 455 | break; 456 | case 'Custom': 457 | this.config.next = null; 458 | break; 459 | } 460 | return this; 461 | }; 462 | 463 | 464 | /* 465 | * Determine cron start and end dates and schedule cron time-based triggers 466 | * @return {TSCron} this object for chaining 467 | * @private 468 | */ 469 | 470 | TSCron.prototype.determineStartEndDates_ = function() { 471 | var scheduleDates_; 472 | scheduleDates_ = (function(_this) { 473 | return function(start, end) { 474 | var endDate, startDate; 475 | startDate = moment.tz(start, Session.getScriptTimeZone()); 476 | if (end) { 477 | endDate = moment.tz(end, Session.getScriptTimeZone()); 478 | } 479 | if (startDate.isSameOrAfter(_this.now.clone().add(15, 'm'), 'm') && !endDate) { 480 | return ScriptApp.newTrigger(_this.startFunction).timeBased().at(startDate.toDate()).create(); 481 | } else if (startDate.isSameOrAfter(_this.now.clone().add(15, 'm'), 'm') && endDate.isSameOrAfter(startDate.clone().add(1, 'h'))) { 482 | ScriptApp.newTrigger(_this.startFunction).timeBased().at(startDate.toDate()).create(); 483 | return ScriptApp.newTrigger(_this.endFunction).timeBased().at(endDate.toDate()).create(); 484 | } else { 485 | Logger.log('TSCron.determineStartEndDates(): Unable to schedule cron because of invalid start/end date configuration'); 486 | throw new Error('TSCron.determineStartEndDates(): Unable to schedule cron because of invalid start/end date configuration'); 487 | } 488 | }; 489 | })(this); 490 | switch (this.config.action) { 491 | case 'Every Minutes': 492 | case 'Every Hours': 493 | case 'Every Days': 494 | case 'Every Weeks': 495 | scheduleDates_(this.config.params[1], this.config.params[2] ? this.config.params[2] : null); 496 | break; 497 | case 'Every Months': 498 | case 'Every Years': 499 | scheduleDates_(this.config.params[2], this.config.params[3] ? this.config.params[3] : null); 500 | break; 501 | case 'Custom': 502 | scheduleDates_(this.config.params[0], null); 503 | break; 504 | } 505 | return this; 506 | }; 507 | 508 | 509 | /* 510 | * Find first form cron configuration element based on its item name 511 | * @param {string} title - title of first form cron configuration element 512 | * @return {number} index of first form cron confiugration element 513 | * @private 514 | */ 515 | 516 | TSCron.prototype.findFirstCronFormElement_ = function(title) { 517 | var cronStartIndex; 518 | cronStartIndex = null; 519 | if (this.response) { 520 | this.response.getItemResponses().forEach(function(item, index) { 521 | if (item.getItem().getTitle() === title) { 522 | return cronStartIndex = index; 523 | } 524 | }); 525 | } else { 526 | Logger.log('TSCron.findFirstCronFromElement(): No Form Response Exists'); 527 | throw new Error('TSCron.findFirstCronFromElement(): No Form Response Exists'); 528 | } 529 | if (cronStartIndex === null) { 530 | Logger.log('TSCron.findFirstCronFromElement(): No Cron Configuration Found'); 531 | throw new Error('TSCron.findFirstCronFromElement(): No Cron Configuration Found'); 532 | } 533 | return cronStartIndex; 534 | }; 535 | 536 | 537 | /* 538 | * Get cron configuration from Form Item Responses 539 | * @return {Object} Cron configuration object 540 | * @private 541 | */ 542 | 543 | TSCron.prototype.getCronConfigFromForm_ = function() { 544 | var config, itemResponses, startIndex; 545 | startIndex = this.findFirstCronFormElement_(this.firstFormCronElement); 546 | config = new Object(); 547 | if (this.response) { 548 | itemResponses = this.response.getItemResponses(); 549 | config.action = itemResponses[startIndex].getResponse(); 550 | config.params = []; 551 | itemResponses.forEach((function(_this) { 552 | return function(item, index) { 553 | if (index > startIndex) { 554 | return config.params.push(item.getResponse()); 555 | } 556 | }; 557 | })(this)); 558 | config.id = this.response.getId(); 559 | config.created = moment.utc(this.response.getTimestamp()).valueOf(); 560 | } else { 561 | Logger.log('TSCron.getCronConfigFromForm(): No Form Response Exists'); 562 | throw new Error('TSCron.getCronConfigFromForm(): No Form Response Exists'); 563 | } 564 | return config; 565 | }; 566 | 567 | 568 | /* 569 | * Get cron scheduler status configuration 570 | * @param {Object} scriptProperties - cron scheduler properties object 571 | * @return {string} cron scheduler dashbaord schedule 572 | * @private 573 | */ 574 | 575 | TSCron.prototype.getDashboardSchedule_ = function(scriptProperties) { 576 | var schedule, setSchedule_; 577 | schedule = new Object(); 578 | setSchedule_ = (function(_this) { 579 | return function(s, props, scheduleStr) { 580 | var startDate; 581 | switch (scheduleStr) { 582 | case 'Minutes': 583 | case 'Hours': 584 | case 'Days': 585 | case 'Weeks': 586 | startDate = moment.tz(props.params[1], Session.getScriptTimeZone()); 587 | s.start = startDate.format('MMMM D, YYYY h:mm A (z)'); 588 | s.end = props.params[2] ? moment.tz(props.params[2], Session.getScriptTimeZone()).format('MMMM D, YYYY h:mm A (z)') : null; 589 | s.every = props.params[0] + ' ' + scheduleStr; 590 | break; 591 | case 'Months': 592 | case 'Years': 593 | startDate = moment.tz(props.params[2], Session.getScriptTimeZone()); 594 | s.start = startDate.format('MMMM D, YYYY h:mm A (z)'); 595 | s.end = props.params[3] ? moment.tz(props.params[3], Session.getScriptTimeZone()).format('MMMM D, YYYY h:mm A (z)') : null; 596 | s.every = props.params[0] + ' ' + scheduleStr; 597 | break; 598 | case 'Custom': 599 | startDate = moment.tz(props.params[0], Session.getScriptTimeZone()); 600 | s.start = startDate.format('MMMM D, YYYY h:mm A (z)'); 601 | s.end = null; 602 | s.every = scheduleStr + ' (run once)'; 603 | break; 604 | } 605 | switch (scheduleStr) { 606 | case 'Minutes': 607 | case 'Hours': 608 | case 'Custom': 609 | return s.near = null; 610 | case 'Days': 611 | return s.near = startDate.format('h:mm A'); 612 | case 'Weeks': 613 | return s.near = startDate.format('dddd @ h:mm A'); 614 | case 'Months': 615 | if (_this.isShortMonth_(startDate) && _this.isLastDayOfMonth_(startDate) && props.params[1] === 'Yes') { 616 | return s.near = 'Last of Month' + startDate.format(' @ h:mm A'); 617 | } else { 618 | return s.near = startDate.format('Do @ h:mm A'); 619 | } 620 | break; 621 | case 'Years': 622 | if (startDate.month() === 1 && startDate.date() >= 28) { 623 | return s.near = 'Last Day Feb' + startDate.format(' @ h:mm A'); 624 | } else { 625 | return s.near = startDate.format('MMM Do @ h:mm A'); 626 | } 627 | break; 628 | } 629 | }; 630 | })(this); 631 | switch (scriptProperties.action) { 632 | case 'Every Minutes': 633 | setSchedule_(schedule, scriptProperties, 'Minutes'); 634 | break; 635 | case 'Every Hours': 636 | setSchedule_(schedule, scriptProperties, 'Hours'); 637 | break; 638 | case 'Every Days': 639 | setSchedule_(schedule, scriptProperties, 'Days'); 640 | break; 641 | case 'Every Weeks': 642 | setSchedule_(schedule, scriptProperties, 'Weeks'); 643 | break; 644 | case 'Every Months': 645 | setSchedule_(schedule, scriptProperties, 'Months'); 646 | break; 647 | case 'Every Years': 648 | setSchedule_(schedule, scriptProperties, 'Years'); 649 | break; 650 | case 'Custom': 651 | setSchedule_(schedule, scriptProperties, 'Custom'); 652 | break; 653 | } 654 | return schedule; 655 | }; 656 | 657 | 658 | /* 659 | * Get properties object from Script Properties store 660 | * @return {Object} Script Properties store object 661 | * @private 662 | */ 663 | 664 | TSCron.prototype.getScriptProperties_ = function() { 665 | return JSON.parse(PropertiesService.getScriptProperties().getProperty(this.propertiesKey)); 666 | }; 667 | 668 | 669 | /* 670 | * Determine if date is last day of month 671 | * @param {Moment} date - date to test 672 | * @return {boolean} if date is last day of month 673 | * @private 674 | */ 675 | 676 | TSCron.prototype.isLastDayOfMonth_ = function(date) { 677 | if (date.clone().endOf('M').isSame(date.clone(), 'd')) { 678 | return true; 679 | } else { 680 | return false; 681 | } 682 | }; 683 | 684 | 685 | /* 686 | * Determine if date is in a month with less than 31 days 687 | * @param {Moment} date - date to test 688 | * @return {boolean} if date is in a month with less than 31 days 689 | * @private 690 | */ 691 | 692 | TSCron.prototype.isShortMonth_ = function(date) { 693 | var shortMonth; 694 | switch (date.month()) { 695 | case 1: 696 | case 3: 697 | case 5: 698 | case 8: 699 | case 10: 700 | shortMonth = true; 701 | break; 702 | default: 703 | shortMonth = false; 704 | } 705 | return shortMonth; 706 | }; 707 | 708 | 709 | /* 710 | * Schedule cron 711 | * @param {Object} e - trigger event object 712 | * @return {TSCron} this object for chaining 713 | */ 714 | 715 | TSCron.prototype.scheduleTSCron_ = function(e) { 716 | var err; 717 | try { 718 | this.config = this.getScriptProperties_(); 719 | if (this.config) { 720 | if (e) { 721 | this.config.last = moment.utc({ 722 | y: e.year, 723 | M: e.month - 1, 724 | d: e['day-of-month'], 725 | h: e.hour, 726 | m: e.minute 727 | }).valueOf(); 728 | } 729 | this.detemineCronAction_(e); 730 | this.setScriptProperties_(); 731 | } else { 732 | Logger.log('TSCron.scheduleTSCron(): No Cron Configuration Exists in Script Properties'); 733 | throw new Error('TSCron.scheduleTSCron(): No Cron Configuration Exists in Script Properties'); 734 | } 735 | } catch (error) { 736 | err = error; 737 | this.stopCron(); 738 | this.sendErrorMsg_(err); 739 | } 740 | return this; 741 | }; 742 | 743 | 744 | /* 745 | * Send error email to form owner 746 | * @param {Error} err - error object 747 | * @return {TSCron} this object for chaining 748 | * @private 749 | */ 750 | 751 | TSCron.prototype.sendErrorMsg_ = function(err) { 752 | var msg; 753 | msg = 'Cron Job Scheduler failed in form ' + this.form.getTitle() + ' with the following error message:

' + '' + err.message + '' + '


' + 'The Cron Job Scheduler has been disabled.
Restart the Scheduler by submitting another form request.



' + '
TSCron - a Google Forms based Cron scheduler powered by Google Apps Script'; 754 | GmailApp.sendEmail(Session.getEffectiveUser().getEmail(), 'Cron Job Schedule Failure', '', { 755 | htmlBody: msg 756 | }); 757 | return this; 758 | }; 759 | 760 | 761 | /* 762 | * Set object in Script Properties store 763 | * @return {TSCron} this object for chaining 764 | * @private 765 | */ 766 | 767 | TSCron.prototype.setScriptProperties_ = function() { 768 | JSON.parse(PropertiesService.getScriptProperties().getProperty(this.propertiesKey)); 769 | PropertiesService.getScriptProperties().setProperty(this.propertiesKey, JSON.stringify(this.config)); 770 | return this; 771 | }; 772 | 773 | return TSCron; 774 | 775 | })(); 776 | })(); 777 | --------------------------------------------------------------------------------