Source: trellinator/Trellinator.js

  1. /**
  2. * @class Trellinator
  3. * @memberof module:TrellinatorCore
  4. * @constructor
  5. * @classdesc The Trellinator object is a special
  6. * instance of Member, which gets it's username
  7. * from the token in the Configuration tab of the
  8. * Trellinator spreadsheet. As such, this is a
  9. * Member object that is always the Trello
  10. * member on behalf of which Trellinator will
  11. * act when making Trello API calls
  12. *
  13. * This is particularly useful when you need to
  14. * avoid taking action on notifications which have
  15. * been caused by actions taken by Trellinator, and
  16. * it is the Trellinator class that is used by
  17. * the Member.notTrellinator method.
  18. *
  19. * Another common use case is to find another
  20. * board other than the one in which the
  21. * notification occurred.
  22. *
  23. * Trellinator also has a useful now() method which
  24. * can be used so that, when you are creating your
  25. * Murphy tests, you will be able to inject a "fake"
  26. * time to act as the current date/time so you can
  27. * get consistent output when testing.
  28. *
  29. * The Trellinator object is also where a bunch of
  30. * Date convenience functions are added to the Date
  31. * prototype, and those functions are documented here
  32. *
  33. * Methods that are added to the Date prototype in
  34. * this class show up as "static" and belonging to
  35. * Date#functionName, but they're not static. If you
  36. * know how to make these not show as static, please
  37. * send a pull request :)
  38. *
  39. * @example
  40. * if(new Notification(posted).member().notTrellinator())
  41. * @example
  42. * new Trellinator().board("Board Name").card("Ohai")
  43. * @example
  44. * new Trellinator().name()
  45. */
  46. var Trellinator = function()
  47. {
  48. if(!Trellinator.data)
  49. {
  50. var trello_token = null;
  51. if(Trellinator.isGoogleAppsScript())
  52. {
  53. var col = Trellinator.fastGetSheetByName(CONFIG_NAME_).getDataRange().getValues();
  54. new IterableCollection(col).each(function(row)
  55. {
  56. if(row[0] == "Trello Token")
  57. {
  58. trello_token = row[1];
  59. }
  60. });
  61. }
  62. else if(Trellinator.override_token)
  63. {
  64. trello_token = Trellinator.override_token;
  65. }
  66. else
  67. {
  68. trello_token = process.argv[3];
  69. }
  70. if(trello_token)
  71. Trellinator.data = TrelloApi.get("tokens/"+trello_token+"/member");
  72. else
  73. throw new Error("Could not find trello token");
  74. }
  75. this.member = new Member(Trellinator.data);
  76. for(var key in this.member)
  77. this[key] = this.member[key];
  78. }
  79. /**
  80. * Cache active spreadsheet sheets in an
  81. * object referenced by name to reduce time to call
  82. * (for example when executing via node
  83. * on the command line)
  84. * @memberof module:TrellinatorCore.Trellinator
  85. */
  86. Trellinator.fastGetSheetByName = function(name)
  87. {
  88. if(!Trellinator.fastGetSheetByName.sheets)
  89. {
  90. Trellinator.fastGetSheetByName.sheets = {};
  91. var all = SpreadsheetApp.getActiveSpreadsheet().getSheets();
  92. for(var i = 0;i < all.length;i++)
  93. {
  94. Trellinator.fastGetSheetByName.sheets[all[i].getName()] = all[i];
  95. }
  96. }
  97. return Trellinator.fastGetSheetByName.sheets[name];
  98. }
  99. Trellinator.fastGetSheetByName.sheets = null;
  100. Trellinator.data = null;
  101. Trellinator.override_token = null;
  102. /**
  103. * Return true if we are running in Google
  104. * Apps Script environment, false if not
  105. * (for example when executing via node
  106. * on the command line)
  107. * @memberof module:TrellinatorCore.Trellinator
  108. */
  109. Trellinator.isGoogleAppsScript = function()
  110. {
  111. return (typeof DriveApp !== "undefined");
  112. }
  113. /**
  114. * Create a log entry in the Info Log
  115. * when running in Google Apps Script
  116. * or console.log when running in
  117. * node
  118. * @memberof module:TrellinatorCore.Trellinator
  119. * @param msg {string} the log entry
  120. * @example
  121. * Trellinator.log("Oops, what went wrong?");
  122. */
  123. Trellinator.log = function(msg)
  124. {
  125. writeInfo_(msg);
  126. }
  127. /**
  128. * Add a board name to a global command group.
  129. * This is used when you create a new board
  130. * using Trellinator and need that board to
  131. * inherit a bunch of functionality from a
  132. * global command group.
  133. * @memberof module:TrellinatorCore.Trellinator
  134. * @param board {Board} a Board object to add to the global command group
  135. * @param group_name {string} the name of the group to add this board to
  136. * @example
  137. * var trellinator = new Trellinator();
  138. * var copied = trellinator.board("Template").copy("Project",trellinator.team("Whitefish"));
  139. * Trellinator.addBoardToGlobalCommandGroup(copied,"Project Boards");
  140. */
  141. Trellinator.addBoardToGlobalCommandGroup = function(board,group_name)
  142. {
  143. if(Trellinator.isGoogleAppsScript())
  144. {
  145. var ss = SpreadsheetApp.getActiveSpreadsheet();
  146. var globSheet = Trellinator.fastGetSheetByName(GLOBAL_GROUP_NAME_);
  147. var globData = globSheet.getDataRange().getValues();
  148. var added = false;
  149. for(var row = 1; row < globData.length; row++)
  150. {
  151. if(globData[row][0] == group_name)
  152. {
  153. var value = globData[row][1].trim();
  154. var to_add = board.name()+" ["+board.id()+"]";
  155. var to_test = board.id();
  156. var arr = new IterableCollection(value.split(GLOBAL_GROUP_SEPARATOR_)).find(function(elem)
  157. {
  158. if(elem && (new RegExp("^[^]+ \\[(.+)\\]$").exec(elem.trim())[1].trim() == to_test))
  159. {
  160. return false;
  161. }
  162. else
  163. {
  164. return elem;
  165. }
  166. }).asArray();
  167. arr.push(to_add);
  168. globSheet.getRange(row+1, 2).setValue(arr.join(GLOBAL_GROUP_SEPARATOR_));
  169. getBoardData_.cache = null;
  170. getBoardNamesFromGlobalCommandGroups.cache = null;
  171. getBoardNames4mGroup_.grpData = null;
  172. getBoardNames4mGroup_.board_names_by_group = {};
  173. timeTrigger4NewBoard_(board.id());
  174. writeInfo_("Added "+board.name()+" to "+group_name);
  175. added = true;
  176. }
  177. }//loop for all global commmands ends
  178. if(!added)
  179. {
  180. globSheet.appendRow([group_name,board.name()+" ["+board.id()+"]"]);
  181. getBoardData_.cache = null;
  182. getBoardNamesFromGlobalCommandGroups.cache = null;
  183. getBoardNames4mGroup_.grpData = null;
  184. getBoardNames4mGroup_.board_names_by_group = {};
  185. timeTrigger4NewBoard_(board.id());
  186. writeInfo_("Added "+board.name()+" to NEW global command group "+group_name);
  187. }
  188. }
  189. return board;
  190. }
  191. Trellinator.boardsInGlobalCommandGroup = function(group_name)
  192. {
  193. var ret = false;
  194. if(Trellinator.isGoogleAppsScript())
  195. {
  196. var ss = SpreadsheetApp.getActiveSpreadsheet();
  197. var globSheet = Trellinator.fastGetSheetByName(GLOBAL_GROUP_NAME_);
  198. var globData = globSheet.getDataRange().getValues();
  199. var added = false;
  200. for(var row = 1; row < globData.length; row++)
  201. {
  202. if(globData[row][0] == group_name)
  203. {
  204. ret = new IterableCollection(globData[row][1].trim().split(";;;")).find(function(id)
  205. {
  206. try
  207. {
  208. return new Board({id: /.+\[([A-Za-z0-9]+)\]/.exec(id)[1]}).load();
  209. }
  210. catch(e)
  211. {
  212. return false;
  213. }
  214. });
  215. }
  216. }//loop for all global commmands ends
  217. }
  218. return ret;
  219. }
  220. Trellinator.boardIsInGlobalCommandGroup = function(board,group_name)
  221. {
  222. var ret = false;
  223. if(Trellinator.isGoogleAppsScript())
  224. {
  225. var ss = SpreadsheetApp.getActiveSpreadsheet();
  226. var globSheet = Trellinator.fastGetSheetByName(GLOBAL_GROUP_NAME_);
  227. var globData = globSheet.getDataRange().getValues();
  228. var added = false;
  229. for(var row = 1; row < globData.length; row++)
  230. {
  231. if(globData[row][0] == group_name)
  232. {
  233. var value = globData[row][1].trim();
  234. if(value.indexOf(board.id()) > -1)
  235. ret = true;
  236. }
  237. }//loop for all global commmands ends
  238. }
  239. return ret;
  240. }
  241. /**
  242. * Remove a board name from a global command group.
  243. * @memberof module:TrellinatorCore.Trellinator
  244. * @param board {Board} a Board object to remove from the global command group
  245. * @param group_name {string} the name of the group to remove this board from
  246. * @example
  247. * Trellinator.removeBoardFromGlobalCommandGroup(new Notification(posted).board(),"Project Boards");
  248. */
  249. Trellinator.removeBoardFromGlobalCommandGroup = function(board,group_name)
  250. {
  251. if(Trellinator.isGoogleAppsScript())
  252. {
  253. var ss = SpreadsheetApp.getActiveSpreadsheet();
  254. var globSheet = Trellinator.fastGetSheetByName(GLOBAL_GROUP_NAME_);
  255. var globData = globSheet.getDataRange().getValues();
  256. var added = false;
  257. for(var row = 1; row < globData.length; row++)
  258. {
  259. if(globData[row][0] == group_name)
  260. {
  261. var value = globData[row][1].trim();
  262. var to_remove = board.id();
  263. globSheet.getRange(row+1, 2).setValue(
  264. new IterableCollection(value.split(GLOBAL_GROUP_SEPARATOR_)).find(function(elem)
  265. {
  266. if(elem && (new RegExp("^[^]+ \\[(.+)\\]$").exec(elem.trim())[1].trim() == to_remove))
  267. return false;
  268. else
  269. return elem;
  270. }).asArray().join(GLOBAL_GROUP_SEPARATOR_)
  271. );
  272. //We need to do this, but calling this now will
  273. //also clear all triggers for the board that are
  274. //not from this global command group, so put this
  275. //back in when the Trigger system is more consistent
  276. //and better refactored
  277. //clearTimeTriggers4Board_(board.id());
  278. writeInfo_("Removed "+board.name()+" from "+group_name);
  279. }
  280. }//loop for all global commmands ends
  281. }
  282. }
  283. //USED INTERNALLY
  284. Trellinator.getStack = function()
  285. {
  286. var stack = "";
  287. try
  288. {
  289. throw new Error("Whoops!");
  290. }
  291. catch (e)
  292. {
  293. stack = e.stack;
  294. }
  295. return stack;
  296. }
  297. //USED INTERNALLY
  298. Trellinator.fakeNow = function()
  299. {
  300. return new Date(Trellinator.fake_now);
  301. }
  302. /**
  303. * You can use this instead of new Date()
  304. * and this means you will be able to get
  305. * consistency when writing tests
  306. * by setting Trellinator.fake_now to
  307. * fixed date and time
  308. * @memberof module:TrellinatorCore.Trellinator
  309. */
  310. Trellinator.now = function()
  311. {
  312. return (Trellinator.fake_now) ? Trellinator.fakeNow() : new Date();
  313. }
  314. Trellinator.username = null;
  315. Trellinator.fake_now = null;
  316. /**
  317. * Get a random number between 2 numbers
  318. * @memberof module:TrellinatorCore.Trellinator
  319. */
  320. Trellinator.getRandomArbitrary = function(min, max) {
  321. return Math.floor(Math.random() * (max - min) + min);
  322. }
  323. /**
  324. * Any Date object will have this method, allowing
  325. * you to tell if the time is between 2 values
  326. * @param start {string} A time in 24 hour format eg. 17:00
  327. * @param finish {string} A time in 24 hour format eg. 13:00
  328. * @memberof module:TrellinatorCore.Trellinator
  329. * @example
  330. * if(Trellinator.now().timeIsBetween("9:00","17:00"))
  331. */
  332. Date.prototype.timeIsBetween = function(start,finish)
  333. {
  334. var start_parts = start.split(":");
  335. var finish_parts = finish.split(":");
  336. var start = new Date(this);
  337. start.setHours(start_parts[0],start_parts[1],0,0);
  338. var finish = new Date(this);
  339. finish.setHours(finish_parts[0],finish_parts[1],0,0);
  340. return ((this.getTime() >= start.getTime()) && (this.getTime() <= finish.getTime()));
  341. }
  342. /**
  343. * Returns true if this is monday - friday
  344. * @memberof module:TrellinatorCore.Trellinator
  345. * @example
  346. * if(Trellinator.now().isWeekDay())
  347. */
  348. Date.prototype.isWeekDay = function()
  349. {
  350. return !((this.getDay() == 6) || (this.getDay() == 0));
  351. }
  352. //DEPRECATED: use on()
  353. Date.prototype.onDate = function(date)
  354. {
  355. this.setDate(date);
  356. return this;
  357. }
  358. /**
  359. * Set the day component of this date
  360. * to the given day, eg. 27
  361. * @memberof module:TrellinatorCore.Trellinator
  362. * @param date {int} the day to set this date to, eg. 27
  363. * @example
  364. * //1st day of next month
  365. * Trellinator.now().addMonths(1).on(1);
  366. */
  367. Date.prototype.on = function(date)
  368. {
  369. this.setDate(date);
  370. return this;
  371. }
  372. /**
  373. * Set the time component of this date
  374. * to the given time in 24 format, HH:MM
  375. * seconds are not supportd
  376. * @memberof module:TrellinatorCore.Trellinator
  377. * @param time {string} format HH:MM in 24 hour time
  378. * @example
  379. * //9am tomorrow
  380. * Trellinator.now().addDays(1).at("9:00");
  381. */
  382. Date.prototype.at = function(time)
  383. {
  384. var time_parts = time.split(":");
  385. this.setHours(time_parts[0],time_parts[1],0,0);
  386. return this;
  387. }
  388. /**
  389. * Add X minutes to the date
  390. * @memberof module:TrellinatorCore.Trellinator
  391. * @param minutes {int} number of minutes to add
  392. * @example
  393. * Trellinator.now().addMinutes(20);
  394. */
  395. Date.prototype.addMinutes = function(minutes)
  396. {
  397. this.setMinutes(this.getMinutes() + parseInt(minutes));
  398. return this;
  399. }
  400. /**
  401. * Minus X minutes from the date
  402. * @memberof module:TrellinatorCore.Trellinator
  403. * @param minutes {int} number of minutes to minus
  404. * @example
  405. * Trellinator.now().minusMinutes(20);
  406. */
  407. Date.prototype.minusMinutes = function(minutes)
  408. {
  409. this.setMinutes(this.getMinutes() - parseInt(minutes));
  410. return this;
  411. }
  412. /**
  413. * Add X hours to the date
  414. * @memberof module:TrellinatorCore.Trellinator
  415. * @param hours {int} number of hours to add
  416. * @example
  417. * Trellinator.now().addHours(1);
  418. */
  419. Date.prototype.addHours = function(hours)
  420. {
  421. return this.addMinutes(parseFloat(hours)*60);
  422. }
  423. /**
  424. * Minus X hours from the date
  425. * @memberof module:TrellinatorCore.Trellinator
  426. * @param hours {int} number of hours to minus
  427. * @example
  428. * Trellinator.now().minusHours(1);
  429. */
  430. Date.prototype.minusHours = function(hours)
  431. {
  432. return this.minusMinutes(parseFloat(hours)*60);
  433. }
  434. /**
  435. * Add X weekdays to the date
  436. * @memberof module:TrellinatorCore.Trellinator
  437. * @param days {int} number of days to add
  438. * @example
  439. * //Tomorrow
  440. * Trellinator.now().addWeekDays(1);
  441. */
  442. Date.prototype.addWeekDays = function(days)
  443. {
  444. var days = parseInt(days);
  445. var cur_day = 1;
  446. while(days > 0)
  447. {
  448. this.setDate(this.getDate() + cur_day);
  449. if(this.isWeekDay())
  450. days--;
  451. }
  452. return this;
  453. }
  454. /**
  455. * Add X days to the date
  456. * @memberof module:TrellinatorCore.Trellinator
  457. * @param days {int} number of days to add
  458. * @example
  459. * //Tomorrow
  460. * Trellinator.now().addDays(1);
  461. */
  462. Date.prototype.addDays = function(days)
  463. {
  464. this.setDate(this.getDate() + parseInt(days));
  465. return this;
  466. }
  467. /**
  468. * Add X weeks to the date
  469. * @memberof module:TrellinatorCore.Trellinator
  470. * @param weeks {int} number of weeks to add
  471. * @example
  472. * Trellinator.now().addWeeks(1);
  473. */
  474. Date.prototype.addWeeks = function(weeks)
  475. {
  476. return this.addDays(parseInt(weeks)*7);
  477. }
  478. /**
  479. * Subtract X weeks from the date
  480. * @memberof module:TrellinatorCore.Trellinator
  481. * @param weeks {int} number of weeks to subtract
  482. * @example
  483. * Trellinator.now().minusWeeks(1);
  484. */
  485. Date.prototype.minusWeeks = function(weeks)
  486. {
  487. return this.minusDays(parseInt(weeks)*7);
  488. }
  489. Date.isLeapYear = function (year) {
  490. return (((year % 4 === 0) && (year % 100 !== 0)) || (year % 400 === 0));
  491. };
  492. Date.getDaysInMonth = function (year, month) {
  493. return [31, (Date.isLeapYear(year) ? 29 : 28), 31, 30, 31, 30, 31, 31, 30, 31, 30, 31][month];
  494. };
  495. Date.prototype.isLeapYear = function () {
  496. return Date.isLeapYear(this.getFullYear());
  497. };
  498. Date.prototype.getDaysInMonth = function () {
  499. return Date.getDaysInMonth(this.getFullYear(), this.getMonth());
  500. };
  501. Date.prototype.addMonths = function (value) {
  502. };
  503. /**
  504. * Add X months to the date
  505. * @memberof module:TrellinatorCore.Trellinator
  506. * @param months {int} number of months to add
  507. * @example
  508. * Trellinator.now().addMonths(1);
  509. */
  510. Date.prototype.addMonths = function(value)
  511. {
  512. var n = this.getDate();
  513. this.setDate(1);
  514. this.setMonth(this.getMonth() + parseInt(value));
  515. this.setDate(Math.min(n, this.getDaysInMonth()));
  516. return this;
  517. }
  518. /**
  519. * Subtract X months from the date
  520. * @memberof module:TrellinatorCore.Trellinator
  521. * @param months {int} number of months to subtract
  522. * @example
  523. * Trellinator.now().minusMonths(1);
  524. */
  525. Date.prototype.minusMonths = function(months)
  526. {
  527. this.setMonth(this.getMonth() - parseInt(months));
  528. return this;
  529. }
  530. /**
  531. * Return the week of the month, starting
  532. * with 0. Useful if you want to say that
  533. * you are in the 3rd week of August, for
  534. * example
  535. * @memberof module:TrellinatorCore.Trellinator
  536. * @example
  537. * Trellinator.now().weekOfMonth()
  538. */
  539. Date.prototype.weekOfMonth = function()
  540. {
  541. return this.getWeekOfMonth();
  542. }
  543. //DEPRECATED: use weekOfMonth
  544. Date.prototype.getWeekOfMonth = function() {
  545. var firstWeekday = new Date(this.getFullYear(), this.getMonth(), 1).getDay();
  546. var offsetDate = this.getDate() + firstWeekday - 1;
  547. return Math.floor(offsetDate / 7);
  548. }
  549. /**
  550. * Find out the name of the day, returned
  551. * as the full day name eg. Monday
  552. * @memberof module:TrellinatorCore.Trellinator
  553. * @example
  554. * Trellinator.now().dayName();
  555. */
  556. Date.prototype.dayName = function()
  557. {
  558. var weekday = new Array(7);
  559. weekday[0] = "Sunday";
  560. weekday[1] = "Monday";
  561. weekday[2] = "Tuesday";
  562. weekday[3] = "Wednesday";
  563. weekday[4] = "Thursday";
  564. weekday[5] = "Friday";
  565. weekday[6] = "Saturday";
  566. return weekday[this.getDay()];
  567. }
  568. /**
  569. * Find out the name of the day, returned
  570. * as the short day name eg. Mon
  571. * @memberof module:TrellinatorCore.Trellinator
  572. * @example
  573. * Trellinator.now().shortDayName();
  574. */
  575. Date.prototype.shortDayName = function()
  576. {
  577. var weekday = new Array(7);
  578. weekday[0] = "Sun";
  579. weekday[1] = "Mon";
  580. weekday[2] = "Tue";
  581. weekday[3] = "Wed";
  582. weekday[4] = "Thu";
  583. weekday[5] = "Fri";
  584. weekday[6] = "Sat";
  585. return weekday[this.getDay()];
  586. }
  587. /**
  588. * Find out the name of the month, returned
  589. * as the full month name eg. August
  590. * @memberof module:TrellinatorCore.Trellinator
  591. * @example
  592. * Trellinator.now().monthName();
  593. */
  594. Date.prototype.monthName = function()
  595. {
  596. return this.getMonthName();
  597. }
  598. /**
  599. * Find out the name of the month, returned
  600. * as the short month name eg. Aug
  601. * @memberof module:TrellinatorCore.Trellinator
  602. * @example
  603. * Trellinator.now().shortMonthName();
  604. */
  605. Date.prototype.shortMonthName = function()
  606. {
  607. var monthNames = ["Jan", "Feb", "Mar", "Apr", "May", "Jun","Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
  608. return monthNames[this.getMonth()];
  609. }
  610. //DEPRECATED use monthName
  611. Date.prototype.getMonthName = function()
  612. {
  613. var monthNames = ["January", "February", "March", "April", "May", "June","July", "August", "September", "October", "November", "December"];
  614. return monthNames[this.getMonth()];
  615. }
  616. /**
  617. * Find the previous instance of the given
  618. * day of the week, before the current date
  619. * @memberof module:TrellinatorCore.Trellinator
  620. * @param day {string} the name of the day of the
  621. * week you wish to find the previous instance of
  622. * @example
  623. * Trellinator.now().previous("Monday")
  624. */
  625. Date.prototype.previous = function(day)
  626. {
  627. index = new Array();
  628. index["sunday"] = 0;
  629. index["monday"] = 1;
  630. index["tuesday"] = 2;
  631. index["wednesday"] = 3;
  632. index["thursday"] = 4;
  633. index["friday"] = 5;
  634. index["saturday"] = 6;
  635. var target = index[day.toLowerCase()];
  636. var daynum = this.getDay();
  637. var diff = ((target - daynum)-7);
  638. if(diff != -7)
  639. var offset = diff % 7;
  640. else
  641. var offset = diff;
  642. this.setDate(this.getDate() + offset);
  643. return this;
  644. }
  645. /**
  646. * Find the next instance of the given
  647. * day of the week, after the current date
  648. * @memberof module:TrellinatorCore.Trellinator
  649. * @param day {string} the name of the day of the
  650. * week you wish to find the next instance of
  651. * @example
  652. * Trellinator.now().next("Monday")
  653. */
  654. Date.prototype.next = function(day)
  655. {
  656. var index = new Array();
  657. index["sunday"] = 0;
  658. index["monday"] = 1;
  659. index["tuesday"] = 2;
  660. index["wednesday"] = 3;
  661. index["thursday"] = 4;
  662. index["friday"] = 5;
  663. index["saturday"] = 6;
  664. var day_to_find = index[day.toLowerCase()];
  665. var diff = (day_to_find + 7) - this.getDay();
  666. if(diff != 7)
  667. var offset = diff % 7;
  668. else
  669. var offset = diff;
  670. this.setDate(this.getDate() + offset);
  671. return this;
  672. }
  673. /**
  674. * Return the current date in the format
  675. * MONTH DAY, YEAR where MONTH is the full
  676. * month such as August or September,
  677. * day is the date without leading zeroes
  678. * such as 1 or 19 and YEAR is the current
  679. * year with 4 digits such as 2018.
  680. * This method is created to return the date
  681. * in the same format that Butler for Trello
  682. * stores in the {date} variable
  683. * @memberof module:TrellinatorCore.Trellinator
  684. * @example
  685. * Trellinator.now().butlerDefaultDate();
  686. */
  687. Date.prototype.butlerDefaultDate = function()
  688. {
  689. //May 8, 2018
  690. return this.monthName()+" "+this.getDate()+", "+this.getFullYear();
  691. }
  692. /**
  693. * Find the last day of the current month
  694. * @memberof module:TrellinatorCore.Trellinator
  695. * @example
  696. * Trellinator.now().lastDayOfMonth();
  697. */
  698. Date.prototype.lastDayOfMonth = function()
  699. {
  700. return new Date(this.getFullYear(), this.getMonth()+1, 0);
  701. }
  702. /**
  703. * Subtract X weekdays from the current date
  704. * @memberof module:TrellinatorCore.Trellinator
  705. * @param days {int} number of days to subtract
  706. * @example
  707. * //3 days ago
  708. * Trellinator.now().minusWeekDays(3);
  709. */
  710. Date.prototype.minusWeekDays = function(days)
  711. {
  712. var days = parseInt(days);
  713. var cur_day = 1;
  714. while(days > 0)
  715. {
  716. this.setDate(this.getDate()-cur_day);
  717. if(this.isWeekDay())
  718. days--;
  719. }
  720. return this;
  721. }
  722. /**
  723. * Subtract X days from the current date
  724. * @memberof module:TrellinatorCore.Trellinator
  725. * @param days {int} number of days to subtract
  726. * @example
  727. * //3 days ago
  728. * Trellinator.now().minusDays(3);
  729. */
  730. Date.prototype.minusDays = function(days)
  731. {
  732. this.setDate(this.getDate()-parseInt(days));
  733. return this;
  734. }
  735. /**
  736. * Return a string formatted date from an object
  737. * @memberof module:TrellinatorCore.Trellinator
  738. * @param format {string} the desired format. Currently supports
  739. * only 2 formats: YYYY-MM-DD and HH:MM
  740. * @example
  741. * Trellinator.now().stringFormat("YYYY-MM-DD");
  742. */
  743. Date.prototype.stringFormat = function(format)
  744. {
  745. if(format == "YYYY-MM-DD")
  746. {
  747. var year = this.getFullYear();
  748. var month = this.getMonth()+1;
  749. var day = this.getDate();
  750. if (day < 10) {
  751. day = '0' + day;
  752. }
  753. if (month < 10) {
  754. month = '0' + month;
  755. }
  756. var ret = format.replace("YYYY",year).replace("MM",month).replace("DD",day);
  757. }
  758. else if(format == "HH:MM")
  759. {
  760. var hours = this.getHours();
  761. var minutes = this.getMinutes();
  762. if (hours < 10) {
  763. day = '0' + day;
  764. }
  765. if (minutes < 10) {
  766. month = '0' + month;
  767. }
  768. var ret = format.replace("HH",hours).replace("MM",minutes);
  769. }
  770. else
  771. throw new Error("Unsupported format passed to Date.stringFormat: "+format+" add more formats!");
  772. return ret;
  773. }
  774. /**
  775. * Return a string that is the day number
  776. * followed by ordinal suffix (th,rd,st)
  777. * @memberof module:TrellinatorCore.Trellinator
  778. * @example
  779. * Trellinator.now().ordinalDay();
  780. */
  781. Date.prototype.ordinalDay = function()
  782. {
  783. return new Number(this.getDate()).nth();
  784. }
  785. //https://stackoverflow.com/a/15397539
  786. Number.prototype.nth= function(){
  787. if(this%1) return this;
  788. var s= this%100;
  789. if(s>3 && s<21) return this+'th';
  790. switch(s%10){
  791. case 1: return this+'st';
  792. case 2: return this+'nd';
  793. case 3: return this+'rd';
  794. default: return this+'th';
  795. }
  796. }
  797. Trellinator.googleDriveIdRegExp = function()
  798. {
  799. return /.*[^-\w]([-\w]{25,})[^-\w]?.*/;
  800. }
  801. ////////////////////////////////////////////////////////////////////////////////////////
  802. function getFolderByURL_(folderUrl)
  803. {
  804. var folderID = folderUrl.match(Trellinator.googleDriveIdRegExp())[1];
  805. var folder = DriveApp.getFolderById(folderID);
  806. return folder;
  807. }
  808. ////////////////////////////////////////////////////////////////////////////////////////
  809. Trellinator.getFileByURL = function(fileUrl)
  810. {
  811. var fileID = fileUrl.match(Trellinator.googleDriveIdRegExp());
  812. if(fileID)
  813. fileID = fileID[1];
  814. else
  815. throw new InvalidDataException("Could not get file by URL: "+fileUrl);
  816. var file = DriveApp.getFileById(fileID);
  817. return file;
  818. }
  819. ////////////////////////////////////////////////////////////////////////////////////////
  820. Trellinator.getFolderByURL = function(folderUrl)
  821. {
  822. var folderID = folderUrl.match(/.*[^-\w]([-\w]{25,})[^-\w]?.*/);
  823. if(folderID)
  824. folderID = folderID[1];
  825. else
  826. throw new InvalidDataException("Could not get folder by URL: "+folderUrl);
  827. var folder = DriveApp.getFolderById(folderID);
  828. return folder;
  829. }
  830. Trellinator.findOrCreateFileByName = function(filename,creator,parent)
  831. {
  832. if(!parent)
  833. {
  834. parent = DriveApp.getRootFolder();
  835. }
  836. if(!creator)
  837. {
  838. creator = DocumentApp;
  839. }
  840. try
  841. {
  842. var ret = Trellinator.getFileByName(filename,parent);
  843. }
  844. catch(e)
  845. {
  846. Notification.expectException(InvalidDataException,e);
  847. var ret = DriveApp.getFileById(creator.create(filename).getId());
  848. DriveApp.getRootFolder().removeFile(ret);
  849. parent.addFile(ret);
  850. }
  851. return ret;
  852. }
  853. Trellinator.getFileByName = function(filename,parent)
  854. {
  855. if(!parent)
  856. {
  857. parent = DriveApp.getRootFolder();
  858. }
  859. var iter = parent.getFilesByName(filename);
  860. if(iter.hasNext())
  861. {
  862. return iter.next();
  863. }
  864. else
  865. {
  866. throw new InvalidDataException("No file: "+filename+" in parent: "+parent.getName());
  867. }
  868. }
  869. Trellinator.findOrCreateFolderByName = function(filename,parent)
  870. {
  871. if(!parent)
  872. {
  873. parent = DriveApp.getRootFolder();
  874. }
  875. var id = Trellinator.getFolderByName(filename,parent);
  876. if(!id.id)
  877. {
  878. var ret = parent.createFolder(filename);
  879. }
  880. else
  881. {
  882. var ret = DriveApp.getFolderById(id.id);
  883. }
  884. return ret;
  885. }
  886. Trellinator.getFolderByName = function(fileName, fileInFolder)
  887. {
  888. var filecount = 0;
  889. var dupFileArray = [];
  890. var folderID = "";
  891. if(!fileInFolder)
  892. {
  893. fileInFolder = DriveApp.getRootFolder();
  894. }
  895. fileInFolder = fileInFolder.getId();
  896. var files = DriveApp.getFoldersByName(fileName);
  897. while(files.hasNext()){
  898. var file = files.next();
  899. dupFileArray.push(file.getId());
  900. filecount++;
  901. };
  902. if(filecount > 1){
  903. if(typeof fileInFolder === 'undefined'){
  904. folderID = {"id":false,"error":"More than one file with name: "+fileName+". \nTry adding the file's folder name as a reference in Argument 2 of this function."}
  905. }else{
  906. //iterate through list of files with the same name
  907. for(fl = 0; fl < dupFileArray.length; fl++){
  908. var activeFile = DriveApp.getFileById(dupFileArray[fl]);
  909. var folders = activeFile.getParents();
  910. var folder = ""
  911. var foldercount = 0;
  912. //Get the folder name for each file
  913. while(folders.hasNext()){
  914. folder = folders.next().getName();
  915. foldercount++;
  916. };
  917. if(folder === fileInFolder && foldercount > 1){
  918. folderID = {"id":false,"error":"There is more than one parent folder: "+fileInFolder+" for file "+fileName}
  919. };
  920. if(folder === fileInFolder){
  921. folderID = {"id":dupFileArray[fl],"error":false};
  922. }else{
  923. folderID = {"id":false,"error":"There are multiple files named: "+fileName+". \nBut none of them are in folder, "+fileInFolder}
  924. };
  925. };
  926. };
  927. }else if(filecount === 0){
  928. folderID = {"id":false,"error":"No file in your drive exists with name: "+fileName};
  929. }else{ //IF there is only 1 file with fileName
  930. folderID = {"id":dupFileArray[0],"error":false};
  931. };
  932. return folderID;
  933. }
  934. /**
  935. * Static method to escape user input to be
  936. * used as part of a regular expression
  937. * @memberof module:TrellinatorCore.Trellinator
  938. */
  939. Trellinator.downloadFileToGoogleDrive = function(fileURL)
  940. {
  941. if(Trellinator.isGoogleAppsScript())
  942. {
  943. try
  944. {
  945. var folder = DriveApp.getFoldersByName("Trellinator Downloads").next();
  946. }
  947. catch(e)
  948. {
  949. var folder = DriveApp.createFolder("Trellinator Downloads");
  950. }
  951. var response = UrlFetchApp.fetch(fileURL, {muteHttpExceptions: true});
  952. var rc = response.getResponseCode();
  953. if (rc == 200) {
  954. var fileBlob = response.getBlob()
  955. var file = folder.createFile(fileBlob);
  956. }
  957. else
  958. throw new Error("Unable to get file: "+rc);
  959. }
  960. else
  961. {
  962. var file = new MockDriveFile(fileURL, "Mock Drive File");
  963. }
  964. return file;
  965. }
  966. var MockDriveFile = function(url,name)
  967. {
  968. this.url = url;
  969. this.name = name;
  970. this.getUrl = function()
  971. {
  972. return this.url;
  973. }
  974. this.getName = function()
  975. {
  976. return this.name;
  977. }
  978. }
  979. /**
  980. * Static method to escape user input to be
  981. * used as part of a regular expression
  982. * @memberof module:TrellinatorCore.Trellinator
  983. */
  984. RegExp.escape = function(s) {
  985. return s.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&');
  986. };
  987. String.prototype.replaceAll = function(search, replacement) {
  988. var target = this;
  989. return target.split(search).join(replacement);
  990. };
  991. //SOME EXPERIMENTAL DATE PARSING FUNCTIONS
  992. Trellinator.isMonthFirstDate = function()
  993. {
  994. var ret = false;
  995. if(Trellinator.isGoogleAppsScript())
  996. {
  997. var loc = SpreadsheetApp.getActiveSpreadsheet().getSpreadsheetLocale();
  998. if((loc.indexOf("_CA") > -1) || (loc.indexOf("_US") > -1))
  999. ret = true;
  1000. }
  1001. return ret;
  1002. }
  1003. Trellinator.parseDate = function(text)
  1004. {
  1005. var ret = Trellinator.now();
  1006. var start = new Date(ret);
  1007. var replace = null;
  1008. text = text.toLowerCase();
  1009. var at_index = 0;
  1010. //deal with tomorrow at a given time or not, default to 9am
  1011. if(parts = new RegExp("(.*)\\b(tomorrow|today)(.*)","i").exec(text))
  1012. {
  1013. replace = parts[1];
  1014. if(parts[2] == "tomorrow")
  1015. ret.addDays(1);
  1016. ret.at(Trellinator.optionalAt(parts[3]));
  1017. }
  1018. else if(parts = new RegExp("(.*)\\bnext( week| month| (.+)day)?( (on)? ((.+)day|(the ([0-9]+)(st|rd|th))))?( at .+)?","i").exec(text))
  1019. {
  1020. replace = parts[1];
  1021. //Specific day next wek
  1022. if(parts[3])
  1023. ret.next(parts[3]+"day");
  1024. //Sometime next week optionally on a day optionally at a time
  1025. else if(parts[2].trim() == "week")
  1026. {
  1027. if(parts[7])
  1028. var day = parts[7]+"day";
  1029. else
  1030. var day = "monday";
  1031. ret.next(day);
  1032. }
  1033. else if(parts[2].trim() == "month")
  1034. {
  1035. if(parts[9])
  1036. var day = parseInt(parts[9].trim());
  1037. else
  1038. var day = 1;
  1039. ret.addMonths(1).on(day);
  1040. }
  1041. ret.at(Trellinator.optionalAt(parts[11]));
  1042. }
  1043. else if(parts = new RegExp("(.*)\\bon ((.+)day|(([0-9]+)(st|th|rd)?( of)? (jan[A-Za-z]*|feb[A-Za-z]*|mar[A-Za-z]*|apr[A-Za-z]*|may|jun[A-Za-z]*|jul[A-Za-z]*|aug[A-Za-z]*|sept[A-Za-z]*|oct[A-Za-z]*|nov[A-Za-z]*|dec[A-Za-z]*),?( ([0-9]+))?)|((jan[A-Za-z]*|feb[A-Za-z]*|mar[A-Za-z]*|apr[A-Za-z]*|may|jun[A-Za-z]*|jul[A-Za-z]*|aug[A-Za-z]*|sept[A-Za-z]*|oct[A-Za-z]*|nov[A-Za-z]*|dec[A-Za-z]*) ([0-9]+)(th|st|rd)?),?( ([0-9]+))?|(([0-9]+)(/|-)([0-9]+)((/|-)([0-9]+))?))( at .+)?","i").exec(text))
  1044. {
  1045. replace = parts[1];
  1046. //on a day
  1047. if(parts[3])
  1048. {
  1049. var day = parts[3]+"day";
  1050. ret.next(day);
  1051. }
  1052. //on a date with month name first
  1053. else if(parts[13])
  1054. {
  1055. var month = Trellinator.fullMonth(parts[12]);
  1056. var day = parts[13];
  1057. ret = new Date(day+" "+month+" "+Trellinator.optionalYear(parts[15]));
  1058. }
  1059. //on a date in numeric format with separators
  1060. else if(parts[17])
  1061. {
  1062. var day = Trellinator.isMonthFirstDate() ? parts[20]:parts[18];
  1063. var month = Trellinator.isMonthFirstDate() ? parts[18]:parts[20];
  1064. if(month.length == 1)
  1065. month = "0"+month;
  1066. if(day.length == 1)
  1067. day = "0"+day;
  1068. ret = new Date(Trellinator.optionalYear(parts[23])+"-"+month+"-"+day);
  1069. }
  1070. //on a date with date first followed by month name
  1071. else if(parts[5])
  1072. {
  1073. var month = Trellinator.fullMonth(parts[8]);
  1074. var day = parseInt(parts[5]);
  1075. ret = new Date(day+" "+month+" "+Trellinator.optionalYear(parts[10]));
  1076. }
  1077. ret.at(Trellinator.optionalAt(parts[24]));
  1078. }
  1079. //Check if the date changed since the start, if not, then
  1080. //no parseable date strings were found. We have to floor/1000
  1081. //because new Date(other_date) creates a new date with 000
  1082. //for the milliseconds part
  1083. if(Math.floor(start.getTime()/1000) == Math.floor(ret.getTime()/1000))
  1084. throw new Error("No date parseable strings found");
  1085. return {date: ret,comment: text.replace(text.replace(replace,""),"")};
  1086. }
  1087. Trellinator.fullMonth = function(month)
  1088. {
  1089. var map = {"jan":"january",
  1090. "feb":"february",
  1091. "mar":"march",
  1092. "apr":"april",
  1093. "may":"may",
  1094. "jun":"june",
  1095. "jul":"july",
  1096. "aug":"august",
  1097. "sept":"september",
  1098. "oct":"october",
  1099. "nov":"november",
  1100. "dec":"december"};
  1101. return map[month] ? map[month]:month;
  1102. }
  1103. Trellinator.optionalYear = function(str)
  1104. {
  1105. return str ? parseInt(str):Trellinator.now().getFullYear();
  1106. }
  1107. Trellinator.optionalAt = function(str)
  1108. {
  1109. //at time included
  1110. if(str)
  1111. ret = Trellinator.atString(str);
  1112. //default to 9am
  1113. else
  1114. ret = "9:00";
  1115. return ret;
  1116. }
  1117. Trellinator.atString = function(str)
  1118. {
  1119. var ampm = new RegExp("(at )?([0-9]+):?([0-9]*) ?(am|pm)?","i").exec(str.trim());
  1120. var hours = (ampm[4] != "pm") ? ampm[2]:(parseInt(ampm[2])+12);
  1121. var minutes = ampm[3] ? ampm[3]:"00";
  1122. return hours+":"+minutes;
  1123. }
  1124. Trellinator.oppoIdFromCard = function(card)
  1125. {
  1126. var ret = 0;
  1127. notif.card().attachments().each(function(attachment)
  1128. {
  1129. if(parts = new RegExp(".+Opportunity/(.+)/view$").exec(attachment.url))
  1130. {
  1131. ret = parts[1];
  1132. }
  1133. });
  1134. return ret;
  1135. }
  1136. Trellinator.testDateParsing = function()
  1137. {
  1138. var cmts = [
  1139. "do this thing on Thursday",
  1140. "do this thing on Thursday at 10am",
  1141. "do this thing on September 23",
  1142. "do this thing on Sept 23",
  1143. "do this thing on September 23rd",
  1144. "do this thing on Sept 23rd",
  1145. "do this thing on September 23 at 10am",
  1146. "do this thing on Sept 23 at 10am",
  1147. "do this thing on September 23rd at 10am",
  1148. "do this thing on September 23, 2019",
  1149. "do this thing on September 23rd, 2019",
  1150. "do this thing on September 23, 2019 at 10am",
  1151. "do this thing on September 23rd, 2019 at 10am",
  1152. "do this thing on 9/23/2019 at 10am",
  1153. "do this thing on 9/23 at 10am",
  1154. "do this thing on 23 September",
  1155. "do this thing on 23rd September",
  1156. "do this thing on 23rd of September",
  1157. "do this thing on 23 September at 10am",
  1158. "do this thing on 23rd September at 10am",
  1159. "do this thing on 23rd of September at 10am",
  1160. "do this thing on 23 September, 2019",
  1161. "do this thing on 23rd September, 2019",
  1162. "do this thing on 23rd of September, 2019",
  1163. "do this thing on 23 September, 2019 at 10am",
  1164. "do this thing on 23rd September, 2019 at 10am",
  1165. "do this thing on 23rd of September, 2019 at 10am",
  1166. "do this thing on 23 September 2019",
  1167. "do this thing on 23rd September 2019",
  1168. "do this thing on 23rd of September 2019",
  1169. "do this thing on 23 September 2019 at 10am",
  1170. "do this thing on 23rd September 2019 at 10am",
  1171. "do this thing on 23rd of September 2019 at 10am",
  1172. "do this thing next Monday at 9am",
  1173. "do this thing next Monday",
  1174. "do this thing next week on Monday at 9am",
  1175. "do this thing next Monday at 9am",
  1176. "do this thing next week",
  1177. "do this thing next month",
  1178. "do this thing next month on the 1st at 9am"];
  1179. for(var i = 0;i < cmts.length;i++)
  1180. {
  1181. console.log("from: "+cmts[i]);
  1182. console.log(Trellinator.parseDate(cmts[i]).date.toLocaleString());
  1183. console.log(Trellinator.parseDate(cmts[i]).comment);
  1184. }
  1185. }
  1186. // https://github.com/uxitten/polyfill/blob/master/string.polyfill.js
  1187. // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/padStart
  1188. if (!String.prototype.padStart) {
  1189. String.prototype.padStart = function padStart(targetLength,padString) {
  1190. targetLength = targetLength>>0; //truncate if number or convert non-number to 0;
  1191. padString = String((typeof padString !== 'undefined' ? padString : ' '));
  1192. if (this.length > targetLength) {
  1193. return String(this);
  1194. }
  1195. else {
  1196. targetLength = targetLength-this.length;
  1197. if (targetLength > padString.length) {
  1198. padString += padString.repeat(targetLength/padString.length); //append to original to ensure we are longer than needed
  1199. }
  1200. return padString.slice(0,targetLength) + String(this);
  1201. }
  1202. };
  1203. }
  1204. //https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/endsWith
  1205. if (!String.prototype.endsWith) {
  1206. String.prototype.endsWith = function(search, this_len) {
  1207. if (this_len === undefined || this_len > this.length) {
  1208. this_len = this.length;
  1209. }
  1210. return this.substring(this_len - search.length, this_len) === search;
  1211. };
  1212. }
  1213. var trellinator_global_functions_list = this;
  1214. Trellinator.cacheCollection = function(key,collection)
  1215. {
  1216. if(Trellinator.isGoogleAppsScript());
  1217. {
  1218. try
  1219. {
  1220. var first = collection.first();
  1221. to_cache = [first.constructor.toString(),collection.obj];
  1222. }
  1223. catch(e)
  1224. {
  1225. Notification.expectException(InvalidDataException,e);
  1226. to_cache = [];
  1227. }
  1228. var cache_card = Card.findOrCreate(Board
  1229. .findOrCreate("TrelloBackedCache")
  1230. .findOrCreateList("Cached Collections"),key);
  1231. var cache_text = Utilities.base64Encode(JSON.stringify(to_cache));
  1232. if(cache_text.length < 16384)
  1233. {
  1234. cache_card.setDescription(cache_text);
  1235. }
  1236. else
  1237. {
  1238. var cache_file = Trellinator.findOrCreateFileByName("TRELLOBACKEDCACHE-"+key+cache_card.id(),DocumentApp,Trellinator.findOrCreateFolderByName("TRELLOBACKEDCACHE"));
  1239. DocumentApp.openById(cache_file.getId()).getBody().setText(cache_text);
  1240. cache_card.attachLink(cache_file.getUrl());
  1241. }
  1242. PropertiesService
  1243. .getUserProperties()
  1244. .setProperty(key,cache_card.link());
  1245. }
  1246. }
  1247. Trellinator.cachedCollection = function(key)
  1248. {
  1249. var ret = null;
  1250. var props = PropertiesService.getUserProperties();
  1251. if(Trellinator.isGoogleAppsScript())
  1252. {
  1253. var link = props.getProperty(key);
  1254. try
  1255. {
  1256. var card = new Card({link: link});
  1257. if(!card.isArchived())
  1258. {
  1259. if(card.attachments().length())
  1260. {
  1261. var cached = JSON.parse(Utilities.newBlob(Utilities.base64Decode(DocumentApp.openByUrl(card.attachments().first().link()).getBody().getText())).getDataAsString());
  1262. }
  1263. else
  1264. {
  1265. var cached = JSON.parse(Utilities.newBlob(Utilities.base64Decode(card.description())).getDataAsString());
  1266. }
  1267. if(Object.keys(cached).length)
  1268. {
  1269. var cons = new IterableCollection(trellinator_global_functions_list).find(function(glob)
  1270. {
  1271. var ret = false;
  1272. if((glob instanceof Function) && (glob.toString() === cached[0]))
  1273. {
  1274. ret = glob;
  1275. }
  1276. return ret;
  1277. }).first();
  1278. var obj = cached[1];
  1279. ret = new IterableCollection(obj).find(function(inst)
  1280. {
  1281. var ret = new cons(inst.data);
  1282. for(var key in inst)
  1283. {
  1284. if(key != "data")
  1285. {
  1286. ret[key] = inst[key];
  1287. }
  1288. }
  1289. return ret;
  1290. });
  1291. }
  1292. else
  1293. {
  1294. ret = new IterableCollection({});
  1295. }
  1296. }
  1297. else
  1298. {
  1299. props.deleteProperty(key);
  1300. }
  1301. }
  1302. catch(e)
  1303. {
  1304. //Doesn't matter why this fails, we just return null if we
  1305. //can't get a cached value for any reason. At the same time,
  1306. //clear existing value from PropertiesService in case it's
  1307. //because of some corrupt data
  1308. props.deleteProperty(key);
  1309. }
  1310. }
  1311. return ret;
  1312. }
  1313. Trellinator.unCacheCollection = function(key)
  1314. {
  1315. var props = PropertiesService.getUserProperties();
  1316. if(Trellinator.isGoogleAppsScript())
  1317. {
  1318. if(link = props.getProperty(key))
  1319. {
  1320. try
  1321. {
  1322. new Card({link: link}).del();
  1323. }
  1324. catch(e)
  1325. {
  1326. }
  1327. props.deleteProperty(key);
  1328. }
  1329. }
  1330. }
  1331. /**
  1332. * Static method to safely wrap a regex for
  1333. * re-use because of this insane JavaScript bug:
  1334. * https://stackoverflow.com/questions/3891641/regex-test-only-works-every-other-time
  1335. * @memberof module:TrellinatorCore.Trellinator
  1336. */
  1337. Trellinator.regex = function(reg)
  1338. {
  1339. return (new RegExp(reg));
  1340. }