bamboozled-gitlab 0.2.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|