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 +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|
|