Module: CanvasSync

Defined in:
lib/canvas_sync.rb,
lib/canvas_sync/job.rb,
lib/canvas_sync/config.rb,
lib/canvas_sync/engine.rb,
lib/canvas_sync/record.rb,
lib/canvas_sync/version.rb,
lib/canvas_sync/job_chain.rb,
lib/canvas_sync/sidekiq_job.rb,
app/models/canvas_sync/job_log.rb,
lib/canvas_sync/jobs/fork_gather.rb,
lib/canvas_sync/jobs/report_checker.rb,
lib/canvas_sync/jobs/report_starter.rb,
lib/canvas_sync/jobs/sync_roles_job.rb,
lib/canvas_sync/jobs/sync_terms_job.rb,
lib/canvas_sync/jobs/sync_admins_job.rb,
lib/canvas_sync/jobs/sync_accounts_job.rb,
lib/canvas_sync/class_callback_executor.rb,
lib/canvas_sync/importers/bulk_importer.rb,
lib/canvas_sync/importers/legacy_importer.rb,
lib/canvas_sync/jobs/report_processor_job.rb,
lib/canvas_sync/jobs/sync_assignments_job.rb,
lib/canvas_sync/jobs/sync_submissions_job.rb,
lib/canvas_sync/jobs/sync_simple_table_job.rb,
lib/canvas_sync/processors/normal_processor.rb,
lib/canvas_sync/processors/report_processor.rb,
lib/canvas_sync/generators/install_generator.rb,
lib/canvas_sync/jobs/sync_context_modules_job.rb,
lib/canvas_sync/jobs/sync_assignment_groups_job.rb,
lib/canvas_sync/processors/assignments_processor.rb,
lib/canvas_sync/processors/submissions_processor.rb,
lib/canvas_sync/jobs/sync_provisioning_report_job.rb,
lib/canvas_sync/jobs/sync_context_module_items_job.rb,
lib/canvas_sync/processors/context_modules_processor.rb,
lib/canvas_sync/processors/assignment_groups_processor.rb,
lib/canvas_sync/generators/install_live_events_generator.rb,
lib/canvas_sync/processors/provisioning_report_processor.rb,
lib/canvas_sync/processors/context_module_items_processor.rb

Defined Under Namespace

Modules: ApiSyncable, Concerns, Importers, Jobs, Processors, Record, Sidekiq Classes: ClassCallbackExecutor, Config, Engine, InstallGenerator, InstallLiveEventsGenerator, Job, JobChain, JobLog

Constant Summary collapse

SUPPORTED_MODELS =
%w[
  users
  pseudonyms
  courses
  groups
  group_memberships
  accounts
  terms
  enrollments
  sections
  assignments
  submissions
  roles
  admins
  assignment_groups
  context_modules
  context_module_items
  xlist
].freeze
SUPPORTED_LIVE_EVENTS =
%w[
  course
  enrollment
  submission
  assignment
  user
  syllabus
  grade
  module
  module_item
  course_section
].freeze
SUPPORTED_NON_PROV_REPORTS =
%w[
  graded_submissions
].freeze
VERSION =
"0.15.0".freeze

Class Method Summary collapse

Class Method Details

.configObject

Returns the CanvasSync config



336
337
338
# File 'lib/canvas_sync.rb', line 336

def config
  @config ||= CanvasSync::Config.new
end

.configure {|config| ... } ⇒ Object

Configure options for CanvasSync. See config.rb for valid configuration options.

Example:

CanvasSync.configure do |config|

config.classes_to_only_log_errors_on << "Blah"

end

Yields:



330
331
332
333
# File 'lib/canvas_sync.rb', line 330

def configure
  yield config
  config
end

.default_provisioning_report_chain(models, term_scope = nil, legacy_support = false, account_id = nil, options: {}) ⇒ Hash

Syncs terms, users/roles/admins if necessary, then the rest of the specified models.

Parameters:

  • models (Array<String>)
  • term_scope (String) (defaults to: nil)
  • legacy_support (Boolean, false) (defaults to: false)

    This enables legacy_support, where rows are not bulk inserted. For this to work your models must have a ‘create_or_udpate_from_csv` class method that takes a row and inserts it into the database.

  • account_id (Integer, nil) (defaults to: nil)

    This optional parameter can be used if your Term creation and canvas_sync_client methods require an account ID.

Returns:

  • (Hash)


190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
# File 'lib/canvas_sync.rb', line 190

def default_provisioning_report_chain(models, term_scope=nil, legacy_support=false, =nil, options: {}) # rubocop:disable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity, Metrics/LineLength
  return unless models.present?
  models.map! &:to_s
  term_scope = term_scope.to_s if term_scope
  options = options.deep_symbolize_keys!

  model_job_map = {
    terms: CanvasSync::Jobs::SyncTermsJob,
    accounts: CanvasSync::Jobs::SyncAccountsJob,
    roles: CanvasSync::Jobs::SyncRolesJob,
    admins: CanvasSync::Jobs::SyncAdminsJob,

    assignments: CanvasSync::Jobs::SyncAssignmentsJob,
    submissions: CanvasSync::Jobs::SyncSubmissionsJob,
    assignment_groups: CanvasSync::Jobs::SyncAssignmentGroupsJob,
    context_modules: CanvasSync::Jobs::SyncContextModulesJob,
    context_module_items: CanvasSync::Jobs::SyncContextModuleItemsJob,
  }.with_indifferent_access

  jobs = []
  try_add_model_job = ->(model) {
    return unless models.include?(model)
    jobs.push(job: model_job_map[model].to_s, options: options[model.to_sym] || {})
    models -= [model]
  }

  ##############################
  # Pre provisioning report jobs
  ##############################

  # Always sync Terms first
  models.unshift('terms') unless models.include?('terms')
  try_add_model_job.call('terms')

  # Accounts, users, roles, and admins are synced before provisioning because they cannot be scoped to term
  try_add_model_job.call('accounts')

  # These Models use the provisioning report, but are not term-scoped,
  # so we sync them before to ensure work is not duplicated
  if term_scope.present?
    models -= (first_provisioning_models = models & ['users', 'pseudonyms'])
    jobs.concat(
      generate_provisioning_jobs(first_provisioning_models, options)
    )
  end

  try_add_model_job.call('roles')
  try_add_model_job.call('admins')
  pre_provisioning_jobs = jobs

  ###############################
  # Post provisioning report jobs
  ###############################

  jobs = []
  try_add_model_job.call('assignments')
  try_add_model_job.call('submissions')
  try_add_model_job.call('assignment_groups')
  try_add_model_job.call('context_modules')
  try_add_model_job.call('context_module_items')
  post_provisioning_jobs = jobs

  ###############################
  # Main provisioning job and queueing
  ###############################

  jobs = [
    *pre_provisioning_jobs,
    *generate_provisioning_jobs(models, options, job_options: { term_scope: term_scope }, only_split: ['users']),
    *post_provisioning_jobs,
  ]

  global_options = { legacy_support: legacy_support }
  global_options[:account_id] =  if .present?
  global_options.merge!(options[:global]) if options[:global].present?

  JobChain.new(jobs: jobs, global_options: global_options)
end

.duplicate_chain(job_chain) ⇒ Object



102
103
104
# File 'lib/canvas_sync.rb', line 102

def duplicate_chain(job_chain)
  Marshal.load(Marshal.dump(job_chain))
end

.fork(job_log, job_chain, keys: []) ⇒ Object



128
129
130
131
132
133
134
135
136
137
138
139
140
141
# File 'lib/canvas_sync.rb', line 128

def fork(job_log, job_chain, keys: [])
  job_chain = job_chain.chain_data if job_chain.is_a?(JobChain)

  duped_job_chain = Marshal.load(Marshal.dump(job_chain))
  duped_job_chain[:global_options][:fork_path] ||= []
  duped_job_chain[:global_options][:fork_keys] ||= []
  duped_job_chain[:global_options][:fork_path] << job_log.job_id
  duped_job_chain[:global_options][:fork_keys] << ['canvas_term_id']
  duped_job_chain[:global_options][:on_failure] ||= 'CanvasSync::Jobs::ForkGather.handle_branch_error'
  sub_items = yield duped_job_chain
  sub_count = sub_items.respond_to?(:count) ? sub_items.count : sub_items
  job_log.fork_count = sub_count
  sub_items
end

.generate_provisioning_jobs(model_list, options_hash, job_options: {}, only_split: nil, default_key: :provisioning) ⇒ Object



290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
# File 'lib/canvas_sync.rb', line 290

def generate_provisioning_jobs(model_list, options_hash, job_options: {}, only_split: nil, default_key: :provisioning)
  # Group the model options as best we can.
  # This is mainly for backwards compatibility, since 'users' was previously it's own job
  unique_option_models = group_by_job_options(
    model_list,
    options_hash,
    only_split: only_split,
    default_key: default_key,
  )

  unique_option_models.map do |mopts, models|
    opts = { models: models }
    opts.merge!(job_options)
    opts.merge!(mopts) if mopts.present?
    {
      job: CanvasSync::Jobs::SyncProvisioningReportJob.to_s,
      options: opts,
    }
  end
end

.get_canvas_sync_client(options) ⇒ Object

Calls the canvas_sync_client in your app. If you have specified an account ID when starting the job it will pass the account ID to your canvas_sync_client method.

Parameters:

  • options (Hash)


315
316
317
318
319
320
321
# File 'lib/canvas_sync.rb', line 315

def get_canvas_sync_client(options)
  if options[:account_id]
    canvas_sync_client(options[:account_id])
  else
    canvas_sync_client
  end
end

.group_by_job_options(model_list, options_hash, only_split: nil, default_key: :provisioning) ⇒ Object



269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
# File 'lib/canvas_sync.rb', line 269

def group_by_job_options(model_list, options_hash, only_split: nil, default_key: :provisioning)
  dup_models = [ *model_list ]
  unique_option_models = {}

  filtered_models = only_split ? (only_split & model_list) : model_list
  filtered_models.each do |m|
    mopts = options_hash[m.to_sym] || options_hash[default_key]
    unique_option_models[mopts] ||= []
    unique_option_models[mopts] << m
    dup_models.delete(m)
  end

  if dup_models.present?
    mopts = options_hash[default_key]
    unique_option_models[mopts] ||= []
    unique_option_models[mopts].concat(dup_models)
  end

  unique_option_models
end

.invoke_next(job_chain, extra_options: {}) ⇒ Object

Invokes the next job in a chain of jobs.

This should typically be called automatically by the gem where necessary.

Parameters:

  • job_chain (Hash)

    A chain of jobs to execute



111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
# File 'lib/canvas_sync.rb', line 111

def invoke_next(job_chain, extra_options: {})
  job_chain = job_chain.chain_data if job_chain.is_a?(JobChain)

  return if job_chain[:jobs].empty?

  # Make sure all job classes are serialized as strings
  job_chain[:jobs].each { |job| job[:job] = job[:job].to_s }

  duped_job_chain = Marshal.load(Marshal.dump(job_chain))
  jobs = duped_job_chain[:jobs]
  next_job = jobs.shift
  next_job_class = next_job[:job].constantize
  next_options = next_job[:options] || {}
  next_options.merge!(extra_options)
  next_job_class.perform_later(duped_job_chain, next_options)
end

.process_jobs(job_chain) ⇒ Object

Runs a chain of ordered jobs

See the README for usage and examples

Parameters:

  • job_chain (Hash)


98
99
100
# File 'lib/canvas_sync.rb', line 98

def process_jobs(job_chain)
  invoke_next(job_chain)
end

.provisioning_sync(models, term_scope: nil, legacy_support: false, account_id: nil) ⇒ Object

Runs a standard provisioning sync job with no extra report types. Terms will be synced first using the API. If you are syncing users/roles/admins and have also specified a Term scope, Users/Roles/Admins will by synced first, before every other model (as Users/Roles/Admins are never scoped to Term).

Parameters:

  • models (Array<String>)

    A list of models to sync. e.g., [‘users’, ‘courses’]. must be one of SUPPORTED_MODELS

  • term_scope (Symbol, nil) (defaults to: nil)

    An optional symbol representing a scope that exists on the Term model. The provisioning report will be run for each of the terms contained in that scope.

  • legacy_support (Boolean | Array<String>, false) (defaults to: false)

    This enables legacy_support, where rows are not bulk inserted. For this to work your models must have a ‘create_or_udpate_from_csv` class method that takes a row and inserts it into the database. If an array of model names is provided then only those models will use legacy support.

  • account_id (Integer, nil) (defaults to: nil)

    This optional parameter can be used if your Term creation and canvas_sync_client methods require an account ID.



75
76
77
78
# File 'lib/canvas_sync.rb', line 75

def provisioning_sync(models, term_scope: nil, legacy_support: false, account_id: nil)
  validate_models!(models)
  invoke_next(default_provisioning_report_chain(models, term_scope, legacy_support, ))
end

.simple_report_chain(reports_mapping, term_scope = nil, account_id = nil) ⇒ Object

Syn any report to an specific set of models

Parameters:

  • reports_mapping (Array<Hash>)

    List of reports with their specific model and params

  • term_scope (String) (defaults to: nil)
  • account_id (Integer, nil) (defaults to: nil)

    This optional parameter can be used if your Term creation and canvas_sync_client methods require an account ID.



159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
# File 'lib/canvas_sync.rb', line 159

def simple_report_chain(reports_mapping, term_scope=nil, =nil)
  jobs = reports_mapping.map do |report|
    {
      job: CanvasSync::Jobs::SyncSimpleTableJob.to_s,
      options: {
        report_name: report[:report_name],
        model: report[:model],
        mapping: report[:model],
        klass: report[:model].singularize.capitalize.to_s,
        term_scope: term_scope,
        params: report[:params]
      }
    }
  end

  global_options = {}
  global_options[:account_id] =  if .present?

  JobChain.new(jobs: jobs, global_options: global_options)
end

.simple_report_sync(reports_mapping, term_scope: nil, account_id: nil) ⇒ Object

Runs a report different from provisioning sync job with no extra report types.

Parameters:

  • reports_mapping (Array<Hash>)

    An Array of hash that list the model and params with their report you want to import:

    {model: ‘submissions’, report_name: ‘my_report_name_csv’, params: { “parameters” => true } }, …
  • term_scope (Symbol, nil) (defaults to: nil)

    An optional symbol representing a scope that exists on the Term model. The provisioning report will be run for each of the terms contained in that scope.

  • account_id (Integer, nil) (defaults to: nil)

    This optional parameter can be used if your Term creation and canvas_sync_client methods require an account ID.



89
90
91
# File 'lib/canvas_sync.rb', line 89

def simple_report_sync(reports_mapping, term_scope: nil, account_id: nil)
  invoke_next(simple_report_chain(reports_mapping, term_scope, ))
end

.sync_scope(scope) ⇒ Object

Given a Model or Relation, scope it down to items that should be synced



144
145
146
147
148
149
150
151
# File 'lib/canvas_sync.rb', line 144

def sync_scope(scope)
  terms = %i[should_canvas_sync active_for_canvas_sync should_sync active_for_sync active]
  terms.each do |t|
    return scope.send(t) if scope.respond_to?(t)
  end
  Rails.logger.warn("Could not filter Syncable Scope for model '#{scope.try(:model)&.name || scope.name}'")
  scope
end

.validate_live_events!(events) ⇒ Object



346
347
348
349
350
# File 'lib/canvas_sync.rb', line 346

def validate_live_events!(events)
  invalid = events - SUPPORTED_LIVE_EVENTS
  return if invalid.empty?
  raise "Invalid live event(s) specified: #{invalid.join(', ')}. Only #{SUPPORTED_LIVE_EVENTS.join(', ')} are supported."
end

.validate_models!(models) ⇒ Object



340
341
342
343
344
# File 'lib/canvas_sync.rb', line 340

def validate_models!(models)
  invalid = models - SUPPORTED_MODELS
  return if invalid.empty?
  raise "Invalid model(s) specified: #{invalid.join(', ')}. Only #{SUPPORTED_MODELS.join(', ')} are supported."
end