├── 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://www.buymeacoffee.com/techstreams)
7 |
8 | ---
9 |
10 | 
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 | 
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 | 
89 |
90 | Be sure to add these optional form elements to form section(s) **BEFORE** the pre-existing TSCron configuration form elements.
91 |
92 | 
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 | 
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 | 
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 | 
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 | 
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 | 
192 |
193 |
194 | 
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 | 
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 | 
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 | 
259 |
260 | The TSCron status sidebar will open.
261 |
262 | 
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 |
279 | Check email for an error message (form owner) .
280 | Run the TSCron > Configure
option and re-authorize the script in the event any new authorization scopes were added to the cronJob()
function.
281 | Ensure the TSCron configuration start and end dates are set to properly allow the cron interval to run. Start date should be 15+ minutes in future of the form submission and optional end date should be 1+ hours after the start date.
282 |
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 |
58 |
59 |
60 |
61 | if (display.enabled) { ?>
62 | Mode: Enabled
63 | if (display.scheduled) { ?>
64 | Status: Scheduled
65 | } else if (display.running) { ?>
66 | Status: Running
67 | } else { ?>
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 | if (display.schedule) { ?>
73 | Run!= display.schedule.every == 'Custom' ? "" : " Every" ?>: != display.schedule.every ?>
74 | if (display.schedule.near) { ?>
75 | Near: != display.schedule.near ?>
76 | } ?>
77 |
78 | } ?>
79 | } else { ?>
80 | Mode: Disabled
81 |
82 | Enable Cron Scheduler using the form's TSCron > Configure add-ons menu
83 | } ?>
84 |
85 | if (display.enabled) { ?>
86 | if (display.running && display.last) { ?>
87 |
88 | Last Run On:
89 |
90 | != display.last ?>
91 |
92 |
93 | } ?>
94 | if (display.running && display.next) { ?>
95 |
96 | Next Scheduled On:
97 |
98 | != display.next ?>
99 |
100 | Actual time may vary +/- 15 minutes
101 |
102 |
103 | } ?>
104 | if (display.scheduled) { ?>
105 |
106 | Start Date:
107 |
108 | != display.schedule.start ?>
109 |
110 | Actual time may vary +/- 15 minutes
111 |
112 |
113 | } else { ?>
114 | if (display.running) { ?>
115 |
116 | Start Date:
117 |
118 | != display.schedule.start ?>
119 |
120 |
121 | } ?>
122 | } ?>
123 | if (display.end) { ?>
124 |
125 | End Date:
126 |
127 | != display.schedule.end ?>
128 |
129 | Actual time may vary +/- 15 minutes
130 |
131 |
132 | } ?>
133 | if (display.created) { ?>
134 |
135 | Created On:
136 | != display.created ?>
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 |
--------------------------------------------------------------------------------