canvas_sync 0.22.5 → 0.22.8
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +0 -0
- data/lib/canvas_sync/concerns/api_syncable.rb +9 -6
- data/lib/canvas_sync/concerns/sync_mapping.rb +11 -1
- data/lib/canvas_sync/generators/templates/migrations/create_enrollments.rb +1 -0
- data/lib/canvas_sync/generators/templates/models/course_progress.rb +8 -0
- data/lib/canvas_sync/importers/bulk_importer.rb +39 -69
- data/lib/canvas_sync/job.rb +0 -0
- data/lib/canvas_sync/job_batches/batch.rb +1 -1
- data/lib/canvas_sync/job_batches/chain_builder.rb +3 -24
- data/lib/canvas_sync/job_batches/jobs/managed_batch_job.rb +0 -4
- data/lib/canvas_sync/job_batches/status.rb +0 -1
- data/lib/canvas_sync/job_uniqueness/lock_context.rb +3 -15
- data/lib/canvas_sync/jobs/term_batches_job.rb +1 -4
- data/lib/canvas_sync/processors/model_mappings.yml +3 -0
- data/lib/canvas_sync/version.rb +1 -1
- data/spec/canvas_sync/canvas_sync_spec.rb +41 -59
- data/spec/canvas_sync/processors/provisioning_report_processor_spec.rb +4 -0
- data/spec/dummy/app/models/course_progress.rb +8 -0
- data/spec/dummy/app/models/learning_outcome_result.rb +0 -0
- data/spec/dummy/app/models/rubric.rb +0 -0
- data/spec/dummy/app/models/rubric_assessment.rb +0 -0
- data/spec/dummy/app/models/rubric_association.rb +0 -0
- data/spec/dummy/app/models/user.rb +0 -0
- data/spec/dummy/db/migrate/20190702203624_create_enrollments.rb +1 -0
- data/spec/dummy/db/migrate/20240408223326_create_course_nicknames.rb +0 -0
- data/spec/dummy/db/migrate/20240509105100_create_rubrics.rb +0 -0
- data/spec/dummy/db/schema.rb +1 -0
- metadata +196 -211
- data/lib/canvas_sync/concerns/auto_relations.rb +0 -11
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: '0882011929080b4e55e1798ae3639c4431a583dd2a31634b2b9c2ba5f450fcb4'
|
4
|
+
data.tar.gz: 7a339eef193de9b640b3d68d5474a7771d363f7dee723be49da33cf319afe610
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: a54b1779112dce3d106a503cc720b74d5b54ed2688c95c9d2c3523024813a0ac34d144417a8959bc81561dc4f78a7cf9914dd124037c85376a8a35b455bd5be1
|
7
|
+
data.tar.gz: 74e8b3e7d9317bbc2bbd7f902cfbedeabfd826ddbfd70aa4c8bf57c6f05dc2f4176f7bf2c0cc0352adae4ecaa4978f57f02fc3dcbbed8d1d8130df58bbaea51d
|
data/README.md
CHANGED
File without changes
|
@@ -40,14 +40,17 @@ module CanvasSync::Concerns
|
|
40
40
|
options = { validate: false, on_duplicate_key_update: update_conditions }.merge(import_args)
|
41
41
|
|
42
42
|
if all_pages
|
43
|
-
|
44
|
-
|
43
|
+
enumer = api_array.all_pages_each.lazy
|
44
|
+
|
45
|
+
# Map the API response to instances of this model
|
46
|
+
enumer = enumer.map do |api_item|
|
47
|
+
new.assign_from_api_params(api_item)
|
45
48
|
end
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
+
|
50
|
+
# Import in batches
|
51
|
+
enumer.each_slice(batch_size) do |batch|
|
52
|
+
import(columns, batch, options)
|
49
53
|
end
|
50
|
-
batcher.flush
|
51
54
|
else
|
52
55
|
items = api_array.map do |api_item|
|
53
56
|
new.assign_from_api_params(api_item)
|
@@ -51,10 +51,11 @@ module CanvasSync::Concerns
|
|
51
51
|
m = maps[mname] = {}
|
52
52
|
|
53
53
|
m[:conflict_target] = Array(legacy[:conflict_target]).map(&:to_sym).map do |lct|
|
54
|
-
legacy[:report_columns][lct]
|
54
|
+
legacy[:report_columns]&.[](lct)&.[](:database_column_name)
|
55
55
|
end
|
56
56
|
|
57
57
|
m[:columns] = {}
|
58
|
+
|
58
59
|
legacy[:report_columns].each do |rcol, opts|
|
59
60
|
m[:columns][opts[:database_column_name]] = opts.except(:database_column_name).merge!(
|
60
61
|
report_column: rcol,
|
@@ -68,6 +69,15 @@ module CanvasSync::Concerns
|
|
68
69
|
def self.default_v1_mappings
|
69
70
|
@legacy_mappings ||= begin
|
70
71
|
mapping = YAML.load_file(File.join(__dir__, '../processors', "model_mappings.yml")).deep_symbolize_keys!
|
72
|
+
# Default columns can be excluded if the table has not been migrated to accept them
|
73
|
+
mapping.each do |mname, legacy|
|
74
|
+
legacy[:report_columns].select! do |rcol, opts|
|
75
|
+
model = mname.to_s&.classify&.safe_constantize
|
76
|
+
# we need to make sure this is a model otherwise they will be systematically removed - some mappings are not models (e.g xlists)
|
77
|
+
model.present? && !model.column_names.include?(opts[:database_column_name].to_s) ? false : true
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
71
81
|
override_filepath = Rails.root.join("config/canvas_sync_provisioning_mapping.yml")
|
72
82
|
|
73
83
|
if File.file?(override_filepath)
|
@@ -3,6 +3,7 @@
|
|
3
3
|
# CourseProgress is not a Canvas model. It is a table built from the Custom Report
|
4
4
|
class CourseProgress < ApplicationRecord
|
5
5
|
include CanvasSync::Record
|
6
|
+
include CanvasSync::Concerns::ApiSyncable
|
6
7
|
|
7
8
|
canvas_sync_features :defaults
|
8
9
|
|
@@ -11,4 +12,11 @@ class CourseProgress < ApplicationRecord
|
|
11
12
|
|
12
13
|
validates_presence_of :canvas_user_id, :canvas_course_id
|
13
14
|
validates_uniqueness_of :canvas_user_id, scope: :canvas_course_id
|
15
|
+
|
16
|
+
api_syncable({
|
17
|
+
requirement_count: :requirement_count,
|
18
|
+
requirement_completed_count: :requirement_completed_count,
|
19
|
+
# provisioning report has completion_date instead of completed_at in the API
|
20
|
+
completion_date: :completed_at
|
21
|
+
}, -> (api) { api.course_progress(canvas_course_id, canvas_user_id) })
|
14
22
|
end
|
@@ -23,7 +23,7 @@ module CanvasSync
|
|
23
23
|
end
|
24
24
|
end
|
25
25
|
|
26
|
-
def self.perform_in_batches(report_file_path, raw_mapping, klass, conflict_target, import_args: {})
|
26
|
+
def self.perform_in_batches(report_file_path, raw_mapping, klass, conflict_target, import_args: {}, &block)
|
27
27
|
mapping = {}.with_indifferent_access
|
28
28
|
raw_mapping.each do |db_col, opts|
|
29
29
|
next if opts[:deprecated] && !klass.column_names.include?(db_col.to_s)
|
@@ -37,14 +37,27 @@ module CanvasSync
|
|
37
37
|
conflict_target = Array(conflict_target).map(&:to_s)
|
38
38
|
conflict_target_indices = conflict_target.map{|ct| database_column_names.index(ct) }
|
39
39
|
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
40
|
+
enumer = CSV.foreach(report_file_path, headers: true, header_converters: :symbol).lazy
|
41
|
+
|
42
|
+
# Optionally filter rows by a passed block
|
43
|
+
if block
|
44
|
+
enumer = enumer.filter_map do |row|
|
45
|
+
catch :skip do
|
46
|
+
block.call(row)
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
# Optionally chunk by a computed value. Mainly so we can group duplicate rows and choose one
|
52
|
+
chunker = nil
|
53
|
+
chunker = UserChunker.new if defined?(User) && klass == User && csv_column_names.include?('user_id')
|
54
|
+
if chunker
|
55
|
+
enumer = enumer.chunk{|row| chunker.key(row) }.flat_map{|key, rows| chunker.choose(key, rows) }
|
44
56
|
end
|
45
57
|
|
46
|
-
|
47
|
-
|
58
|
+
# Prepare the rows for import
|
59
|
+
enumer = enumer.map do |row|
|
60
|
+
mapping.map do |db_col, col_def|
|
48
61
|
value = nil
|
49
62
|
value = row[col_def[:report_column]] if col_def[:report_column]
|
50
63
|
|
@@ -63,33 +76,25 @@ module CanvasSync
|
|
63
76
|
|
64
77
|
value
|
65
78
|
end
|
79
|
+
end
|
66
80
|
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
81
|
+
# Reject rows within a single batch that have the same ID
|
82
|
+
row_ids = nil
|
83
|
+
if conflict_target.present?
|
84
|
+
enumer = enumer.reject do |row|
|
85
|
+
key = conflict_target_indices.map{|ct| row[ct] }
|
86
|
+
skip = row_ids[key]
|
71
87
|
row_ids[key] = true
|
88
|
+
skip
|
72
89
|
end
|
73
|
-
|
74
|
-
batcher << formatted_row
|
75
|
-
}
|
76
|
-
|
77
|
-
row_buffer = nil
|
78
|
-
if defined?(User) && klass == User && csv_column_names.include?('user_id')
|
79
|
-
row_buffer = UserRowBuffer.new(&row_buffer_out)
|
80
|
-
else
|
81
|
-
row_buffer = NullRowBuffer.new(&row_buffer_out)
|
82
90
|
end
|
83
91
|
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
92
|
+
# Start importing
|
93
|
+
row_ids = {}
|
94
|
+
enumer.each_slice(batch_size) do |batch|
|
95
|
+
perform_import(klass, database_column_names, batch, conflict_target, import_args)
|
96
|
+
row_ids = {}
|
89
97
|
end
|
90
|
-
|
91
|
-
row_buffer.flush
|
92
|
-
batcher.flush
|
93
98
|
end
|
94
99
|
|
95
100
|
def self.perform_import(klass, columns, rows, conflict_target, import_args={})
|
@@ -157,52 +162,17 @@ module CanvasSync
|
|
157
162
|
batch_size > 0 ? batch_size : DEFAULT_BATCH_SIZE
|
158
163
|
end
|
159
164
|
|
160
|
-
class RowBuffer
|
161
|
-
def initialize(&block)
|
162
|
-
@flush_out = block
|
163
|
-
@buffered_rows = []
|
164
|
-
end
|
165
|
-
|
166
|
-
def <<(v)
|
167
|
-
@buffered_rows << v
|
168
|
-
end
|
169
|
-
|
170
|
-
def flush(value = @buffered_rows)
|
171
|
-
if value.is_a?(Array)
|
172
|
-
value.each do |v|
|
173
|
-
@flush_out.call(v)
|
174
|
-
end
|
175
|
-
else
|
176
|
-
@flush_out.call(value)
|
177
|
-
end
|
178
|
-
@buffered_rows = []
|
179
|
-
end
|
180
|
-
end
|
181
|
-
|
182
|
-
class NullRowBuffer
|
183
|
-
def initialize(&block)
|
184
|
-
@flush_out = block
|
185
|
-
end
|
186
|
-
|
187
|
-
def <<(v)
|
188
|
-
@flush_out.call(v)
|
189
|
-
end
|
190
|
-
|
191
|
-
def flush; end
|
192
|
-
end
|
193
|
-
|
194
165
|
# Ensures that, if a User has multiple rows, one with a SIS ID is preferred.
|
195
166
|
# This is mainly to fix issues in legacy apps - the suggested approach for new apps
|
196
167
|
# is to sync and use the Pseudonymes table
|
197
|
-
class
|
198
|
-
def
|
199
|
-
|
200
|
-
super
|
168
|
+
class UserChunker
|
169
|
+
def key(row)
|
170
|
+
row[:canvas_user_id]
|
201
171
|
end
|
202
172
|
|
203
|
-
def
|
204
|
-
row =
|
205
|
-
|
173
|
+
def choose(key, rows)
|
174
|
+
row = rows.find{|r| r[:user_id].present? } || rows.last
|
175
|
+
row.present? ? [row] : []
|
206
176
|
end
|
207
177
|
end
|
208
178
|
end
|
data/lib/canvas_sync/job.rb
CHANGED
File without changes
|
@@ -337,7 +337,7 @@ module CanvasSync::JobBatches
|
|
337
337
|
trigger_callback.call(:complete)
|
338
338
|
end
|
339
339
|
|
340
|
-
if all_successful # All
|
340
|
+
if all_successful # All Successful
|
341
341
|
trigger_callback.call(:success)
|
342
342
|
elsif pending_jobs == dead_jobs && pending_batches == stagnated_batches # Stagnated
|
343
343
|
trigger_callback.call(:stagnated)
|
@@ -4,9 +4,7 @@ module CanvasSync::JobBatches
|
|
4
4
|
|
5
5
|
attr_reader :base_job
|
6
6
|
|
7
|
-
def initialize(base_type = SerialBatchJob
|
8
|
-
@chain_id = chain_id || SecureRandom.urlsafe_base64(10)
|
9
|
-
|
7
|
+
def initialize(base_type = SerialBatchJob)
|
10
8
|
if base_type.is_a?(Hash)
|
11
9
|
@base_job = base_type
|
12
10
|
@base_job[:args] ||= @base_job[:parameters] || []
|
@@ -111,7 +109,7 @@ module CanvasSync::JobBatches
|
|
111
109
|
return nil if matching_jobs.count == 0
|
112
110
|
|
113
111
|
job = matching_jobs[0][0]
|
114
|
-
job = self.class.new(job
|
112
|
+
job = self.class.new(job) unless job.is_a?(ChainBuilder)
|
115
113
|
job
|
116
114
|
end
|
117
115
|
|
@@ -120,7 +118,6 @@ module CanvasSync::JobBatches
|
|
120
118
|
job_def.normalize!
|
121
119
|
else
|
122
120
|
job_def[:job] = job_def[:job].to_s
|
123
|
-
job_def[:chain_link] ||= "#{@chain_id}-#{SecureRandom.urlsafe_base64(10)}"
|
124
121
|
if (chain = self.class.get_chain_parameter(job_def, raise_error: false)).present?
|
125
122
|
chain.map! { |sub_job| normalize!(sub_job) }
|
126
123
|
end
|
@@ -141,7 +138,7 @@ module CanvasSync::JobBatches
|
|
141
138
|
args: args,
|
142
139
|
kwargs: kwargs,
|
143
140
|
}
|
144
|
-
self.class.new(hsh
|
141
|
+
self.class.new(hsh).apply_block(&blk) if blk.present?
|
145
142
|
hsh
|
146
143
|
end
|
147
144
|
|
@@ -242,24 +239,6 @@ module CanvasSync::JobBatches
|
|
242
239
|
job_class.perform_later(*job_args, **job_kwargs)
|
243
240
|
end
|
244
241
|
end
|
245
|
-
|
246
|
-
def link_to_batch!(chain_link, batch)
|
247
|
-
# Or make chains a separate entity - Chains show batches, but batches don't show chain?
|
248
|
-
# Or "Annotate" a Batch with chain data - could extract chain id from any job entry
|
249
|
-
end
|
250
|
-
|
251
|
-
def annotate_batch!(batch, chain)
|
252
|
-
|
253
|
-
end
|
254
|
-
|
255
|
-
def handle_step_complete(status, opts)
|
256
|
-
chain_link = opts[:chain_link]
|
257
|
-
chain_id, chain_step_id = chain_link.split('-')
|
258
|
-
|
259
|
-
CanvasSync::JobBatches::Batch.redis.multi do |r|
|
260
|
-
r.hset("CHAIN-#{chain_id}-steps", chain_step_id, "complete")
|
261
|
-
end
|
262
|
-
end
|
263
242
|
end
|
264
243
|
end
|
265
244
|
|
@@ -144,10 +144,6 @@ module CanvasSync::JobBatches
|
|
144
144
|
if next_job[:chain_link].present?
|
145
145
|
# Annotate Batch with chain-step info
|
146
146
|
batch.context["csb:chain_link"] = next_job[:chain_link]
|
147
|
-
# TODO Add Fiber Batch to chain-link
|
148
|
-
# With the exception of the top of the Chain, all nested ManagedBatch Roots should be within a Fiber,
|
149
|
-
# so we shouldn't really need to make the Root checkin with the chain
|
150
|
-
# ...except to cleanup the chain
|
151
147
|
batch.on(:complete, "#{ChainBuilder.to_s}.chain_step_complete", chain_link: next_job[:chain_link])
|
152
148
|
end
|
153
149
|
|
@@ -6,6 +6,8 @@ module CanvasSync::JobUniqueness
|
|
6
6
|
context_class.new(data, **kwargs)
|
7
7
|
end
|
8
8
|
|
9
|
+
attr_reader :lock_id
|
10
|
+
|
9
11
|
# { job_clazz, jid, queue, args?, kwargs?, base_key? }
|
10
12
|
def initialize(data, job_instance: nil, config: nil)
|
11
13
|
@base_key = data[:base_key]
|
@@ -14,9 +16,7 @@ module CanvasSync::JobUniqueness
|
|
14
16
|
@config = config || @context_data[:config]
|
15
17
|
|
16
18
|
# TODO Consider (somewhere) updating the lock_id to the BID of the wrapping Batch (when applicable)
|
17
|
-
@lock_id ||= data[:lid] || Thread.current[:unique_jobs_previous_context]&.lock_id
|
18
|
-
@lock_id_locked = @lock_id.present?
|
19
|
-
@lock_id ||= job_id
|
19
|
+
@lock_id ||= data[:lid] || Thread.current[:unique_jobs_previous_context]&.lock_id || job_id
|
20
20
|
end
|
21
21
|
|
22
22
|
# This is primarily for rehydrating in a Batch Callback, so it is unlikely that args and kwargs are needed.
|
@@ -84,18 +84,6 @@ module CanvasSync::JobUniqueness
|
|
84
84
|
end
|
85
85
|
end
|
86
86
|
|
87
|
-
def lock_id
|
88
|
-
@lock_id_locked = true
|
89
|
-
@lock_id
|
90
|
-
end
|
91
|
-
|
92
|
-
def lock_id=(new_id)
|
93
|
-
raise "Lock ID already set" if @lock_id_locked
|
94
|
-
return unless new_id.present?
|
95
|
-
@lock_id = new_id
|
96
|
-
@lock_id_locked = true
|
97
|
-
end
|
98
|
-
|
99
87
|
def job_id
|
100
88
|
@context_data[:jid]
|
101
89
|
end
|
@@ -12,17 +12,14 @@ module CanvasSync
|
|
12
12
|
# Override the delta-syncing date if:
|
13
13
|
# 1. the Term hasn't been synced before or
|
14
14
|
# 2. the Term underwent a period of not syncing
|
15
|
+
term_last_sync = CanvasSync.redis.get(self.class.last_sync_key(term_id))
|
15
16
|
if batch_context[:updated_after]
|
16
|
-
term_last_sync = CanvasSync.redis.get(self.class.last_sync_key(term_id))
|
17
17
|
if !term_last_sync.present? || batch_context[:updated_after] > term_last_sync
|
18
18
|
local_context[:updated_after] = term_last_sync.presence
|
19
19
|
end
|
20
20
|
end
|
21
21
|
|
22
22
|
JobBatches::ManagedBatchJob.make_batch(jobs, ordered: false, concurrency: true) do |b|
|
23
|
-
# TODO If we do a Chain UI, this will need to checkin somehow to indicate that the chain forked
|
24
|
-
# Or chain steps just show a summary - eg "Started", "X Jobs Running", "Done" or "X Jobs Running, Y Jobs Done" - and not individual forks
|
25
|
-
# For a step to be considered done, all previous sibling-level steps must be done and no batches pending
|
26
23
|
b.description = "TermBatchJob(#{term_id}) Root"
|
27
24
|
b.context = local_context
|
28
25
|
b.on(:success, "#{self.class.to_s}.batch_finished") unless options[:mark_synced] == false
|
data/lib/canvas_sync/version.rb
CHANGED