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.
Files changed (75) hide show
  1. checksums.yaml +7 -0
  2. data/.github/ISSUE_TEMPLATE/bug_report.md +39 -0
  3. data/.github/ISSUE_TEMPLATE/feature_request.md +19 -0
  4. data/.github/ISSUE_TEMPLATE/question.md +19 -0
  5. data/.github/main.workflow +38 -0
  6. data/.github/pull_request_template.md +27 -0
  7. data/.gitignore +38 -0
  8. data/.rubocop.yml +39 -0
  9. data/.rubocop_todo.yml +305 -0
  10. data/.travis.yml +18 -0
  11. data/CHANGELOG.md +48 -0
  12. data/CONTRIBUTING.md +91 -0
  13. data/Dockerfile +8 -0
  14. data/Gemfile +16 -0
  15. data/Guardfile +21 -0
  16. data/LICENSE +22 -0
  17. data/README.md +170 -0
  18. data/Rakefile +11 -0
  19. data/bamboozled.gemspec +30 -0
  20. data/examples/employees_over_time.rb +53 -0
  21. data/lib/bamboozled.rb +24 -0
  22. data/lib/bamboozled/api/base.rb +101 -0
  23. data/lib/bamboozled/api/employee.rb +118 -0
  24. data/lib/bamboozled/api/field_collection.rb +107 -0
  25. data/lib/bamboozled/api/meta.rb +25 -0
  26. data/lib/bamboozled/api/report.rb +20 -0
  27. data/lib/bamboozled/api/time_off.rb +34 -0
  28. data/lib/bamboozled/api/time_tracking.rb +24 -0
  29. data/lib/bamboozled/base.rb +31 -0
  30. data/lib/bamboozled/errors.rb +33 -0
  31. data/lib/bamboozled/ext/yesno.rb +11 -0
  32. data/lib/bamboozled/version.rb +3 -0
  33. data/logos/bamboozled_logo_black.png +0 -0
  34. data/logos/bamboozled_logo_green.png +0 -0
  35. data/logos/skookum_mark_black.png +0 -0
  36. data/logos/skookum_mark_black.svg +175 -0
  37. data/relnotes/v0.1.0.md +13 -0
  38. data/spec/fixtures/add_employee_details.json +7 -0
  39. data/spec/fixtures/add_employee_response.json +4 -0
  40. data/spec/fixtures/add_employee_xml.yml +8 -0
  41. data/spec/fixtures/all_employees.json +58 -0
  42. data/spec/fixtures/custom_report.json +38 -0
  43. data/spec/fixtures/employee_emails.json +9 -0
  44. data/spec/fixtures/employee_table_details.json +17 -0
  45. data/spec/fixtures/job_info.xml +22 -0
  46. data/spec/fixtures/last_changed.json +28 -0
  47. data/spec/fixtures/meta_fields.json +5 -0
  48. data/spec/fixtures/meta_lists.json +5 -0
  49. data/spec/fixtures/meta_tables.json +5 -0
  50. data/spec/fixtures/meta_users.json +4 -0
  51. data/spec/fixtures/one_employee.json +9 -0
  52. data/spec/fixtures/time_off_estimate.json +23 -0
  53. data/spec/fixtures/time_tracking_add_200_response.json +7 -0
  54. data/spec/fixtures/time_tracking_add_empty_response.json +9 -0
  55. data/spec/fixtures/time_tracking_adjust_200_response.json +7 -0
  56. data/spec/fixtures/time_tracking_adjust_400_response.json +11 -0
  57. data/spec/fixtures/time_tracking_record_200_response.json +9 -0
  58. data/spec/fixtures/time_tracking_record_400_response.json +11 -0
  59. data/spec/fixtures/time_tracking_record_401_response.json +10 -0
  60. data/spec/fixtures/time_tracking_record_404_response.json +8 -0
  61. data/spec/fixtures/update_employee_details.json +7 -0
  62. data/spec/fixtures/update_employee_response.json +3 -0
  63. data/spec/fixtures/update_employee_table.json +8 -0
  64. data/spec/fixtures/update_employee_table_xml.yml +6 -0
  65. data/spec/fixtures/update_employee_xml.yml +8 -0
  66. data/spec/lib/bamboozled/api/base_spec.rb +18 -0
  67. data/spec/lib/bamboozled/api/employee_spec.rb +186 -0
  68. data/spec/lib/bamboozled/api/field_collection_spec.rb +17 -0
  69. data/spec/lib/bamboozled/api/meta_spec.rb +47 -0
  70. data/spec/lib/bamboozled/api/report_spec.rb +17 -0
  71. data/spec/lib/bamboozled/api/time_tracking_spec.rb +123 -0
  72. data/spec/lib/bamboozled/base_spec.rb +26 -0
  73. data/spec/lib/bamboozled_spec.rb +33 -0
  74. data/spec/spec_helper.rb +32 -0
  75. 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