canvas_sync 0.22.5 → 0.22.8
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.
- 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