effective_learndash 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (37) hide show
  1. checksums.yaml +7 -0
  2. data/MIT-LICENSE +20 -0
  3. data/README.md +126 -0
  4. data/Rakefile +18 -0
  5. data/app/assets/config/effective_learndash_manifest.js +3 -0
  6. data/app/assets/javascripts/effective_learndash/base.js +0 -0
  7. data/app/assets/javascripts/effective_learndash.js +1 -0
  8. data/app/assets/stylesheets/effective_learndash/base.scss +0 -0
  9. data/app/assets/stylesheets/effective_learndash.scss +1 -0
  10. data/app/controllers/admin/learndash_courses_controller.rb +15 -0
  11. data/app/controllers/admin/learndash_enrollments_controller.rb +9 -0
  12. data/app/controllers/admin/learndash_users_controller.rb +11 -0
  13. data/app/datatables/admin/effective_learndash_courses_datatable.rb +26 -0
  14. data/app/datatables/admin/effective_learndash_enrollments_datatable.rb +39 -0
  15. data/app/datatables/admin/effective_learndash_users_datatable.rb +25 -0
  16. data/app/helpers/effective_learndash_helper.rb +2 -0
  17. data/app/models/concerns/effective_learndash_owner.rb +64 -0
  18. data/app/models/effective/learndash_api.rb +269 -0
  19. data/app/models/effective/learndash_course.rb +39 -0
  20. data/app/models/effective/learndash_enrollment.rb +73 -0
  21. data/app/models/effective/learndash_user.rb +97 -0
  22. data/app/views/admin/learndash_courses/_learndash_course.html.haml +12 -0
  23. data/app/views/admin/learndash_enrollments/_form.html.haml +16 -0
  24. data/app/views/admin/learndash_owners/_form.html.haml +5 -0
  25. data/app/views/admin/learndash_users/_form.html.haml +6 -0
  26. data/app/views/admin/learndash_users/_learndash_user.html.haml +21 -0
  27. data/config/effective_learndash.rb +18 -0
  28. data/config/routes.rb +25 -0
  29. data/db/migrate/01_create_effective_learndash.rb.erb +48 -0
  30. data/db/seeds.rb +1 -0
  31. data/lib/effective_learndash/engine.rb +22 -0
  32. data/lib/effective_learndash/version.rb +3 -0
  33. data/lib/effective_learndash.rb +52 -0
  34. data/lib/generators/effective_learndash/install_generator.rb +29 -0
  35. data/lib/generators/templates/effective_learndash_mailer_preview.rb +4 -0
  36. data/lib/tasks/effective_learndash_tasks.rake +8 -0
  37. metadata +232 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: '0970ec691c0993f6c77e5131eba3da9ed11ef1c74a5cd00f0cdba5373cf1f5b4'
4
+ data.tar.gz: dd0229161ba6030e9711202259063af16e506ec6d029647d43a112b4ac4f8150
5
+ SHA512:
6
+ metadata.gz: a6ff74e9f0842c69d6f69886a8a7ef86b1196bbfd83e803afdd58e075c8ac99371a4b125d22b67e136268b4b5d4ac6718371b3ff6db936abddcb123ac3d8b28a
7
+ data.tar.gz: e742085fcf413769c5936203091663b95d704a98d4b26b3778d77668f62d7ec79a53c52ffade1e9cf0d4cf56941ab83797d67a3efac1905ae30755f416958ff2
data/MIT-LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright 2022 Code and Effect Inc.
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,126 @@
1
+ # Effective Learndash
2
+
3
+ Create Wordpress users and read Learndash course progress. This is an unoffocial integration that is not supported or affiliated with WordPress or Learndash.
4
+
5
+ ## Getting Started
6
+
7
+ This requires Rails 6+ and Twitter Bootstrap 4 and just works with Devise.
8
+
9
+ Please first install the [effective_datatables](https://github.com/code-and-effect/effective_datatables) gem.
10
+
11
+ Please download and install the [Twitter Bootstrap4](http://getbootstrap.com)
12
+
13
+ Add to your Gemfile:
14
+
15
+ ```ruby
16
+ gem 'haml-rails' # or try using gem 'hamlit-rails'
17
+ gem 'effective_learndash'
18
+ ```
19
+
20
+ Run the bundle command to install it:
21
+
22
+ ```console
23
+ bundle install
24
+ ```
25
+
26
+ Then run the generator:
27
+
28
+ ```ruby
29
+ rails generate effective_learndash:install
30
+ ```
31
+
32
+ The generator will install an initializer which describes all configuration options and creates a database migration.
33
+
34
+ If you want to tweak the table names, manually adjust both the configuration file and the migration now.
35
+
36
+ Then migrate the database:
37
+
38
+ ```ruby
39
+ rake db:migrate
40
+ ```
41
+
42
+ Add to your user class:
43
+
44
+ ```
45
+ class User < ApplicationRecord
46
+ effective_learndash_owner
47
+ end
48
+ ```
49
+
50
+ Add a link to the admin menu:
51
+
52
+ ```haml
53
+ - if can? :admin, :effective_learndash
54
+ = nav_dropdown 'Learndash' do
55
+ - if can? :index, Effective::LearndashUser
56
+ = nav_link_to 'Learndash Users', effective_learndash.admin_learndash_users_path
57
+
58
+ - if can? :index, Effective::LearndashCourse
59
+ = nav_link_to 'Learndash Courses', effective_learndash.admin_learndash_courses_path
60
+
61
+ - if can? :index, Effective::LearndashEnrollment
62
+ = nav_link_to 'Learndash Enrollments', effective_learndash.admin_learndash_enrollments_path
63
+ ```
64
+
65
+ ## Authorization
66
+
67
+ All authorization checks are handled via the effective_resources gem found in the `config/initializers/effective_resources.rb` file.
68
+
69
+ ## Permissions
70
+
71
+ The permissions you actually want to define are as follows (using CanCan):
72
+
73
+ ```ruby
74
+ if user.admin?
75
+ can :admin, :effective_learndash
76
+
77
+ can(crud + [:refresh], Effective::LearndashUser)
78
+ can(crud + [:refresh], Effective::LearndashCourse)
79
+
80
+ can(crud, Effective::LearndashEnrollment)
81
+ can(:refresh, Effective::LearndashEnrollment) { |enrollment| !enrollment.completed? }
82
+ end
83
+ ```
84
+
85
+ ## Configuring Learndash
86
+
87
+ Your Wordpress should be configured ahead of time with the Learndash plugin.
88
+
89
+ Please generate an application password via:
90
+
91
+ https://make.wordpress.org/core/2020/11/05/application-passwords-integration-guide/
92
+
93
+ and fill in the username/password details in config/initializers/effective_leardash.rb
94
+
95
+ ## Working with Learndash
96
+
97
+ Visit `/admin/learndash_courses` and Refresh the list of Courses.
98
+
99
+ Create a New Learndash User will create a new Wordpress/Learndash account with a username/password according to the settings in the config file.
100
+
101
+ When you create a user, you only get access to the password once. So any existing users will have an unknown password.
102
+
103
+ Create a new Learndash Enrollment to enroll a learndash user into a course. This will begin tracking their progress.
104
+
105
+ There are no webhooks or callbacks from Learndash, everything is a GET request that updates the local database.
106
+
107
+ You can refresh an entire learndash user in one operation and it will sync the entire user at once, `user.learndash_user.refresh!`
108
+
109
+ ## License
110
+
111
+ MIT License. Copyright [Code and Effect Inc.](http://www.codeandeffect.com/)
112
+
113
+ ## Testing
114
+
115
+ ```ruby
116
+ rails test
117
+ ```
118
+
119
+ ## Contributing
120
+
121
+ 1. Fork it
122
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
123
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
124
+ 4. Push to the branch (`git push origin my-new-feature`)
125
+ 5. Bonus points for test coverage
126
+ 6. Create new Pull Request
data/Rakefile ADDED
@@ -0,0 +1,18 @@
1
+ require "bundler/setup"
2
+
3
+ APP_RAKEFILE = File.expand_path("test/dummy/Rakefile", __dir__)
4
+ load "rails/tasks/engine.rake"
5
+
6
+ load "rails/tasks/statistics.rake"
7
+
8
+ require "bundler/gem_tasks"
9
+
10
+ require "rake/testtask"
11
+
12
+ Rake::TestTask.new(:test) do |t|
13
+ t.libs << 'test'
14
+ t.pattern = 'test/**/*_test.rb'
15
+ t.verbose = false
16
+ end
17
+
18
+ task default: :test
@@ -0,0 +1,3 @@
1
+ //= link_directory ../javascripts .js
2
+ //= link_directory ../stylesheets .css
3
+ //= link_directory ../images/effective_learndash
@@ -0,0 +1 @@
1
+ //= require_tree ./effective_learndash
@@ -0,0 +1 @@
1
+ @import 'effective_learndash/base';
@@ -0,0 +1,15 @@
1
+ module Admin
2
+ class LearndashCoursesController < ApplicationController
3
+ before_action(:authenticate_user!) if defined?(Devise)
4
+ before_action { EffectiveResources.authorize!(self, :admin, :effective_learndash) }
5
+
6
+ include Effective::CrudController
7
+
8
+ def refresh
9
+ resource_scope.refresh!
10
+ flash[:success] = "Successfully refreshed Courses from Learndash"
11
+ redirect_to effective_learndash.admin_learndash_courses_path
12
+ end
13
+
14
+ end
15
+ end
@@ -0,0 +1,9 @@
1
+ module Admin
2
+ class LearndashEnrollmentsController < ApplicationController
3
+ before_action(:authenticate_user!) if defined?(Devise)
4
+ before_action { EffectiveResources.authorize!(self, :admin, :effective_learndash) }
5
+
6
+ include Effective::CrudController
7
+
8
+ end
9
+ end
@@ -0,0 +1,11 @@
1
+ module Admin
2
+ class LearndashUsersController < ApplicationController
3
+ before_action(:authenticate_user!) if defined?(Devise)
4
+ before_action { EffectiveResources.authorize!(self, :admin, :effective_learndash) }
5
+
6
+ include Effective::CrudController
7
+
8
+ on :refresh, success: -> { "Successfully refreshed #{resource} and all course enrollments" }
9
+
10
+ end
11
+ end
@@ -0,0 +1,26 @@
1
+ module Admin
2
+ class EffectiveLearndashCoursesDatatable < Effective::Datatable
3
+ datatable do
4
+ order :title
5
+
6
+ col :id, visible: false
7
+ col :course_id, label: 'Learndash Id', visible: false
8
+
9
+ col :title
10
+ col :status
11
+
12
+ col :link do |course|
13
+ link_to(course.link, course.link, target: '_blank')
14
+ end
15
+
16
+ col :learndash_users, visible: false
17
+
18
+ actions_col
19
+ end
20
+
21
+ collection do
22
+ Effective::LearndashCourse.deep.all
23
+ end
24
+
25
+ end
26
+ end
@@ -0,0 +1,39 @@
1
+ module Admin
2
+ class EffectiveLearndashEnrollmentsDatatable < Effective::Datatable
3
+ filters do
4
+ scope :all
5
+ scope :completed
6
+ scope :in_progress
7
+ scope :not_started
8
+ end
9
+
10
+ datatable do
11
+ col :id, visible: false
12
+
13
+ col :last_refreshed, visible: false do |enrollment|
14
+ time_ago_in_words(enrollment.last_synced_at) + ' ago'
15
+ end
16
+
17
+ col :owner, visible: false
18
+
19
+ col :learndash_course
20
+ col :learndash_user
21
+
22
+ col :progress_status
23
+
24
+ col :last_step, visible: false
25
+ col :steps_completed, visible: false
26
+ col :steps_total, visible: false
27
+
28
+ col :date_started, as: :date
29
+ col :date_completed, as: :date
30
+
31
+ actions_col
32
+ end
33
+
34
+ collection do
35
+ Effective::LearndashEnrollment.deep.all
36
+ end
37
+
38
+ end
39
+ end
@@ -0,0 +1,25 @@
1
+ module Admin
2
+ class EffectiveLearndashUsersDatatable < Effective::Datatable
3
+ datatable do
4
+ col :id, visible: false
5
+ col :user_id, label: 'Learndash Id', visible: false
6
+
7
+ col :last_refreshed, visible: true do |user|
8
+ time_ago_in_words(user.last_synced_at) + ' ago'
9
+ end
10
+
11
+ col :owner
12
+ col :email
13
+ col :username
14
+ col :password
15
+ col :learndash_courses
16
+
17
+ actions_col
18
+ end
19
+
20
+ collection do
21
+ Effective::LearndashUser.deep.all
22
+ end
23
+
24
+ end
25
+ end
@@ -0,0 +1,2 @@
1
+ module EffectiveLearndashHelper
2
+ end
@@ -0,0 +1,64 @@
1
+ # EffectiveLearndashOwner
2
+ #
3
+ # Mark your user model with effective_learndash_owner to get all the includes
4
+
5
+ module EffectiveLearndashOwner
6
+ extend ActiveSupport::Concern
7
+
8
+ module Base
9
+ def effective_learndash_owner
10
+ include ::EffectiveLearndashOwner
11
+ end
12
+ end
13
+
14
+ module ClassMethods
15
+ def effective_learndash_owner?; true; end
16
+ end
17
+
18
+ included do
19
+ # Effective Scoped - this is a has_one in preactice
20
+ has_many :learndash_users, class_name: 'Effective::LearndashUser', as: :owner, inverse_of: :owner, dependent: :delete_all
21
+ accepts_nested_attributes_for :learndash_users, allow_destroy: true
22
+
23
+ # Not used
24
+ has_many :learndash_enrollments, class_name: 'Effective::LearndashEnrollment', as: :owner, inverse_of: :owner, dependent: :delete_all
25
+ accepts_nested_attributes_for :learndash_enrollments, allow_destroy: true
26
+ end
27
+
28
+ # Find
29
+ def learndash_user
30
+ learndash_users.first
31
+ end
32
+
33
+ # Find or create
34
+ def create_learndash_user
35
+ learndash_user || learndash_users.create!(owner: self)
36
+ end
37
+
38
+ # Find
39
+ def learndash_enrollment(course:)
40
+ learndash_user&.enrollment(course: course)
41
+ end
42
+
43
+ # Find or create
44
+ def create_learndash_enrollment(course:)
45
+ raise('expected a persisted learndash_user') unless learndash_user&.persisted?
46
+ learndash_user.create_enrollment(course: course)
47
+ end
48
+
49
+ # Find or sync and check completed?
50
+ def learndash_completed?(course:)
51
+ enrollment = learndash_enrollment(course: course)
52
+
53
+ # We haven't been enrolled
54
+ return false if enrollment.blank?
55
+
56
+ # Return completed right away if previously marked completed
57
+ return true if enrollment.completed?
58
+
59
+ # Check the API
60
+ enrollment.sync!
61
+ enrollment.completed?
62
+ end
63
+
64
+ end
@@ -0,0 +1,269 @@
1
+ require 'net/http'
2
+
3
+ module Effective
4
+ class LearndashApi
5
+
6
+ attr_accessor :url
7
+ attr_accessor :username
8
+ attr_accessor :password
9
+
10
+ def initialize(url:, username:, password:)
11
+ @url = url
12
+ @username = username
13
+ @password = password
14
+ end
15
+
16
+ # Methods
17
+ # https://developer.wordpress.org/rest-api/reference/users/#definition
18
+ # curl --user username:password http://www.example.com/wp-json/wp/v2/users/me
19
+ def me
20
+ get('/wp/v2/users/me')
21
+ end
22
+
23
+ # List users
24
+ def users
25
+ get('/wp/v2/users')
26
+ end
27
+
28
+ # Returns a WP Hash of User or nil
29
+ # This find by EMAIL doesn't work reliably
30
+ def find_user(value)
31
+ # Find by email
32
+ if value.kind_of?(String) && value.include?('@')
33
+ return find_by("/wp/v2/users", :email, value)
34
+ end
35
+
36
+ # Fetch by saved param value
37
+ user_id = user_id(value)
38
+ user = find("/wp/v2/users/#{user_id}", context: :edit) if user_id
39
+ return user if user.present?
40
+
41
+ # Find by email
42
+ email = value.try(:email)
43
+ user = find_by("/wp/v2/users", :email, email) if email
44
+ return user if user.present?
45
+
46
+ # Find by username
47
+ username = username_for(value) if value.class.respond_to?(:effective_learndash_owner?)
48
+ user = find_by("/wp/v2/users", :username, username) if username
49
+ return user if user.present?
50
+
51
+ # Otherwise none
52
+ nil
53
+ end
54
+
55
+ # Create User
56
+ # Usernames can only contain lowercase letters (a-z) and numbers.
57
+ def create_user(owner)
58
+ raise ('expected a leardash owner') unless owner.class.respond_to?(:effective_learndash_owner?)
59
+ raise('owner must have an email') unless owner.try(:email).present?
60
+
61
+ username = username_for(owner)
62
+ password = password_for(owner)
63
+
64
+ payload = {
65
+ username: username,
66
+ password: password,
67
+
68
+ name: owner.to_s,
69
+ email: owner.email,
70
+ roles: ['subscriber'],
71
+
72
+ first_name: owner.try(:first_name),
73
+ last_name: owner.try(:last_name)
74
+ }.compact
75
+
76
+ post("/wp/v2/users", payload.stringify_keys).merge(password: password)
77
+ end
78
+
79
+ # List Courses
80
+ def courses
81
+ get('/ldlms/v2/sfwd-courses')
82
+ end
83
+
84
+ # List User Course Progress
85
+ def user_enrollments(user)
86
+ user = user_id(user) || raise('expected a user')
87
+
88
+ get("/ldlms/v2/users/#{user}/course-progress")
89
+ end
90
+
91
+ # Helper methods for enrollments
92
+ def find_enrollment(enrollment)
93
+ find_user_course(enrollment.learndash_user, enrollment.learndash_course)
94
+ end
95
+
96
+ def create_enrollment(enrollment)
97
+ create_user_course(enrollment.learndash_user, enrollment.learndash_course)
98
+ end
99
+
100
+ # Find User Course Progress
101
+ def find_user_course(user, course)
102
+ user = user_id(user) || raise('expected a user')
103
+ course = course_id(course) || raise('expected a course')
104
+
105
+ find("/ldlms/v2/users/#{user}/course-progress/#{course}")
106
+ end
107
+
108
+ # Crete Course User
109
+ def create_user_course(user, course)
110
+ user = user_id(user) || raise('expected a user')
111
+ course = course_id(course) || raise('expected a course')
112
+
113
+ response = post("/ldlms/v2/sfwd-courses/#{course}/users", user_ids: [user])
114
+
115
+ unless (response.first.fetch(:code) rescue nil) == 'learndash_rest_enroll_success'
116
+ raise("unsuccessful course creation: #{response}")
117
+ end
118
+
119
+ find_user_course(user, course)
120
+ end
121
+
122
+ # private under this point
123
+
124
+
125
+ def user_id(resource)
126
+ if resource.class.respond_to?(:effective_learndash_owner?) # This is a user
127
+ resource.learndash_user&.user_id
128
+ elsif resource.kind_of?(LearndashEnrollment)
129
+ resource.learndash_user&.user_id
130
+ elsif resource.kind_of?(LearndashUser)
131
+ resource.user_id
132
+ else
133
+ resource
134
+ end
135
+ end
136
+
137
+ def course_id(resource)
138
+ if resource.kind_of?(LearndashCourse)
139
+ resource.course_id
140
+ elsif resource.kind_of?(LearndashEnrollment)
141
+ resource.learndash_course&.course_id
142
+ else
143
+ resource
144
+ end
145
+ end
146
+
147
+ def username_for(resource)
148
+ raise('expected a learndash owner') unless resource.class.respond_to?(:effective_learndash_owner?) # This is a user
149
+
150
+ name = EffectiveLearndash.wp_username_for(resource)
151
+ name = "test#{name}" unless Rails.env.production?
152
+ name
153
+ end
154
+
155
+ def password_for(resource)
156
+ raise('expected a learndash owner') unless resource.class.respond_to?(:effective_learndash_owner?) # This is a user
157
+ EffectiveLearndash.wp_password_for(resource)
158
+ end
159
+
160
+ def find(endpoint, params = nil)
161
+ response = get(endpoint, params)
162
+
163
+ if response == false
164
+ nil
165
+ elsif response.kind_of?(Hash) && response.dig(:data, :status) == 404
166
+ nil
167
+ elsif response.kind_of?(Hash)
168
+ response
169
+ elsif response.kind_of?(Array)
170
+ response.first
171
+ else
172
+ raise("unexpected Learndash API response #{response}")
173
+ end
174
+ end
175
+
176
+ # We can't just like, find by email, so we gotta search then filter on our side
177
+ def find_by(endpoint, key, value)
178
+ raise('expected a symbol key') unless key.kind_of?(Symbol)
179
+ raise('expected a value') unless value.present?
180
+
181
+ response = get(endpoint, { search: value, context: :edit })
182
+
183
+ collection = Array(
184
+ if response == false
185
+ nil
186
+ elsif response.kind_of?(Hash) && response.dig(:data, :status) == 404
187
+ nil
188
+ elsif response.kind_of?(Hash)
189
+ response
190
+ elsif response.kind_of?(Array)
191
+ response
192
+ else
193
+ raise("unexpected Learndash API find_by response #{response}")
194
+ end
195
+ )
196
+
197
+ resource = collection.find { |data| data[key] == value }
198
+ resource
199
+ end
200
+
201
+ def get(endpoint, params = nil)
202
+ query = ('?' + params.compact.map { |k, v| "#{k}=#{v}" }.join('&')) if params.present?
203
+
204
+ uri = URI.parse(api_url + endpoint + query.to_s)
205
+
206
+ http = Net::HTTP.new(uri.host, uri.port)
207
+ http.use_ssl = (uri.scheme == 'https')
208
+ http.read_timeout = 10
209
+
210
+ response = with_retries do
211
+ puts("[GET] #{uri}") if Rails.env.development?
212
+ http.get(uri, headers)
213
+ end
214
+
215
+ unless response.code.start_with?('2')
216
+ puts("Response code: #{response.code} #{response.body}") if Rails.env.development?
217
+ return false
218
+ end
219
+
220
+ JSON.parse(response.body, symbolize_names: true)
221
+ end
222
+
223
+ def post(endpoint, params = nil)
224
+ uri = URI.parse(api_url + endpoint)
225
+
226
+ http = Net::HTTP.new(uri.host, uri.port)
227
+ http.use_ssl = (uri.scheme == 'https')
228
+ http.read_timeout = 10
229
+
230
+ response = with_retries do
231
+ puts("[POST] #{uri} #{params}") if Rails.env.development?
232
+ http.post(uri.path, (params || {}).to_json, headers)
233
+ end
234
+
235
+ unless response.code.start_with?('2')
236
+ raise("Invalid Learndash API request: #{response.body}")
237
+ end
238
+
239
+ JSON.parse(response.body, symbolize_names: true)
240
+ end
241
+
242
+ def api_url
243
+ url.chomp('/') + '/wp-json'
244
+ end
245
+
246
+ def headers
247
+ {
248
+ 'Authorization': "Basic #{Base64.strict_encode64("#{username}:#{password}")}",
249
+ 'Accept': 'application/json',
250
+ 'Content-Type': 'application/json'
251
+ }
252
+ end
253
+
254
+ def with_retries(retries: 3, wait: 2, &block)
255
+ raise('expected a block') unless block_given?
256
+
257
+ begin
258
+ return yield
259
+ rescue Exception => e
260
+ if (retries -= 1) > 0
261
+ sleep(wait); retry
262
+ else
263
+ raise
264
+ end
265
+ end
266
+ end
267
+
268
+ end
269
+ end