canvas_sync 0.10.3 → 0.11.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (66) hide show
  1. checksums.yaml +5 -5
  2. data/README.md +57 -2
  3. data/db/migrate/20190916154829_add_fork_count_to_canvas_sync_job_logs.rb +13 -0
  4. data/lib/canvas_sync.rb +43 -8
  5. data/lib/canvas_sync/api_syncable.rb +8 -0
  6. data/lib/canvas_sync/generators/templates/migrations/create_admins.rb +7 -4
  7. data/lib/canvas_sync/generators/templates/migrations/create_roles.rb +3 -4
  8. data/lib/canvas_sync/generators/templates/models/account.rb +2 -0
  9. data/lib/canvas_sync/generators/templates/models/admin.rb +4 -5
  10. data/lib/canvas_sync/generators/templates/models/role.rb +0 -4
  11. data/lib/canvas_sync/generators/templates/models/term.rb +0 -2
  12. data/lib/canvas_sync/importers/bulk_importer.rb +7 -4
  13. data/lib/canvas_sync/job.rb +24 -1
  14. data/lib/canvas_sync/jobs/fork_gather.rb +59 -0
  15. data/lib/canvas_sync/jobs/report_starter.rb +12 -1
  16. data/lib/canvas_sync/jobs/sync_admins_job.rb +9 -5
  17. data/lib/canvas_sync/jobs/sync_assignment_groups_job.rb +2 -8
  18. data/lib/canvas_sync/jobs/sync_assignments_job.rb +2 -8
  19. data/lib/canvas_sync/jobs/sync_context_module_items_job.rb +2 -8
  20. data/lib/canvas_sync/jobs/sync_context_modules_job.rb +2 -8
  21. data/lib/canvas_sync/jobs/sync_provisioning_report_job.rb +23 -11
  22. data/lib/canvas_sync/jobs/sync_roles_job.rb +8 -5
  23. data/lib/canvas_sync/jobs/sync_simple_table_job.rb +17 -7
  24. data/lib/canvas_sync/jobs/sync_submissions_job.rb +1 -5
  25. data/lib/canvas_sync/jobs/sync_terms_job.rb +8 -2
  26. data/lib/canvas_sync/jobs/sync_users_job.rb +5 -7
  27. data/lib/canvas_sync/processors/assignment_groups_processor.rb +3 -2
  28. data/lib/canvas_sync/processors/assignments_processor.rb +3 -2
  29. data/lib/canvas_sync/processors/context_module_items_processor.rb +3 -2
  30. data/lib/canvas_sync/processors/context_modules_processor.rb +3 -2
  31. data/lib/canvas_sync/processors/normal_processor.rb +2 -1
  32. data/lib/canvas_sync/processors/provisioning_report_processor.rb +7 -2
  33. data/lib/canvas_sync/processors/submissions_processor.rb +3 -2
  34. data/lib/canvas_sync/version.rb +1 -1
  35. data/spec/canvas_sync/canvas_sync_spec.rb +17 -0
  36. data/spec/canvas_sync/jobs/fork_gather_spec.rb +73 -0
  37. data/spec/canvas_sync/jobs/job_spec.rb +39 -5
  38. data/spec/canvas_sync/jobs/sync_admins_job_spec.rb +2 -1
  39. data/spec/canvas_sync/jobs/sync_assignment_groups_job_spec.rb +1 -1
  40. data/spec/canvas_sync/jobs/sync_assignments_job_spec.rb +2 -2
  41. data/spec/canvas_sync/jobs/sync_context_module_items_job_spec.rb +2 -2
  42. data/spec/canvas_sync/jobs/sync_context_modules_job_spec.rb +2 -2
  43. data/spec/canvas_sync/jobs/sync_provisioning_report_job_spec.rb +20 -11
  44. data/spec/canvas_sync/jobs/sync_roles_job_spec.rb +2 -1
  45. data/spec/canvas_sync/jobs/sync_simple_table_job_spec.rb +1 -0
  46. data/spec/canvas_sync/jobs/sync_submissions_job_spec.rb +1 -1
  47. data/spec/canvas_sync/jobs/sync_users_job_spec.rb +1 -1
  48. data/spec/canvas_sync/models/admins_spec.rb +3 -5
  49. data/spec/canvas_sync/models/roles_spec.rb +5 -5
  50. data/spec/canvas_sync/models/term_spec.rb +3 -3
  51. data/spec/dummy/app/models/account.rb +2 -0
  52. data/spec/dummy/app/models/admin.rb +4 -5
  53. data/spec/dummy/app/models/role.rb +0 -4
  54. data/spec/dummy/app/models/term.rb +0 -2
  55. data/spec/dummy/db/migrate/{20190702203628_create_roles.rb → 20190927204545_create_roles.rb} +3 -4
  56. data/spec/dummy/db/migrate/{20190702203629_create_admins.rb → 20190927204546_create_admins.rb} +7 -4
  57. data/spec/dummy/db/schema.rb +12 -9
  58. data/spec/dummy/db/test.sqlite3 +0 -0
  59. data/spec/dummy/log/development.log +1248 -0
  60. data/spec/dummy/log/test.log +43258 -0
  61. data/spec/factories/admin_factory.rb +0 -1
  62. data/spec/support/fake_canvas.rb +2 -2
  63. data/spec/support/fixtures/canvas_responses/roles.json +6 -0
  64. data/spec/support/fixtures/reports/provisioning_csv_unzipped/courses.csv +3 -0
  65. data/spec/support/fixtures/reports/provisioning_csv_unzipped/users.csv +4 -0
  66. metadata +25 -12
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
- SHA1:
3
- metadata.gz: f220a92d0d85cce6ff10b1ef1a10784039e93583
4
- data.tar.gz: 0c305e04af7875261f6c36561b1b39e073f41c41
2
+ SHA256:
3
+ metadata.gz: bff01b4de68a53faba86076d5390dd4c95d1feec6be899cc28a21ccf2c80a918
4
+ data.tar.gz: c676786d55e600ed58167c00290cd29e119b62b1bf7974c7ff4df46697759223
5
5
  SHA512:
6
- metadata.gz: ec6ceaf390ae8a814420d927b1294b66492bd855b62ef9d80042e973871aac7dfb45b061861d503b8ee5303371e67c3df1446be4b159f7f8991ec237684007ad
7
- data.tar.gz: 83c67e7bdd93c2ac6e068309955c04a08e8f6204481f667049cb3ac6dc8ba5212c31f5e25febde46a788bc5582d18c893cf421718f7604a2399623289e9ff422
6
+ metadata.gz: c7340c9158ecc077e4f2246c24c6b4a608229cc2568a74b25b2afe8d7bebfd42e79deb23b49d22a7d7842e9a7923caaa1a43843fc68fa04497901477d480e9a2
7
+ data.tar.gz: a4f9ca68735ccc0ceb42cc9f8b486dd18b02e508b7ece0242b1a56c49c33db7e426126f0cb3d7a5bd5ba1057ee69082fe66ecfad252cab0fcb5d16c9c3fc12f4
data/README.md CHANGED
@@ -91,6 +91,29 @@ This gem also helps with syncing and processing other reports if needed. In orde
91
91
  - Integrate your reports with the `ReportStarter`
92
92
  - Tell the gem what jobs to run
93
93
 
94
+ ### Extensible chain
95
+ It is sometimes desired to extend or customize the chain of jobs that are run with CanvasSync.
96
+ This can be achieved with the following pattern:
97
+
98
+ ```ruby
99
+ job_chain = CanvasSync.default_provisioning_report_chain(
100
+ %i[list of models to sync]
101
+ )
102
+
103
+ # CanvasSync forks the chain for each term within a term scope. The ForkGather Job can be used to unfork the chain.
104
+ # For example multiple Terms are in the term_scope. CanvasSync syncs Accounts, Terms, and Users (if enabled) and then
105
+ # forks the chain to sync other models (eg Course, Sections, etc.) per-Term.
106
+ # ForkGather will wait until all the forked chains are complete before continuing.
107
+ # TL;DR: Jobs placed after SyncProvisioningReportJob and before ForkGather will run once per Term per Sync;
108
+ # Jobs placed before SyncProvisioningReportJob or after ForkGather will run once per Sync
109
+ job_chain[:jobs] << { job: CanvasSync::Jobs::ForkGather, options: {} }
110
+
111
+ # Add a custom job to the end of the chain. Custom jobs must accept 2 arguments (job_chain, options) and call CanvasSync.invoke_next(job_chain) when complete
112
+ job_chain[:jobs] << { job: CanvasSyncCompleteWorker, options: { job_id: job.id } }
113
+
114
+ CanvasSync.invoke_next(job_chain)
115
+ ```
116
+
94
117
  ### Processor
95
118
 
96
119
  Your processor class must implement a `process` class method that receives a `report_file_path` and a hash of `options`. (See the `CanvasSync::Processors::ProvisioningReportProcessor` for an example.) The gem handles the work of enqueueing and downloading the report and then passes the file path to your class to process as needed. A simple example might be:
@@ -141,7 +164,7 @@ The `CanvasSync.process_jobs` method allows you to pass in a chain of jobs to ru
141
164
 
142
165
  Here is an example that runs our new report job first followed by the builtin provisioning job:
143
166
 
144
- ```
167
+ ```ruby
145
168
  job_chain = {
146
169
  jobs: [
147
170
  { job: MyReallyCoolReportJob, options: {} },
@@ -156,7 +179,7 @@ CanvasSync.process_jobs(job_chain)
156
179
  What if you've got some other job that you want run that doesn't deal with a report? No problem! Just make sure you call `CanvasSync.invoke_next` at the end of your job. Example:
157
180
 
158
181
 
159
- ```
182
+ ```ruby
160
183
  class SomeRandomJob < CanvasSync::Job
161
184
  def perform(job_chain, options)
162
185
  i_dunno_do_something!
@@ -375,6 +398,38 @@ Available config options (if you add more, please update this!):
375
398
 
376
399
  * `config.classes_to_only_log_errors_on` - use this if you are utilizing the `CanvasSync::JobLog` table, but want certain classes to only persist in the `job_logs` table if an error is encountered. This is useful if you've got a very frequently used job that's filling up your database, and only really care about tracking failures.
377
400
 
401
+ ## Handling Job errors
402
+
403
+ If you need custom handling for when a CanvasSync Job fails, you can add an `:on_failure` option to you Job Chain's `:global_options`.
404
+ The value should be a String in the following format: `ModuleOrClass::AnotherModuleOrClass.class_method`.
405
+ The given method of the given class will be called when an error occurs.
406
+ The handling method should accept 2 arguments: `[error, **options]`
407
+
408
+ The current parameters provided in `**options` are:
409
+ - `job_chain`
410
+ - `job_log`
411
+
412
+ Example:
413
+ ```ruby
414
+ class CanvasSyncStarterWorker
415
+ def perform
416
+ job_chain = CanvasSync.default_provisioning_report_chain(
417
+ %w[desired models],
418
+ options: {
419
+ global: {
420
+ on_failure: 'CanvasSyncStarterWorker.handle_canvas_sync_error',
421
+ }
422
+ }
423
+ )
424
+ CanvasSync.invoke_next(job_chain)
425
+ end
426
+
427
+ def self.handle_canvas_sync_error(error, **options)
428
+ # Do Stuff
429
+ end
430
+ end
431
+ ```
432
+
378
433
  ## Upgrading
379
434
 
380
435
  Re-running the generator when there's been a gem change will give you several choices if it detects conflicts between your local files and the updated generators. You can either view a diff or allow the generator to overwrite your local file. In most cases you may just want to add the code from the diff yourself so as not to break any of your customizations.
@@ -0,0 +1,13 @@
1
+ if Rails.version.to_f >= 5.0
2
+ class AddForkCountToCanvasSyncJobLogs < ActiveRecord::Migration[Rails.version.to_f]
3
+ def change
4
+ add_column :canvas_sync_job_logs, :fork_count, :integer
5
+ end
6
+ end
7
+ else
8
+ class AddForkCountToCanvasSyncJobLogs < ActiveRecord::Migration
9
+ def change
10
+ add_column :canvas_sync_job_logs, :fork_count, :integer
11
+ end
12
+ end
13
+ end
@@ -6,6 +6,7 @@ require "canvas_sync/sidekiq_job"
6
6
  require "canvas_sync/api_syncable"
7
7
  require "canvas_sync/jobs/report_starter"
8
8
  require "canvas_sync/jobs/report_checker"
9
+ require "canvas_sync/jobs/fork_gather"
9
10
  require "canvas_sync/jobs/report_processor_job"
10
11
  require "canvas_sync/jobs/sync_provisioning_report_job"
11
12
  require "canvas_sync/jobs/sync_simple_table_job.rb"
@@ -100,12 +101,16 @@ module CanvasSync
100
101
  invoke_next(job_chain)
101
102
  end
102
103
 
104
+ def duplicate_chain(job_chain)
105
+ Marshal.load(Marshal.dump(job_chain))
106
+ end
107
+
103
108
  # Invokes the next job in a chain of jobs.
104
109
  #
105
110
  # This should typically be called automatically by the gem where necessary.
106
111
  #
107
112
  # @param job_chain [Hash] A chain of jobs to execute
108
- def invoke_next(job_chain)
113
+ def invoke_next(job_chain, extra_options: {})
109
114
  return if job_chain[:jobs].empty?
110
115
 
111
116
  # Make sure all job classes are serialized as strings
@@ -115,7 +120,32 @@ module CanvasSync
115
120
  jobs = duped_job_chain[:jobs]
116
121
  next_job = jobs.shift
117
122
  next_job_class = next_job[:job].constantize
118
- next_job_class.perform_later(duped_job_chain, next_job[:options])
123
+ next_options = next_job[:options] || {}
124
+ next_options.merge!(extra_options)
125
+ next_job_class.perform_later(duped_job_chain, next_options)
126
+ end
127
+
128
+ def fork(job_log, job_chain, keys: [])
129
+ duped_job_chain = Marshal.load(Marshal.dump(job_chain))
130
+ duped_job_chain[:global_options][:fork_path] ||= []
131
+ duped_job_chain[:global_options][:fork_keys] ||= []
132
+ duped_job_chain[:global_options][:fork_path] << job_log.job_id
133
+ duped_job_chain[:global_options][:fork_keys] << ['canvas_term_id']
134
+ duped_job_chain[:global_options][:on_failure] ||= 'CanvasSync::Jobs::ForkGather.handle_branch_error'
135
+ sub_items = yield duped_job_chain
136
+ sub_count = sub_items.respond_to?(:count) ? sub_items.count : sub_items
137
+ job_log.fork_count = sub_count
138
+ sub_items
139
+ end
140
+
141
+ # Given a Model or Relation, scope it down to items that should be synced
142
+ def sync_scope(scope)
143
+ terms = %i[should_canvas_sync active_for_canvas_sync should_sync active_for_sync active]
144
+ terms.each do |t|
145
+ return scope.send(t) if scope.respond_to?(t)
146
+ end
147
+ Rails.logger.warn("Could not filter Syncable Scope for model '#{scope.try(:model)&.name || scope.name}'")
148
+ scope
119
149
  end
120
150
 
121
151
  # Syn any report to an specific set of models
@@ -159,7 +189,7 @@ module CanvasSync
159
189
  return unless models.present?
160
190
  models.map! &:to_s
161
191
  term_scope = term_scope.to_s if term_scope
162
- options = options.with_indifferent_access
192
+ options = options.deep_symbolize_keys!
163
193
 
164
194
  model_job_map = {
165
195
  terms: CanvasSync::Jobs::SyncTermsJob,
@@ -177,7 +207,7 @@ module CanvasSync
177
207
  jobs = []
178
208
  try_add_model_job = ->(model) {
179
209
  return unless models.include?(model)
180
- jobs.push(job: model_job_map[model].to_s, options: options[model] || {})
210
+ jobs.push(job: model_job_map[model].to_s, options: options[model.to_sym] || {})
181
211
  models -= [model]
182
212
  }
183
213
 
@@ -210,14 +240,19 @@ module CanvasSync
210
240
  post_provisioning_jobs = jobs
211
241
 
212
242
  jobs = pre_provisioning_jobs
213
- jobs += Array.wrap({
214
- job: CanvasSync::Jobs::SyncProvisioningReportJob.to_s,
215
- options: { term_scope: term_scope, models: models },
216
- }) if models.present?
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
217
251
  jobs += post_provisioning_jobs
218
252
 
219
253
  global_options = { legacy_support: legacy_support }
220
254
  global_options[:account_id] = account_id if account_id.present?
255
+ global_options.merge!(options[:global]) if options[:global].present?
221
256
 
222
257
  { jobs: jobs, global_options: global_options }
223
258
  end
@@ -21,6 +21,14 @@ module CanvasSync::ApiSyncable
21
21
  inst
22
22
  end
23
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
+
24
32
  def api_sync_options=(opts)
25
33
  @api_sync_options = opts
26
34
  end
@@ -5,13 +5,16 @@ class CreateAdmins < ActiveRecord::Migration[5.1]
5
5
  create_table :admins do |t|
6
6
  t.bigint :canvas_id, null: false
7
7
  t.string :role_name
8
- t.bigint :canvas_role_id, null: false
9
- t.json :user_data
10
- t.bigint :canvas_user_id, null: false
11
- t.string :workflow_state, null: false
8
+ t.bigint :canvas_account_id
9
+ t.bigint :canvas_role_id
10
+ t.bigint :canvas_user_id
11
+ t.string :workflow_state
12
12
 
13
13
  t.timestamps
14
14
  end
15
15
  add_index :admins, :canvas_id, unique: true
16
+ add_index :admins, :canvas_role_id
17
+ add_index :admins, :canvas_user_id
18
+ add_index :admins, :canvas_account_id
16
19
  end
17
20
  end
@@ -4,11 +4,10 @@ class CreateRoles < ActiveRecord::Migration[5.1]
4
4
  def change
5
5
  create_table :roles do |t|
6
6
  t.integer :canvas_id, null: false
7
- t.string :label, null: false
8
- t.string :base_role_type, null: false
9
- t.json :account
7
+ t.string :label
8
+ t.string :base_role_type
10
9
  t.integer :canvas_account_id
11
- t.string :workflow_state, null: false
10
+ t.string :workflow_state
12
11
  t.json :permissions
13
12
 
14
13
  t.timestamps
@@ -5,6 +5,8 @@ class Account < ApplicationRecord
5
5
 
6
6
  validates :canvas_id, uniqueness: true, presence: true
7
7
 
8
+ has_many :admins, primary_key: :canvas_id, foreign_key: :canvas_account_id
9
+
8
10
  api_syncable({
9
11
  name: :name,
10
12
  workflow_state: :workflow_state,
@@ -4,6 +4,7 @@ class Admin < ApplicationRecord
4
4
  include CanvasSync::ApiSyncable
5
5
 
6
6
  validates :canvas_id, uniqueness: true, presence: true
7
+ belongs_to :account, primary_key: :canvas_id, foreign_key: :canvas_account_id, optional: true
7
8
  belongs_to :user, primary_key: :canvas_id, foreign_key: :canvas_user_id, optional: true
8
9
  belongs_to :role, primary_key: :canvas_id, foreign_key: :canvas_role_id, optional: true
9
10
 
@@ -11,16 +12,14 @@ class Admin < ApplicationRecord
11
12
  canvas_id: :id,
12
13
  role_name: :role,
13
14
  canvas_role_id: :role_id,
14
- user_data: :user,
15
15
  canvas_user_id: ->(r) { r['user']['id'] },
16
+ # NOTICE: The :account_id field is added by CanvasSync - it is not included in the Canvas API response
17
+ canvas_account_id: :account_id,
16
18
  workflow_state: :workflow_state,
17
19
  }, -> (api) {
18
- admins = api.account_admins('self').all_pages!
20
+ admins = api.account_admins(canvas_account_id).all_pages!
19
21
  admin_data = admins.find{|admin| admin['id'] == canvas_id }
20
22
  raise Footrest::HttpError::NotFound unless admin_data.present?
21
23
  admin_data
22
24
  })
23
-
24
- def self.create_or_update(params); find_or_initialize_by(canvas_id: params['id']).update_from_api_params!(params); end
25
-
26
25
  end
@@ -10,12 +10,8 @@ class Role < ApplicationRecord
10
10
  canvas_id: :id,
11
11
  label: :label,
12
12
  base_role_type: :base_role_type,
13
- account: :account,
14
13
  canvas_account_id: ->(r) { r.dig('account', 'id') },
15
14
  permissions: :permissions,
16
15
  workflow_state: :workflow_state,
17
16
  }, -> (api) { api.get("/api/v1/accounts/#{canvas_account_id}/roles/#{canvas_id}") })
18
-
19
- def self.create_or_update(params); find_or_initialize_by(canvas_id: params['id']).update_from_api_params!(params); end
20
-
21
17
  end
@@ -21,8 +21,6 @@ class Term < ApplicationRecord
21
21
  term_data
22
22
  })
23
23
 
24
- def self.create_or_update(params); find_or_initialize_by(canvas_id: params['id']).update_from_api_params!(params); end
25
-
26
24
  # This is a sample scope created by the CanvasSync gem; feel
27
25
  # free to customize it for your tool's requirements.
28
26
  scope :active, -> {
@@ -58,13 +58,12 @@ module CanvasSync
58
58
  columns = columns.dup
59
59
 
60
60
  update_conditions = {
61
- condition: condition_sql(klass, columns),
61
+ condition: condition_sql(klass, columns, import_args[:sync_start_time]),
62
62
  columns: columns
63
63
  }
64
64
  update_conditions[:conflict_target] = conflict_target if conflict_target
65
65
 
66
66
  options = { validate: false, on_duplicate_key_update: update_conditions }.merge(import_args)
67
-
68
67
  options.delete(:on_duplicate_key_update) if options.key?(:on_duplicate_key_ignore)
69
68
  klass.import(columns, rows, options)
70
69
  end
@@ -79,10 +78,14 @@ module CanvasSync
79
78
  # started_at = Time.now
80
79
  # run_the_users_sync!
81
80
  # changed = User.where("updated_at >= ?", started_at)
82
- def self.condition_sql(klass, columns)
81
+ def self.condition_sql(klass, columns, report_start)
83
82
  columns_str = columns.map { |c| "#{klass.quoted_table_name}.#{c}" }.join(", ")
84
83
  excluded_str = columns.map { |c| "EXCLUDED.#{c}" }.join(", ")
85
- "(#{columns_str}) IS DISTINCT FROM (#{excluded_str})"
84
+ condition_sql = "(#{columns_str}) IS DISTINCT FROM (#{excluded_str})"
85
+ if klass.column_names.include?("updated_at") && report_start
86
+ condition_sql += " AND #{klass.quoted_table_name}.updated_at < '#{report_start}'"
87
+ end
88
+ condition_sql
86
89
  end
87
90
 
88
91
  def self.batch_size
@@ -13,13 +13,26 @@ module CanvasSync
13
13
  @job_log.started_at = Time.now
14
14
  @job_log.save
15
15
 
16
+ @job_chain = job.arguments[0] if job.arguments[0].is_a?(Hash) && job.arguments[0].include?(:jobs)
17
+
16
18
  begin
17
19
  block.call
18
20
  @job_log.status = JobLog::SUCCESS_STATUS
19
21
  rescue => e # rubocop:disable Style/RescueStandardError
20
22
  @job_log.exception = "#{e.class}: #{e.message}"
21
- @job_log.backtrace = e.backtrace
23
+ @job_log.backtrace = e.backtrace.join('\n')
22
24
  @job_log.status = JobLog::ERROR_STATUS
25
+ if @job_chain&.[](:global_options)&.[](:on_failure)&.present?
26
+ begin
27
+ class_name, method = @job_chain[:global_options][:on_failure].split('.')
28
+ klass = class_name.constantize
29
+ klass.send(method.to_sym, e, job_chain: @job_chain, job_log: @job_log)
30
+ rescue => e2
31
+ @job_log.backtrace += "\n\nError Occurred while handling an Error: #{e2.class}: #{e2.message}"
32
+ @job_log.backtrace += "\n" + e2.backtrace.join('\n')
33
+ Raven.captureException(e2) if defined? Raven
34
+ end
35
+ end
23
36
  raise e
24
37
  ensure
25
38
  if CanvasSync.config.classes_to_only_log_errors_on.include?(@job_log.job_class) && @job_log.status != JobLog::ERROR_STATUS
@@ -43,5 +56,15 @@ module CanvasSync
43
56
  status: JobLog::ENQUEUED_STATUS,
44
57
  )
45
58
  end
59
+
60
+ def update_or_create_model(model, params)
61
+ if model.respond_to? :create_or_update
62
+ model.create_or_update(params)
63
+ elsif model.method_defined? :update_from_api_params!
64
+ model.find_or_initialize_by(canvas_id: params['id']).update_from_api_params!(params)
65
+ else
66
+ raise "Could not create/update #{model.name}. It must have a create_or_update(params) ClassMethod or implement ApiSyncable."
67
+ end
68
+ end
46
69
  end
47
70
  end
@@ -0,0 +1,59 @@
1
+ module CanvasSync
2
+ module Jobs
3
+ class ForkGather < CanvasSync::Job
4
+ def perform(job_chain, options)
5
+ forked_job = self.class.forked_at_job(job_chain)
6
+
7
+ if forked_job.present?
8
+ forked_job.with_lock do
9
+ forked_job.fork_count -= 1
10
+ forked_job.save!
11
+ end
12
+
13
+ if forked_job.fork_count <= 0
14
+ (job_chain[:global_options][:fork_keys] || []).pop&.each do |k|
15
+ job_chain[:global_options].delete(k.to_sym)
16
+ end
17
+ CanvasSync.invoke_next(job_chain)
18
+ end
19
+ else
20
+ CanvasSync.invoke_next(job_chain)
21
+ end
22
+ end
23
+
24
+ def self.handle_branch_error(e, job_chain:, skip_invoke: false, **kwargs)
25
+ return nil unless job_chain&.[](:global_options)&.[](:fork_path).present?
26
+
27
+ duped_chain = CanvasSync.duplicate_chain(job_chain)
28
+ job_list = duped_chain[:jobs]
29
+ while job_list.count > 0
30
+ job_class_name = job_list[0][:job]
31
+ job_class = job_class_name.constantize
32
+ break if job_class <= CanvasSync::Jobs::ForkGather
33
+ job_list.shift
34
+ end
35
+
36
+ return nil unless job_list.present?
37
+
38
+ if skip_invoke
39
+ duped_chain
40
+ else
41
+ CanvasSync.invoke_next(duped_chain)
42
+ true
43
+ end
44
+ end
45
+
46
+ protected
47
+
48
+ def self.forked_at_job(job_chain)
49
+ fork_item = (job_chain[:global_options][:fork_path] || []).pop
50
+
51
+ if fork_item.present?
52
+ CanvasSync::JobLog.find_by(job_id: fork_item)
53
+ else
54
+ nil
55
+ end
56
+ end
57
+ end
58
+ end
59
+ end