canvas_sync 0.17.20 → 0.17.23.beta5

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 (41) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +24 -1
  3. data/config/initializers/apartment.rb +10 -0
  4. data/lib/canvas_sync/concerns/sync_mapping.rb +114 -0
  5. data/lib/canvas_sync/generators/install_generator.rb +1 -0
  6. data/lib/canvas_sync/generators/templates/migrations/create_grading_period_groups.rb +22 -0
  7. data/lib/canvas_sync/generators/templates/migrations/create_grading_periods.rb +22 -0
  8. data/lib/canvas_sync/generators/templates/migrations/create_user_observers.rb +17 -0
  9. data/lib/canvas_sync/generators/templates/models/grading_period.rb +8 -0
  10. data/lib/canvas_sync/generators/templates/models/grading_period_group.rb +9 -0
  11. data/lib/canvas_sync/generators/templates/models/term.rb +1 -0
  12. data/lib/canvas_sync/generators/templates/models/user_observer.rb +11 -0
  13. data/lib/canvas_sync/importers/bulk_importer.rb +27 -16
  14. data/lib/canvas_sync/job_batches/chain_builder.rb +1 -1
  15. data/lib/canvas_sync/processors/assignment_groups_processor.rb +1 -7
  16. data/lib/canvas_sync/processors/assignments_processor.rb +1 -7
  17. data/lib/canvas_sync/processors/context_module_items_processor.rb +1 -7
  18. data/lib/canvas_sync/processors/context_modules_processor.rb +1 -7
  19. data/lib/canvas_sync/processors/model_mappings.yml +68 -0
  20. data/lib/canvas_sync/processors/normal_processor.rb +3 -3
  21. data/lib/canvas_sync/processors/provisioning_report_processor.rb +21 -63
  22. data/lib/canvas_sync/processors/report_processor.rb +14 -9
  23. data/lib/canvas_sync/processors/submissions_processor.rb +1 -7
  24. data/lib/canvas_sync/record.rb +4 -0
  25. data/lib/canvas_sync/version.rb +1 -1
  26. data/lib/canvas_sync.rb +4 -1
  27. data/spec/canvas_sync/processors/provisioning_report_processor_spec.rb +40 -0
  28. data/spec/dummy/app/models/grading_period.rb +14 -0
  29. data/spec/dummy/app/models/grading_period_group.rb +15 -0
  30. data/spec/dummy/app/models/user_observer.rb +17 -0
  31. data/spec/dummy/db/migrate/20210907233329_create_user_observers.rb +23 -0
  32. data/spec/dummy/db/migrate/20210907233330_create_grading_periods.rb +28 -0
  33. data/spec/dummy/db/migrate/20210907233331_create_grading_period_groups.rb +28 -0
  34. data/spec/dummy/db/schema.rb +42 -1
  35. data/spec/dummy/log/development.log +1105 -1186
  36. data/spec/dummy/log/test.log +9781 -42513
  37. data/spec/support/fixtures/reports/grading_period_groups.csv +2 -0
  38. data/spec/support/fixtures/reports/grading_periods.csv +3 -0
  39. data/spec/support/fixtures/reports/user_observers.csv +3 -0
  40. metadata +34 -25
  41. data/spec/dummy/db/test.sqlite3 +0 -0
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 761f46113fd5ade5684101672ca0fda352e0fbec602bb8fe0fdbcaf584e728ba
4
- data.tar.gz: '0827e3a31d1e2bf7bf0afdece5a6a123f2221f2db9a4d239a4c543024641da57'
3
+ metadata.gz: ff0bdfef2b2ae070b727fefe7b16c20149a82e8237a922102516de8aaee508ae
4
+ data.tar.gz: da0f508fe25b22b3a9913d81891cf729effaf55d0d0800001079015101be1179
5
5
  SHA512:
6
- metadata.gz: 0ae25ea9b6305902f220604a4d040737cca48aa6a7d89960357179ad852951bcb99a93693c3fd684ef3a12b3db89874023f48f5ac6d54dac35edb3fd7eefcc57
7
- data.tar.gz: ed161a60791e7d326faabea6431d6352eee48ea2c23dd0ff224e902ac9e545c881f18a3d0d31bffa186e95d5d840f605e492f58ef7b68d9583da7afd011dde89
6
+ metadata.gz: 48c84c48103000a58b3fd5803c650ccc3bf485c39d8cb09467892de0621d2607757542ed9dd275a741040231639d269978f2e5fe23d7830649470fb409bd2fd6
7
+ data.tar.gz: 36cd14b22dc554ecb7110770e4cb13ce9e0ec7cc1f000ee6b567d65493f07b1c616ec0b2ac3b7234a8ba3bf633f7468a709c10ae771183ae31237ad726476826
data/README.md CHANGED
@@ -190,7 +190,30 @@ Overrides are useful for two scenarios:
190
190
  - You have an existing application where the column names do not match up with what CanvasSync expects
191
191
  - You want to sync some other column in the report that CanvasSync is not configured to sync
192
192
 
193
- In order to create an override, place a file called `canvas_sync_provisioning_mapping.yml` in your Rails `config` directory. Define the tables and columns you want to override using the following format:
193
+ Mappings can be modified by editing the Model class like such:
194
+ ```ruby
195
+ class User < ApplicationRecord
196
+ include CanvasSync::Record
197
+
198
+ sync_mapping(reset: false) do # `reset: false` is the default
199
+ # The mapping can be totally cleared with `reset: true` in the `sync_mapping` call, or like such:
200
+ reset_links
201
+
202
+ # Add a new column:
203
+ link_column :column_in_report => :column_in_database, type: :datetime
204
+
205
+ # If the column name on the report and in the DB are the same, a shorthand can be used:
206
+ link_column :omit_from_final_grade, type: :datetime
207
+
208
+ # If the defaults define a column you don't want synced, you can remove it from the mapping:
209
+ unlink_column :column_in_database
210
+ end
211
+
212
+ # ...
213
+ end
214
+ ```
215
+
216
+ You can also create a file called `canvas_sync_provisioning_mapping.yml` in your Rails `config` directory. However, this approach requires you to re-specify the complete table in order to modify a table. Define the tables and columns you want to override using the following format:
194
217
 
195
218
  ```ruby
196
219
  users:
@@ -5,6 +5,16 @@
5
5
  # * https://github.com/influitive/apartment/issues/508
6
6
 
7
7
  Rails.application.config.after_initialize do
8
+ next unless defined?(Apartment)
9
+
10
+ # Apartment already solves this issue (and in a better way) in newer versions
11
+ begin
12
+ require('apartment/version')
13
+ next if Gem::Version.new(Apartment::VERSION) >= Gem::Version.new('2.8.1')
14
+ rescue LoadError
15
+ end
16
+
17
+
8
18
  begin
9
19
  Rails.application.eager_load!
10
20
  ActiveRecord::Base.descendants.each do |model|
@@ -0,0 +1,114 @@
1
+ module CanvasSync::Concerns
2
+ module SyncMapping
3
+ extend ActiveSupport::Concern
4
+
5
+ class_methods do
6
+ def sync_mapping(key = nil, reset: false, &blk)
7
+ key ||= Mapping.normalize_model_name(self)
8
+ key = key.to_s
9
+ existing_map = get_sync_mapping(key)
10
+ mapper = Mapping.new(existing_map&.deep_dup || {}.with_indifferent_access)
11
+ mapper.reset_links if reset
12
+ mapper.instance_exec(&blk)
13
+ @sync_mappings[key] = mapper.map_def.freeze
14
+ end
15
+
16
+ def get_sync_mapping(key = nil)
17
+ key ||= Mapping.normalize_model_name(self)
18
+ key = key.to_s
19
+ @sync_mappings ||= {}
20
+ @sync_mappings[key] || superclass.try(:get_sync_mapping, key) || Mapping.default_for(key)
21
+ end
22
+ end
23
+
24
+ class Mapping
25
+ attr_reader :map_def
26
+
27
+ def initialize(map_def, model: nil)
28
+ @model = model
29
+ @map_def = map_def
30
+ @map_def[:conflict_target] ||= []
31
+ @map_def[:report_columns] ||= {}
32
+ end
33
+
34
+ def self.normalize_model_name(model)
35
+ model = model.name unless model.is_a?(String)
36
+ model.pluralize.underscore
37
+ end
38
+
39
+ def self.default_for(key)
40
+ default_mappings[key]
41
+ end
42
+
43
+ def self.default_mappings
44
+ @mappings ||= begin
45
+ maps = {}
46
+ default_v1_mappings.each do |mname, legacy|
47
+ m = maps[mname] = {}
48
+
49
+ m[:conflict_target] = Array(legacy[:conflict_target]).map(&:to_sym).map do |lct|
50
+ legacy[:report_columns][lct][:database_column_name]
51
+ end
52
+
53
+ m[:report_columns] = {}
54
+ legacy[:report_columns].each do |rcol, opts|
55
+ m[:report_columns][opts[:database_column_name]] = opts.except(:database_column_name).merge!(
56
+ report_column: rcol,
57
+ ).freeze
58
+ end
59
+ end
60
+ maps.with_indifferent_access.freeze
61
+ end
62
+ end
63
+
64
+ def self.default_v1_mappings
65
+ @legacy_mappings ||= begin
66
+ mapping = YAML.load_file(File.join(__dir__, '../processors', "model_mappings.yml")).deep_symbolize_keys!
67
+ override_filepath = Rails.root.join("config/canvas_sync_provisioning_mapping.yml")
68
+
69
+ if File.file?(override_filepath)
70
+ override = YAML.load_file(override_filepath).deep_symbolize_keys!
71
+ mapping = mapping.merge(override)
72
+ end
73
+
74
+ mapping.freeze
75
+ end
76
+ end
77
+
78
+ def conflict_target(*columns)
79
+ if columns.count == 0
80
+ @map_def[:conflict_target]
81
+ else
82
+ @map_def[:conflict_target] = columns.flatten.compact
83
+ end
84
+ end
85
+
86
+ def reset_links
87
+ @map_def[:report_columns] = {}.with_indifferent_access
88
+ end
89
+
90
+ def unlink_column(key)
91
+ @map_def.delete(key)
92
+ end
93
+
94
+ def link_column(m, type: nil, &blk)
95
+ if m.is_a?(Hash)
96
+ raise "Hash should have exactly 1 entry" if m && m.count != 1
97
+ @map_def[:report_columns][m.values[0]] = {
98
+ report_column: m.keys[0],
99
+ type: type,
100
+ transform: blk,
101
+ }
102
+ elsif m.is_a?(Symbol)
103
+ @map_def[:report_columns][m] = {
104
+ report_column: m,
105
+ type: type,
106
+ transform: blk,
107
+ }
108
+ else
109
+ raise "Cannot handle argument of type #{m.class}"
110
+ end
111
+ end
112
+ end
113
+ end
114
+ end
@@ -48,6 +48,7 @@ module CanvasSync
48
48
  models.each do |model|
49
49
  migration_template "migrations/create_#{model}.rb", "db/migrate/create_#{model}.rb"
50
50
  template "models/#{model.singularize}.rb", "app/models/#{model.singularize}.rb"
51
+ rescue
51
52
  end
52
53
  end
53
54
  end
@@ -0,0 +1,22 @@
1
+ # <%= autogenerated_migration_warning %>
2
+
3
+ class CreateGradingPeriodGroups < ActiveRecord::Migration[5.1]
4
+ def change
5
+ create_table :grading_period_groups do |t|
6
+ t.bigint :canvas_id, null: false
7
+ t.bigint :canvas_course_id
8
+ t.bigint :canvas_account_id
9
+ t.string :title
10
+ t.boolean :weighted
11
+ t.boolean :display_totals_for_all_grading_periods
12
+
13
+ t.string :workflow_state
14
+
15
+ t.timestamps
16
+ end
17
+
18
+ add_index :grading_period_groups, :canvas_id, unique: true
19
+ add_index :grading_period_groups, :canvas_course_id
20
+ add_index :grading_period_groups, :canvas_account_id
21
+ end
22
+ end
@@ -0,0 +1,22 @@
1
+ # <%= autogenerated_migration_warning %>
2
+
3
+ class CreateGradingPeriods < ActiveRecord::Migration[5.1]
4
+ def change
5
+ create_table :grading_periods do |t|
6
+ t.bigint :canvas_id, null: false
7
+ t.string :title
8
+ t.float :weight
9
+ t.datetime :start_date
10
+ t.datetime :end_date
11
+ t.datetime :close_date
12
+ t.bigint :canvas_grading_period_group_id
13
+
14
+ t.string :workflow_state
15
+
16
+ t.timestamps
17
+ end
18
+
19
+ add_index :grading_periods, :canvas_id, unique: true
20
+ add_index :grading_periods, :canvas_grading_period_group_id
21
+ end
22
+ end
@@ -0,0 +1,17 @@
1
+ # <%= autogenerated_migration_warning %>
2
+
3
+ class CreateUserObservers < ActiveRecord::Migration[5.1]
4
+ def change
5
+ create_table :user_observers do |t|
6
+ t.bigint :observing_user_id
7
+ t.bigint :observed_user_id
8
+ t.string :workflow_state
9
+
10
+ t.timestamps
11
+ end
12
+
13
+ add_index :user_observers, [:observed_user_id, :observing_user_id], unique: true
14
+ add_index :user_observers, :observing_user_id
15
+ add_index :user_observers, :observed_user_id
16
+ end
17
+ end
@@ -0,0 +1,8 @@
1
+ # <%= autogenerated_model_warning %>
2
+
3
+ class GradingPeriod < ApplicationRecord
4
+ include CanvasSync::Record
5
+
6
+ validates :canvas_id, uniqueness: true, presence: true
7
+ belongs_to :grading_period_group, primary_key: :canvas_id, foreign_key: :canvas_grading_period_group_id, optional: true
8
+ end
@@ -0,0 +1,9 @@
1
+ # <%= autogenerated_model_warning %>
2
+
3
+ class GradingPeriodGroup < ApplicationRecord
4
+ include CanvasSync::Record
5
+
6
+ validates :canvas_id, uniqueness: true, presence: true
7
+ belongs_to :course, primary_key: :canvas_id, foreign_key: :canvas_course_id, optional: true
8
+ belongs_to :account, primary_key: :canvas_id, foreign_key: :canvas_account_id, optional: true
9
+ end
@@ -6,6 +6,7 @@ class Term < ApplicationRecord
6
6
 
7
7
  validates :canvas_id, uniqueness: true, presence: true
8
8
  has_many :courses, foreign_key: :canvas_term_id, primary_key: :canvas_id
9
+ belongs_to :grading_period_group, primary_key: :canvas_id, foreign_key: :grading_period_group_id, optional: true
9
10
 
10
11
  api_syncable({
11
12
  canvas_id: :id,
@@ -0,0 +1,11 @@
1
+ # <%= autogenerated_model_warning %>
2
+
3
+ class UserObserver < ApplicationRecord
4
+ include CanvasSync::Record
5
+ include CanvasSync::Concerns::ApiSyncable
6
+
7
+ validates :canvas_id, uniqueness: true, presence: true
8
+
9
+ belongs_to :observing_user, primary_key: :canvas_id, foreign_key: :observing_user_id, class_name: 'User', optional: true
10
+ belongs_to :observed_user, primary_key: :canvas_id, foreign_key: :observed_user_id, class_name: 'User', optional: true
11
+ end
@@ -24,30 +24,41 @@ module CanvasSync
24
24
  end
25
25
 
26
26
  def self.perform_in_batches(report_file_path, mapping, klass, conflict_target, import_args: {})
27
- csv_column_names = mapping.keys
28
- database_column_names = mapping.values.map { |value| value[:database_column_name] }
29
- database_conflict_column_name = conflict_target ? mapping[conflict_target][:database_column_name] : nil
27
+ csv_column_names = mapping.values.map { |value| value[:report_column].to_s }
28
+ database_column_names = mapping.keys
29
+
30
+ conflict_target = Array(conflict_target).map(&:to_s)
31
+ conflict_target_indices = conflict_target.map{|ct| database_column_names.index(ct) }
30
32
 
31
33
  row_ids = {}
32
34
  batcher = CanvasSync::BatchProcessor.new(of: batch_size) do |batch|
33
35
  row_ids = {}
34
- perform_import(klass, database_column_names, batch, database_conflict_column_name, import_args)
36
+ perform_import(klass, database_column_names, batch, conflict_target, import_args)
35
37
  end
36
38
 
37
39
  row_buffer_out = ->(row) {
38
- if conflict_target
39
- next if row_ids[row[conflict_target]]
40
- row_ids[row[conflict_target]] = true
40
+ formatted_row = mapping.map do |db_col, col_def|
41
+ value = nil
42
+ value = row[col_def[:report_column]] if col_def[:report_column]
43
+
44
+ if col_def[:type]
45
+ if col_def[:type].to_sym == :datetime
46
+ # TODO: add some timezone config to the mapping.
47
+ # In cases where the timestamp or date doesn't include a timezone, you should be able to specify one
48
+ value = DateTime.parse(value).utc rescue nil # rubocop:disable Style/RescueModifier
49
+ end
50
+ end
51
+
52
+ value = col_def[:transform].call(value, row) if col_def[:transform]
53
+
54
+ value
41
55
  end
42
56
 
43
- formatted_row = csv_column_names.map do |column|
44
- if mapping[column][:type].to_sym == :datetime
45
- # TODO: add some timezone config to the mapping.
46
- # In cases where the timestamp or date doesn't include a timezone, you should be able to specify one
47
- DateTime.parse(row[column]).utc rescue nil # rubocop:disable Style/RescueModifier
48
- else
49
- row[column]
50
- end
57
+ if conflict_target.present?
58
+ key = conflict_target_indices.map{|ct| formatted_row[ct] }
59
+ next if row_ids[key]
60
+
61
+ row_ids[key] = true
51
62
  end
52
63
 
53
64
  batcher << formatted_row
@@ -79,7 +90,7 @@ module CanvasSync
79
90
  condition: condition_sql(klass, columns, import_args[:sync_start_time]),
80
91
  columns: columns
81
92
  }
82
- update_conditions[:conflict_target] = conflict_target if conflict_target
93
+ update_conditions[:conflict_target] = conflict_target if conflict_target.present?
83
94
 
84
95
  options = { validate: false, on_duplicate_key_update: update_conditions }.merge(import_args)
85
96
  options.delete(:on_duplicate_key_update) if options.key?(:on_duplicate_key_ignore)
@@ -40,7 +40,7 @@ module CanvasSync
40
40
  def insert_at(position, new_jobs)
41
41
  chain = self.class.get_chain_parameter(base_job)
42
42
  new_jobs = [new_jobs] unless new_jobs.is_a?(Array)
43
- chain.insert(-1, *new_jobs)
43
+ chain.insert(position, *new_jobs)
44
44
  end
45
45
 
46
46
  def insert(new_jobs, **kwargs)
@@ -12,13 +12,7 @@ module CanvasSync
12
12
  end
13
13
 
14
14
  def initialize(report_file_path, options)
15
- CanvasSync::Importers::BulkImporter.import(
16
- report_file_path,
17
- mapping[:assignment_groups][:report_columns],
18
- AssignmentGroup,
19
- mapping[:assignment_groups][:conflict_target].to_sym,
20
- import_args: options
21
- )
15
+ do_bulk_import(report_file_path, AssignmentGroup, options: options)
22
16
  end
23
17
  end
24
18
  end
@@ -12,13 +12,7 @@ module CanvasSync
12
12
  end
13
13
 
14
14
  def initialize(report_file_path, options)
15
- CanvasSync::Importers::BulkImporter.import(
16
- report_file_path,
17
- mapping[:assignments][:report_columns],
18
- Assignment,
19
- mapping[:assignments][:conflict_target].to_sym,
20
- import_args: options
21
- )
15
+ do_bulk_import(report_file_path, Assignment, options: options)
22
16
  end
23
17
  end
24
18
  end
@@ -12,13 +12,7 @@ module CanvasSync
12
12
  end
13
13
 
14
14
  def initialize(report_file_path, options)
15
- CanvasSync::Importers::BulkImporter.import(
16
- report_file_path,
17
- mapping[:context_module_items][:report_columns],
18
- ContextModuleItem,
19
- mapping[:context_module_items][:conflict_target].to_sym,
20
- import_args: options
21
- )
15
+ do_bulk_import(report_file_path, ContextModuleItem, options: options)
22
16
  end
23
17
  end
24
18
  end
@@ -12,13 +12,7 @@ module CanvasSync
12
12
  end
13
13
 
14
14
  def initialize(report_file_path, options)
15
- CanvasSync::Importers::BulkImporter.import(
16
- report_file_path,
17
- mapping[:context_modules][:report_columns],
18
- ContextModule,
19
- mapping[:context_modules][:conflict_target].to_sym,
20
- import_args: options
21
- )
15
+ do_bulk_import(report_file_path, ContextModule, options: options)
22
16
  end
23
17
  end
24
18
  end
@@ -407,3 +407,71 @@ group_memberships:
407
407
  status:
408
408
  database_column_name: workflow_state
409
409
  type: string
410
+
411
+ user_observers:
412
+ conflict_target:
413
+ - canvas_observer_id
414
+ - canvas_student_id
415
+ report_columns:
416
+ canvas_observer_id:
417
+ database_column_name: observing_user_id
418
+ type: integer
419
+ canvas_student_id:
420
+ database_column_name: observed_user_id
421
+ type: integer
422
+ status:
423
+ database_column_name: workflow_state
424
+ type: string
425
+
426
+ grading_periods:
427
+ conflict_target: grading_period_id
428
+ report_columns:
429
+ grading_period_id:
430
+ database_column_name: canvas_id
431
+ type: integer
432
+ title:
433
+ database_column_name: title
434
+ type: string
435
+ weight:
436
+ database_column_name: weight
437
+ type: float
438
+ start_date:
439
+ database_column_name: start_date
440
+ type: datetime
441
+ end_date:
442
+ database_column_name: end_date
443
+ type: datetime
444
+ close_date:
445
+ database_column_name: close_date
446
+ type: datetime
447
+ grading_period_group_id:
448
+ database_column_name: canvas_grading_period_group_id
449
+ type: integer
450
+ status:
451
+ database_column_name: workflow_state
452
+ type: string
453
+
454
+ grading_period_groups:
455
+ conflict_target: grading_period_group_id
456
+ report_columns:
457
+ grading_period_group_id:
458
+ database_column_name: canvas_id
459
+ type: integer
460
+ canvas_course_id:
461
+ database_column_name: canvas_course_id
462
+ type: integer
463
+ canvas_account_id:
464
+ database_column_name: canvas_account_id
465
+ type: integer
466
+ title:
467
+ database_column_name: title
468
+ type: string
469
+ weighted:
470
+ database_column_name: weighted
471
+ type: boolean
472
+ display_totals_for_all_grading_periods:
473
+ database_column_name: display_totals_for_all_grading_periods
474
+ type: boolean
475
+ status:
476
+ database_column_name: workflow_state
477
+ type: string