canvas_sync 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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