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.
Files changed (75) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +1 -1
  3. data/lib/canvas_sync.rb +85 -30
  4. data/lib/canvas_sync/api_syncable.rb +4 -162
  5. data/lib/canvas_sync/class_callback_executor.rb +35 -0
  6. data/lib/canvas_sync/concerns/account/ancestry.rb +60 -0
  7. data/lib/canvas_sync/concerns/api_syncable.rb +189 -0
  8. data/lib/canvas_sync/concerns/legacy_columns.rb +34 -0
  9. data/lib/canvas_sync/generators/templates/migrations/create_group_memberships.rb +18 -0
  10. data/lib/canvas_sync/generators/templates/migrations/create_groups.rb +23 -0
  11. data/lib/canvas_sync/generators/templates/migrations/create_pseudonyms.rb +18 -0
  12. data/lib/canvas_sync/generators/templates/migrations/create_submissions.rb +1 -0
  13. data/lib/canvas_sync/generators/templates/models/account.rb +11 -1
  14. data/lib/canvas_sync/generators/templates/models/admin.rb +2 -1
  15. data/lib/canvas_sync/generators/templates/models/assignment.rb +2 -1
  16. data/lib/canvas_sync/generators/templates/models/assignment_group.rb +2 -1
  17. data/lib/canvas_sync/generators/templates/models/context_module.rb +2 -1
  18. data/lib/canvas_sync/generators/templates/models/context_module_item.rb +2 -1
  19. data/lib/canvas_sync/generators/templates/models/course.rb +4 -2
  20. data/lib/canvas_sync/generators/templates/models/enrollment.rb +2 -1
  21. data/lib/canvas_sync/generators/templates/models/group.rb +19 -0
  22. data/lib/canvas_sync/generators/templates/models/group_membership.rb +17 -0
  23. data/lib/canvas_sync/generators/templates/models/pseudonym.rb +8 -0
  24. data/lib/canvas_sync/generators/templates/models/role.rb +2 -1
  25. data/lib/canvas_sync/generators/templates/models/section.rb +2 -1
  26. data/lib/canvas_sync/generators/templates/models/submission.rb +3 -1
  27. data/lib/canvas_sync/generators/templates/models/term.rb +2 -1
  28. data/lib/canvas_sync/generators/templates/models/user.rb +4 -1
  29. data/lib/canvas_sync/importers/bulk_importer.rb +7 -1
  30. data/lib/canvas_sync/importers/legacy_importer.rb +4 -2
  31. data/lib/canvas_sync/job.rb +3 -1
  32. data/lib/canvas_sync/job_chain.rb +57 -0
  33. data/lib/canvas_sync/jobs/{sync_users_job.rb → sync_accounts_job.rb} +11 -6
  34. data/lib/canvas_sync/jobs/sync_provisioning_report_job.rb +2 -0
  35. data/lib/canvas_sync/processors/model_mappings.yml +81 -0
  36. data/lib/canvas_sync/processors/provisioning_report_processor.rb +28 -0
  37. data/lib/canvas_sync/record.rb +9 -0
  38. data/lib/canvas_sync/version.rb +1 -1
  39. data/spec/canvas_sync/canvas_sync_spec.rb +19 -16
  40. data/spec/canvas_sync/models/accounts_spec.rb +3 -0
  41. data/spec/canvas_sync/models/course_spec.rb +4 -0
  42. data/spec/canvas_sync/models/group_membership_spec.rb +26 -0
  43. data/spec/canvas_sync/models/group_spec.rb +26 -0
  44. data/spec/canvas_sync/models/user_spec.rb +2 -0
  45. data/spec/canvas_sync/processors/provisioning_report_processor_spec.rb +20 -0
  46. data/spec/dummy/app/models/account.rb +8 -1
  47. data/spec/dummy/app/models/admin.rb +2 -1
  48. data/spec/dummy/app/models/assignment.rb +2 -1
  49. data/spec/dummy/app/models/assignment_group.rb +2 -1
  50. data/spec/dummy/app/models/context_module.rb +2 -1
  51. data/spec/dummy/app/models/context_module_item.rb +2 -1
  52. data/spec/dummy/app/models/course.rb +4 -2
  53. data/spec/dummy/app/models/enrollment.rb +2 -1
  54. data/spec/dummy/app/models/group.rb +25 -0
  55. data/spec/dummy/app/models/group_membership.rb +23 -0
  56. data/spec/dummy/app/models/role.rb +2 -1
  57. data/spec/dummy/app/models/section.rb +2 -1
  58. data/spec/dummy/app/models/submission.rb +2 -1
  59. data/spec/dummy/app/models/term.rb +2 -1
  60. data/spec/dummy/app/models/user.rb +3 -1
  61. data/spec/dummy/config/application.rb +12 -1
  62. data/spec/dummy/config/database.yml +11 -11
  63. data/spec/dummy/config/environments/development.rb +3 -3
  64. data/spec/dummy/config/initializers/assets.rb +1 -1
  65. data/spec/dummy/db/migrate/20190702203627_create_submissions.rb +1 -0
  66. data/spec/dummy/db/migrate/20200415171620_create_groups.rb +29 -0
  67. data/spec/dummy/db/migrate/20200416214248_create_group_memberships.rb +24 -0
  68. data/spec/dummy/db/schema.rb +31 -1
  69. data/spec/factories/group_factory.rb +8 -0
  70. data/spec/factories/group_membership_factory.rb +6 -0
  71. data/spec/support/fixtures/reports/group_memberships.csv +3 -0
  72. data/spec/support/fixtures/reports/groups.csv +3 -0
  73. data/spec/support/fixtures/reports/submissions.csv +3 -3
  74. metadata +36 -6
  75. 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: 7795e4388557d88e9134d2c5ae6dee8770ba282f1dd72a10686aaa7974741c92
4
- data.tar.gz: db4251351aabda748429adb17bdcb3c3dbc383d0dc2a7a112c324d0e8e273067
3
+ metadata.gz: b7038c74006653dd7e024f38b64df7e73862aab4f6249df28ef9f7ac342700bc
4
+ data.tar.gz: 7a485d873c8c76ef54d6bd7fe11bd98d697dd0d8a1954035396da74b414ec105
5
5
  SHA512:
6
- metadata.gz: 3babc49ea2a583a81560aed52f08dcf7c4a706b74123fb1128b04f77030c575c3764feda1f5aab33c0e009b5c7d959cd2ebd7cdd79f566d9fdbad1f7a57d204d
7
- data.tar.gz: '0048f0ebb889753af134a28a592a7b1ebee8f6b5c4d67c9ed816271fc4a14356a62ff8643b9bdfaf9a3e04046a95035768248790d56366b0db6f6a58701c2d4f'
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
@@ -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
- { jobs: jobs, global_options: global_options }
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
- users: CanvasSync::Jobs::SyncUsersJob,
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
- # Users, roles, and admins are synced before provisioning because they cannot be scoped to term
223
- try_add_model_job.call('users') if term_scope.present?
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
- jobs = pre_provisioning_jobs
243
- if models.present?
244
- provisioning_job = {
245
- job: CanvasSync::Jobs::SyncProvisioningReportJob.to_s,
246
- options: { term_scope: term_scope, models: models },
247
- }
248
- provisioning_job[:options].merge!(options[:provisioning]) if options[:provisioning].present?
249
- jobs += Array.wrap(provisioning_job)
250
- end
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
- { jobs: jobs, global_options: global_options }
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
- class_methods do
6
- def find_or_fetch(canvas_id, save: false, retries: 1)
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