Commit efcc91ab authored by Farid Neshat's avatar Farid Neshat

Make the project work!

parent 86c656fc
......@@ -92,8 +92,7 @@
"balanced-match": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz",
"integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=",
"dev": true
"integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c="
},
"base64-url": {
"version": "1.2.1",
......@@ -227,7 +226,6 @@
"version": "1.1.8",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.8.tgz",
"integrity": "sha1-wHshHHyVLsH479Uad+8NHTmQopI=",
"dev": true,
"requires": {
"balanced-match": "1.0.0",
"concat-map": "0.0.1"
......@@ -364,8 +362,7 @@
"concat-map": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
"integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=",
"dev": true
"integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s="
},
"connect": {
"version": "2.30.2",
......@@ -612,6 +609,11 @@
}
}
},
"debuglog": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/debuglog/-/debuglog-1.0.1.tgz",
"integrity": "sha1-qiT/uaw9+aI1GDfPstJ5NgzXhJI="
},
"deep-eql": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-2.0.2.tgz",
......@@ -1077,6 +1079,14 @@
}
}
},
"hubot-conversation": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/hubot-conversation/-/hubot-conversation-1.1.1.tgz",
"integrity": "sha1-+Ak36pIue+m1XsPoWUZlaKX9p20=",
"requires": {
"debuglog": "1.0.1"
}
},
"hubot-test-helper": {
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/hubot-test-helper/-/hubot-test-helper-1.6.0.tgz",
......@@ -1333,7 +1343,6 @@
"version": "3.0.4",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz",
"integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==",
"dev": true,
"requires": {
"brace-expansion": "1.1.8"
}
......@@ -1392,6 +1401,20 @@
}
}
},
"moment": {
"version": "2.18.1",
"resolved": "https://registry.npmjs.org/moment/-/moment-2.18.1.tgz",
"integrity": "sha1-w2GT3Tzhwu7SrbfIAtu8d6gbHA8="
},
"moment-business-time": {
"version": "0.7.0",
"resolved": "https://registry.npmjs.org/moment-business-time/-/moment-business-time-0.7.0.tgz",
"integrity": "sha512-a8zz0penJwKFd173GvtfpFTTdKW1cXY6DulQS36Gfp3YPRDDVSOcYbuE0EieT7A+86tJ+PIVdzsirDAwJfSHMA==",
"requires": {
"minimatch": "3.0.4",
"moment": "2.18.1"
}
},
"morgan": {
"version": "1.6.1",
"resolved": "https://registry.npmjs.org/morgan/-/morgan-1.6.1.tgz",
......@@ -2072,6 +2095,11 @@
"integrity": "sha1-45mpgiV6J22uQou5KEXLcb3CbRk=",
"dev": true
},
"timestring": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/timestring/-/timestring-5.0.0.tgz",
"integrity": "sha512-azKsH5XEbh7cQsJHocNRq6SRRi+NU2vri4OkSutYi9vyYaYCAyzb5Gru5EWV1CuuXZzcNcqiHupyFCsBSucPWw=="
},
"tough-cookie": {
"version": "2.3.2",
"resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.3.2.tgz",
......
......@@ -19,10 +19,12 @@
},
"dependencies": {
"coffee-script": "^1.12.7",
"human-interval": "^0.1.6",
"hubot-conversation": "^1.1.1",
"moment": "^2.18.1",
"moment-business-time": "^0.7.0",
"node-bamboohr": "0.0.4",
"sugar": "^2.0.4",
"sugar-date": "^2.0.4"
"sugar-date": "^2.0.4",
"timestring": "^5.0.0"
},
"peerDependencies": {
"hubot": "2.x"
......
#!/bin/bash
!/bin/bash
# bootstrap environment
source script/bootstrap
......
// Description
// A hubot scripts that adds the ablity to make timeoff requests and their approval
// A hubot scripts that adds the ablity to make timeoff requests and manage them for BambooHR
//
// Configuration:
// LIST_OF_ENV_VARS_TO_SET
// HUBOT_BAMBOOHR_APIKEY, HUBOT_BAMBOOHR_DOMAIN
//
// Commands
// hubot hello - <what the respond trigger does>
// orly - <what the hear trigger does>
// timeoff help
// request [type] timeoff|leave|vacation [from|on <date>] [for <period>] [[un]till|to <date>] because <reason>
//
// Notes:
// <optional notes required for the script>
//
// Author:
// Farid Neshat <FaridN_SOAD@Yahoo.com>
// Farid Nouri Neshat <FaridN_SOAD@Yahoo.com>
const timestring = require('timestring');
const { Date } = require('sugar-date');
const humanInterval = require('human-interval');
const Conversation = require('hubot-conversation');
const bamboohr = new (require('node-bamboohr'))({
apikey: process.env.HUBOT_BAMBOOHR_APIKEY,
subdomain: process.env.HUBOT_BAMBOOHR_DOMAIN,
});
const moment = require('moment');
require('moment-business-time');
const HOUR = 3600000;
const DAY = 24 * 3600000;
const DAY = 24 * HOUR;
const WORKING_HOURS = 8;
const WORKING_DAY_START_HOUR = 8;
const HUMAN_DATE_FORMAT = '{Dow} {Mon} {d}';
const WORKING_DAY_START_HOUR = 9;
const WORKING_DAY_END_HOUR = 18;
const HUMAN_DATE_FORMAT = 'ddd MMM D';
const DIALOG_TIMEOUT = 600000;
let timeOffTypes;
bamboohr.timeOffTypes((err, result) => {
console.log(result)
if (err) {
throw err;
}
timeOffTypes = result.timeOffTypes;
timeOffTypes.forEach(type => {
type.unit = type.units;
type.shortName = type.name.toLowerCase().replace('leave', '').trim();
});
});
module.exports = function(robot) {
robot.respond(/request\s+((\w+?)[\s-]+)?(time-?off|leave|vacation)\s+(from|on)\s+(.+?)\s+for\s+(.+?)\s*(becuase (.+?))?\s*$/i,
const switchBoard = new Conversation(robot, ['user']);
robot.respond(/request\s+((\w+?)[\s-]+)?(time[- ]?off|leave|vacation)\s+(.+?)\s*$/i,
res => {
let [,, type, type2,, start, amount, reason] = res.match;
let [,, type, type2, args] = res.match;
if (type2 === 'vacation') {
type = 'vacation';
}
let reason;
type = getType(type);
start = Date(Date.create(start, { future: true }));
if (args.includes('because')) {
[args, reason] = args.split(/\s*because\s*/);
}
amount = humanInterval(amount) / DAY;
amount = Math.round(amount);
const end = start.clone().addDays(amount);
if (end.isBefore(end.clone().set({ hour: WORKING_DAY_START_HOUR }, true))) {
end.addDays(-1);
if (type2 === 'vacation') {
type = 'vacation';
}
if (type.unit === 'hours') {
amount *= WORKING_HOURS;
}
type = getType(type, reason);
let { start, amount, end } = normalizeArgs(parseArgs(args), type.unit);
room = res.envelope.user.name;
robot.messageRoom(room, `So you want ${type.shortName} leave from ${
start.format(HUMAN_DATE_FORMAT + ' {yyyy}')} to ${end.format(HUMAN_DATE_FORMAT)} for ${
moment(start).format(HUMAN_DATE_FORMAT + ' YYYY')} to ${moment(end).format(HUMAN_DATE_FORMAT)} for ${
amount} working ${type.unit}?`);
const dialog = switchBoard.startDialog(res, DIALOG_TIMEOUT,
'You didn\'t answer. I don\'t have all day to wait for you. Next time answer in 60 seconds!');
dialog.addChoice(/yes/, (msg) => {
const request = {
status: 'requested',
start,
end,
timeOffTypeId: type.id,
amount,
};
if (reason) {
request.notes = [{
from: 'employee',
note: reason
}];
}
bamboohr.employee(10).requestTimeOff(request, function (err, result) {
if (err) {
console.log(err,result)
msg.reply('I\'m sorry, something went wrong: ' + err.message);
return;
}
msg.reply('Everything worked out');
});
});
dialog.addChoice(/no/, (msg) => {
dialog.resetChoices();
});
});
};
function getType() {
const parseArgs = module.exports.parseArgs = args => {
// Throw away punctuations. This doesn't support non-english languages
args = args.replace(/([^A-Za-z0-9])/g, ' ').replace(/\s+/g, ' ');
const regex = /\b(from|on|for|untill|till|to)\b/g;
let previousMatch = regex.exec(args);
if (!previousMatch) {
throw new Error("Couldn't parse any arguments. Specify the details with the following keywords: " +
regex.source().split('|').join(', '));
}
const result = {};
let currentDate;
while (true) {
let nextMatch = regex.exec(args);
const currentArg = args.substring(previousMatch[0].length + previousMatch.index, nextMatch && nextMatch.index || args.length);
if (previousMatch[0] === 'for') {
result.amount = timestring(currentArg, 'hours', {
hoursPerDay: 8,
daysPerWeek: 5,
});
} else {
// If keyword is not 'for', must be a date argument
const isStart = /from|on/.test(previousMatch[0]);
if (currentDate) {
const dateOption = { [isStart ? 'past': 'future'] : true };
currentDate = Date.get(currentDate, currentArg, dateOption);
} else {
currentDate = Date.create(currentArg, { future: true});
}
result[isStart ? 'start' : 'end'] = currentDate;
}
if (!nextMatch) {
if (result.end) {
result.end = Date.endOfDay(result.end);
}
return result;
}
previousMatch = nextMatch;
}
};
const normalizeArgs = module.exports.normalizeArgs = ({start, amount, end}, unit = 'hours') => {
if (!end && !amount) {
end = Date.endOfDay(Date.clone(start));
amount = WORKING_HOURS;
}
if (!end) {
end = moment(start).addWorkingTime(amount, 'hours');
}
start = moment(start).nextWorkingTime();
end = moment(end).lastWorkingTime();
if (!amount) {
amount = Math.ceil(moment(end).workingDiff(start, unit, true));
// Handle lunch hour at the middle of the day
if (unit === 'hours' && amount % WORKING_HOURS > (WORKING_HOURS / 2)) {
amount -= 1;
}
}
return {
shortName: 'vacation',
unit: 'hours',
start: start.toDate(),
end: end.toDate(),
amount
};
};
function getType(name, reason) {
if (!name) {
if (/\bsick\b/.test(reason)) {
name = 'sick';
} else if (/conf\b/.test(reason)) {
name = 'conference';
}
}
return timeOffTypes.find(type => type.shortName === name) || timeOffTypes[0];
}
......@@ -8,19 +8,33 @@ chai.use(require('sinon-chai'));
const { expect } = chai;
const Bamboohr = require('node-bamboohr');
const BambooEmployee = require('node-bamboohr/lib/employee');
const stubs = [
sinon.stub(Bamboohr.prototype, 'timeOffTypes').yields(null, {
timeOffTypes: [{
id: '77',
name: 'Vacation',
units: 'hours',
}],
defaultHours: [
{ name: 'Saturday', amount: '0' },
{ name: 'Sunday', amount: '0' },
{ name: 'default', amount: '8' }
]
}),
sinon.stub(BambooEmployee.prototype, 'requestTimeOff').yields(),
];
const sctiptPath = '../src/bamboohr-timeoff.js';
const script = require(sctiptPath);
const helper = new Helper(sctiptPath);
const roomName = '';
const Bamboohr = require('node-bamboohr');
describe('bamboohr-timeoff', function() {
before(function () {
this.timeoffTypes = sinon.stub(Bamboohr.prototype, 'timeOffTypes').yield;
});
after(function () {
this.timeoffTypes.restore();
stubs.forEach(stub => stub.restore());
});
beforeEach(function() {
this.room = helper.createRoom({
......@@ -38,4 +52,161 @@ describe('bamboohr-timeoff', function() {
' 2017 to Wed Jul 26 for 16 working hours?');
});
});
it('from for because', function() {
return this.room.user.say('bob', 'hubot request timeoff from 25 July 2017 for 2 days because fuck').then(() => {
expect(this.room.messages[1][1]).to.be.equal('So you want vacation leave from Tue Jul 25' +
' 2017 to Wed Jul 26 for 16 working hours?');
});
});
it('from for 8 hours because', function() {
return this.room.user.say('bob', 'hubot request timeoff from 25 July 2017 for 8 hours because fuck').then(() => {
expect(this.room.messages[1][1]).to.be.equal('So you want vacation leave from Tue Jul 25' +
' 2017 to Tue Jul 25 for 8 working hours?');
});
});
describe('Parsing arguments', function () {
before(function () {
this.clock = sinon.useFakeTimers(Date.parse('1 July 2017'));
});
after(function () {
this.clock.restore();
});
it('for 1 day', function () {
expect(script.parseArgs('for 1 day')).to.be.eql({
amount: 8, // unit is in hours.
});
});
it('on <date>', function () {
expect(script.parseArgs('on 1 Feb 2017').start).to.be.eql(new Date('1 Feb 2017'));
});
it('to <date>', function () {
expect(script.parseArgs('to 1 Feb 2017').end).to.be.eql(new Date(new Date('2 Feb 2017') - 1));
});
it('Two arguments', function () {
expect(script.parseArgs('to 1 Feb 2017 for 2 days')).to.be.eql({
end: new Date(new Date('2 Feb 2017') - 1),
amount: 16,
});
expect(script.parseArgs('from tomorrow for 8 hours')).to.be.eql({
start: new Date('2 July 2017'),
amount: 8,
});
});
it('Three arguments', function () {
expect(script.parseArgs('from 1 Jan 2017 to 1 Feb 2017 for 2 days')).to.be.eql({
start: new Date('1 Jan 2017'),
end: new Date(new Date('2 Feb 2017') - 1),
amount: 16,
});
});
it('Keeps date context', function () {
expect(script.parseArgs('from 14 Aug 2017 to saturday')).to.be.eql({
start: new Date('14 Aug 2017'),
end: new Date(new Date('20 Aug 2017') - 1),
});
});
it('Only looks at future', function () {
expect(script.parseArgs('from tuesday to saturday')).to.be.eql({
start: new Date('4 July 2017'),
end: new Date(new Date('9 July 2017') - 1),
});
});
});
describe('Normalizing Arguments', function () {
it('start only', function () {
expect(script.normalizeArgs({
start: new Date('14 August 2017')
})).to.be.eql({
start: new Date('14 August 2017 9:00'),
amount: 8, // unit is in hours.
end: new Date('14 August 2017 17:00'),
});
});
it('start and amount', function () {
expect(script.normalizeArgs({
start: new Date('14 August 2017'),
amount: 16
})).to.be.eql({
start: new Date('14 August 2017 9:00'),
amount: 16, // unit is in hours.
end: new Date('15 August 2017 17:00'),
});
});
it('start and end', function () {
expect(script.normalizeArgs({
start: new Date('14 August 2017'),
end: new Date('16 August 2017'),
})).to.be.eql({
start: new Date('14 August 2017 9:00'),
amount: 16, // unit is in hours.
end: new Date('15 August 2017 17:00'),
});
});
it('convert unit to days', function () {
expect(script.normalizeArgs({
start: new Date('14 August 2017'),
end: new Date('16 August 2017'),
}, 'days')).to.be.eql({
start: new Date('14 August 2017 9:00'),
amount: 2, // unit is in hours.
end: new Date('15 August 2017 17:00'),
});
});
it('Handle weekends', function () {
expect(script.normalizeArgs({
start: new Date('4 August 2017'),
end: new Date('7 August 2017'),
}, 'days')).to.be.eql({
start: new Date('4 August 2017 9:00'),
amount: 1, // unit is in hours.
end: new Date('4 August 2017 17:00'),
});
expect(script.normalizeArgs({
start: new Date('4 August 2017'),
amount: 16,
})).to.be.eql({
start: new Date('4 August 2017 9:00'),
amount: 16, // unit is in hours.
end: new Date('7 August 2017 17:00'),
});
});
it('Handle partial days', function () {
expect(script.normalizeArgs({
start: new Date('4 August 2017'),
end: new Date('4 August 2017 13:00'),
})).to.be.eql({
start: new Date('4 August 2017 9:00'),
amount: 4, // unit is in hours.
end: new Date('4 August 2017 13:00'),
});
expect(script.normalizeArgs({
start: new Date('4 August 2017'),
end: new Date('4 August 2017 13:00'),
}, 'days')).to.be.eql({
start: new Date('4 August 2017 9:00'),
amount: 1, // unit is in hours.
end: new Date('4 August 2017 13:00'),
});
expect(script.normalizeArgs({
start: new Date('4 August 2017'),
amount: 4,
})).to.be.eql({
start: new Date('4 August 2017 9:00'),
amount: 4, // unit is in hours.
end: new Date('4 August 2017 13:00'),
});
});
it('Handle lunch hour', function () {
expect(script.normalizeArgs({
start: new Date('4 August 2017'),
end: new Date('4 August 2017 15:00'),
})).to.be.eql({
start: new Date('4 August 2017 9:00'),
amount: 5, // unit is in hours.
end: new Date('4 August 2017 15:00'),
});
});
});
});
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment