canvas_sync 0.12.0 → 0.16.1

Sign up to get free protection for your applications and to get access to all the features.
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