canvas_sync 0.12.0 → 0.13.0
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 +4 -4
- data/README.md +1 -1
- data/lib/canvas_sync.rb +16 -15
- data/lib/canvas_sync/api_syncable.rb +4 -162
- data/lib/canvas_sync/class_callback_executor.rb +35 -0
- data/lib/canvas_sync/concerns/account/ancestry.rb +60 -0
- data/lib/canvas_sync/concerns/api_syncable.rb +189 -0
- data/lib/canvas_sync/concerns/legacy_columns.rb +34 -0
- data/lib/canvas_sync/generators/templates/models/account.rb +7 -1
- data/lib/canvas_sync/generators/templates/models/admin.rb +2 -1
- data/lib/canvas_sync/generators/templates/models/assignment.rb +2 -1
- data/lib/canvas_sync/generators/templates/models/assignment_group.rb +2 -1
- data/lib/canvas_sync/generators/templates/models/context_module.rb +2 -1
- data/lib/canvas_sync/generators/templates/models/context_module_item.rb +2 -1
- data/lib/canvas_sync/generators/templates/models/course.rb +3 -2
- data/lib/canvas_sync/generators/templates/models/enrollment.rb +2 -1
- data/lib/canvas_sync/generators/templates/models/role.rb +2 -1
- data/lib/canvas_sync/generators/templates/models/section.rb +2 -1
- data/lib/canvas_sync/generators/templates/models/submission.rb +2 -1
- data/lib/canvas_sync/generators/templates/models/term.rb +2 -1
- data/lib/canvas_sync/generators/templates/models/user.rb +2 -1
- data/lib/canvas_sync/importers/bulk_importer.rb +7 -1
- data/lib/canvas_sync/importers/legacy_importer.rb +4 -2
- data/lib/canvas_sync/job.rb +3 -1
- data/lib/canvas_sync/job_chain.rb +57 -0
- data/lib/canvas_sync/jobs/sync_accounts_job.rb +31 -0
- data/lib/canvas_sync/record.rb +9 -0
- data/lib/canvas_sync/version.rb +1 -1
- data/spec/canvas_sync/canvas_sync_spec.rb +14 -14
- data/spec/dummy/app/models/account.rb +7 -1
- data/spec/dummy/app/models/admin.rb +2 -1
- data/spec/dummy/app/models/assignment.rb +2 -1
- data/spec/dummy/app/models/assignment_group.rb +2 -1
- data/spec/dummy/app/models/context_module.rb +2 -1
- data/spec/dummy/app/models/context_module_item.rb +2 -1
- data/spec/dummy/app/models/course.rb +3 -2
- data/spec/dummy/app/models/enrollment.rb +2 -1
- data/spec/dummy/app/models/role.rb +2 -1
- data/spec/dummy/app/models/section.rb +2 -1
- data/spec/dummy/app/models/submission.rb +2 -1
- data/spec/dummy/app/models/term.rb +2 -1
- data/spec/dummy/app/models/user.rb +2 -1
- data/spec/dummy/config/application.rb +12 -1
- data/spec/dummy/config/database.yml +11 -11
- data/spec/dummy/config/environments/development.rb +3 -3
- data/spec/dummy/config/initializers/assets.rb +1 -1
- metadata +9 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 5c8386afbd924c0148b5c2158d23ea7acd5b0b85349da51312103e1ea9368c54
|
4
|
+
data.tar.gz: 051471b40b5a464b66df8ca7ae482874f2791c55604e28f7fe9ce54cb07f875a
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: '00742595c4797ba758ef98ab8b034545eb2193a2cae92dfc585e720c7cd9adef663ba0aadcae2c7085e19497ff74461c6d5553d49114f42dac520c8fe48ca448'
|
7
|
+
data.tar.gz: d8c7eb61e630bb65ea9aa2e86b86c03c2c1f87c151e127314c938f7d1b2438c90dfc5f5ddcc4cb74fe0b525bc4ef9c03dfb140d09fc10045b2f7f56b41524e43
|
data/README.md
CHANGED
@@ -223,7 +223,7 @@ users:
|
|
223
223
|
|
224
224
|
### API Sync
|
225
225
|
Several models implement the `ApiSyncable` Concern. This is done in the Model Templates so as to be customizable and tweakable.
|
226
|
-
Models that `include CanvasSync::ApiSyncable` should also call the `api_syncable` class method to configure the Synchronization.
|
226
|
+
Models that `include CanvasSync::Concerns::ApiSyncable` should also call the `api_syncable` class method to configure the Synchronization.
|
227
227
|
`api_syncable` takes two arguments and an optional block callback:
|
228
228
|
```ruby
|
229
229
|
class CanvasSyncModel < ApplicationRecord
|
data/lib/canvas_sync.rb
CHANGED
@@ -1,28 +1,23 @@
|
|
1
1
|
require "bearcat"
|
2
|
+
|
2
3
|
require "canvas_sync/version"
|
3
4
|
require "canvas_sync/engine"
|
5
|
+
require "canvas_sync/class_callback_executor"
|
4
6
|
require "canvas_sync/job"
|
7
|
+
require "canvas_sync/job_chain"
|
5
8
|
require "canvas_sync/sidekiq_job"
|
6
9
|
require "canvas_sync/api_syncable"
|
10
|
+
require "canvas_sync/record"
|
7
11
|
require "canvas_sync/jobs/report_starter"
|
8
12
|
require "canvas_sync/jobs/report_checker"
|
9
|
-
require "canvas_sync/jobs/fork_gather"
|
10
13
|
require "canvas_sync/jobs/report_processor_job"
|
11
|
-
require "canvas_sync/jobs/sync_provisioning_report_job"
|
12
|
-
require "canvas_sync/jobs/sync_simple_table_job.rb"
|
13
|
-
require "canvas_sync/jobs/sync_assignments_job"
|
14
|
-
require "canvas_sync/jobs/sync_submissions_job"
|
15
|
-
require "canvas_sync/jobs/sync_assignment_groups_job"
|
16
|
-
require "canvas_sync/jobs/sync_context_modules_job"
|
17
|
-
require "canvas_sync/jobs/sync_context_module_items_job"
|
18
|
-
require "canvas_sync/jobs/sync_terms_job"
|
19
|
-
require "canvas_sync/jobs/sync_users_job"
|
20
|
-
require "canvas_sync/jobs/sync_roles_job"
|
21
|
-
require "canvas_sync/jobs/sync_admins_job"
|
22
14
|
require "canvas_sync/config"
|
15
|
+
|
16
|
+
Dir[File.dirname(__FILE__) + "/canvas_sync/jobs/*.rb"].each { |file| require file }
|
23
17
|
Dir[File.dirname(__FILE__) + "/canvas_sync/processors/*.rb"].each { |file| require file }
|
24
18
|
Dir[File.dirname(__FILE__) + "/canvas_sync/importers/*.rb"].each { |file| require file }
|
25
19
|
Dir[File.dirname(__FILE__) + "/canvas_sync/generators/*.rb"].each { |file| require file }
|
20
|
+
Dir[File.dirname(__FILE__) + "/canvas_sync/concerns/**/*.rb"].each { |file| require file }
|
26
21
|
|
27
22
|
module CanvasSync
|
28
23
|
SUPPORTED_MODELS = %w[
|
@@ -111,6 +106,8 @@ module CanvasSync
|
|
111
106
|
#
|
112
107
|
# @param job_chain [Hash] A chain of jobs to execute
|
113
108
|
def invoke_next(job_chain, extra_options: {})
|
109
|
+
job_chain = job_chain.chain_data if job_chain.is_a?(JobChain)
|
110
|
+
|
114
111
|
return if job_chain[:jobs].empty?
|
115
112
|
|
116
113
|
# Make sure all job classes are serialized as strings
|
@@ -126,6 +123,8 @@ module CanvasSync
|
|
126
123
|
end
|
127
124
|
|
128
125
|
def fork(job_log, job_chain, keys: [])
|
126
|
+
job_chain = job_chain.chain_data if job_chain.is_a?(JobChain)
|
127
|
+
|
129
128
|
duped_job_chain = Marshal.load(Marshal.dump(job_chain))
|
130
129
|
duped_job_chain[:global_options][:fork_path] ||= []
|
131
130
|
duped_job_chain[:global_options][:fork_keys] ||= []
|
@@ -172,7 +171,7 @@ module CanvasSync
|
|
172
171
|
global_options = {}
|
173
172
|
global_options[:account_id] = account_id if account_id.present?
|
174
173
|
|
175
|
-
|
174
|
+
JobChain.new(jobs: jobs, global_options: global_options)
|
176
175
|
end
|
177
176
|
|
178
177
|
# Syncs terms, users/roles/admins if necessary, then the rest of the specified models.
|
@@ -193,6 +192,7 @@ module CanvasSync
|
|
193
192
|
|
194
193
|
model_job_map = {
|
195
194
|
terms: CanvasSync::Jobs::SyncTermsJob,
|
195
|
+
accounts: CanvasSync::Jobs::SyncAccountsJob,
|
196
196
|
users: CanvasSync::Jobs::SyncUsersJob,
|
197
197
|
roles: CanvasSync::Jobs::SyncRolesJob,
|
198
198
|
admins: CanvasSync::Jobs::SyncAdminsJob,
|
@@ -219,7 +219,8 @@ module CanvasSync
|
|
219
219
|
models.unshift('terms') unless models.include?('terms')
|
220
220
|
try_add_model_job.call('terms')
|
221
221
|
|
222
|
-
#
|
222
|
+
# Accounts, users, roles, and admins are synced before provisioning because they cannot be scoped to term
|
223
|
+
try_add_model_job.call('accounts')
|
223
224
|
try_add_model_job.call('users') if term_scope.present?
|
224
225
|
try_add_model_job.call('roles')
|
225
226
|
try_add_model_job.call('admins')
|
@@ -254,7 +255,7 @@ module CanvasSync
|
|
254
255
|
global_options[:account_id] = account_id if account_id.present?
|
255
256
|
global_options.merge!(options[:global]) if options[:global].present?
|
256
257
|
|
257
|
-
|
258
|
+
JobChain.new(jobs: jobs, global_options: global_options)
|
258
259
|
end
|
259
260
|
|
260
261
|
# Calls the canvas_sync_client in your app. If you have specified an account
|
@@ -1,167 +1,9 @@
|
|
1
|
+
# DEPRECATED - See CHANGELOG for 0.13.0
|
2
|
+
# TODO: (0.14.0) Remove this module
|
1
3
|
module CanvasSync::ApiSyncable
|
2
4
|
extend ActiveSupport::Concern
|
3
|
-
NON_EXISTANT_ERRORS = [Faraday::Error::ResourceNotFound, Footrest::HttpError::NotFound]
|
4
5
|
|
5
|
-
|
6
|
-
|
7
|
-
inst = find_by(canvas_id: canvas_id)
|
8
|
-
return inst if inst.present?
|
9
|
-
inst = new(canvas_id: canvas_id)
|
10
|
-
api_response = inst.request_from_api(retries: retries)
|
11
|
-
inst.update_from_api_params(api_response)
|
12
|
-
inst.save! if save
|
13
|
-
inst
|
14
|
-
rescue *NON_EXISTANT_ERRORS
|
15
|
-
nil
|
16
|
-
end
|
17
|
-
|
18
|
-
def find_or_fetch!(*args)
|
19
|
-
inst = find_or_fetch(*args)
|
20
|
-
raise ActiveRecord::RecordNotFound unless inst.present?
|
21
|
-
inst
|
22
|
-
end
|
23
|
-
|
24
|
-
def create_or_update_from_api_params(api_params)
|
25
|
-
api_params = api_params.with_indifferent_access
|
26
|
-
inst = find_or_initialize_by(canvas_id: api_params[:id])
|
27
|
-
inst.update_from_api_params(api_params)
|
28
|
-
inst.save! if inst.changed?
|
29
|
-
inst
|
30
|
-
end
|
31
|
-
|
32
|
-
def api_sync_options=(opts)
|
33
|
-
@api_sync_options = opts
|
34
|
-
end
|
35
|
-
|
36
|
-
def api_sync_options
|
37
|
-
@api_sync_options || superclass.try(:api_sync_options)
|
38
|
-
end
|
39
|
-
|
40
|
-
# Define the model as being syncable via the Canvas API and configure sync options/parameters
|
41
|
-
# @param [Hash] map A hash of local_field => (:api_response_key | ->(api_response){ value })
|
42
|
-
# @param [->(bearcat?){ api_response }] fetch <description>
|
43
|
-
# @param [Hash] options <description>
|
44
|
-
# @option options [] :mark_deleted Hash to be merged | Symbol to invoke | ->(){ }
|
45
|
-
# @yield [api_response, [mapped_data]] Callback to merge data into a Model instance
|
46
|
-
def api_syncable(map, fetch, options={}, &blk)
|
47
|
-
default_options = {
|
48
|
-
mark_deleted: -> {
|
49
|
-
%i[workflow_state= status=].each do |sym|
|
50
|
-
next unless self.respond_to?(sym)
|
51
|
-
self.send(sym, 'deleted')
|
52
|
-
return
|
53
|
-
end
|
54
|
-
},
|
55
|
-
field_map: map,
|
56
|
-
fetch_from_api: fetch,
|
57
|
-
process_response: blk,
|
58
|
-
}
|
59
|
-
default_options.merge!(options)
|
60
|
-
self.api_sync_options = default_options.merge!(options)
|
61
|
-
end
|
62
|
-
end
|
63
|
-
|
64
|
-
# Call the API and Syncs this model.
|
65
|
-
# Calls the mark_deleted workflow if a 404 is received.
|
66
|
-
# @param [Number] retries Number of retries
|
67
|
-
# @return [Hash] Response Hash from API
|
68
|
-
def sync_from_api(retries: 3)
|
69
|
-
api_response = request_from_api(retries: retries)
|
70
|
-
update_from_api_params!(api_response)
|
71
|
-
api_response
|
72
|
-
rescue *NON_EXISTANT_ERRORS
|
73
|
-
api_mark_deleted
|
74
|
-
save! if changed?
|
75
|
-
nil
|
76
|
-
end
|
77
|
-
|
78
|
-
# Fetch this model from the API and return the response
|
79
|
-
# @param [Number] retries Number of retries
|
80
|
-
# @return [Hash] Response Hash from API
|
81
|
-
def request_from_api(retries: 3)
|
82
|
-
api_call_with_retry(retries || 3) {
|
83
|
-
blk = api_sync_options[:fetch_from_api]
|
84
|
-
case blk.arity
|
85
|
-
when 1
|
86
|
-
self.instance_exec(canvas_sync_client, &blk)
|
87
|
-
else
|
88
|
-
self.instance_exec(&blk)
|
89
|
-
end
|
90
|
-
}
|
91
|
-
end
|
92
|
-
|
93
|
-
# Apply a response Hash from the API to this model's attributes, but do not save
|
94
|
-
# @param [Hash] api_params API-format Hash
|
95
|
-
# @return [self] self
|
96
|
-
def update_from_api_params(api_params)
|
97
|
-
options = self.api_sync_options
|
98
|
-
api_params = api_params.with_indifferent_access
|
99
|
-
|
100
|
-
map = options[:field_map]
|
101
|
-
mapped_params = {}
|
102
|
-
if map.present?
|
103
|
-
map.each do |local_name, remote_name|
|
104
|
-
if remote_name.respond_to?(:call)
|
105
|
-
mapped_params[local_name] = self.instance_exec(api_params, &remote_name)
|
106
|
-
elsif api_params.include?(remote_name)
|
107
|
-
mapped_params[local_name] = api_params[remote_name]
|
108
|
-
if remote_name == :id
|
109
|
-
current_value = send("#{local_name}")
|
110
|
-
raise "Mismatched Canvas ID" if current_value.present? && current_value != api_params[remote_name]
|
111
|
-
end
|
112
|
-
end
|
113
|
-
end
|
114
|
-
end
|
115
|
-
|
116
|
-
apply_block = options[:process_response]
|
117
|
-
if apply_block.present?
|
118
|
-
case apply_block.arity
|
119
|
-
when 1
|
120
|
-
self.instance_exec(api_params, &apply_block)
|
121
|
-
when 2
|
122
|
-
self.instance_exec(api_params, mapped_params, &apply_block)
|
123
|
-
end
|
124
|
-
else
|
125
|
-
mapped_params.each do |local_name, val|
|
126
|
-
send("#{local_name}=", val)
|
127
|
-
end
|
128
|
-
end
|
129
|
-
self
|
130
|
-
end
|
131
|
-
|
132
|
-
# Apply a response Hash from the API to this model's attributes, and save if changed?
|
133
|
-
# @param [Hash] api_params API-format Hash
|
134
|
-
# @return [self] self
|
135
|
-
def update_from_api_params!(api_params)
|
136
|
-
update_from_api_params(api_params)
|
137
|
-
save! if changed?
|
138
|
-
self
|
139
|
-
end
|
140
|
-
|
141
|
-
def api_sync_options
|
142
|
-
self.class.api_sync_options
|
143
|
-
end
|
144
|
-
|
145
|
-
private
|
146
|
-
|
147
|
-
def api_call_with_retry(retries=3)
|
148
|
-
tries ||= retries
|
149
|
-
yield
|
150
|
-
rescue Faraday::ConnectionFailed => e
|
151
|
-
raise e if (tries -= 1).zero?
|
152
|
-
sleep 5
|
153
|
-
retry
|
154
|
-
end
|
155
|
-
|
156
|
-
def api_mark_deleted
|
157
|
-
action = api_sync_options[:mark_deleted]
|
158
|
-
case action
|
159
|
-
when Hash
|
160
|
-
assign_attributes(action)
|
161
|
-
when Symbol
|
162
|
-
send(action)
|
163
|
-
when Proc
|
164
|
-
self.instance_exec(&action)
|
165
|
-
end
|
6
|
+
included do
|
7
|
+
raise "CanvasSync 0.13.0 includes breaking changes to ApiSyncable. See the CHANGELOG for upgrade steps."
|
166
8
|
end
|
167
9
|
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
module CanvasSync
|
2
|
+
# Helper/Hack class to allow calling ActiveSupport callbacks on a class instead of just on instances
|
3
|
+
class ClassCallbackExecutor
|
4
|
+
include ActiveSupport::Callbacks
|
5
|
+
|
6
|
+
attr_reader :callback_class
|
7
|
+
delegate :__callbacks, to: :callback_class
|
8
|
+
delegate_missing_to :callback_class
|
9
|
+
|
10
|
+
def initialize(cls, env)
|
11
|
+
@callback_class = cls
|
12
|
+
env.keys.each do |k|
|
13
|
+
define_singleton_method(k) do
|
14
|
+
env[k]
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
def clazz
|
20
|
+
callback_class
|
21
|
+
end
|
22
|
+
|
23
|
+
def self.run_callbacks(cls, callback, env={}, &blk)
|
24
|
+
new(cls, env).run_callbacks(callback, &blk)
|
25
|
+
end
|
26
|
+
|
27
|
+
def self.run_if_defined(cls, callback, *args, &blk)
|
28
|
+
if cls.respond_to?(:"_#{callback}_callbacks")
|
29
|
+
run_callbacks(cls, callback, *args, &blk)
|
30
|
+
else
|
31
|
+
blk.call
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
@@ -0,0 +1,60 @@
|
|
1
|
+
module CanvasSync::Concerns
|
2
|
+
module Account
|
3
|
+
# Add support for the ancestry Gem to Accounts
|
4
|
+
#
|
5
|
+
# Requires `ancestry` to be added to the Gemfile and a migration to execute these steps:
|
6
|
+
# add_column :accounts, :ancestry, :string
|
7
|
+
# add_index :accounts, :ancestry
|
8
|
+
#
|
9
|
+
# Handles syncing any Ancestry changes after CanvasSync syncs Accounts.
|
10
|
+
module Ancestry
|
11
|
+
extend ActiveSupport::Concern
|
12
|
+
include CanvasSync::Record
|
13
|
+
|
14
|
+
included do
|
15
|
+
has_ancestry
|
16
|
+
before_save :relink_ancestry, if: :canvas_parent_account_id_changed?
|
17
|
+
after_sync_import :ancestry_after_sync
|
18
|
+
end
|
19
|
+
|
20
|
+
class_methods do
|
21
|
+
def ancestry_after_sync
|
22
|
+
trails = {}
|
23
|
+
includes(:canvas_parent).find_each do |account|
|
24
|
+
parent = account.canvas_parent
|
25
|
+
trail = trails[parent.canvas_id] if parent.present?
|
26
|
+
|
27
|
+
if trail.present?
|
28
|
+
account.ancestry = trail
|
29
|
+
new_trail = "#{trail}/#{account.id.to_s}"
|
30
|
+
elsif parent.present?
|
31
|
+
account.parent = parent
|
32
|
+
new_trail = "#{account.ancestry}/#{account.id.to_s}"
|
33
|
+
else
|
34
|
+
account.parent = parent
|
35
|
+
new_trail = account.id.to_s
|
36
|
+
end
|
37
|
+
|
38
|
+
trails[account.canvas_id] = new_trail
|
39
|
+
account.save! if account.changed?
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
def relink_ancestry
|
45
|
+
self.parent = canvas_parent
|
46
|
+
end
|
47
|
+
|
48
|
+
def ensure_ancestry
|
49
|
+
return unless canvas_parent_account_id.present?
|
50
|
+
return if canvas_parent.present?
|
51
|
+
|
52
|
+
self.canvas_parent = Account.find_or_fetch(canvas_parent_account_id)
|
53
|
+
canvas_parent.save!
|
54
|
+
canvas_parent.ensure_ancestry
|
55
|
+
relink_ancestry
|
56
|
+
save! if changed?
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
@@ -0,0 +1,189 @@
|
|
1
|
+
module CanvasSync::Concerns
|
2
|
+
module ApiSyncable
|
3
|
+
extend ActiveSupport::Concern
|
4
|
+
NON_EXISTANT_ERRORS = [Faraday::Error::ResourceNotFound, Footrest::HttpError::NotFound]
|
5
|
+
|
6
|
+
class_methods do
|
7
|
+
def find_or_fetch(canvas_id, save: false, retries: 1, **kwargs)
|
8
|
+
inst = find_or_initialize_by(canvas_id: canvas_id)
|
9
|
+
return inst if inst.persisted?
|
10
|
+
|
11
|
+
api_response = inst.request_from_api(retries: retries, **kwargs)
|
12
|
+
api_sync_race_create!(inst, save: save) do |inst2|
|
13
|
+
inst2.assign_from_api_params(api_response, **kwargs)
|
14
|
+
end
|
15
|
+
rescue *NON_EXISTANT_ERRORS
|
16
|
+
nil
|
17
|
+
end
|
18
|
+
|
19
|
+
def find_or_fetch!(*args)
|
20
|
+
inst = find_or_fetch(*args)
|
21
|
+
raise ActiveRecord::RecordNotFound unless inst.present?
|
22
|
+
inst
|
23
|
+
end
|
24
|
+
|
25
|
+
def create_or_update_from_api_params(api_params)
|
26
|
+
api_params = api_params.with_indifferent_access
|
27
|
+
api_sync_race_create!(api_params[:id]) do |inst|
|
28
|
+
inst.assign_from_api_params(api_params)
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
def api_sync_options=(opts)
|
33
|
+
@api_sync_options = opts
|
34
|
+
end
|
35
|
+
|
36
|
+
def api_sync_options
|
37
|
+
@api_sync_options || superclass.try(:api_sync_options)
|
38
|
+
end
|
39
|
+
|
40
|
+
# Define the model as being syncable via the Canvas API and configure sync options/parameters
|
41
|
+
# @param [Hash] map A hash of local_field => (:api_response_key | ->(api_response){ value })
|
42
|
+
# @param [->(bearcat?){ api_response }] fetch <description>
|
43
|
+
# @param [Hash] options <description>
|
44
|
+
# @option options [] :mark_deleted Hash to be merged | Symbol to invoke | ->(){ }
|
45
|
+
# @yield [api_response, [mapped_data]] Callback to merge data into a Model instance
|
46
|
+
def api_syncable(map, fetch, options={}, &blk)
|
47
|
+
default_options = {
|
48
|
+
mark_deleted: -> {
|
49
|
+
%i[workflow_state= status=].each do |sym|
|
50
|
+
next unless self.respond_to?(sym)
|
51
|
+
self.send(sym, 'deleted')
|
52
|
+
return
|
53
|
+
end
|
54
|
+
},
|
55
|
+
field_map: map,
|
56
|
+
fetch_from_api: fetch,
|
57
|
+
process_response: blk,
|
58
|
+
}
|
59
|
+
default_options.merge!(options)
|
60
|
+
self.api_sync_options = default_options.merge!(options)
|
61
|
+
end
|
62
|
+
|
63
|
+
private
|
64
|
+
|
65
|
+
def api_sync_race_create!(inst, save: true)
|
66
|
+
inst = find_or_initialize_by(canvas_id: inst) unless inst.is_a?(self)
|
67
|
+
yield inst
|
68
|
+
inst.save! if save && inst.changed?
|
69
|
+
inst
|
70
|
+
rescue ActiveRecord::RecordNotUnique
|
71
|
+
inst = find_by(canvas_id: inst.canvas_id)
|
72
|
+
yield inst
|
73
|
+
inst.save! if save && inst.changed?
|
74
|
+
inst
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
# Call the API and Syncs this model.
|
79
|
+
# Calls the mark_deleted workflow if a 404 is received.
|
80
|
+
# @param [Number] retries Number of retries
|
81
|
+
# @return [Hash] Response Hash from API
|
82
|
+
def sync_from_api(retries: 3, **kwargs)
|
83
|
+
api_response = request_from_api(retries: retries, **kwargs)
|
84
|
+
update_from_api_params!(api_response, **kwargs)
|
85
|
+
api_response
|
86
|
+
rescue *NON_EXISTANT_ERRORS
|
87
|
+
api_mark_deleted
|
88
|
+
save! if changed?
|
89
|
+
nil
|
90
|
+
end
|
91
|
+
|
92
|
+
# Fetch this model from the API and return the response
|
93
|
+
# @param [Number] retries Number of retries
|
94
|
+
# @return [Hash] Response Hash from API
|
95
|
+
def request_from_api(retries: 3, **kwargs)
|
96
|
+
api_call_with_retry(retries || 3) {
|
97
|
+
blk = api_sync_options[:fetch_from_api]
|
98
|
+
case blk.arity
|
99
|
+
when 1
|
100
|
+
self.instance_exec(canvas_sync_client, &blk)
|
101
|
+
else
|
102
|
+
self.instance_exec(&blk)
|
103
|
+
end
|
104
|
+
}
|
105
|
+
end
|
106
|
+
|
107
|
+
# Apply a response Hash from the API to this model's attributes, but do not save
|
108
|
+
# @param [Hash] api_params API-format Hash
|
109
|
+
# @return [self] self
|
110
|
+
def assign_from_api_params(api_params, **kwargs)
|
111
|
+
options = self.api_sync_options
|
112
|
+
api_params = api_params.with_indifferent_access
|
113
|
+
|
114
|
+
map = options[:field_map]
|
115
|
+
mapped_params = {}
|
116
|
+
if map.present?
|
117
|
+
map.each do |local_name, remote_name|
|
118
|
+
if remote_name.respond_to?(:call)
|
119
|
+
mapped_params[local_name] = self.instance_exec(api_params, &remote_name)
|
120
|
+
elsif api_params.include?(remote_name)
|
121
|
+
mapped_params[local_name] = api_params[remote_name]
|
122
|
+
if remote_name == :id
|
123
|
+
current_value = send("#{local_name}")
|
124
|
+
raise "Mismatched Canvas ID" if current_value.present? && current_value != api_params[remote_name]
|
125
|
+
end
|
126
|
+
end
|
127
|
+
end
|
128
|
+
end
|
129
|
+
|
130
|
+
apply_block = options[:process_response]
|
131
|
+
if apply_block.present?
|
132
|
+
case apply_block.arity
|
133
|
+
when 1
|
134
|
+
self.instance_exec(api_params, &apply_block)
|
135
|
+
when 2
|
136
|
+
self.instance_exec(api_params, mapped_params, &apply_block)
|
137
|
+
end
|
138
|
+
else
|
139
|
+
mapped_params.each do |local_name, val|
|
140
|
+
send("#{local_name}=", val)
|
141
|
+
end
|
142
|
+
end
|
143
|
+
self
|
144
|
+
end
|
145
|
+
|
146
|
+
# Apply a response Hash from the API to this model's attributes and save if changed?
|
147
|
+
# @param [Hash] api_params API-format Hash
|
148
|
+
# @return [self] self
|
149
|
+
def update_from_api_params(api_params, **kwargs)
|
150
|
+
assign_from_api_params(*args)
|
151
|
+
save if changed?
|
152
|
+
end
|
153
|
+
|
154
|
+
# Apply a response Hash from the API to this model's attributes, and save! if changed?
|
155
|
+
# @param [Hash] api_params API-format Hash
|
156
|
+
# @return [self] self
|
157
|
+
def update_from_api_params!(*args)
|
158
|
+
assign_from_api_params(*args)
|
159
|
+
save! if changed?
|
160
|
+
end
|
161
|
+
|
162
|
+
def api_sync_options
|
163
|
+
self.class.api_sync_options
|
164
|
+
end
|
165
|
+
|
166
|
+
private
|
167
|
+
|
168
|
+
def api_call_with_retry(retries=3)
|
169
|
+
tries ||= retries
|
170
|
+
yield
|
171
|
+
rescue Faraday::ConnectionFailed => e
|
172
|
+
raise e if (tries -= 1).zero?
|
173
|
+
sleep 5
|
174
|
+
retry
|
175
|
+
end
|
176
|
+
|
177
|
+
def api_mark_deleted
|
178
|
+
action = api_sync_options[:mark_deleted]
|
179
|
+
case action
|
180
|
+
when Hash
|
181
|
+
assign_attributes(action)
|
182
|
+
when Symbol
|
183
|
+
send(action)
|
184
|
+
when Proc
|
185
|
+
self.instance_exec(&action)
|
186
|
+
end
|
187
|
+
end
|
188
|
+
end
|
189
|
+
end
|