canvas_sync 0.17.0.beta9 → 0.17.0.beta14

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 19cb0891aa47a07315b1a27acb63dc16e173afd018ff68a4503259b292928580
4
- data.tar.gz: 44394476cf446ee97bcafe471153f53c0a4fbb50c8ce3da8c0a1152201ca4480
3
+ metadata.gz: 28f1b87e0b26bf68a13a9a1558d8c78746a4058d2fa8a822dc62c5b3b7ad92ea
4
+ data.tar.gz: 0bbc9ec37d31e848a8ec4997dc91391720deb826b2a93b52528661193788ebe4
5
5
  SHA512:
6
- metadata.gz: ee2ee669ada624911c67dc22f7aa52e781f03c58904625011a37713b595af7d6348e82be801d22dca1801c623e7316a52fae71f494c727e0b1ce3bd8e31772e0
7
- data.tar.gz: d3ef2a3d77d28ba3aa4e343ed051633926c4df7aba2619a798b6ea7e81d9699c5d05e9fa6bfd72d157bde926acacf63e1a8546bc7df66f49093bd89dd057444d
6
+ metadata.gz: 1cf0b4e64133d94b2841e77399a0dc3a94de42e17832587af14144d9ed28fb1df9953df3486590473abbe9745a8b919c4f910155ff71abe8eb8cc373d0d2704a
7
+ data.tar.gz: 166edeb5b97e5879b70eebbdf9a3f9848bc91de5a71c5cb6066860b4a6001a272faec24ddf78661d9a5322b3234d1dc69a214bf1e8e8d2c7c16cec6336f01e7a
data/README.md CHANGED
@@ -103,6 +103,20 @@ It may be one of the following values:
103
103
  * An ISO-8601 Date - Will pass the supplied date ad the `updated_after` param for the requested reports
104
104
  * `true` (Default) - Will use the start date of the last successful sync
105
105
 
106
+ If `updated_after` is true, CanvasSync will, by default, perform a full sync every other Sunday.
107
+ This logic can be customized by passing `full_sync_every` parameter.
108
+ If you pass a date to `updated_after`, this logic will be disabled unless you explicitly pass a `full_sync_every` parameter.
109
+ `full_sync_every` accepts the following format strings:
110
+ - `15%` - Each sync will have a 15% chance of running a full sync
111
+ - `10 days` - A full sync will be run every 10 days
112
+ - `sunday` - A full sync will run every Sunday
113
+ - `saturday/4` - A full sync will run every fourth Saturday
114
+
115
+ #### Multiple Sync Chains
116
+ If your app uses multiple Sync Chains, you may run into issues with the automatic `updated_after` and `full_sync_every` logic.
117
+ You can fix this by using custom logic or by setting the `batch_genre` parameter when creating the Job Chain. Chains will only
118
+ use chains of the same genre when computing `updated_after` and `full_sync_every`.
119
+
106
120
  ### Extensible chain
107
121
  It is sometimes desired to extend or customize the chain of jobs that are run with CanvasSync.
108
122
  This can be achieved with the following pattern:
@@ -0,0 +1,7 @@
1
+ class AddFullSyncToCanvasSyncSyncBatch < CanvasSync::MiscHelper::MigrationClass
2
+ def change
3
+ add_column :canvas_sync_sync_batches, :full_sync, :boolean, default: false
4
+ add_column :canvas_sync_sync_batches, :batch_genre, :string
5
+ add_column :canvas_sync_sync_batches, :batch_bid, :string
6
+ end
7
+ end
@@ -106,7 +106,16 @@ module CanvasSync
106
106
  # @param account_id [Integer, nil] This optional parameter can be used if your Term creation and
107
107
  # canvas_sync_client methods require an account ID.
108
108
  # @return [Hash]
109
- def default_provisioning_report_chain(models, term_scope: nil, legacy_support: false, account_id: nil, updated_after: nil, options: {}) # rubocop:disable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity, Metrics/LineLength
109
+ def default_provisioning_report_chain(
110
+ models,
111
+ term_scope: nil,
112
+ legacy_support: false,
113
+ account_id: nil,
114
+ updated_after: nil,
115
+ full_sync_every: nil,
116
+ batch_genre: nil,
117
+ options: {}
118
+ ) # rubocop:disable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity, Metrics/LineLength
110
119
  return unless models.present?
111
120
  models.map! &:to_s
112
121
  term_scope = term_scope.to_s if term_scope
@@ -179,7 +188,12 @@ module CanvasSync
179
188
  # Wrap it all up
180
189
  ###############################
181
190
 
182
- global_options = { legacy_support: legacy_support, updated_after: updated_after }
191
+ global_options = {
192
+ legacy_support: legacy_support,
193
+ updated_after: updated_after,
194
+ full_sync_every: full_sync_every,
195
+ batch_genre: batch_genre,
196
+ }
183
197
  global_options[:account_id] = account_id if account_id.present?
184
198
  global_options.merge!(options[:global]) if options[:global].present?
185
199
 
@@ -28,7 +28,7 @@ module CanvasSync
28
28
  database_column_names = mapping.values.map { |value| value[:database_column_name] }
29
29
  rows = []
30
30
  row_ids = {}
31
- database_conflict_column_name = Array(conflict_target).map { |ct| mapping[ct][:database_column_name] }
31
+ database_conflict_column_name = conflict_target ? mapping[conflict_target][:database_column_name] : nil
32
32
 
33
33
  CSV.foreach(report_file_path, headers: true, header_converters: :symbol) do |row|
34
34
  row = yield(row) if block_given?
@@ -67,7 +67,7 @@ module CanvasSync
67
67
  condition: condition_sql(klass, columns),
68
68
  columns: columns
69
69
  }
70
- update_conditions[:conflict_target] = conflict_target if conflict_target.present?
70
+ update_conditions[:conflict_target] = conflict_target if conflict_target
71
71
 
72
72
  options = { validate: false, on_duplicate_key_update: update_conditions }.merge(import_args)
73
73
 
@@ -371,19 +371,19 @@ module CanvasSync
371
371
 
372
372
  def cleanup_redis(bid)
373
373
  logger.debug {"Cleaning redis of batch #{bid}"}
374
- # redis do |r|
375
- # r.del(
376
- # "BID-#{bid}",
377
- # "BID-#{bid}-callbacks-complete",
378
- # "BID-#{bid}-callbacks-success",
379
- # "BID-#{bid}-failed",
380
-
381
- # "BID-#{bid}-batches-success",
382
- # "BID-#{bid}-batches-complete",
383
- # "BID-#{bid}-batches-failed",
384
- # "BID-#{bid}-jids",
385
- # )
386
- # end
374
+ redis do |r|
375
+ r.del(
376
+ "BID-#{bid}",
377
+ "BID-#{bid}-callbacks-complete",
378
+ "BID-#{bid}-callbacks-success",
379
+ "BID-#{bid}-failed",
380
+
381
+ "BID-#{bid}-batches-success",
382
+ "BID-#{bid}-batches-complete",
383
+ "BID-#{bid}-batches-failed",
384
+ "BID-#{bid}-jids",
385
+ )
386
+ end
387
387
  end
388
388
 
389
389
  def redis(*args, &blk)
@@ -60,10 +60,12 @@ module CanvasSync
60
60
  raise "Could not find a \"#{relative_to}\" job in the chain" if matching_jobs.count == 0
61
61
  raise "Found multiple \"#{relative_to}\" jobs in the chain" if matching_jobs.count > 1
62
62
 
63
- parent_job, sub_index = matching_jobs[0]
64
- chain = self.class.get_chain_parameter(parent_job)
63
+ relative_job, sub_index = matching_jobs[0]
64
+ parent_job = find_parent_job(relative_job)
65
65
  needed_parent_type = placement == :with ? ConcurrentBatchJob : SerialBatchJob
66
66
 
67
+ chain = self.class.get_chain_parameter(parent_job)
68
+
67
69
  if parent_job[:job] != needed_parent_type
68
70
  old_job = chain[sub_index]
69
71
  parent_job = chain[sub_index] = {
@@ -129,6 +131,24 @@ module CanvasSync
129
131
  end
130
132
  end
131
133
 
134
+ def find_parent_job(job_def)
135
+ iterate_job_tree do |job, path|
136
+ return path[-1] if job == job_def
137
+ end
138
+ nil
139
+ end
140
+
141
+ def iterate_job_tree(root: self.base_job, path: [], &blk)
142
+ blk.call(root, path)
143
+
144
+ if self.class._job_type_definitions[root[:job]]
145
+ sub_jobs = self.class.get_chain_parameter(root)
146
+ sub_jobs.each_with_index do |sub_job, i|
147
+ iterate_job_tree(root: sub_job, path: [*path, root], &blk)
148
+ end
149
+ end
150
+ end
151
+
132
152
  class << self
133
153
  def _job_type_definitions
134
154
  @job_type_definitions ||= {}
@@ -1,28 +1,76 @@
1
1
  module CanvasSync
2
2
  module Jobs
3
3
  class BeginSyncChainJob < CanvasSync::Job
4
+ attr_reader :globals
5
+
4
6
  def perform(chain_definition, globals = {})
7
+ @globals = globals
8
+
5
9
  if !globals[:updated_after].present? || globals[:updated_after] == true
6
- last_batch = SyncBatch.where(status: 'completed').last
10
+ last_batch = SyncBatch.where(status: 'completed', batch_genre: genre).last
11
+ globals[:full_sync_every] ||= "sunday/2"
7
12
  globals[:updated_after] = last_batch&.started_at&.iso8601
8
13
  end
9
14
 
15
+ if should_full_sync?(globals[:full_sync_every])
16
+ globals[:updated_after] = nil
17
+ end
18
+
10
19
  sync_batch = SyncBatch.create!(
11
20
  started_at: DateTime.now,
12
- status: 'pending',
21
+ full_sync: globals[:updated_after] == nil,
22
+ batch_genre: genre,
23
+ status: 'processing',
13
24
  )
14
25
 
15
26
  JobBatches::Batch.new.tap do |b|
16
- b.description = "CanvasSync Root Batch"
27
+ b.description = "CanvasSync Root Batch (SyncBatch##{sync_batch.id})"
17
28
  b.on(:complete, "#{self.class.to_s}.batch_completed", sync_batch_id: sync_batch.id)
18
29
  b.on(:success, "#{self.class.to_s}.batch_completed", sync_batch_id: sync_batch.id)
19
30
  b.context = globals
20
31
  b.jobs do
21
32
  JobBatches::SerialBatchJob.perform_now(chain_definition)
22
33
  end
34
+ sync_batch.update(batch_bid: b.bid)
35
+ end
36
+ end
37
+
38
+ def should_full_sync?(opt)
39
+ return true unless last_full_sync.present?
40
+ return false unless opt.is_a?(String)
41
+
42
+ case r.strip
43
+ when %r{^(sunday|monday|tuesday|wednesday|thursday|friday|saturday)(?:/(\d+))?$}
44
+ m = Regexp.last_match
45
+ day = m[1]
46
+ skip = m[2] || "1"
47
+ Date.new.send(:"#{day}?") && last_full_sync.end_of_day <= (skip.to_i.weeks.ago.end_of_day)
48
+ when opt.match?(%r{^(\d+)\%$})
49
+ m = Regexp.last_match
50
+ rand(100) < m[1].to_i
51
+ when opt.match?(%r{^(\d+) ?days$})
52
+ m = Regexp.last_match
53
+ last_full_sync.end_of_day <= m[1].to_i.days.ago.end_of_day
54
+ when opt.match?(%r{^(\d+)$}) # N.days is converted to a string of seconds
55
+ m = Regexp.last_match
56
+ last_full_sync.end_of_day <= m[1].to_i.seconds.ago.end_of_day
57
+ else
58
+ false
23
59
  end
24
60
  end
25
61
 
62
+ def last_full_sync_record
63
+ @last_full_sync_record ||= SyncBatch.where(status: 'completed', full_sync: true, batch_genre: genre).last
64
+ end
65
+
66
+ def last_full_sync
67
+ last_full_sync_record&.started_at
68
+ end
69
+
70
+ def genre
71
+ globals[:batch_genre] || "default"
72
+ end
73
+
26
74
  def self.batch_completed(status, options)
27
75
  sbatch = SyncBatch.find(options['sync_batch_id'])
28
76
  sbatch.update!(
@@ -35,7 +35,7 @@ module CanvasSync
35
35
  protected
36
36
 
37
37
  def merge_report_params(options={}, params={}, term_scope: true)
38
- term_scope = batch_context[:canvas_term_id] if term_scope == true
38
+ term_scope = options[:canvas_term_id] || batch_context[:canvas_term_id] if term_scope == true
39
39
  if term_scope.present?
40
40
  params[:enrollment_term_id] = term_scope
41
41
  end
@@ -43,6 +43,7 @@ module CanvasSync
43
43
  params[:updated_after] = updated_after
44
44
  end
45
45
  params.merge!(options[:report_params]) if options[:report_params].present?
46
+ params.merge!(options[:report_parameters]) if options[:report_parameters].present?
46
47
  { parameters: params }
47
48
  end
48
49
 
@@ -1,23 +1,8 @@
1
1
  module CanvasSync
2
2
  module Jobs
3
3
  # ActiveJob class that starts a Canvas provisioning report
4
- class SyncProvisioningReportJob < CanvasSync::Job
4
+ class SyncProvisioningReportJob < ReportStarter
5
5
  def perform(options)
6
- start_report(report_params(options), options)
7
- end
8
-
9
- protected
10
-
11
- def start_report(report_params, options)
12
- CanvasSync::Jobs::ReportStarter.perform_later(
13
- "proservices_provisioning_csv",
14
- report_params,
15
- CanvasSync::Processors::ProvisioningReportProcessor.to_s,
16
- options,
17
- )
18
- end
19
-
20
- def report_params(options, canvas_term_id = options[:canvas_term_id] || batch_context[:canvas_term_id])
21
6
  params = {
22
7
  include_deleted: true,
23
8
  }
@@ -28,11 +13,12 @@ module CanvasSync
28
13
  params[model] = true
29
14
  end
30
15
 
31
- params[:enrollment_term_id] = canvas_term_id if canvas_term_id
32
-
33
- params.merge!(options[:report_parameters]) if options[:report_parameters].present?
34
-
35
- { parameters: params }
16
+ super(
17
+ "proservices_provisioning_csv",
18
+ merge_report_params(options, params, {}),
19
+ CanvasSync::Processors::ProvisioningReportProcessor.to_s,
20
+ options,
21
+ )
36
22
  end
37
23
  end
38
24
  end
@@ -1,3 +1,3 @@
1
1
  module CanvasSync
2
- VERSION = "0.17.0.beta9".freeze
2
+ VERSION = "0.17.0.beta14".freeze
3
3
  end
@@ -42,7 +42,7 @@ RSpec.describe CanvasSync do
42
42
  ]
43
43
  }]}
44
44
  ]]}
45
- ], {:legacy_support=>false, :updated_after=>nil, :d=>4}],
45
+ ], {:legacy_support=>false, :updated_after=>nil, :full_sync_every=>nil, :batch_genre=>nil, :d=>4}],
46
46
  })
47
47
  end
48
48
 
@@ -61,7 +61,7 @@ RSpec.describe CanvasSync do
61
61
  ]
62
62
  }]}
63
63
  ]]}
64
- ], {:legacy_support=>false, :updated_after=>nil}]
64
+ ], {:legacy_support=>false, :updated_after=>nil, :full_sync_every=>nil, :batch_genre=>nil}]
65
65
  })
66
66
  end
67
67
  end
@@ -80,7 +80,7 @@ RSpec.describe CanvasSync do
80
80
  ]
81
81
  }]}
82
82
  ]]}
83
- ], {:legacy_support=>false, :updated_after=>nil}],
83
+ ], {:legacy_support=>false, :updated_after=>nil, :full_sync_every=>nil, :batch_genre=>nil}],
84
84
  })
85
85
  end
86
86
  end
@@ -100,7 +100,7 @@ RSpec.describe CanvasSync do
100
100
  ]
101
101
  }]}
102
102
  ]]}
103
- ], {:legacy_support=>false, :updated_after=>nil}],
103
+ ], {:legacy_support=>false, :updated_after=>nil, :full_sync_every=>nil, :batch_genre=>nil}],
104
104
  })
105
105
  end
106
106
  end
@@ -120,7 +120,7 @@ RSpec.describe CanvasSync do
120
120
  ]
121
121
  }]}
122
122
  ]]}
123
- ], {:legacy_support=>false, :updated_after=>nil}],
123
+ ], {:legacy_support=>false, :updated_after=>nil, :full_sync_every=>nil, :batch_genre=>nil}],
124
124
  })
125
125
  end
126
126
  end
@@ -140,7 +140,7 @@ RSpec.describe CanvasSync do
140
140
  ]
141
141
  }]}
142
142
  ]]}
143
- ], {:legacy_support=>false, :updated_after=>nil}],
143
+ ], {:legacy_support=>false, :updated_after=>nil, :full_sync_every=>nil, :batch_genre=>nil}],
144
144
  })
145
145
  end
146
146
  end
@@ -160,7 +160,7 @@ RSpec.describe CanvasSync do
160
160
  ]
161
161
  }]}
162
162
  ]]}
163
- ], {:legacy_support=>false, :updated_after=>nil}],
163
+ ], {:legacy_support=>false, :updated_after=>nil, :full_sync_every=>nil, :batch_genre=>nil}],
164
164
  })
165
165
  end
166
166
  end
@@ -181,7 +181,7 @@ RSpec.describe CanvasSync do
181
181
  ]
182
182
  }]}
183
183
  ]]}
184
- ], {:legacy_support=>false, :updated_after=>nil}],
184
+ ], {:legacy_support=>false, :updated_after=>nil, :full_sync_every=>nil, :batch_genre=>nil}],
185
185
  )
186
186
  end
187
187
  end
@@ -202,7 +202,7 @@ RSpec.describe CanvasSync do
202
202
  ]
203
203
  }]}
204
204
  ]]}
205
- ], {:legacy_support=>false, :updated_after=>nil}],
205
+ ], {:legacy_support=>false, :updated_after=>nil, :full_sync_every=>nil, :batch_genre=>nil}],
206
206
  )
207
207
  end
208
208
  end
@@ -223,7 +223,7 @@ RSpec.describe CanvasSync do
223
223
  ]
224
224
  }]}
225
225
  ]]}
226
- ], {:legacy_support=>false, :updated_after=>nil}],
226
+ ], {:legacy_support=>false, :updated_after=>nil, :full_sync_every=>nil, :batch_genre=>nil}],
227
227
  )
228
228
  end
229
229
  end
@@ -6,19 +6,18 @@ RSpec.describe CanvasSync::Jobs::SyncProvisioningReportJob do
6
6
  let!(:term) { FactoryGirl.create(:term) }
7
7
 
8
8
  it 'enqueues a ReportStarter for a provisioning report for the specified models for each term' do
9
- expect(CanvasSync::Jobs::ReportStarter).to receive(:perform_later)
9
+ expect_any_instance_of(CanvasSync::Jobs::ReportStarter).to receive(:start_report)
10
10
  .with(
11
+ 'self',
11
12
  'proservices_provisioning_csv',
12
13
  {
13
14
  parameters: {
14
15
  include_deleted: true,
15
16
  'users' => true,
16
17
  'courses' => true,
17
- enrollment_term_id: term.canvas_id
18
+ enrollment_term_id: term.canvas_id,
18
19
  }
19
20
  },
20
- CanvasSync::Processors::ProvisioningReportProcessor.to_s,
21
- { models: ['users', 'courses'], term_scope: 'active' }
22
21
  )
23
22
 
24
23
  set_batch_context(canvas_term_id: term.canvas_id)
@@ -30,8 +29,9 @@ RSpec.describe CanvasSync::Jobs::SyncProvisioningReportJob do
30
29
 
31
30
  context 'a term scope is not specified' do
32
31
  it 'enqueues a single ReportStarter for a provisioning report across all terms for the specified models' do
33
- expect(CanvasSync::Jobs::ReportStarter).to receive(:perform_later)
32
+ expect_any_instance_of(CanvasSync::Jobs::ReportStarter).to receive(:start_report)
34
33
  .with(
34
+ 'self',
35
35
  'proservices_provisioning_csv',
36
36
  {
37
37
  parameters: {
@@ -40,8 +40,6 @@ RSpec.describe CanvasSync::Jobs::SyncProvisioningReportJob do
40
40
  'courses' => true,
41
41
  }
42
42
  },
43
- CanvasSync::Processors::ProvisioningReportProcessor.to_s,
44
- { models: ['users', 'courses'] }
45
43
  )
46
44
 
47
45
  CanvasSync::Jobs::SyncProvisioningReportJob.perform_now(
@@ -10,7 +10,7 @@
10
10
  #
11
11
  # It's strongly recommended that you check this file into your version control system.
12
12
 
13
- ActiveRecord::Schema.define(version: 2020_10_18_210836) do
13
+ ActiveRecord::Schema.define(version: 2020_10_30_210836) do
14
14
 
15
15
  # These are extensions that must be enabled in order to support this database
16
16
  enable_extension "plpgsql"
@@ -105,6 +105,9 @@ ActiveRecord::Schema.define(version: 2020_10_18_210836) do
105
105
  t.string "status"
106
106
  t.datetime "created_at"
107
107
  t.datetime "updated_at"
108
+ t.boolean "full_sync", default: false
109
+ t.string "batch_genre"
110
+ t.string "batch_bid"
108
111
  end
109
112
 
110
113
  create_table "context_module_items", force: :cascade do |t|