canvas_sync 0.17.31 → 0.17.34
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/lib/canvas_sync/concerns/sync_mapping.rb +1 -1
- data/lib/canvas_sync/engine.rb +20 -16
- data/lib/canvas_sync/generators/templates/migrations/create_learning_outcomes.rb +30 -0
- data/lib/canvas_sync/generators/templates/models/learning_outcome.rb +22 -0
- data/lib/canvas_sync/job_batches/batch.rb +21 -1
- data/lib/canvas_sync/job_batches/callback.rb +1 -1
- data/lib/canvas_sync/job_batches/pool.rb +4 -4
- data/lib/canvas_sync/job_batches/sidekiq/web/helpers.rb +1 -1
- data/lib/canvas_sync/job_batches/sidekiq/web/views/batch.erb +10 -0
- data/lib/canvas_sync/job_batches/sidekiq/web.rb +5 -1
- data/lib/canvas_sync/processors/model_mappings.yml +55 -0
- data/lib/canvas_sync/processors/provisioning_report_processor.rb +7 -0
- data/lib/canvas_sync/processors/report_processor.rb +3 -2
- data/lib/canvas_sync/version.rb +1 -1
- data/lib/canvas_sync.rb +1 -0
- data/spec/canvas_sync/processors/provisioning_report_processor_spec.rb +6 -0
- data/spec/dummy/app/models/learning_outcome.rb +28 -0
- data/spec/dummy/db/migrate/20220712210559_create_learning_outcomes.rb +36 -0
- data/spec/dummy/db/schema.rb +25 -1
- data/spec/dummy/log/development.log +148 -2028
- data/spec/dummy/log/test.log +4654 -101591
- data/spec/support/fixtures/reports/learning_outcomes.csv +3 -0
- metadata +14 -6
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 8029fc55239810f8064f8d1c9fb4cd1f1756a7605581e7aced13e150e69c8b6b
|
4
|
+
data.tar.gz: 2deac9eb222ed8c636159c8100e8aa6cd59d2fa26d3b13b83c7be31cf7cbc0bd
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: b1d70b6a2493a49a9eeb48532e9438d02209d3de9609afa0a9bd09a5fe4c9816dcd9b3d7724ffa2385cd65f19b873901cce5fe5b733fa534e4c96ce785146f91
|
7
|
+
data.tar.gz: c985b3da6ca96c4120b852f0b4df938a6393a797dd8365ce3c9e61404b67623484930b9001e13172309399e745774bd63208097954d569e094f86c4a56e94e38
|
data/lib/canvas_sync/engine.rb
CHANGED
@@ -32,24 +32,28 @@ module CanvasSync
|
|
32
32
|
initializer :integrate_pandapal do
|
33
33
|
require 'panda_pal'
|
34
34
|
|
35
|
-
|
36
|
-
if PandaPal::Organization.respond_to?(:
|
37
|
-
PandaPal::Organization.
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
35
|
+
Rails.application.reloader.to_prepare do
|
36
|
+
if PandaPal::Organization.respond_to?(:scheduled_task)
|
37
|
+
if PandaPal::Organization.respond_to?(:define_setting)
|
38
|
+
PandaPal::Organization.define_setting(:canvas_sync, {
|
39
|
+
type: 'Hash',
|
40
|
+
required: false,
|
41
|
+
properties: {
|
42
|
+
job_log_retention: { **RETENTION_TYPE },
|
43
|
+
sync_batch_retention: { **RETENTION_TYPE },
|
44
|
+
}
|
45
|
+
})
|
46
|
+
end
|
46
47
|
|
47
|
-
|
48
|
-
|
49
|
-
|
48
|
+
unless PandaPal::Organization.task_scheduled?(:clean_canvas_sync_logs)
|
49
|
+
PandaPal::Organization.scheduled_task '0 0 3 * * *', :clean_canvas_sync_logs do
|
50
|
+
job_log_retention = ChronicDuration.parse(settings.dig(:canvas_sync, :job_log_retention) || '3 months', keep_zero: true).seconds.ago
|
51
|
+
JobLog.where('updated_at < ?', job_log_retention).delete_all
|
50
52
|
|
51
|
-
|
52
|
-
|
53
|
+
sync_batch_retention = ChronicDuration.parse(settings.dig(:canvas_sync, :sync_batch_retention) || '6 months', keep_zero: true).seconds.ago
|
54
|
+
SyncBatch.where('updated_at < ?', sync_batch_retention).delete_all
|
55
|
+
end
|
56
|
+
end
|
53
57
|
end
|
54
58
|
end
|
55
59
|
rescue LoadError
|
@@ -0,0 +1,30 @@
|
|
1
|
+
# <%= autogenerated_migration_warning %>
|
2
|
+
|
3
|
+
class CreateLearningOutcomes < ActiveRecord::Migration[5.1]
|
4
|
+
def change
|
5
|
+
create_table :learning_outcomes do |t|
|
6
|
+
t.bigint :canvas_id, null: false
|
7
|
+
t.integer :canvas_context_id
|
8
|
+
t.string :canvas_context_type
|
9
|
+
t.string :name
|
10
|
+
t.string :friendly_name
|
11
|
+
t.string :workflow_state
|
12
|
+
t.datetime :canvas_created_at
|
13
|
+
t.datetime :canvas_updated_at
|
14
|
+
t.string :migration_id
|
15
|
+
t.string :vendor_guid
|
16
|
+
t.string :low_grade
|
17
|
+
t.string :high_grade
|
18
|
+
t.string :calculation_method
|
19
|
+
t.string :calculation_int
|
20
|
+
t.integer :outcome_import_id
|
21
|
+
t.integer :root_account_ids, array: true, default: []
|
22
|
+
t.text :description
|
23
|
+
|
24
|
+
t.timestamps
|
25
|
+
end
|
26
|
+
|
27
|
+
add_index :learning_outcomes, :canvas_id, unique: true
|
28
|
+
add_index :learning_outcomes, [:canvas_context_id, :canvas_context_type], name: "index_learning_outcomes_on_context"
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# # <%= autogenerated_migration_warning %>
|
4
|
+
|
5
|
+
class LearningOutcome < ApplicationRecord
|
6
|
+
include CanvasSync::Record
|
7
|
+
include CanvasSync::Concerns::ApiSyncable
|
8
|
+
|
9
|
+
belongs_to :context, polymorphic: true, optional: true, primary_key: :canvas_id, foreign_key: :canvas_context_id, foreign_type: :canvas_context_type
|
10
|
+
|
11
|
+
api_syncable({
|
12
|
+
canvas_id: :id,
|
13
|
+
canvas_context_id: :context_id,
|
14
|
+
canvas_context_type: :context_type,
|
15
|
+
name: :title,
|
16
|
+
friendly_name: :display_name,
|
17
|
+
vendor_guid: :vendor_guid,
|
18
|
+
calculation_method: :calculation_method,
|
19
|
+
calculation_int: :calculation_int,
|
20
|
+
description: :description
|
21
|
+
}, ->(api) { api.get("/api/v1/outcomes/#{canvas_id}") })
|
22
|
+
end
|
@@ -28,7 +28,7 @@ module CanvasSync
|
|
28
28
|
|
29
29
|
delegate :redis, to: :class
|
30
30
|
|
31
|
-
BID_EXPIRE_TTL =
|
31
|
+
BID_EXPIRE_TTL = 90.days.to_i
|
32
32
|
SCHEDULE_CALLBACK = RedisScript.new(Pathname.new(__FILE__) + "../schedule_callback.lua")
|
33
33
|
BID_HIERARCHY = RedisScript.new(Pathname.new(__FILE__) + "../hier_batch_ids.lua")
|
34
34
|
|
@@ -101,6 +101,8 @@ module CanvasSync
|
|
101
101
|
r.hincrby("BID-#{parent_bid}", "children", 1)
|
102
102
|
r.expire("BID-#{parent_bid}", BID_EXPIRE_TTL)
|
103
103
|
r.zadd("BID-#{parent_bid}-bids", created_at, bid)
|
104
|
+
else
|
105
|
+
r.zadd("BID-ROOT-bids", created_at, bid)
|
104
106
|
end
|
105
107
|
end
|
106
108
|
|
@@ -369,6 +371,7 @@ module CanvasSync
|
|
369
371
|
logger.debug {"Cleaning redis of batch #{bid}"}
|
370
372
|
redis do |r|
|
371
373
|
r.zrem("batches", bid)
|
374
|
+
r.zrem("BID-ROOT-bids", bid)
|
372
375
|
r.unlink(
|
373
376
|
"BID-#{bid}",
|
374
377
|
"BID-#{bid}-callbacks-complete",
|
@@ -450,6 +453,23 @@ module CanvasSync
|
|
450
453
|
end
|
451
454
|
end
|
452
455
|
|
456
|
+
def uget(key)
|
457
|
+
Batch.redis do |r|
|
458
|
+
case r.type(key)
|
459
|
+
when 'string'
|
460
|
+
r.get(key)
|
461
|
+
when 'list'
|
462
|
+
r.lrange(key, 0, -1)
|
463
|
+
when 'hash'
|
464
|
+
r.hgetall(key)
|
465
|
+
when 'set'
|
466
|
+
r.smembers(key)
|
467
|
+
when 'zset'
|
468
|
+
r.smembers(key, 0, -1)
|
469
|
+
end
|
470
|
+
end
|
471
|
+
end
|
472
|
+
|
453
473
|
def method_missing(method_name, *arguments, &block)
|
454
474
|
Batch.redis do |r|
|
455
475
|
r.send(method_name, *arguments, &block)
|
@@ -27,7 +27,7 @@ module CanvasSync
|
|
27
27
|
|
28
28
|
if clazz && object = Object.const_get(clazz)
|
29
29
|
target = target == :instance ? object.new : object
|
30
|
-
if target.respond_to?(method)
|
30
|
+
if target.respond_to?(method, true)
|
31
31
|
target.send(method, status, opts)
|
32
32
|
else
|
33
33
|
Batch.logger.warn("Invalid callback method #{definition} - #{target.to_s} does not respond to #{method}")
|
@@ -44,7 +44,7 @@ module CanvasSync
|
|
44
44
|
wrapper.on(checkin_event, "#{self.class.to_s}.job_checked_in", pool_id: pid)
|
45
45
|
wrapper.jobs {}
|
46
46
|
|
47
|
-
job_desc = job_desc.
|
47
|
+
job_desc = job_desc.symbolize_keys
|
48
48
|
job_desc = job_desc.merge!(
|
49
49
|
job: job_desc[:job].to_s,
|
50
50
|
pool_wrapper_batch: wrapper.bid,
|
@@ -149,7 +149,7 @@ module CanvasSync
|
|
149
149
|
if current_count < limit
|
150
150
|
job_desc = pop_job_from_pool
|
151
151
|
if job_desc.present?
|
152
|
-
Batch.new(job_desc[
|
152
|
+
Batch.new(job_desc[:pool_wrapper_batch]).jobs do
|
153
153
|
ChainBuilder.enqueue_job(job_desc)
|
154
154
|
end
|
155
155
|
jobs_added += 1
|
@@ -170,7 +170,7 @@ module CanvasSync
|
|
170
170
|
def push_job_to_pool(job_desc)
|
171
171
|
jobs_key = "#{redis_key}-jobs"
|
172
172
|
# This allows duplicate jobs when a Redis Set is used
|
173
|
-
job_desc[
|
173
|
+
job_desc[:_pool_random_key_] = SecureRandom.urlsafe_base64(10)
|
174
174
|
job_json = JSON.unparse(ActiveJob::Arguments.serialize([job_desc]))
|
175
175
|
order = self.order
|
176
176
|
|
@@ -204,7 +204,7 @@ module CanvasSync
|
|
204
204
|
|
205
205
|
return nil unless job_json.present?
|
206
206
|
|
207
|
-
ActiveJob::Arguments.deserialize(JSON.parse(job_json))[0]
|
207
|
+
ActiveJob::Arguments.deserialize(JSON.parse(job_json))[0]&.symbolize_keys
|
208
208
|
end
|
209
209
|
|
210
210
|
def self.redis(&blk)
|
@@ -10,6 +10,16 @@
|
|
10
10
|
<th colspan="2" scope=row><%= t('Batch') %></td>
|
11
11
|
<td><%= @batch.bid %></td>
|
12
12
|
</tr>
|
13
|
+
<tr>
|
14
|
+
<th colspan="2" scope=row><%= t('Parent') %></td>
|
15
|
+
<td>
|
16
|
+
<% if @batch.parent_bid.present? %>
|
17
|
+
<a href="<%= root_path %>batches/<%= @batch.parent_bid %>"><%= @batch.parent_bid %></a>
|
18
|
+
<% else %>
|
19
|
+
ROOT
|
20
|
+
<% end %>
|
21
|
+
</td>
|
22
|
+
</tr>
|
13
23
|
<tr>
|
14
24
|
<th colspan="2" scope=row><%= t('Started') %></td>
|
15
25
|
<td><%= safe_relative_time(@batch.created_at.to_f) %></td>
|
@@ -10,6 +10,8 @@ require_relative "web/helpers"
|
|
10
10
|
module CanvasSync::JobBatches::Sidekiq
|
11
11
|
module Web
|
12
12
|
DEV_MODE = (defined?(Rails) && !Rails.env.production?) || !!ENV["SIDEKIQ_WEB_TESTING"]
|
13
|
+
Sidekiq::WebHelpers::SAFE_QPARAMS << 'all_batches'
|
14
|
+
Sidekiq::WebHelpers::SAFE_QPARAMS << 'count'
|
13
15
|
|
14
16
|
def self.registered(app) # rubocop:disable Metrics/MethodLength, Metrics/AbcSize
|
15
17
|
app.helpers do
|
@@ -24,7 +26,9 @@ module CanvasSync::JobBatches::Sidekiq
|
|
24
26
|
|
25
27
|
app.get "/batches" do
|
26
28
|
@count = (params['count'] || 25).to_i
|
27
|
-
|
29
|
+
|
30
|
+
source_key = params['all_batches'] ? "batches" : "BID-ROOT-bids"
|
31
|
+
@current_page, @total_size, @batches = page(source_key, params['page'], @count)
|
28
32
|
@batches = @batches.map {|b, score| CanvasSync::JobBatches::Batch.new(b) }
|
29
33
|
|
30
34
|
erb(get_template(:batches))
|
@@ -509,3 +509,58 @@ content_migrations:
|
|
509
509
|
canvas_root_account_id:
|
510
510
|
database_column_name: canvas_root_account_id
|
511
511
|
type: integer
|
512
|
+
|
513
|
+
learning_outcomes:
|
514
|
+
conflict_target: learning_outcome_id
|
515
|
+
report_columns:
|
516
|
+
learning_outcome_id:
|
517
|
+
database_column_name: canvas_id
|
518
|
+
type: integer
|
519
|
+
context_id:
|
520
|
+
database_column_name: canvas_context_id
|
521
|
+
type: integer
|
522
|
+
context_type:
|
523
|
+
database_column_name: canvas_context_type
|
524
|
+
type: string
|
525
|
+
name:
|
526
|
+
database_column_name: name
|
527
|
+
type: string
|
528
|
+
friendly_name:
|
529
|
+
database_column_name: friendly_name
|
530
|
+
type: string
|
531
|
+
workflow_state:
|
532
|
+
database_column_name: workflow_state
|
533
|
+
type: string
|
534
|
+
created_at:
|
535
|
+
database_column_name: canvas_created_at
|
536
|
+
type: datetime
|
537
|
+
updated_at:
|
538
|
+
database_column_name: canvas_updated_at
|
539
|
+
type: datetime
|
540
|
+
migration_id:
|
541
|
+
database_column_name: migration_id
|
542
|
+
type: string
|
543
|
+
vendor_guid:
|
544
|
+
database_column_name: vendor_guid
|
545
|
+
type: string
|
546
|
+
low_grade:
|
547
|
+
database_column_name: low_grade
|
548
|
+
type: string
|
549
|
+
high_grade:
|
550
|
+
database_column_name: high_grade
|
551
|
+
type: string
|
552
|
+
calculation_method:
|
553
|
+
database_column_name: calculation_method
|
554
|
+
type: string
|
555
|
+
calculation_int:
|
556
|
+
database_column_name: calculation_int
|
557
|
+
type: integer
|
558
|
+
outcome_import_id:
|
559
|
+
database_column_name: outcome_import_id
|
560
|
+
type: integer
|
561
|
+
root_account_ids:
|
562
|
+
database_column_name: root_account_ids
|
563
|
+
type: integer
|
564
|
+
description:
|
565
|
+
database_column_name: description
|
566
|
+
type: string
|
@@ -116,6 +116,13 @@ module CanvasSync
|
|
116
116
|
def bulk_process_group_membership(report_file_path)
|
117
117
|
do_bulk_import(report_file_path, GroupMembership, options: @options)
|
118
118
|
end
|
119
|
+
|
120
|
+
def bulk_process_learning_outcomes(report_file_path)
|
121
|
+
do_bulk_import(report_file_path, LearningOutcome, options: @options) do |row|
|
122
|
+
row[:root_account_ids] = JSON.parse row[:root_account_ids]
|
123
|
+
row
|
124
|
+
end
|
125
|
+
end
|
119
126
|
end
|
120
127
|
end
|
121
128
|
end
|
@@ -12,14 +12,15 @@ module CanvasSync
|
|
12
12
|
model.try(:get_sync_mapping, key) || mapping[key || CanvasSync::Concerns::SyncMapping::Mapping.normalize_model_name(model)]
|
13
13
|
end
|
14
14
|
|
15
|
-
def do_bulk_import(report_file_path, model, options: {}, mapping_key: nil)
|
15
|
+
def do_bulk_import(report_file_path, model, options: {}, mapping_key: nil, &blk)
|
16
16
|
m = mapping_for(model, mapping_key)
|
17
17
|
CanvasSync::Importers::BulkImporter.import(
|
18
18
|
report_file_path,
|
19
19
|
m[:report_columns],
|
20
20
|
model,
|
21
21
|
m[:conflict_target],
|
22
|
-
import_args: options
|
22
|
+
import_args: options,
|
23
|
+
&blk
|
23
24
|
)
|
24
25
|
end
|
25
26
|
end
|
data/lib/canvas_sync/version.rb
CHANGED
data/lib/canvas_sync.rb
CHANGED
@@ -104,6 +104,12 @@ RSpec.describe CanvasSync::Processors::ProvisioningReportProcessor do
|
|
104
104
|
expect(obj.workflow_state).to eq 'active'
|
105
105
|
end
|
106
106
|
|
107
|
+
it 'processes learning_outcomes' do
|
108
|
+
expect {
|
109
|
+
subject.process('spec/support/fixtures/reports/learning_outcomes.csv', { models: ['learning_outcomes'] }, 1)
|
110
|
+
}.to change { LearningOutcome.count }.by(2)
|
111
|
+
end
|
112
|
+
|
107
113
|
it 'model with composite key behaves as expected' do
|
108
114
|
expect {
|
109
115
|
subject.process('spec/support/fixtures/reports/user_observers.csv', { models: ['user_observers'] }, 1)
|
@@ -0,0 +1,28 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# # #
|
4
|
+
# AUTO GENERATED MIGRATION
|
5
|
+
# This migration was auto generated by the CanvasSync Gem.
|
6
|
+
# You can add new columns to this table, but removing or
|
7
|
+
# re-naming ones created here may break Canvas Syncing.
|
8
|
+
#
|
9
|
+
|
10
|
+
|
11
|
+
class LearningOutcome < ApplicationRecord
|
12
|
+
include CanvasSync::Record
|
13
|
+
include CanvasSync::Concerns::ApiSyncable
|
14
|
+
|
15
|
+
belongs_to :context, polymorphic: true, optional: true, primary_key: :canvas_id, foreign_key: :canvas_context_id, foreign_type: :canvas_context_type
|
16
|
+
|
17
|
+
api_syncable({
|
18
|
+
canvas_id: :id,
|
19
|
+
canvas_context_id: :context_id,
|
20
|
+
canvas_context_type: :context_type,
|
21
|
+
name: :title,
|
22
|
+
friendly_name: :display_name,
|
23
|
+
vendor_guid: :vendor_guid,
|
24
|
+
calculation_method: :calculation_method,
|
25
|
+
calculation_int: :calculation_int,
|
26
|
+
description: :description
|
27
|
+
}, ->(api) { api.get("/api/v1/outcomes/#{canvas_id}") })
|
28
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
# #
|
2
|
+
# AUTO GENERATED MIGRATION
|
3
|
+
# This migration was auto generated by the CanvasSync Gem.
|
4
|
+
# You can add new columns to this table, but removing or
|
5
|
+
# re-naming ones created here may break Canvas Syncing.
|
6
|
+
#
|
7
|
+
|
8
|
+
|
9
|
+
class CreateLearningOutcomes < ActiveRecord::Migration[5.1]
|
10
|
+
def change
|
11
|
+
create_table :learning_outcomes do |t|
|
12
|
+
t.bigint :canvas_id, null: false
|
13
|
+
t.integer :canvas_context_id
|
14
|
+
t.string :canvas_context_type
|
15
|
+
t.string :name
|
16
|
+
t.string :friendly_name
|
17
|
+
t.string :workflow_state
|
18
|
+
t.datetime :canvas_created_at
|
19
|
+
t.datetime :canvas_updated_at
|
20
|
+
t.string :migration_id
|
21
|
+
t.string :vendor_guid
|
22
|
+
t.string :low_grade
|
23
|
+
t.string :high_grade
|
24
|
+
t.string :calculation_method
|
25
|
+
t.string :calculation_int
|
26
|
+
t.integer :outcome_import_id
|
27
|
+
t.integer :root_account_ids, array: true, default: []
|
28
|
+
t.text :description
|
29
|
+
|
30
|
+
t.timestamps
|
31
|
+
end
|
32
|
+
|
33
|
+
add_index :learning_outcomes, :canvas_id, unique: true
|
34
|
+
add_index :learning_outcomes, [:canvas_context_id, :canvas_context_type], name: "index_learning_outcomes_on_context"
|
35
|
+
end
|
36
|
+
end
|
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: 2022_07_12_210559) do
|
14
14
|
|
15
15
|
# These are extensions that must be enabled in order to support this database
|
16
16
|
enable_extension "plpgsql"
|
@@ -246,6 +246,30 @@ ActiveRecord::Schema.define(version: 2022_03_08_072643) do
|
|
246
246
|
t.index ["canvas_id"], name: "index_groups_on_canvas_id", unique: true
|
247
247
|
end
|
248
248
|
|
249
|
+
create_table "learning_outcomes", force: :cascade do |t|
|
250
|
+
t.bigint "canvas_id", null: false
|
251
|
+
t.integer "canvas_context_id"
|
252
|
+
t.string "canvas_context_type"
|
253
|
+
t.string "name"
|
254
|
+
t.string "friendly_name"
|
255
|
+
t.string "workflow_state"
|
256
|
+
t.datetime "canvas_created_at"
|
257
|
+
t.datetime "canvas_updated_at"
|
258
|
+
t.string "migration_id"
|
259
|
+
t.string "vendor_guid"
|
260
|
+
t.string "low_grade"
|
261
|
+
t.string "high_grade"
|
262
|
+
t.string "calculation_method"
|
263
|
+
t.string "calculation_int"
|
264
|
+
t.integer "outcome_import_id"
|
265
|
+
t.integer "root_account_ids", default: [], array: true
|
266
|
+
t.text "description"
|
267
|
+
t.datetime "created_at", null: false
|
268
|
+
t.datetime "updated_at", null: false
|
269
|
+
t.index ["canvas_context_id", "canvas_context_type"], name: "index_learning_outcomes_on_context"
|
270
|
+
t.index ["canvas_id"], name: "index_learning_outcomes_on_canvas_id", unique: true
|
271
|
+
end
|
272
|
+
|
249
273
|
create_table "pseudonyms", force: :cascade do |t|
|
250
274
|
t.bigint "canvas_id", null: false
|
251
275
|
t.bigint "canvas_user_id"
|