canvas_sync 0.1.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/README.md +178 -0
- data/Rakefile +23 -0
- data/lib/canvas_sync.rb +96 -0
- data/lib/canvas_sync/generators/install_generator.rb +54 -0
- data/lib/canvas_sync/generators/templates/course.rb +8 -0
- data/lib/canvas_sync/generators/templates/create_courses.rb +21 -0
- data/lib/canvas_sync/generators/templates/create_enrollments.rb +26 -0
- data/lib/canvas_sync/generators/templates/create_sections.rb +20 -0
- data/lib/canvas_sync/generators/templates/create_terms.rb +18 -0
- data/lib/canvas_sync/generators/templates/create_users.rb +18 -0
- data/lib/canvas_sync/generators/templates/enrollment.rb +8 -0
- data/lib/canvas_sync/generators/templates/section.rb +7 -0
- data/lib/canvas_sync/generators/templates/term.rb +28 -0
- data/lib/canvas_sync/generators/templates/user.rb +6 -0
- data/lib/canvas_sync/importers/bulk_importer.rb +54 -0
- data/lib/canvas_sync/jobs/application_job.rb +25 -0
- data/lib/canvas_sync/jobs/report_checker.rb +48 -0
- data/lib/canvas_sync/jobs/report_processor_job.rb +36 -0
- data/lib/canvas_sync/jobs/report_starter.rb +29 -0
- data/lib/canvas_sync/jobs/sync_provisioning_report_job.rb +58 -0
- data/lib/canvas_sync/jobs/sync_terms_job.rb +23 -0
- data/lib/canvas_sync/jobs/sync_users_job.rb +32 -0
- data/lib/canvas_sync/processors/provisioning_report_processor.rb +118 -0
- data/lib/canvas_sync/version.rb +3 -0
- data/spec/canvas_sync/canvas_sync_spec.rb +60 -0
- data/spec/canvas_sync/jobs/report_checker_spec.rb +62 -0
- data/spec/canvas_sync/jobs/report_processor_job_spec.rb +30 -0
- data/spec/canvas_sync/jobs/report_starter_spec.rb +27 -0
- data/spec/canvas_sync/jobs/sync_provisioning_report_job_spec.rb +81 -0
- data/spec/canvas_sync/jobs/sync_terms_job_spec.rb +18 -0
- data/spec/canvas_sync/jobs/sync_users_job_spec.rb +18 -0
- data/spec/canvas_sync/models/course_spec.rb +30 -0
- data/spec/canvas_sync/models/enrollment_spec.rb +30 -0
- data/spec/canvas_sync/models/section_spec.rb +24 -0
- data/spec/canvas_sync/models/term_spec.rb +71 -0
- data/spec/canvas_sync/models/user_spec.rb +18 -0
- data/spec/canvas_sync/processors/provisioning_report_processor_spec.rb +41 -0
- data/spec/dummy/README.rdoc +1 -0
- data/spec/dummy/Rakefile +6 -0
- data/spec/dummy/app/models/application_record.rb +3 -0
- data/spec/dummy/app/models/course.rb +14 -0
- data/spec/dummy/app/models/enrollment.rb +14 -0
- data/spec/dummy/app/models/section.rb +13 -0
- data/spec/dummy/app/models/term.rb +34 -0
- data/spec/dummy/app/models/user.rb +12 -0
- data/spec/dummy/bin/rails +4 -0
- data/spec/dummy/config.ru +4 -0
- data/spec/dummy/config/application.rb +26 -0
- data/spec/dummy/config/boot.rb +5 -0
- data/spec/dummy/config/database.yml +25 -0
- data/spec/dummy/config/environment.rb +5 -0
- data/spec/dummy/config/environments/development.rb +41 -0
- data/spec/dummy/config/environments/test.rb +42 -0
- data/spec/dummy/config/initializers/assets.rb +11 -0
- data/spec/dummy/config/initializers/session_store.rb +3 -0
- data/spec/dummy/config/initializers/wrap_parameters.rb +14 -0
- data/spec/dummy/config/routes.rb +2 -0
- data/spec/dummy/config/secrets.yml +22 -0
- data/spec/dummy/db/development.sqlite3 +0 -0
- data/spec/dummy/db/migrate/20170831220702_create_courses.rb +27 -0
- data/spec/dummy/db/migrate/20170831221129_create_users.rb +24 -0
- data/spec/dummy/db/migrate/20170905192509_create_enrollments.rb +32 -0
- data/spec/dummy/db/migrate/20170906193506_create_terms.rb +24 -0
- data/spec/dummy/db/migrate/20170906203438_create_sections.rb +26 -0
- data/spec/dummy/db/schema.rb +88 -0
- data/spec/dummy/db/test.sqlite3 +0 -0
- data/spec/dummy/log/development.log +828 -0
- data/spec/dummy/log/test.log +14582 -0
- data/spec/factories/course_factory.rb +10 -0
- data/spec/factories/enrollment_factory.rb +5 -0
- data/spec/factories/section_factory.rb +5 -0
- data/spec/factories/term_factory.rb +10 -0
- data/spec/factories/user_factory.rb +9 -0
- data/spec/spec_helper.rb +46 -0
- data/spec/support/fake_canvas.rb +22 -0
- data/spec/support/fixtures/canvas_responses/terms.json +64 -0
- data/spec/support/fixtures/reports/courses.csv +3 -0
- data/spec/support/fixtures/reports/enrollments.csv +3 -0
- data/spec/support/fixtures/reports/provisioning_csv +0 -0
- data/spec/support/fixtures/reports/provisioning_csv_unzipped/courses.csv +3 -0
- data/spec/support/fixtures/reports/provisioning_csv_unzipped/users.csv +4 -0
- data/spec/support/fixtures/reports/sections.csv +3 -0
- data/spec/support/fixtures/reports/users.csv +4 -0
- metadata +423 -0
@@ -0,0 +1,18 @@
|
|
1
|
+
<%= autogenerated_migration_warning %>
|
2
|
+
|
3
|
+
class CreateUsers < ActiveRecord::Migration[5.1]
|
4
|
+
def change
|
5
|
+
create_table :users do |t|
|
6
|
+
t.bigint :canvas_user_id, null: false
|
7
|
+
t.string :sis_id
|
8
|
+
t.string :email
|
9
|
+
t.string :first_name
|
10
|
+
t.string :last_name
|
11
|
+
t.string :status
|
12
|
+
|
13
|
+
t.timestamps
|
14
|
+
end
|
15
|
+
|
16
|
+
add_index :users, :canvas_user_id, unique: true
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,8 @@
|
|
1
|
+
<%= autogenerated_model_warning %>
|
2
|
+
|
3
|
+
class Enrollment < ApplicationRecord
|
4
|
+
validates :canvas_enrollment_id, uniqueness: true, presence: true
|
5
|
+
belongs_to :user, primary_key: :canvas_user_id, foreign_key: :canvas_user_id, optional: true
|
6
|
+
belongs_to :course, primary_key: :canvas_course_id, foreign_key: :canvas_course_id, optional: true
|
7
|
+
belongs_to :section, primary_key: :canvas_section_id, foreign_key: :canvas_section_id, optional: true
|
8
|
+
end
|
@@ -0,0 +1,7 @@
|
|
1
|
+
<%= autogenerated_model_warning %>
|
2
|
+
|
3
|
+
class Section < ApplicationRecord
|
4
|
+
validates :canvas_section_id, uniqueness: true, presence: true
|
5
|
+
belongs_to :course, primary_key: :canvas_course_id, foreign_key: :canvas_course_id, optional: true
|
6
|
+
has_many :enrollments, primary_key: :canvas_section_id, foreign_key: :canvas_section_id
|
7
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
<%= autogenerated_model_warning %>
|
2
|
+
|
3
|
+
class Term < ApplicationRecord
|
4
|
+
validates :canvas_term_id, uniqueness: true, presence: true
|
5
|
+
has_many :courses, foreign_key: :canvas_term_id, primary_key: :canvas_term_id
|
6
|
+
|
7
|
+
# This is a sample scope created by the CanvasSync gem; feel
|
8
|
+
# free to customize it for your tool's requirements.
|
9
|
+
scope :active, -> {
|
10
|
+
where(workflow_state: 'active')
|
11
|
+
.where("start_at <= ? OR start_at IS NULL", 15.days.from_now)
|
12
|
+
.where("end_at >= ? OR end_at IS NULL", 15.days.ago)
|
13
|
+
}
|
14
|
+
|
15
|
+
def self.create_or_update(term_params)
|
16
|
+
term = Term.find_or_initialize_by(canvas_term_id: term_params['id'])
|
17
|
+
|
18
|
+
term.assign_attributes(name: term_params['name'],
|
19
|
+
start_at: term_params['start_at'],
|
20
|
+
end_at: term_params['end_at'],
|
21
|
+
workflow_state: term_params['workflow_state'],
|
22
|
+
grading_period_group_id: term_params['grading_period_group_id'],
|
23
|
+
sis_id: term_params['sis_term_id'])
|
24
|
+
|
25
|
+
term.save! if term.changed?
|
26
|
+
term
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,54 @@
|
|
1
|
+
module CanvasSync
|
2
|
+
module Importers
|
3
|
+
class BulkImporter
|
4
|
+
# The batch import size can be customized by setting
|
5
|
+
# the 'BULK_IMPORTER_BATCH_SIZE' environment variable
|
6
|
+
DEFAULT_BATCH_SIZE = 10_000
|
7
|
+
|
8
|
+
# Does a bulk import of a set of models using the activerecord-import gem.
|
9
|
+
#
|
10
|
+
# @param report_file_path [String] path to the report CSV
|
11
|
+
# @param mapping [Hash] a hash of the values to import. The keys are the CSV column names and
|
12
|
+
# the values are the database column names. {CanvasSync::Processors::ProvisioningReportProcessor::USERS_CSV_MAPPING Example}
|
13
|
+
# @param klass [Object] e.g., User
|
14
|
+
# @param conflict_target [Symbol] represents the database column that will determine if we need to update
|
15
|
+
# or insert a given row. e.g.,: canvas_user_id
|
16
|
+
# @yieldparam [Array] row if a block is passed in it will yield the current row from the CSV.
|
17
|
+
# This can be used if you need to filter or massage the data in any way.
|
18
|
+
def self.import(report_file_path, mapping, klass, conflict_target)
|
19
|
+
csv_column_names = mapping.keys
|
20
|
+
database_column_names = mapping.values
|
21
|
+
rows = []
|
22
|
+
|
23
|
+
CSV.foreach(report_file_path, headers: true, header_converters: :symbol) do |row|
|
24
|
+
row = yield(row) if block_given?
|
25
|
+
next if row.nil?
|
26
|
+
rows << csv_column_names.map { |column| row[column] }
|
27
|
+
|
28
|
+
if rows.length >= batch_size
|
29
|
+
perform_import(klass, database_column_names, rows, conflict_target)
|
30
|
+
rows = []
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
perform_import(klass, database_column_names, rows, conflict_target)
|
35
|
+
end
|
36
|
+
|
37
|
+
private
|
38
|
+
|
39
|
+
def self.perform_import(klass, database_column_names, rows, conflict_target)
|
40
|
+
return if rows.length == 0
|
41
|
+
database_column_names = database_column_names.dup
|
42
|
+
klass.import(database_column_names, rows, validate: false, on_duplicate_key_update: {
|
43
|
+
conflict_target: conflict_target,
|
44
|
+
columns: database_column_names
|
45
|
+
})
|
46
|
+
end
|
47
|
+
|
48
|
+
def self.batch_size
|
49
|
+
batch_size = ENV['BULK_IMPORTER_BATCH_SIZE'].to_i
|
50
|
+
batch_size > 0 ? batch_size : DEFAULT_BATCH_SIZE
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
require "active_job"
|
2
|
+
|
3
|
+
module CanvasSync
|
4
|
+
module Jobs
|
5
|
+
# Inherit from this class to build a Job that integrates
|
6
|
+
# well with this gem.
|
7
|
+
class ApplicationJob < ActiveJob::Base
|
8
|
+
# Generates a Bearcat::Client used for Canvas API calls
|
9
|
+
#
|
10
|
+
# @param canvas_base_url [String]
|
11
|
+
# @param canvas_api_token [String]
|
12
|
+
# @return [Bearcat::Client]
|
13
|
+
def canvas_client(canvas_base_url, canvas_api_token)
|
14
|
+
Bearcat::Client.new(token: canvas_api_token, prefix: canvas_base_url)
|
15
|
+
end
|
16
|
+
|
17
|
+
# Returns the amount of time to wait between report status checks
|
18
|
+
#
|
19
|
+
# @return [Integer]
|
20
|
+
def report_checker_wait_time
|
21
|
+
Rails.env.development? ? 1.second : 30.seconds
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,48 @@
|
|
1
|
+
module CanvasSync
|
2
|
+
module Jobs
|
3
|
+
# ActiveJob class used to check the status of a pending Canvas report.
|
4
|
+
# Re-enqueues itself if the report is still processing on Canvas.
|
5
|
+
# Enqueues the ReportProcessor when the report has completed.
|
6
|
+
class ReportChecker < ApplicationJob
|
7
|
+
# @param canvas_base_url [String]
|
8
|
+
# @param canvas_api_token [String]
|
9
|
+
# @param job_chain [Hash]
|
10
|
+
# @param report_name [Hash] e.g., 'provisioning_csv'
|
11
|
+
# @param report_id [Integer]
|
12
|
+
# @param processor [String] a stringified report processor class name
|
13
|
+
# @param options [Hash] hash of options that will be passed to the job processor
|
14
|
+
# @return [nil]
|
15
|
+
def perform(canvas_base_url, canvas_api_token, job_chain, report_name, report_id, processor, options)
|
16
|
+
report_status = canvas_client(canvas_base_url, canvas_api_token).report_status('self', report_name, report_id)
|
17
|
+
|
18
|
+
case report_status['status'].downcase
|
19
|
+
when 'complete'
|
20
|
+
CanvasSync::Jobs::ReportProcessorJob.perform_later(
|
21
|
+
canvas_base_url,
|
22
|
+
canvas_api_token,
|
23
|
+
job_chain,
|
24
|
+
report_name,
|
25
|
+
report_status['attachment']['url'],
|
26
|
+
processor,
|
27
|
+
options
|
28
|
+
)
|
29
|
+
when 'error', 'deleted'
|
30
|
+
message = "Report failed to process; status was #{report_status} for report_name: #{report_name}, report_id: #{report_id}"
|
31
|
+
Rails.logger.error(message)
|
32
|
+
raise message
|
33
|
+
else
|
34
|
+
CanvasSync::Jobs::ReportChecker.set(wait: report_checker_wait_time)
|
35
|
+
.perform_later(
|
36
|
+
canvas_base_url,
|
37
|
+
canvas_api_token,
|
38
|
+
job_chain,
|
39
|
+
report_name,
|
40
|
+
report_id,
|
41
|
+
processor,
|
42
|
+
options
|
43
|
+
)
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
require 'open-uri'
|
2
|
+
|
3
|
+
module CanvasSync
|
4
|
+
module Jobs
|
5
|
+
# ActiveJob class that wraps around a report processor. This job will
|
6
|
+
# download the report, and then pass the file path and options into the
|
7
|
+
# process method on the processor.
|
8
|
+
class ReportProcessorJob < ApplicationJob
|
9
|
+
# @param canvas_base_url [String]
|
10
|
+
# @param canvas_api_token [String]
|
11
|
+
# @param job_chain [Hash]
|
12
|
+
# @param report_name [Hash] e.g., 'provisioning_csv'
|
13
|
+
# @param report_url [String]
|
14
|
+
# @param processor [String] a stringified report processor class name
|
15
|
+
# @param options [Hash] hash of options that will be passed to the job processor
|
16
|
+
# @return [nil]
|
17
|
+
def perform(canvas_base_url, canvas_api_token, job_chain, report_name, report_url, processor, options)
|
18
|
+
download(report_name, report_url) do |file_path|
|
19
|
+
processor.constantize.process(file_path, options)
|
20
|
+
end
|
21
|
+
|
22
|
+
CanvasSync.invoke_next(canvas_base_url, canvas_api_token, job_chain)
|
23
|
+
end
|
24
|
+
|
25
|
+
private
|
26
|
+
|
27
|
+
def download(report_name, report_url)
|
28
|
+
Dir.mktmpdir do |dir|
|
29
|
+
file_path = "#{dir}/#{report_name}"
|
30
|
+
IO.copy_stream(open(report_url), file_path)
|
31
|
+
yield file_path
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
module CanvasSync
|
2
|
+
module Jobs
|
3
|
+
# Starts a Canvas report and enqueues a ReportChecker
|
4
|
+
class ReportStarter < ApplicationJob
|
5
|
+
# @param canvas_base_url [String]
|
6
|
+
# @param canvas_api_token [String]
|
7
|
+
# @param job_chain [Hash]
|
8
|
+
# @param report_name [Hash] e.g., 'provisioning_csv'
|
9
|
+
# @param report_params [Hash] The Canvas report parameters
|
10
|
+
# @param processor [String] a stringified report processor class name
|
11
|
+
# @param options [Hash] hash of options that will be passed to the job processor
|
12
|
+
# @return [nil]
|
13
|
+
def perform(canvas_base_url, canvas_api_token, job_chain, report_name, report_params, processor, options)
|
14
|
+
client = canvas_client(canvas_base_url, canvas_api_token)
|
15
|
+
report = client.start_report('self', report_name, report_params)
|
16
|
+
|
17
|
+
CanvasSync::Jobs::ReportChecker.set(wait: report_checker_wait_time).perform_later(
|
18
|
+
canvas_base_url,
|
19
|
+
canvas_api_token,
|
20
|
+
job_chain,
|
21
|
+
report_name,
|
22
|
+
report['id'],
|
23
|
+
processor,
|
24
|
+
options
|
25
|
+
)
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,58 @@
|
|
1
|
+
module CanvasSync
|
2
|
+
module Jobs
|
3
|
+
# ActiveJob class that starts a Canvas provisioning report
|
4
|
+
class SyncProvisioningReportJob < ApplicationJob
|
5
|
+
# @param canvas_base_url [String]
|
6
|
+
# @param canvas_api_token [String]
|
7
|
+
# @param job_chain [Hash]
|
8
|
+
# @param options [Hash] If options contains a :term_scope a seperate provisioning report
|
9
|
+
# will be started for each term in that scope. :models should be an array of
|
10
|
+
# models to sync.
|
11
|
+
def perform(canvas_base_url, canvas_api_token, job_chain, options)
|
12
|
+
@canvas_base_url = canvas_base_url
|
13
|
+
@canvas_api_token = canvas_api_token
|
14
|
+
@job_chain = job_chain
|
15
|
+
|
16
|
+
if options[:term_scope]
|
17
|
+
Term.send(options[:term_scope]).find_each do |term|
|
18
|
+
# Deep copy the options so each report gets the correct term id passed into
|
19
|
+
# its options with no side effects
|
20
|
+
duped_options = Marshal.load(Marshal.dump(options))
|
21
|
+
duped_options[:canvas_term_id] = term.canvas_term_id
|
22
|
+
start_report(report_params(options, term.canvas_term_id), duped_options)
|
23
|
+
end
|
24
|
+
else
|
25
|
+
start_report(report_params(options), options)
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
private
|
30
|
+
|
31
|
+
def start_report(report_params, options)
|
32
|
+
CanvasSync::Jobs::ReportStarter.perform_later(
|
33
|
+
@canvas_base_url,
|
34
|
+
@canvas_api_token,
|
35
|
+
@job_chain,
|
36
|
+
'proservices_provisioning_csv',
|
37
|
+
report_params,
|
38
|
+
CanvasSync::Processors::ProvisioningReportProcessor.to_s,
|
39
|
+
options
|
40
|
+
)
|
41
|
+
end
|
42
|
+
|
43
|
+
def report_params(options, canvas_term_id=nil)
|
44
|
+
params = {
|
45
|
+
"parameters[include_deleted]" => true
|
46
|
+
}
|
47
|
+
|
48
|
+
options[:models].each do |model|
|
49
|
+
params["parameters[#{model}]"] = true
|
50
|
+
end
|
51
|
+
|
52
|
+
params["parameters[enrollment_term_id]"] = canvas_term_id if canvas_term_id
|
53
|
+
|
54
|
+
params
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
module CanvasSync
|
2
|
+
module Jobs
|
3
|
+
class SyncTermsJob < ApplicationJob
|
4
|
+
# Syncs Terms using the Canvas API
|
5
|
+
#
|
6
|
+
# Terms are pre-synced so that provisioning reports can be scoped to term.
|
7
|
+
#
|
8
|
+
# @param canvas_base_url [String]
|
9
|
+
# @param canvas_api_token [String]
|
10
|
+
# @param job_chain [Hash]
|
11
|
+
# @param options [Hash]
|
12
|
+
def perform(canvas_base_url, canvas_api_token, job_chain, options)
|
13
|
+
client = canvas_client(canvas_base_url, canvas_api_token)
|
14
|
+
|
15
|
+
client.terms('self').all_pages!.each do |term_params|
|
16
|
+
Term.create_or_update(term_params)
|
17
|
+
end
|
18
|
+
|
19
|
+
CanvasSync.invoke_next(canvas_base_url, canvas_api_token, job_chain)
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
module CanvasSync
|
2
|
+
module Jobs
|
3
|
+
class SyncUsersJob < ReportStarter
|
4
|
+
REPORT_PARAMS = {
|
5
|
+
'parameters[users]' => true,
|
6
|
+
'parameters[include_deleted]' => true
|
7
|
+
}
|
8
|
+
|
9
|
+
# Starts a provisioning report for just users.
|
10
|
+
#
|
11
|
+
# Provisioning reports do not scope users by term, so when we are
|
12
|
+
# running provisioning by term we sync users first so we don't duplicate
|
13
|
+
# the work of syncing all users for each term.
|
14
|
+
#
|
15
|
+
# @param canvas_base_url [String]
|
16
|
+
# @param canvas_api_token [String]
|
17
|
+
# @param job_chain [Hash]
|
18
|
+
# @param options [Hash]
|
19
|
+
def perform(canvas_base_url, canvas_api_token, job_chain, options)
|
20
|
+
super(
|
21
|
+
canvas_base_url,
|
22
|
+
canvas_api_token,
|
23
|
+
job_chain,
|
24
|
+
'proservices_provisioning_csv',
|
25
|
+
REPORT_PARAMS,
|
26
|
+
CanvasSync::Processors::ProvisioningReportProcessor.to_s,
|
27
|
+
{ models: ['users'] }
|
28
|
+
)
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,118 @@
|
|
1
|
+
require 'csv'
|
2
|
+
require 'activerecord-import'
|
3
|
+
require 'zip'
|
4
|
+
|
5
|
+
module CanvasSync
|
6
|
+
module Processors
|
7
|
+
class ProvisioningReportProcessor
|
8
|
+
# Used by the {CanvasSync::Importers::BulkImporter bulk importer}. The keys are
|
9
|
+
# CSV columns and the values are the database columns.
|
10
|
+
USERS_CSV_MAPPING = {
|
11
|
+
canvas_user_id: :canvas_user_id,
|
12
|
+
user_id: :sis_id,
|
13
|
+
email: :email,
|
14
|
+
first_name: :first_name,
|
15
|
+
last_name: :last_name,
|
16
|
+
status: :status
|
17
|
+
}
|
18
|
+
|
19
|
+
COURSES_CSV_MAPPING = {
|
20
|
+
canvas_course_id: :canvas_course_id,
|
21
|
+
course_id: :sis_id,
|
22
|
+
short_name: :short_name,
|
23
|
+
long_name: :long_name,
|
24
|
+
canvas_account_id: :canvas_account_id,
|
25
|
+
canvas_term_id: :canvas_term_id,
|
26
|
+
term_id: :term_sis_id,
|
27
|
+
start_date: :start_date,
|
28
|
+
end_date: :end_date
|
29
|
+
}
|
30
|
+
|
31
|
+
ENROLLMENTS_CSV_MAPPING = {
|
32
|
+
canvas_enrollment_id: :canvas_enrollment_id,
|
33
|
+
canvas_course_id: :canvas_course_id,
|
34
|
+
course_id: :course_sis_id,
|
35
|
+
canvas_user_id: :canvas_user_id,
|
36
|
+
user_id: :user_sis_id,
|
37
|
+
role: :role,
|
38
|
+
role_id: :role_id,
|
39
|
+
canvas_section_id: :canvas_section_id,
|
40
|
+
section_id: :section_sis_id,
|
41
|
+
status: :status,
|
42
|
+
base_role_type: :base_role_type
|
43
|
+
}
|
44
|
+
|
45
|
+
SECTIONS_CSV_MAPPING = {
|
46
|
+
canvas_section_id: :canvas_section_id,
|
47
|
+
section_id: :sis_id,
|
48
|
+
canvas_course_id: :canvas_course_id,
|
49
|
+
name: :name,
|
50
|
+
status: :status,
|
51
|
+
start_date: :start_date,
|
52
|
+
end_date: :end_date
|
53
|
+
}
|
54
|
+
|
55
|
+
# Processes a provisioning report using the bulk importer.
|
56
|
+
#
|
57
|
+
# options must contain a models key. If there is only one model
|
58
|
+
# Canvas downloads the single report directly as a CSV. If it's
|
59
|
+
# more than one model Canvas downloads a ZIP file, so we have to
|
60
|
+
# extract that and iterate through it for processing.
|
61
|
+
#
|
62
|
+
# @param report_file_path [String]
|
63
|
+
# @param options [Hash]
|
64
|
+
def self.process(report_file_path, options)
|
65
|
+
if options[:models].length == 1
|
66
|
+
send("process_#{options[:models][0]}", report_file_path)
|
67
|
+
else
|
68
|
+
unzipped_file_path = extract(report_file_path)
|
69
|
+
Dir[unzipped_file_path + "/*.csv"].each do |file_path|
|
70
|
+
model_name = file_path.split("/").last.split(".").first
|
71
|
+
send("process_#{model_name}", file_path)
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
private
|
77
|
+
|
78
|
+
def self.extract(file_path)
|
79
|
+
unzipped_file_path = "#{file_path}_unzipped"
|
80
|
+
|
81
|
+
Zip::File.open(file_path) do |zip_file|
|
82
|
+
zip_file.each do |f|
|
83
|
+
f_path = File.join(unzipped_file_path, f.name)
|
84
|
+
FileUtils.mkdir_p(File.dirname(f_path))
|
85
|
+
zip_file.extract(f, f_path) unless File.exist?(f_path)
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
unzipped_file_path
|
90
|
+
end
|
91
|
+
|
92
|
+
def self.process_users(report_file_path)
|
93
|
+
rows = {}
|
94
|
+
|
95
|
+
# Users can show up more than once in a report if they have multiple logins. This breaks
|
96
|
+
# the bulk import, so the block we pass into the importer filters out users we've already
|
97
|
+
# encountered.
|
98
|
+
CanvasSync::Importers::BulkImporter.import(report_file_path, USERS_CSV_MAPPING, User, :canvas_user_id) do |row|
|
99
|
+
next nil if rows[row[:canvas_user_id]]
|
100
|
+
rows[row[:canvas_user_id]] = true
|
101
|
+
row
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
def self.process_courses(report_file_path)
|
106
|
+
CanvasSync::Importers::BulkImporter.import(report_file_path, COURSES_CSV_MAPPING, Course, :canvas_course_id)
|
107
|
+
end
|
108
|
+
|
109
|
+
def self.process_enrollments(report_file_path)
|
110
|
+
CanvasSync::Importers::BulkImporter.import(report_file_path, ENROLLMENTS_CSV_MAPPING, Enrollment, :canvas_enrollment_id)
|
111
|
+
end
|
112
|
+
|
113
|
+
def self.process_sections(report_file_path)
|
114
|
+
CanvasSync::Importers::BulkImporter.import(report_file_path, SECTIONS_CSV_MAPPING, Section, :canvas_section_id)
|
115
|
+
end
|
116
|
+
end
|
117
|
+
end
|
118
|
+
end
|