canvas_sync 0.12.0 → 0.16.1
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 +85 -30
- 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/migrations/create_group_memberships.rb +18 -0
- data/lib/canvas_sync/generators/templates/migrations/create_groups.rb +23 -0
- data/lib/canvas_sync/generators/templates/migrations/create_pseudonyms.rb +18 -0
- data/lib/canvas_sync/generators/templates/migrations/create_submissions.rb +1 -0
- data/lib/canvas_sync/generators/templates/models/account.rb +11 -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 +4 -2
- data/lib/canvas_sync/generators/templates/models/enrollment.rb +2 -1
- data/lib/canvas_sync/generators/templates/models/group.rb +19 -0
- data/lib/canvas_sync/generators/templates/models/group_membership.rb +17 -0
- data/lib/canvas_sync/generators/templates/models/pseudonym.rb +8 -0
- 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 +3 -1
- data/lib/canvas_sync/generators/templates/models/term.rb +2 -1
- data/lib/canvas_sync/generators/templates/models/user.rb +4 -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_users_job.rb → sync_accounts_job.rb} +11 -6
- data/lib/canvas_sync/jobs/sync_provisioning_report_job.rb +2 -0
- data/lib/canvas_sync/processors/model_mappings.yml +81 -0
- data/lib/canvas_sync/processors/provisioning_report_processor.rb +28 -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 +19 -16
- data/spec/canvas_sync/models/accounts_spec.rb +3 -0
- data/spec/canvas_sync/models/course_spec.rb +4 -0
- data/spec/canvas_sync/models/group_membership_spec.rb +26 -0
- data/spec/canvas_sync/models/group_spec.rb +26 -0
- data/spec/canvas_sync/models/user_spec.rb +2 -0
- data/spec/canvas_sync/processors/provisioning_report_processor_spec.rb +20 -0
- data/spec/dummy/app/models/account.rb +8 -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 +4 -2
- data/spec/dummy/app/models/enrollment.rb +2 -1
- data/spec/dummy/app/models/group.rb +25 -0
- data/spec/dummy/app/models/group_membership.rb +23 -0
- 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 +3 -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
- data/spec/dummy/db/migrate/20190702203627_create_submissions.rb +1 -0
- data/spec/dummy/db/migrate/20200415171620_create_groups.rb +29 -0
- data/spec/dummy/db/migrate/20200416214248_create_group_memberships.rb +24 -0
- data/spec/dummy/db/schema.rb +31 -1
- data/spec/factories/group_factory.rb +8 -0
- data/spec/factories/group_membership_factory.rb +6 -0
- data/spec/support/fixtures/reports/group_memberships.csv +3 -0
- data/spec/support/fixtures/reports/groups.csv +3 -0
- data/spec/support/fixtures/reports/submissions.csv +3 -3
- metadata +36 -6
- data/spec/canvas_sync/jobs/sync_users_job_spec.rb +0 -15
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: b7038c74006653dd7e024f38b64df7e73862aab4f6249df28ef9f7ac342700bc
|
4
|
+
data.tar.gz: 7a485d873c8c76ef54d6bd7fe11bd98d697dd0d8a1954035396da74b414ec105
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 0c1029ad4dc626d9924890d5b7cc9aaac2ae8e5e7b4350965e94e2545a14d29d650db42698bb16bd3cd97c00b6ff8571d4822ac98a9d808b3a7165e23bf449ff
|
7
|
+
data.tar.gz: 15a90b4b8df330db5fd7d4470d34487956de86a7e1e355ceb65bdfc7deb51174ab94b9feb013fc5b5fd431f6c89e58793f05ab23027d0f35fc7626936b0be7e5
|
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,33 +1,31 @@
|
|
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[
|
29
24
|
users
|
25
|
+
pseudonyms
|
30
26
|
courses
|
27
|
+
groups
|
28
|
+
group_memberships
|
31
29
|
accounts
|
32
30
|
terms
|
33
31
|
enrollments
|
@@ -111,6 +109,8 @@ module CanvasSync
|
|
111
109
|
#
|
112
110
|
# @param job_chain [Hash] A chain of jobs to execute
|
113
111
|
def invoke_next(job_chain, extra_options: {})
|
112
|
+
job_chain = job_chain.chain_data if job_chain.is_a?(JobChain)
|
113
|
+
|
114
114
|
return if job_chain[:jobs].empty?
|
115
115
|
|
116
116
|
# Make sure all job classes are serialized as strings
|
@@ -126,6 +126,8 @@ module CanvasSync
|
|
126
126
|
end
|
127
127
|
|
128
128
|
def fork(job_log, job_chain, keys: [])
|
129
|
+
job_chain = job_chain.chain_data if job_chain.is_a?(JobChain)
|
130
|
+
|
129
131
|
duped_job_chain = Marshal.load(Marshal.dump(job_chain))
|
130
132
|
duped_job_chain[:global_options][:fork_path] ||= []
|
131
133
|
duped_job_chain[:global_options][:fork_keys] ||= []
|
@@ -144,6 +146,10 @@ module CanvasSync
|
|
144
146
|
terms.each do |t|
|
145
147
|
return scope.send(t) if scope.respond_to?(t)
|
146
148
|
end
|
149
|
+
model = scope.try(:model) || scope
|
150
|
+
if model.try(:column_names)&.include?(:workflow_state)
|
151
|
+
return scope.where.not(workflow_state: %w[deleted])
|
152
|
+
end
|
147
153
|
Rails.logger.warn("Could not filter Syncable Scope for model '#{scope.try(:model)&.name || scope.name}'")
|
148
154
|
scope
|
149
155
|
end
|
@@ -172,7 +178,7 @@ module CanvasSync
|
|
172
178
|
global_options = {}
|
173
179
|
global_options[:account_id] = account_id if account_id.present?
|
174
180
|
|
175
|
-
|
181
|
+
JobChain.new(jobs: jobs, global_options: global_options)
|
176
182
|
end
|
177
183
|
|
178
184
|
# Syncs terms, users/roles/admins if necessary, then the rest of the specified models.
|
@@ -193,7 +199,7 @@ module CanvasSync
|
|
193
199
|
|
194
200
|
model_job_map = {
|
195
201
|
terms: CanvasSync::Jobs::SyncTermsJob,
|
196
|
-
|
202
|
+
accounts: CanvasSync::Jobs::SyncAccountsJob,
|
197
203
|
roles: CanvasSync::Jobs::SyncRolesJob,
|
198
204
|
admins: CanvasSync::Jobs::SyncAdminsJob,
|
199
205
|
|
@@ -219,42 +225,91 @@ module CanvasSync
|
|
219
225
|
models.unshift('terms') unless models.include?('terms')
|
220
226
|
try_add_model_job.call('terms')
|
221
227
|
|
222
|
-
#
|
223
|
-
try_add_model_job.call('
|
228
|
+
# Accounts, users, roles, and admins are synced before provisioning because they cannot be scoped to term
|
229
|
+
try_add_model_job.call('accounts')
|
230
|
+
|
231
|
+
# These Models use the provisioning report, but are not term-scoped,
|
232
|
+
# so we sync them before to ensure work is not duplicated
|
233
|
+
if term_scope.present?
|
234
|
+
models -= (first_provisioning_models = models & ['users', 'pseudonyms'])
|
235
|
+
jobs.concat(
|
236
|
+
generate_provisioning_jobs(first_provisioning_models, options)
|
237
|
+
)
|
238
|
+
end
|
239
|
+
|
224
240
|
try_add_model_job.call('roles')
|
225
241
|
try_add_model_job.call('admins')
|
226
|
-
|
227
242
|
pre_provisioning_jobs = jobs
|
228
|
-
jobs = []
|
229
243
|
|
230
244
|
###############################
|
231
245
|
# Post provisioning report jobs
|
232
246
|
###############################
|
233
247
|
|
248
|
+
jobs = []
|
234
249
|
try_add_model_job.call('assignments')
|
235
250
|
try_add_model_job.call('submissions')
|
236
251
|
try_add_model_job.call('assignment_groups')
|
237
252
|
try_add_model_job.call('context_modules')
|
238
253
|
try_add_model_job.call('context_module_items')
|
239
|
-
|
240
254
|
post_provisioning_jobs = jobs
|
241
255
|
|
242
|
-
|
243
|
-
|
244
|
-
|
245
|
-
|
246
|
-
|
247
|
-
|
248
|
-
|
249
|
-
|
250
|
-
|
251
|
-
jobs += post_provisioning_jobs
|
256
|
+
###############################
|
257
|
+
# Main provisioning job and queueing
|
258
|
+
###############################
|
259
|
+
|
260
|
+
jobs = [
|
261
|
+
*pre_provisioning_jobs,
|
262
|
+
*generate_provisioning_jobs(models, options, job_options: { term_scope: term_scope }, only_split: ['users']),
|
263
|
+
*post_provisioning_jobs,
|
264
|
+
]
|
252
265
|
|
253
266
|
global_options = { legacy_support: legacy_support }
|
254
267
|
global_options[:account_id] = account_id if account_id.present?
|
255
268
|
global_options.merge!(options[:global]) if options[:global].present?
|
256
269
|
|
257
|
-
|
270
|
+
JobChain.new(jobs: jobs, global_options: global_options)
|
271
|
+
end
|
272
|
+
|
273
|
+
def group_by_job_options(model_list, options_hash, only_split: nil, default_key: :provisioning)
|
274
|
+
dup_models = [ *model_list ]
|
275
|
+
unique_option_models = {}
|
276
|
+
|
277
|
+
filtered_models = only_split ? (only_split & model_list) : model_list
|
278
|
+
filtered_models.each do |m|
|
279
|
+
mopts = options_hash[m.to_sym] || options_hash[default_key]
|
280
|
+
unique_option_models[mopts] ||= []
|
281
|
+
unique_option_models[mopts] << m
|
282
|
+
dup_models.delete(m)
|
283
|
+
end
|
284
|
+
|
285
|
+
if dup_models.present?
|
286
|
+
mopts = options_hash[default_key]
|
287
|
+
unique_option_models[mopts] ||= []
|
288
|
+
unique_option_models[mopts].concat(dup_models)
|
289
|
+
end
|
290
|
+
|
291
|
+
unique_option_models
|
292
|
+
end
|
293
|
+
|
294
|
+
def generate_provisioning_jobs(model_list, options_hash, job_options: {}, only_split: nil, default_key: :provisioning)
|
295
|
+
# Group the model options as best we can.
|
296
|
+
# This is mainly for backwards compatibility, since 'users' was previously it's own job
|
297
|
+
unique_option_models = group_by_job_options(
|
298
|
+
model_list,
|
299
|
+
options_hash,
|
300
|
+
only_split: only_split,
|
301
|
+
default_key: default_key,
|
302
|
+
)
|
303
|
+
|
304
|
+
unique_option_models.map do |mopts, models|
|
305
|
+
opts = { models: models }
|
306
|
+
opts.merge!(job_options)
|
307
|
+
opts.merge!(mopts) if mopts.present?
|
308
|
+
{
|
309
|
+
job: CanvasSync::Jobs::SyncProvisioningReportJob.to_s,
|
310
|
+
options: opts,
|
311
|
+
}
|
312
|
+
end
|
258
313
|
end
|
259
314
|
|
260
315
|
# 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
|