canvas_sync 0.17.20 → 0.17.23.beta1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (39) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +24 -1
  3. data/lib/canvas_sync/concerns/sync_mapping.rb +112 -0
  4. data/lib/canvas_sync/generators/install_generator.rb +1 -0
  5. data/lib/canvas_sync/generators/templates/migrations/create_grading_period_groups.rb +22 -0
  6. data/lib/canvas_sync/generators/templates/migrations/create_grading_periods.rb +22 -0
  7. data/lib/canvas_sync/generators/templates/migrations/create_user_observers.rb +17 -0
  8. data/lib/canvas_sync/generators/templates/models/grading_period.rb +8 -0
  9. data/lib/canvas_sync/generators/templates/models/grading_period_group.rb +9 -0
  10. data/lib/canvas_sync/generators/templates/models/user_observer.rb +11 -0
  11. data/lib/canvas_sync/importers/bulk_importer.rb +27 -16
  12. data/lib/canvas_sync/job_batches/chain_builder.rb +1 -1
  13. data/lib/canvas_sync/processors/assignment_groups_processor.rb +1 -7
  14. data/lib/canvas_sync/processors/assignments_processor.rb +1 -7
  15. data/lib/canvas_sync/processors/context_module_items_processor.rb +1 -7
  16. data/lib/canvas_sync/processors/context_modules_processor.rb +1 -7
  17. data/lib/canvas_sync/processors/model_mappings.yml +68 -0
  18. data/lib/canvas_sync/processors/normal_processor.rb +3 -3
  19. data/lib/canvas_sync/processors/provisioning_report_processor.rb +21 -63
  20. data/lib/canvas_sync/processors/report_processor.rb +14 -9
  21. data/lib/canvas_sync/processors/submissions_processor.rb +1 -7
  22. data/lib/canvas_sync/record.rb +4 -0
  23. data/lib/canvas_sync/version.rb +1 -1
  24. data/lib/canvas_sync.rb +4 -1
  25. data/spec/canvas_sync/processors/provisioning_report_processor_spec.rb +40 -0
  26. data/spec/dummy/app/models/grading_period.rb +14 -0
  27. data/spec/dummy/app/models/grading_period_group.rb +15 -0
  28. data/spec/dummy/app/models/user_observer.rb +17 -0
  29. data/spec/dummy/db/migrate/20210907233329_create_user_observers.rb +23 -0
  30. data/spec/dummy/db/migrate/20210907233330_create_grading_periods.rb +28 -0
  31. data/spec/dummy/db/migrate/20210907233331_create_grading_period_groups.rb +28 -0
  32. data/spec/dummy/db/schema.rb +42 -1
  33. data/spec/dummy/log/development.log +1105 -1186
  34. data/spec/dummy/log/test.log +2555 -43038
  35. data/spec/support/fixtures/reports/grading_period_groups.csv +2 -0
  36. data/spec/support/fixtures/reports/grading_periods.csv +3 -0
  37. data/spec/support/fixtures/reports/user_observers.csv +3 -0
  38. metadata +34 -25
  39. 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: 6fe925c97d18da5736681b55e51a1702d727ef2900097c9058de6f247748af9f
4
+ data.tar.gz: 27c0bd4eb8d57efc705cbe3affdfa6ce4af9580b1b2374e11a8e7e84ddf2b8bd
5
5
  SHA512:
6
- metadata.gz: 0ae25ea9b6305902f220604a4d040737cca48aa6a7d89960357179ad852951bcb99a93693c3fd684ef3a12b3db89874023f48f5ac6d54dac35edb3fd7eefcc57
7
- data.tar.gz: ed161a60791e7d326faabea6431d6352eee48ea2c23dd0ff224e902ac9e545c881f18a3d0d31bffa186e95d5d840f605e492f58ef7b68d9583da7afd011dde89
6
+ metadata.gz: 82f682b5e2b713ba2adc45483f215c1de8103dc16bdd3872a1656649527053a8819de35b50d1f4bc8e9961bedbc647fd7a03dbebbe4f2ad0fb3235d6754788be
7
+ data.tar.gz: 674159253800095ba66e15d9d14f58fd41d46f7fa70a33a8303160488544d721f7e8c83700c8a5e4978436e01d34e070b7bc22ff7433219e68fe00d4620dc38f
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:
@@ -0,0 +1,112 @@
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
+ @map_def = map_def
29
+ @model = model
30
+ end
31
+
32
+ def self.normalize_model_name(model)
33
+ model = model.name unless model.is_a?(String)
34
+ model.pluralize.underscore
35
+ end
36
+
37
+ def self.default_for(key)
38
+ default_mappings[key]
39
+ end
40
+
41
+ def self.default_mappings
42
+ @mappings ||= begin
43
+ maps = {}
44
+ default_v1_mappings.each do |mname, legacy|
45
+ m = maps[mname] = {}
46
+
47
+ m[:conflict_target] = Array(legacy[:conflict_target]).map(&:to_sym).map do |lct|
48
+ legacy[:report_columns][lct][:database_column_name]
49
+ end
50
+
51
+ m[:report_columns] = {}
52
+ legacy[:report_columns].each do |rcol, opts|
53
+ m[:report_columns][opts[:database_column_name]] = opts.except(:database_column_name).merge!(
54
+ report_column: rcol,
55
+ ).freeze
56
+ end
57
+ end
58
+ maps.with_indifferent_access.freeze
59
+ end
60
+ end
61
+
62
+ def self.default_v1_mappings
63
+ @legacy_mappings ||= begin
64
+ mapping = YAML.load_file(File.join(__dir__, '../processors', "model_mappings.yml")).deep_symbolize_keys!
65
+ override_filepath = Rails.root.join("config/canvas_sync_provisioning_mapping.yml")
66
+
67
+ if File.file?(override_filepath)
68
+ override = YAML.load_file(override_filepath).deep_symbolize_keys!
69
+ mapping = mapping.merge(override)
70
+ end
71
+
72
+ mapping.freeze
73
+ end
74
+ end
75
+
76
+ def conflict_target(*columns)
77
+ if columns.count == 0
78
+ @map_def[:conflict_target]
79
+ else
80
+ @map_def[:conflict_target] = columns.flatten.compact
81
+ end
82
+ end
83
+
84
+ def reset_links
85
+ @map_def = {}
86
+ end
87
+
88
+ def unlink_column(key)
89
+ @map_def.delete(key)
90
+ end
91
+
92
+ def link_column(m, type: nil, &blk)
93
+ if m.is_a?(Hash)
94
+ raise "Hash should have exactly 1 entry" if m && m.count != 1
95
+ @map_def[:report_columns][m.values[0]] = {
96
+ report_column: m.keys[0],
97
+ type: type,
98
+ transform: blk,
99
+ }
100
+ elsif m.is_a?(Symbol)
101
+ @map_def[:report_columns][m] = {
102
+ report_column: m,
103
+ type: type,
104
+ transform: blk,
105
+ }
106
+ else
107
+ raise "Cannot handle argument of type #{m.class}"
108
+ end
109
+ end
110
+ end
111
+ end
112
+ 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
@@ -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