canvas_sync 0.1.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (85) hide show
  1. checksums.yaml +7 -0
  2. data/README.md +178 -0
  3. data/Rakefile +23 -0
  4. data/lib/canvas_sync.rb +96 -0
  5. data/lib/canvas_sync/generators/install_generator.rb +54 -0
  6. data/lib/canvas_sync/generators/templates/course.rb +8 -0
  7. data/lib/canvas_sync/generators/templates/create_courses.rb +21 -0
  8. data/lib/canvas_sync/generators/templates/create_enrollments.rb +26 -0
  9. data/lib/canvas_sync/generators/templates/create_sections.rb +20 -0
  10. data/lib/canvas_sync/generators/templates/create_terms.rb +18 -0
  11. data/lib/canvas_sync/generators/templates/create_users.rb +18 -0
  12. data/lib/canvas_sync/generators/templates/enrollment.rb +8 -0
  13. data/lib/canvas_sync/generators/templates/section.rb +7 -0
  14. data/lib/canvas_sync/generators/templates/term.rb +28 -0
  15. data/lib/canvas_sync/generators/templates/user.rb +6 -0
  16. data/lib/canvas_sync/importers/bulk_importer.rb +54 -0
  17. data/lib/canvas_sync/jobs/application_job.rb +25 -0
  18. data/lib/canvas_sync/jobs/report_checker.rb +48 -0
  19. data/lib/canvas_sync/jobs/report_processor_job.rb +36 -0
  20. data/lib/canvas_sync/jobs/report_starter.rb +29 -0
  21. data/lib/canvas_sync/jobs/sync_provisioning_report_job.rb +58 -0
  22. data/lib/canvas_sync/jobs/sync_terms_job.rb +23 -0
  23. data/lib/canvas_sync/jobs/sync_users_job.rb +32 -0
  24. data/lib/canvas_sync/processors/provisioning_report_processor.rb +118 -0
  25. data/lib/canvas_sync/version.rb +3 -0
  26. data/spec/canvas_sync/canvas_sync_spec.rb +60 -0
  27. data/spec/canvas_sync/jobs/report_checker_spec.rb +62 -0
  28. data/spec/canvas_sync/jobs/report_processor_job_spec.rb +30 -0
  29. data/spec/canvas_sync/jobs/report_starter_spec.rb +27 -0
  30. data/spec/canvas_sync/jobs/sync_provisioning_report_job_spec.rb +81 -0
  31. data/spec/canvas_sync/jobs/sync_terms_job_spec.rb +18 -0
  32. data/spec/canvas_sync/jobs/sync_users_job_spec.rb +18 -0
  33. data/spec/canvas_sync/models/course_spec.rb +30 -0
  34. data/spec/canvas_sync/models/enrollment_spec.rb +30 -0
  35. data/spec/canvas_sync/models/section_spec.rb +24 -0
  36. data/spec/canvas_sync/models/term_spec.rb +71 -0
  37. data/spec/canvas_sync/models/user_spec.rb +18 -0
  38. data/spec/canvas_sync/processors/provisioning_report_processor_spec.rb +41 -0
  39. data/spec/dummy/README.rdoc +1 -0
  40. data/spec/dummy/Rakefile +6 -0
  41. data/spec/dummy/app/models/application_record.rb +3 -0
  42. data/spec/dummy/app/models/course.rb +14 -0
  43. data/spec/dummy/app/models/enrollment.rb +14 -0
  44. data/spec/dummy/app/models/section.rb +13 -0
  45. data/spec/dummy/app/models/term.rb +34 -0
  46. data/spec/dummy/app/models/user.rb +12 -0
  47. data/spec/dummy/bin/rails +4 -0
  48. data/spec/dummy/config.ru +4 -0
  49. data/spec/dummy/config/application.rb +26 -0
  50. data/spec/dummy/config/boot.rb +5 -0
  51. data/spec/dummy/config/database.yml +25 -0
  52. data/spec/dummy/config/environment.rb +5 -0
  53. data/spec/dummy/config/environments/development.rb +41 -0
  54. data/spec/dummy/config/environments/test.rb +42 -0
  55. data/spec/dummy/config/initializers/assets.rb +11 -0
  56. data/spec/dummy/config/initializers/session_store.rb +3 -0
  57. data/spec/dummy/config/initializers/wrap_parameters.rb +14 -0
  58. data/spec/dummy/config/routes.rb +2 -0
  59. data/spec/dummy/config/secrets.yml +22 -0
  60. data/spec/dummy/db/development.sqlite3 +0 -0
  61. data/spec/dummy/db/migrate/20170831220702_create_courses.rb +27 -0
  62. data/spec/dummy/db/migrate/20170831221129_create_users.rb +24 -0
  63. data/spec/dummy/db/migrate/20170905192509_create_enrollments.rb +32 -0
  64. data/spec/dummy/db/migrate/20170906193506_create_terms.rb +24 -0
  65. data/spec/dummy/db/migrate/20170906203438_create_sections.rb +26 -0
  66. data/spec/dummy/db/schema.rb +88 -0
  67. data/spec/dummy/db/test.sqlite3 +0 -0
  68. data/spec/dummy/log/development.log +828 -0
  69. data/spec/dummy/log/test.log +14582 -0
  70. data/spec/factories/course_factory.rb +10 -0
  71. data/spec/factories/enrollment_factory.rb +5 -0
  72. data/spec/factories/section_factory.rb +5 -0
  73. data/spec/factories/term_factory.rb +10 -0
  74. data/spec/factories/user_factory.rb +9 -0
  75. data/spec/spec_helper.rb +46 -0
  76. data/spec/support/fake_canvas.rb +22 -0
  77. data/spec/support/fixtures/canvas_responses/terms.json +64 -0
  78. data/spec/support/fixtures/reports/courses.csv +3 -0
  79. data/spec/support/fixtures/reports/enrollments.csv +3 -0
  80. data/spec/support/fixtures/reports/provisioning_csv +0 -0
  81. data/spec/support/fixtures/reports/provisioning_csv_unzipped/courses.csv +3 -0
  82. data/spec/support/fixtures/reports/provisioning_csv_unzipped/users.csv +4 -0
  83. data/spec/support/fixtures/reports/sections.csv +3 -0
  84. data/spec/support/fixtures/reports/users.csv +4 -0
  85. 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,6 @@
1
+ <%= autogenerated_model_warning %>
2
+
3
+ class User < ApplicationRecord
4
+ validates :canvas_user_id, uniqueness: true, presence: true
5
+ has_many :enrollments, primary_key: :canvas_user_id, foreign_key: :canvas_user_id
6
+ 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