canvas_sync 0.17.0.beta9 → 0.17.0.beta14
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 +14 -0
- data/db/migrate/20201030210836_add_full_sync_to_canvas_sync_sync_batch.rb +7 -0
- data/lib/canvas_sync.rb +16 -2
- data/lib/canvas_sync/importers/bulk_importer.rb +2 -2
- data/lib/canvas_sync/job_batches/batch.rb +13 -13
- data/lib/canvas_sync/job_batches/chain_builder.rb +22 -2
- data/lib/canvas_sync/jobs/begin_sync_chain_job.rb +51 -3
- data/lib/canvas_sync/jobs/report_starter.rb +2 -1
- data/lib/canvas_sync/jobs/sync_provisioning_report_job.rb +7 -21
- data/lib/canvas_sync/version.rb +1 -1
- data/spec/canvas_sync/canvas_sync_spec.rb +10 -10
- data/spec/canvas_sync/jobs/sync_provisioning_report_job_spec.rb +5 -7
- data/spec/dummy/db/schema.rb +4 -1
- data/spec/dummy/log/development.log +474 -0
- data/spec/dummy/log/test.log +14962 -0
- metadata +3 -3
- data/lib/canvas_sync/concerns/role/launch_querying.rb +0 -42
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 28f1b87e0b26bf68a13a9a1558d8c78746a4058d2fa8a822dc62c5b3b7ad92ea
|
4
|
+
data.tar.gz: 0bbc9ec37d31e848a8ec4997dc91391720deb826b2a93b52528661193788ebe4
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
|
data/lib/canvas_sync.rb
CHANGED
@@ -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(
|
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 = {
|
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 =
|
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
|
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
|
-
|
375
|
-
|
376
|
-
|
377
|
-
|
378
|
-
|
379
|
-
|
380
|
-
|
381
|
-
|
382
|
-
|
383
|
-
|
384
|
-
|
385
|
-
|
386
|
-
|
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
|
-
|
64
|
-
|
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
|
-
|
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 <
|
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
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
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
|
data/lib/canvas_sync/version.rb
CHANGED
@@ -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
|
-
|
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
|
-
|
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(
|
data/spec/dummy/db/schema.rb
CHANGED
@@ -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:
|
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|
|