bamboozled-gitlab 0.2.9
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.github/ISSUE_TEMPLATE/bug_report.md +39 -0
- data/.github/ISSUE_TEMPLATE/feature_request.md +19 -0
- data/.github/ISSUE_TEMPLATE/question.md +19 -0
- data/.github/main.workflow +38 -0
- data/.github/pull_request_template.md +27 -0
- data/.gitignore +38 -0
- data/.rubocop.yml +39 -0
- data/.rubocop_todo.yml +305 -0
- data/.travis.yml +18 -0
- data/CHANGELOG.md +48 -0
- data/CONTRIBUTING.md +91 -0
- data/Dockerfile +8 -0
- data/Gemfile +16 -0
- data/Guardfile +21 -0
- data/LICENSE +22 -0
- data/README.md +170 -0
- data/Rakefile +11 -0
- data/bamboozled.gemspec +30 -0
- data/examples/employees_over_time.rb +53 -0
- data/lib/bamboozled.rb +24 -0
- data/lib/bamboozled/api/base.rb +101 -0
- data/lib/bamboozled/api/employee.rb +118 -0
- data/lib/bamboozled/api/field_collection.rb +107 -0
- data/lib/bamboozled/api/meta.rb +25 -0
- data/lib/bamboozled/api/report.rb +20 -0
- data/lib/bamboozled/api/time_off.rb +34 -0
- data/lib/bamboozled/api/time_tracking.rb +24 -0
- data/lib/bamboozled/base.rb +31 -0
- data/lib/bamboozled/errors.rb +33 -0
- data/lib/bamboozled/ext/yesno.rb +11 -0
- data/lib/bamboozled/version.rb +3 -0
- data/logos/bamboozled_logo_black.png +0 -0
- data/logos/bamboozled_logo_green.png +0 -0
- data/logos/skookum_mark_black.png +0 -0
- data/logos/skookum_mark_black.svg +175 -0
- data/relnotes/v0.1.0.md +13 -0
- data/spec/fixtures/add_employee_details.json +7 -0
- data/spec/fixtures/add_employee_response.json +4 -0
- data/spec/fixtures/add_employee_xml.yml +8 -0
- data/spec/fixtures/all_employees.json +58 -0
- data/spec/fixtures/custom_report.json +38 -0
- data/spec/fixtures/employee_emails.json +9 -0
- data/spec/fixtures/employee_table_details.json +17 -0
- data/spec/fixtures/job_info.xml +22 -0
- data/spec/fixtures/last_changed.json +28 -0
- data/spec/fixtures/meta_fields.json +5 -0
- data/spec/fixtures/meta_lists.json +5 -0
- data/spec/fixtures/meta_tables.json +5 -0
- data/spec/fixtures/meta_users.json +4 -0
- data/spec/fixtures/one_employee.json +9 -0
- data/spec/fixtures/time_off_estimate.json +23 -0
- data/spec/fixtures/time_tracking_add_200_response.json +7 -0
- data/spec/fixtures/time_tracking_add_empty_response.json +9 -0
- data/spec/fixtures/time_tracking_adjust_200_response.json +7 -0
- data/spec/fixtures/time_tracking_adjust_400_response.json +11 -0
- data/spec/fixtures/time_tracking_record_200_response.json +9 -0
- data/spec/fixtures/time_tracking_record_400_response.json +11 -0
- data/spec/fixtures/time_tracking_record_401_response.json +10 -0
- data/spec/fixtures/time_tracking_record_404_response.json +8 -0
- data/spec/fixtures/update_employee_details.json +7 -0
- data/spec/fixtures/update_employee_response.json +3 -0
- data/spec/fixtures/update_employee_table.json +8 -0
- data/spec/fixtures/update_employee_table_xml.yml +6 -0
- data/spec/fixtures/update_employee_xml.yml +8 -0
- data/spec/lib/bamboozled/api/base_spec.rb +18 -0
- data/spec/lib/bamboozled/api/employee_spec.rb +186 -0
- data/spec/lib/bamboozled/api/field_collection_spec.rb +17 -0
- data/spec/lib/bamboozled/api/meta_spec.rb +47 -0
- data/spec/lib/bamboozled/api/report_spec.rb +17 -0
- data/spec/lib/bamboozled/api/time_tracking_spec.rb +123 -0
- data/spec/lib/bamboozled/base_spec.rb +26 -0
- data/spec/lib/bamboozled_spec.rb +33 -0
- data/spec/spec_helper.rb +32 -0
- metadata +237 -0
@@ -0,0 +1,53 @@
|
|
1
|
+
# File: employees_over_time.rb
|
2
|
+
# Date Created: 2014-08-07
|
3
|
+
# Author(s): Mark Rickert (mjar81@gmail.com) / Skookum Digital Works (http://skookum.com)
|
4
|
+
#
|
5
|
+
# Description: This example script grabs all the historical users in BambooHR
|
6
|
+
# and determines how many employees there were at the end of each of the
|
7
|
+
# previous 12 months.
|
8
|
+
#
|
9
|
+
# Run this script with: ruby employees_over_time.rb
|
10
|
+
|
11
|
+
require '../lib/bamboozled/'
|
12
|
+
require 'active_support'
|
13
|
+
require 'active_support/core_ext/integer'
|
14
|
+
require 'active_support/core_ext/date'
|
15
|
+
|
16
|
+
@subdomain = 'your_subdomain'
|
17
|
+
@api_key = 'your_api_key'
|
18
|
+
|
19
|
+
def main
|
20
|
+
client = Bamboozled.client(subdomain:@subdomain, api_key:@api_key)
|
21
|
+
|
22
|
+
# Get all users in the system. Even terminated employees.
|
23
|
+
# Calling client.employee.all only gets active users.
|
24
|
+
employee_data = client.meta.users.map do |e|
|
25
|
+
# Sometimes employees are in the system but don't have an emaployee ID.
|
26
|
+
# This makes them unqueryable and they're usually a duplicate or admin user.
|
27
|
+
next unless e[:employeeId]
|
28
|
+
|
29
|
+
# Get each employee's start_date and termination date
|
30
|
+
client.employee.find(e[:employeeId], %w(displayName department hireDate terminationDate))
|
31
|
+
end.compact.reject{|e| e['hireDate'] == '0000-00-00'}
|
32
|
+
|
33
|
+
# Start from today and go back 12 months and print out how many employees were
|
34
|
+
# at the company on that date.
|
35
|
+
d = (Date.today - 1.month).end_of_month
|
36
|
+
12.times do
|
37
|
+
d = (d - 1.month).end_of_month
|
38
|
+
puts "#{d},#{employees_on_date(employee_data, d)}"
|
39
|
+
end
|
40
|
+
|
41
|
+
end
|
42
|
+
|
43
|
+
# Simple method to compare hire and termination dates and get the count of employees
|
44
|
+
# who were with the company on that day.
|
45
|
+
def employees_on_date(employees, date)
|
46
|
+
employees.map do |e|
|
47
|
+
hire_date = Date.parse(e["hireDate"]) rescue nil
|
48
|
+
termination_date = Date.parse(e["terminationDate"]) rescue nil
|
49
|
+
(hire_date <= date && (termination_date.nil? || termination_date >= date)) ? 1 : 0
|
50
|
+
end.inject(:+)
|
51
|
+
end
|
52
|
+
|
53
|
+
main
|
data/lib/bamboozled.rb
ADDED
@@ -0,0 +1,24 @@
|
|
1
|
+
require "httparty"
|
2
|
+
require "json"
|
3
|
+
require "uri"
|
4
|
+
|
5
|
+
require "bamboozled/version"
|
6
|
+
require "bamboozled/base"
|
7
|
+
require "bamboozled/errors"
|
8
|
+
require "bamboozled/ext/yesno"
|
9
|
+
require "bamboozled/api/base"
|
10
|
+
require "bamboozled/api/field_collection"
|
11
|
+
require "bamboozled/api/employee"
|
12
|
+
require "bamboozled/api/report"
|
13
|
+
require "bamboozled/api/time_off"
|
14
|
+
require "bamboozled/api/time_tracking"
|
15
|
+
require "bamboozled/api/meta"
|
16
|
+
|
17
|
+
module Bamboozled
|
18
|
+
class << self
|
19
|
+
# Creates a standard client that will raise all errors it encounters
|
20
|
+
def client(subdomain: nil, api_key: nil, httparty_options: {})
|
21
|
+
Bamboozled::Base.new(subdomain: subdomain, api_key: api_key, httparty_options: httparty_options)
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,101 @@
|
|
1
|
+
require 'json'
|
2
|
+
require "time"
|
3
|
+
|
4
|
+
module Bamboozled
|
5
|
+
module API
|
6
|
+
class Base
|
7
|
+
attr_reader :subdomain, :api_key
|
8
|
+
|
9
|
+
def initialize(subdomain, api_key, httparty_options = {})
|
10
|
+
@subdomain = subdomain
|
11
|
+
@api_key = api_key
|
12
|
+
@httparty_options = httparty_options || {}
|
13
|
+
end
|
14
|
+
|
15
|
+
protected
|
16
|
+
|
17
|
+
def post_file(path, options)
|
18
|
+
response = HTTParty.post(
|
19
|
+
"#{path_prefix}#{path}",
|
20
|
+
multipart: true,
|
21
|
+
basic_auth: auth,
|
22
|
+
body: options
|
23
|
+
)
|
24
|
+
parse_response(response, options)
|
25
|
+
end
|
26
|
+
|
27
|
+
def request(method, path, options = {})
|
28
|
+
params = {
|
29
|
+
path: path,
|
30
|
+
options: options,
|
31
|
+
method: method
|
32
|
+
}
|
33
|
+
|
34
|
+
httparty_options = @httparty_options.merge({
|
35
|
+
query: options[:query],
|
36
|
+
body: options[:body],
|
37
|
+
format: :plain,
|
38
|
+
basic_auth: auth,
|
39
|
+
headers: {
|
40
|
+
"Accept" => "application/json",
|
41
|
+
"User-Agent" => "Bamboozled/#{Bamboozled::VERSION}"
|
42
|
+
}.update(options[:headers] || {})
|
43
|
+
})
|
44
|
+
|
45
|
+
response = HTTParty.send(method, "#{path_prefix}#{path}", httparty_options)
|
46
|
+
params[:response] = response.inspect.to_s
|
47
|
+
parse_response(response, params)
|
48
|
+
end
|
49
|
+
|
50
|
+
private
|
51
|
+
|
52
|
+
def auth
|
53
|
+
{ username: api_key, password: "x" }
|
54
|
+
end
|
55
|
+
|
56
|
+
def path_prefix
|
57
|
+
"https://api.bamboohr.com/api/gateway.php/#{subdomain}/v1/"
|
58
|
+
end
|
59
|
+
|
60
|
+
def parse_response(response, params)
|
61
|
+
case response.code
|
62
|
+
when 200..201
|
63
|
+
begin
|
64
|
+
if response.body.to_s.empty?
|
65
|
+
{ "headers" => response.headers }
|
66
|
+
else
|
67
|
+
JSON.parse(response)
|
68
|
+
end
|
69
|
+
rescue
|
70
|
+
typecast = options.fetch(:typecast_values, true)
|
71
|
+
MultiXml.parse(response,
|
72
|
+
symbolize_keys: true,
|
73
|
+
typecast_xml_value: typecast)
|
74
|
+
end
|
75
|
+
when 400
|
76
|
+
raise Bamboozled::BadRequest.new(response, params, 'The request was invalid or could not be understood by the server. Resubmitting the request will likely result in the same error.')
|
77
|
+
when 401
|
78
|
+
raise Bamboozled::AuthenticationFailed.new(response, params, 'Your API key is missing.')
|
79
|
+
when 403
|
80
|
+
raise Bamboozled::Forbidden.new(response, params, 'The application is attempting to perform an action it does not have privileges to access. Verify your API key belongs to an enabled user with the required permissions.')
|
81
|
+
when 404
|
82
|
+
raise Bamboozled::NotFound.new(response, params, 'The resource was not found with the given identifier. Either the URL given is not a valid API, or the ID of the object specified in the request is invalid.')
|
83
|
+
when 406
|
84
|
+
raise Bamboozled::NotAcceptable.new(response, params, 'The request contains references to non-existent fields.')
|
85
|
+
when 409
|
86
|
+
raise Bamboozled::Conflict.new(response, params, 'The request attempts to create a duplicate. For employees, duplicate emails are not allowed. For lists, duplicate values are not allowed.')
|
87
|
+
when 429
|
88
|
+
raise Bamboozled::LimitExceeded.new(response, params, 'The account has reached its employee limit. No additional employees could be added.')
|
89
|
+
when 500
|
90
|
+
raise Bamboozled::InternalServerError.new(response, params, 'The server encountered an error while processing your request and failed.')
|
91
|
+
when 502
|
92
|
+
raise Bamboozled::GatewayError.new(response, params, 'The load balancer or web server had trouble connecting to the Bamboo app. Please try the request again.')
|
93
|
+
when 503
|
94
|
+
raise Bamboozled::ServiceUnavailable.new(response, params, 'The service is temporarily unavailable. Please try the request again.')
|
95
|
+
else
|
96
|
+
raise Bamboozled::InformBamboo.new(response, params, 'An error occurred that we do not now how to handle. Please contact BambooHR.')
|
97
|
+
end
|
98
|
+
end
|
99
|
+
end
|
100
|
+
end
|
101
|
+
end
|
@@ -0,0 +1,118 @@
|
|
1
|
+
module Bamboozled
|
2
|
+
module API
|
3
|
+
class Employee < Base
|
4
|
+
|
5
|
+
def all(fields = nil)
|
6
|
+
response = request(:get, "employees/directory")
|
7
|
+
|
8
|
+
if fields.nil? || fields == :default
|
9
|
+
Array(response['employees'])
|
10
|
+
else
|
11
|
+
employees = []
|
12
|
+
response['employees'].map{|e| e['id']}.each do |id|
|
13
|
+
employees << find(id, fields)
|
14
|
+
end
|
15
|
+
employees
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
def find(employee_id, fields = nil)
|
20
|
+
fields = FieldCollection.wrap(fields).to_csv
|
21
|
+
|
22
|
+
request(:get, "employees/#{employee_id}?fields=#{fields}&onlyCurrent=false")
|
23
|
+
end
|
24
|
+
|
25
|
+
def last_changed(date = "2011-06-05T00:00:00+00:00", type = nil)
|
26
|
+
query = Hash.new
|
27
|
+
query[:since] = date.respond_to?(:iso8601) ? date.iso8601 : date
|
28
|
+
query[:type] = type unless type.nil?
|
29
|
+
|
30
|
+
response = request(:get, "employees/changed", query: query)
|
31
|
+
response["employees"]
|
32
|
+
end
|
33
|
+
|
34
|
+
# Tabular data
|
35
|
+
[:job_info, :employment_status, :compensation, :dependents, :contacts].each do |action|
|
36
|
+
define_method(action.to_s) do |argument_id|
|
37
|
+
request(:get, "employees/#{argument_id}/tables/#{action.to_s.gsub(/_(.)/) {|e| $1.upcase}}")
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
def add_table_row(employee_id, table_name, table_row_data)
|
42
|
+
details = generate_xml(table_row_data)
|
43
|
+
options = {body: details}
|
44
|
+
request(:post, "employees/#{employee_id}/tables/#{table_name}", options)
|
45
|
+
end
|
46
|
+
|
47
|
+
def update_table_row(employee_id, table_name, row_id, table_row_data)
|
48
|
+
details = generate_xml(table_row_data)
|
49
|
+
options = {body: details}
|
50
|
+
request(:post, "employees/#{employee_id}/tables/#{table_name}/#{row_id}", options)
|
51
|
+
end
|
52
|
+
|
53
|
+
def table_data(employee_id, table_name)
|
54
|
+
request(:get, "employees/#{employee_id}/tables/#{table_name}")
|
55
|
+
end
|
56
|
+
|
57
|
+
def time_off_estimate(employee_id, end_date)
|
58
|
+
end_date = end_date.strftime("%F") unless end_date.is_a?(String)
|
59
|
+
request(:get, "employees/#{employee_id}/time_off/calculator?end=#{end_date}")
|
60
|
+
end
|
61
|
+
|
62
|
+
def photo_binary(employee_id)
|
63
|
+
request(:get, "employees/#{employee_id}/photo/small")
|
64
|
+
end
|
65
|
+
|
66
|
+
def photo_url(employee)
|
67
|
+
if (Float(employee) rescue false)
|
68
|
+
e = find(employee, ['workEmail', 'homeEmail'])
|
69
|
+
employee = e['workEmail'].nil? ? e['homeEmail'] : e['workEmail']
|
70
|
+
end
|
71
|
+
|
72
|
+
digest = Digest::MD5.new
|
73
|
+
digest.update(employee.strip.downcase)
|
74
|
+
"http://#{@subdomain}.bamboohr.com/employees/photos/?h=#{digest}"
|
75
|
+
end
|
76
|
+
|
77
|
+
def add(employee_details)
|
78
|
+
details = generate_xml(employee_details)
|
79
|
+
options = {body: details}
|
80
|
+
|
81
|
+
request(:post, "employees/", options)
|
82
|
+
end
|
83
|
+
|
84
|
+
def update(bamboo_id, employee_details)
|
85
|
+
details = generate_xml(employee_details)
|
86
|
+
options = { body: details }
|
87
|
+
|
88
|
+
request(:post, "employees/#{bamboo_id}", options)
|
89
|
+
end
|
90
|
+
|
91
|
+
def files(employee_id)
|
92
|
+
request(:get, "employees/#{employee_id}/files/view/")
|
93
|
+
end
|
94
|
+
|
95
|
+
def add_file(employee_id, file_details)
|
96
|
+
post_file("employees/#{employee_id}/files", file_details)
|
97
|
+
end
|
98
|
+
|
99
|
+
private
|
100
|
+
|
101
|
+
def generate_xml(employee_details)
|
102
|
+
"".tap do |xml|
|
103
|
+
xml << "<employee>"
|
104
|
+
employee_details.each do |k, v|
|
105
|
+
if v.is_a?(Hash)
|
106
|
+
value = Integer(v[:value], exception: false) ? v[:value] : v[:value].encode(xml: :text)
|
107
|
+
xml << "<field id='#{k}' currency='#{v[:currency]}'>#{value}</field>"
|
108
|
+
else
|
109
|
+
value = Integer(v, exception: false) ? v : v.encode(xml: :text)
|
110
|
+
xml << "<field id='#{k}'>#{value}</field>"
|
111
|
+
end
|
112
|
+
end
|
113
|
+
xml << "</employee>"
|
114
|
+
end
|
115
|
+
end
|
116
|
+
end
|
117
|
+
end
|
118
|
+
end
|
@@ -0,0 +1,107 @@
|
|
1
|
+
module Bamboozled
|
2
|
+
module API
|
3
|
+
class FieldCollection
|
4
|
+
def self.wrap(fields)
|
5
|
+
fields = all_names if fields == :all
|
6
|
+
fields = fields.split(",") if fields.is_a?(String)
|
7
|
+
new(fields)
|
8
|
+
end
|
9
|
+
|
10
|
+
def self.all_names # rubocop:disable Metrics/MethodLength
|
11
|
+
%w[
|
12
|
+
address1
|
13
|
+
address2
|
14
|
+
age
|
15
|
+
bestEmail
|
16
|
+
birthday
|
17
|
+
bonusAmount
|
18
|
+
bonusComment
|
19
|
+
bonusDate
|
20
|
+
bonusReason
|
21
|
+
city
|
22
|
+
commisionDate
|
23
|
+
commissionAmount
|
24
|
+
commissionComment
|
25
|
+
commissionDate
|
26
|
+
country
|
27
|
+
dateOfBirth
|
28
|
+
department
|
29
|
+
displayName
|
30
|
+
division
|
31
|
+
eeo
|
32
|
+
employeeNumber
|
33
|
+
employmentHistoryStatus
|
34
|
+
ethnicity
|
35
|
+
exempt
|
36
|
+
firstName
|
37
|
+
flsaCode
|
38
|
+
fullName1
|
39
|
+
fullName2
|
40
|
+
fullName3
|
41
|
+
fullName4
|
42
|
+
fullName5
|
43
|
+
gender
|
44
|
+
hireDate
|
45
|
+
homeEmail
|
46
|
+
homePhone
|
47
|
+
id
|
48
|
+
includeInPayroll
|
49
|
+
isPhotoUploaded
|
50
|
+
jobTitle
|
51
|
+
lastChanged
|
52
|
+
lastName
|
53
|
+
location
|
54
|
+
maritalStatus
|
55
|
+
middleName
|
56
|
+
mobilePhone
|
57
|
+
originalHireDate
|
58
|
+
paidPer
|
59
|
+
payChangeReason
|
60
|
+
payFrequency
|
61
|
+
payGroup
|
62
|
+
payGroupId
|
63
|
+
payPer
|
64
|
+
payRate
|
65
|
+
payRateEffectiveDate
|
66
|
+
paySchedule
|
67
|
+
payScheduleId
|
68
|
+
payType
|
69
|
+
preferredName
|
70
|
+
sin
|
71
|
+
ssn
|
72
|
+
standardHoursPerWeek
|
73
|
+
state
|
74
|
+
stateCode
|
75
|
+
status
|
76
|
+
supervisor
|
77
|
+
supervisorEId
|
78
|
+
supervisorId
|
79
|
+
terminationDate
|
80
|
+
workEmail
|
81
|
+
workPhone
|
82
|
+
workPhoneExtension
|
83
|
+
workPhonePlusExtension
|
84
|
+
zipcode
|
85
|
+
]
|
86
|
+
end
|
87
|
+
|
88
|
+
def initialize(fields)
|
89
|
+
self.fields = fields || []
|
90
|
+
end
|
91
|
+
|
92
|
+
def to_csv
|
93
|
+
fields.join(",")
|
94
|
+
end
|
95
|
+
|
96
|
+
def to_xml
|
97
|
+
"<fields>" +
|
98
|
+
fields.map { |field| "<field id=\"#{field}\" />" }.join +
|
99
|
+
"</fields>"
|
100
|
+
end
|
101
|
+
|
102
|
+
private
|
103
|
+
|
104
|
+
attr_accessor :fields
|
105
|
+
end
|
106
|
+
end
|
107
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
module Bamboozled
|
2
|
+
module API
|
3
|
+
class Meta < Base
|
4
|
+
def users
|
5
|
+
request(:get, "meta/users").values
|
6
|
+
end
|
7
|
+
|
8
|
+
def fields
|
9
|
+
request(:get, "meta/fields")
|
10
|
+
end
|
11
|
+
|
12
|
+
def lists
|
13
|
+
request(:get, "meta/lists")
|
14
|
+
end
|
15
|
+
|
16
|
+
def tables
|
17
|
+
request(
|
18
|
+
:get, "meta/tables",
|
19
|
+
typecast_values: false)
|
20
|
+
end
|
21
|
+
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
@@ -0,0 +1,20 @@
|
|
1
|
+
module Bamboozled
|
2
|
+
module API
|
3
|
+
class Report < Base
|
4
|
+
|
5
|
+
def find(number, format = "JSON", fd_param = true)
|
6
|
+
response = request(:get, "reports/#{number}?format=#{format.upcase}&fd=#{fd_param.yesno}")
|
7
|
+
response["employees"]
|
8
|
+
end
|
9
|
+
|
10
|
+
def custom(fields, format = "JSON", only_current = false)
|
11
|
+
options = {
|
12
|
+
body: "<report>#{FieldCollection.wrap(fields).to_xml}</report>"
|
13
|
+
}
|
14
|
+
|
15
|
+
response = request(:post, "reports/custom?format=#{format.upcase}&onlyCurrent=#{only_current}", options)
|
16
|
+
response["employees"]
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|