canvas_sync 0.27.1.beta3 → 0.27.3
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/app/controllers/canvas_sync/api/v1/live_events_controller.rb +0 -1
- data/lib/canvas_sync/engine.rb +12 -2
- data/lib/canvas_sync/generators/templates/migrations/create_enrollments.rb +1 -0
- data/lib/canvas_sync/jobs/report_sync_task.rb +36 -14
- data/lib/canvas_sync/processors/model_mappings.yml +3 -0
- data/lib/canvas_sync/reports_map.rb +130 -0
- data/lib/canvas_sync/version.rb +1 -1
- data/lib/canvas_sync.rb +7 -30
- data/lib/tasks/canvas_sync_tasks.rake +6 -0
- data/spec/canvas_sync/canvas_sync_spec.rb +0 -40
- data/spec/factories/account_factory.rb +1 -1
- data/spec/support/fixtures/reports/enrollments.csv +3 -3
- metadata +23 -10
- data/lib/canvas_sync/jobs/beta_cleanup/create_temp_tables_job.rb +0 -30
- data/lib/canvas_sync/jobs/beta_cleanup/delete_related_records_job.rb +0 -125
- data/lib/canvas_sync/jobs/beta_cleanup/delete_temp_tables_job.rb +0 -15
- data/spec/canvas_sync/jobs/beta_cleanup/create_temp_tables_spec.rb +0 -22
- data/spec/canvas_sync/jobs/beta_cleanup/delete_related_records_spec.rb +0 -56
- data/spec/canvas_sync/jobs/beta_cleanup/delete_temp_tables_spec.rb +0 -19
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 778924d69dbb2a1f29a6e9b5101851ebda4042536949619b1aa5a2ebdd0f1f69
|
|
4
|
+
data.tar.gz: 06ce5ef0c498612f864dd54f817cb24200b9f64d6dcce644ab235ba784bd4d79
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 571f4296981c5fd72386d6b58d68864f719340da7862b4a5720a5d21d7ba5d146c23e1cfaf2f88b33c55bf80b0b02dd174a22e49519eecea8793b82d7c95b883
|
|
7
|
+
data.tar.gz: 484661a54a3b0fbeee189b9ee64e9554e6baa455da0ef3ed641327c71ba7cfbb40ddcab8394c23f4992f6d98447afe5c7abf58c56f225ef0fdf2689f7f8dfb1c
|
|
@@ -111,7 +111,6 @@ module CanvasSync::Api
|
|
|
111
111
|
end
|
|
112
112
|
|
|
113
113
|
def switch_tenant(&block)
|
|
114
|
-
# TODO Move detection of this param into the PandaPal Apartment Elevator
|
|
115
114
|
if defined?(PandaPal) && (org = params[:organization] || params[:org]).present?
|
|
116
115
|
org = PandaPal::Organization.find(org)
|
|
117
116
|
if org.respond_to?(:switch_tenant)
|
data/lib/canvas_sync/engine.rb
CHANGED
|
@@ -45,10 +45,10 @@ module CanvasSync
|
|
|
45
45
|
}
|
|
46
46
|
|
|
47
47
|
initializer 'canvas_sync.global_methods' do
|
|
48
|
-
next if defined?(canvas_sync_client)
|
|
49
|
-
|
|
50
48
|
require 'panda_pal'
|
|
51
49
|
|
|
50
|
+
next if defined?(canvas_sync_client)
|
|
51
|
+
|
|
52
52
|
class ::Object
|
|
53
53
|
def canvas_sync_client(account_id = nil)
|
|
54
54
|
org = PandaPal::Organization.current
|
|
@@ -88,6 +88,16 @@ module CanvasSync
|
|
|
88
88
|
end
|
|
89
89
|
end
|
|
90
90
|
end
|
|
91
|
+
|
|
92
|
+
if PandaPal::Organization.respond_to?(:after_panda_pal_interactive_install)
|
|
93
|
+
PandaPal::Organization.after_panda_pal_interactive_install do
|
|
94
|
+
ReportsMap.pretty_print_required_reports
|
|
95
|
+
puts ""
|
|
96
|
+
PandaPal::ConsoleHelpers.pause
|
|
97
|
+
rescue => ex
|
|
98
|
+
puts "[CanvasSync] Failed to print required reports: #{ex}"
|
|
99
|
+
end
|
|
100
|
+
end
|
|
91
101
|
end
|
|
92
102
|
rescue LoadError
|
|
93
103
|
end
|
|
@@ -59,14 +59,30 @@ module CanvasSync
|
|
|
59
59
|
{ parameters: params }
|
|
60
60
|
end
|
|
61
61
|
|
|
62
|
-
def report_name(n)
|
|
63
|
-
|
|
62
|
+
def report_name(n = nil)
|
|
63
|
+
if n.present?
|
|
64
|
+
const_set("REPORT_NAME", n)
|
|
65
|
+
define_method(:report_name) { n }
|
|
66
|
+
else
|
|
67
|
+
const_get("REPORT_NAME") rescue nil
|
|
68
|
+
end
|
|
64
69
|
end
|
|
65
70
|
|
|
66
|
-
def model(m)
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
71
|
+
def model(m = nil)
|
|
72
|
+
if m.present?
|
|
73
|
+
const_set("MODEL", m)
|
|
74
|
+
else
|
|
75
|
+
mdl = const_get("MODEL") rescue nil
|
|
76
|
+
|
|
77
|
+
mdl ||= begin
|
|
78
|
+
mbits = self.to_s.scan(AUTO_MODEL_REGEX)
|
|
79
|
+
if (m = mbits.last) && m[0].present?
|
|
80
|
+
model_name = m[0].singularize
|
|
81
|
+
model_name
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
mdl
|
|
70
86
|
end
|
|
71
87
|
end
|
|
72
88
|
|
|
@@ -85,7 +101,11 @@ module CanvasSync
|
|
|
85
101
|
end
|
|
86
102
|
|
|
87
103
|
def report_name
|
|
88
|
-
|
|
104
|
+
if defined?(self.class::REPORT_NAME)
|
|
105
|
+
self.class::REPORT_NAME
|
|
106
|
+
else
|
|
107
|
+
raise NotImplementedError
|
|
108
|
+
end
|
|
89
109
|
end
|
|
90
110
|
|
|
91
111
|
def report_parameters
|
|
@@ -95,13 +115,7 @@ module CanvasSync
|
|
|
95
115
|
AUTO_MODEL_REGEX = /Sync(\w+)Job/
|
|
96
116
|
|
|
97
117
|
def process(file)
|
|
98
|
-
|
|
99
|
-
if (m = mbits.last) && m[0].present?
|
|
100
|
-
model_name = m[0].singularize
|
|
101
|
-
model = model_name.safe_constantize
|
|
102
|
-
return do_bulk_import(file, model) if model.present?
|
|
103
|
-
end
|
|
104
|
-
raise NotImplementedError
|
|
118
|
+
do_bulk_import(file, model_class)
|
|
105
119
|
end
|
|
106
120
|
|
|
107
121
|
def check_frequency
|
|
@@ -118,6 +132,14 @@ module CanvasSync
|
|
|
118
132
|
|
|
119
133
|
protected
|
|
120
134
|
|
|
135
|
+
def model_class
|
|
136
|
+
mdl = self.class.model
|
|
137
|
+
mdl = mdl.constantize if mdl.is_a?(String) || mdl.is_a?(Symbol)
|
|
138
|
+
mdl
|
|
139
|
+
rescue NameError
|
|
140
|
+
raise NotImplementedError, "Model #{self.class.model} does not exist"
|
|
141
|
+
end
|
|
142
|
+
|
|
121
143
|
def do_bulk_import(report_file_path, model, options: {}, mapping_key: nil, &blk)
|
|
122
144
|
Processors::ReportProcessor.new().do_bulk_import(report_file_path, model, options: options, mapping_key: mapping_key, &blk)
|
|
123
145
|
end
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
module CanvasSync
|
|
2
|
+
module ReportsMap
|
|
3
|
+
REPORTS = {
|
|
4
|
+
"proservices_provisioning_csv" => {
|
|
5
|
+
human_name: "Proserv Provisioning Report",
|
|
6
|
+
module_name: "CustomReports::Proservices::Provisioning",
|
|
7
|
+
},
|
|
8
|
+
"proserv_assignment_group_export_csv" => {
|
|
9
|
+
human_name: "Assignment Group Export",
|
|
10
|
+
module_name: "CustomReports::Proserv",
|
|
11
|
+
},
|
|
12
|
+
"proserv_assignment_overrides_csv" => {
|
|
13
|
+
human_name: "Assignment Overrides Export",
|
|
14
|
+
module_name: "CustomReports::Proservices::AssignmentOverrides",
|
|
15
|
+
},
|
|
16
|
+
"proserv_content_migrations_csv" => {
|
|
17
|
+
human_name: "Professional Services Content Migrations Report",
|
|
18
|
+
module_name: "CustomReports::Proservices::ContentMigrations",
|
|
19
|
+
},
|
|
20
|
+
"proserv_context_module_items_csv" => {
|
|
21
|
+
human_name: "Professional Services Context Module Items Report",
|
|
22
|
+
module_name: "CustomReports::Proservices::ContextModuleItems",
|
|
23
|
+
},
|
|
24
|
+
"proserv_context_modules_csv" => {
|
|
25
|
+
human_name: "Professional Services Context Modules Report",
|
|
26
|
+
module_name: "CustomReports::Proservices::ContextModules",
|
|
27
|
+
},
|
|
28
|
+
"proserv_course_completion_csv" => {
|
|
29
|
+
human_name: "Student Course Completion Dates",
|
|
30
|
+
module_name: "CustomReports::Proserv",
|
|
31
|
+
},
|
|
32
|
+
|
|
33
|
+
"rubrics_csv" => {
|
|
34
|
+
human_name: "CustomReports::Rubrics::Rubrics",
|
|
35
|
+
module_name: "Rubrics Export Report",
|
|
36
|
+
},
|
|
37
|
+
"rubric_associations_csv" => {
|
|
38
|
+
human_name: "Rubrics Associations Export Report",
|
|
39
|
+
module_name: "CustomReports::Rubrics::RubricAssociations",
|
|
40
|
+
},
|
|
41
|
+
"rubric_assessments_csv" => {
|
|
42
|
+
human_name: "Rubrics Assessments Export Report",
|
|
43
|
+
module_name: "CustomReports::Rubrics::RubricAssessments",
|
|
44
|
+
},
|
|
45
|
+
|
|
46
|
+
"proserv_scores_data_csv" => {
|
|
47
|
+
human_name: "Professional Services Scores Data Report",
|
|
48
|
+
module_name: "CustomReports::Proservices::ScoresData",
|
|
49
|
+
},
|
|
50
|
+
"proserv_student_submissions_csv" => {
|
|
51
|
+
human_name: "Student Submissions",
|
|
52
|
+
module_name: "CustomReports::Proserv",
|
|
53
|
+
},
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
def self.define_report(name, human_name: nil, module_name: nil)
|
|
57
|
+
REPORTS[name] ||= {}
|
|
58
|
+
REPORTS[name][:human_name] = human_name if human_name
|
|
59
|
+
REPORTS[name][:module_name] = module_name if module_name
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
EXPLICIT_REQUIRED_REPORTS = Set.new
|
|
63
|
+
|
|
64
|
+
def self.require_report!(key, **kwargs)
|
|
65
|
+
define_report(key, **kwargs)
|
|
66
|
+
EXPLICIT_REQUIRED_REPORTS << key
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def self.model_reports
|
|
70
|
+
mapping = {}
|
|
71
|
+
|
|
72
|
+
Jobs::ReportSyncTask.subclasses.each do |cls|
|
|
73
|
+
model = cls.model.to_s
|
|
74
|
+
report = cls.report_name
|
|
75
|
+
next unless model.present?
|
|
76
|
+
mapping[model] ||= []
|
|
77
|
+
mapping[model] << report
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
mapping
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def self.required_reports
|
|
84
|
+
req = Set.new
|
|
85
|
+
req << "proservices_provisioning_csv"
|
|
86
|
+
req.merge(EXPLICIT_REQUIRED_REPORTS)
|
|
87
|
+
|
|
88
|
+
model_reports.each do |model, reports|
|
|
89
|
+
model_class = model.safe_constantize
|
|
90
|
+
next unless model_class.present?
|
|
91
|
+
|
|
92
|
+
req.merge(reports)
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
req
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def self.pretty_print_required_reports(client = nil)
|
|
99
|
+
require 'colorized_string'
|
|
100
|
+
client ||= canvas_sync_client rescue nil
|
|
101
|
+
|
|
102
|
+
req_reports = required_reports
|
|
103
|
+
return "[CanvasSync] No reports required... supposedly" if req_reports.empty?
|
|
104
|
+
|
|
105
|
+
enabled_reports = client.report_list('self').map{|r| r["report"] }.to_set rescue nil
|
|
106
|
+
|
|
107
|
+
puts "[CanvasSync] Required Custom Reports:"
|
|
108
|
+
req_reports.each do |report|
|
|
109
|
+
color = enabled_reports.nil? ? :cyan : (enabled_reports.include?(report) ? :green : :red)
|
|
110
|
+
puts "- #{ColorizedString[report].colorize(color)}"
|
|
111
|
+
if (meta = REPORTS[report]) && (meta[:human_name] || meta[:module_name])
|
|
112
|
+
puts " #{meta[:human_name]} (#{meta[:module_name]})"
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
puts "- #{ColorizedString["NB: Double check if you know this app requires non-CanvasSync provided models."].italic}"
|
|
116
|
+
puts " #{ColorizedString["Non-standard reports must be registered with `CanvasSync.require_report!` from an initializer to be included in this list."].italic}"
|
|
117
|
+
|
|
118
|
+
puts ""
|
|
119
|
+
|
|
120
|
+
bits = req_reports.map do |report|
|
|
121
|
+
"settings[#{report}]=checked"
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
base_url = client.config[:prefix] rescue "<CANVAS URL>/"
|
|
125
|
+
plugins_url = "#{base_url}/plugins/account_reports".gsub(/\/+/, "/")
|
|
126
|
+
plugins_url += "?#{bits.join("&")}" if bits.any?
|
|
127
|
+
puts "Pre-filled link: " + ColorizedString[plugins_url].cyan
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
end
|
data/lib/canvas_sync/version.rb
CHANGED
data/lib/canvas_sync.rb
CHANGED
|
@@ -12,10 +12,11 @@ require "canvas_sync/live_events"
|
|
|
12
12
|
require "canvas_sync/jobs/report_starter"
|
|
13
13
|
require "canvas_sync/batch_processor"
|
|
14
14
|
require "canvas_sync/config"
|
|
15
|
+
require "canvas_sync/reports_map"
|
|
15
16
|
|
|
16
17
|
require "joblin"
|
|
17
18
|
|
|
18
|
-
Dir[File.dirname(__FILE__) + "/canvas_sync/jobs
|
|
19
|
+
Dir[File.dirname(__FILE__) + "/canvas_sync/jobs/*.rb"].each { |file| require file }
|
|
19
20
|
Dir[File.dirname(__FILE__) + "/canvas_sync/processors/*.rb"].each { |file| require file }
|
|
20
21
|
Dir[File.dirname(__FILE__) + "/canvas_sync/importers/*.rb"].each { |file| require file }
|
|
21
22
|
Dir[File.dirname(__FILE__) + "/canvas_sync/generators/*.rb"].each { |file| require file }
|
|
@@ -127,6 +128,10 @@ module CanvasSync
|
|
|
127
128
|
default_provisioning_report_chain(models, **kwargs).process!
|
|
128
129
|
end
|
|
129
130
|
|
|
131
|
+
def require_report!(*args, **kwargs)
|
|
132
|
+
ReportsMap.require_report!(*args, **kwargs)
|
|
133
|
+
end
|
|
134
|
+
|
|
130
135
|
# Given a Model or Relation, scope it down to items that should be synced
|
|
131
136
|
def sync_scope(scope, fallback_scopes: [])
|
|
132
137
|
terms = [
|
|
@@ -170,11 +175,6 @@ module CanvasSync
|
|
|
170
175
|
options: {},
|
|
171
176
|
**kwargs
|
|
172
177
|
) # rubocop:disable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity, Metrics/LineLength
|
|
173
|
-
if CanvasSync::SyncBatch.where(batch_genre: 'beta_cleanup', status: 'processing').count > 0
|
|
174
|
-
Rails.logger.warn("There is currently a batch of genre 'beta_cleanup' in progress, skipping new provisioning sync to avoid conflicts.")
|
|
175
|
-
return
|
|
176
|
-
end
|
|
177
|
-
|
|
178
178
|
return unless models.present?
|
|
179
179
|
models.map! &:to_s
|
|
180
180
|
term_scope = term_scope.to_s if term_scope
|
|
@@ -264,31 +264,8 @@ module CanvasSync
|
|
|
264
264
|
if given_models.include?('pseudonyms')
|
|
265
265
|
root_chain << Concerns::UserViaPseudonym::RefreshUserCachesJob
|
|
266
266
|
end
|
|
267
|
-
root_chain
|
|
268
|
-
end
|
|
269
267
|
|
|
270
|
-
|
|
271
|
-
def default_beta_cleanup_chain(models, term_scope: nil, term_scoped_models: DEFAULT_TERM_SCOPE_MODELS, options: {}, **kwargs)
|
|
272
|
-
updated_after = kwargs[:updated_after] || 2.weeks.ago
|
|
273
|
-
|
|
274
|
-
job_chain = default_provisioning_report_chain(
|
|
275
|
-
models,
|
|
276
|
-
term_scope: term_scope,
|
|
277
|
-
term_scoped_models: term_scoped_models,
|
|
278
|
-
batch_genre: 'beta_cleanup',
|
|
279
|
-
updated_after:,
|
|
280
|
-
options: options,
|
|
281
|
-
**kwargs
|
|
282
|
-
)
|
|
283
|
-
return if job_chain.nil?
|
|
284
|
-
|
|
285
|
-
# We add a job to create temporary tables at the start of the chain for each model
|
|
286
|
-
# and transfer data into it in order to compare it later with the re-synced data
|
|
287
|
-
job_chain.insert_at(0, { job: CanvasSync::Jobs::BetaCleanup::CreateTempTablesJob.to_s, options: { models:, updated_after: } })
|
|
288
|
-
# >> CanvasSync default provisioning chain would be in this position HERE <<
|
|
289
|
-
job_chain.insert({ job: CanvasSync::Jobs::BetaCleanup::DeleteRelatedRecordsJob.to_s, options: { models: } })
|
|
290
|
-
job_chain.insert({ job: CanvasSync::Jobs::BetaCleanup::DeleteTempTablesJob.to_s, options: { models: } })
|
|
291
|
-
job_chain
|
|
268
|
+
root_chain
|
|
292
269
|
end
|
|
293
270
|
|
|
294
271
|
def base_canvas_sync_chain(
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
namespace :canvas_sync do
|
|
2
|
+
desc 'List the reports that CanvasSync requires to be present in the Canvas instance for it to operate correctly, based on the currently enabled features and models.'
|
|
3
|
+
task reports: :environment do
|
|
4
|
+
CanvasSync::ReportsMap.pretty_print_required_reports
|
|
5
|
+
end
|
|
6
|
+
end
|
|
@@ -344,46 +344,6 @@ RSpec.describe CanvasSync do
|
|
|
344
344
|
end
|
|
345
345
|
end
|
|
346
346
|
end
|
|
347
|
-
context "there is a beta cleanup batch in progress" do
|
|
348
|
-
it "does not start a provisioning sync" do
|
|
349
|
-
CanvasSync::SyncBatch.create!(batch_genre: "beta_cleanup", status: "processing")
|
|
350
|
-
expect(CanvasSync.default_beta_cleanup_chain(%w[users])).to be_nil
|
|
351
|
-
end
|
|
352
|
-
end
|
|
353
|
-
end
|
|
354
|
-
|
|
355
|
-
describe ".default_beta_cleanup_chain" do
|
|
356
|
-
it "creates a batch of genre 'beta_cleanup'" do
|
|
357
|
-
updated_after = 2.weeks.ago
|
|
358
|
-
chain = CanvasSync.default_beta_cleanup_chain(%w[users enrollments], updated_after:)
|
|
359
|
-
expect(chain.normalize!).to eq({
|
|
360
|
-
:job=>"CanvasSync::Jobs::BeginSyncChainJob",
|
|
361
|
-
:args=>
|
|
362
|
-
[[{:job=>"CanvasSync::Jobs::BetaCleanup::CreateTempTablesJob",
|
|
363
|
-
:options=>
|
|
364
|
-
{:models=>["users", "enrollments"], :updated_after=>updated_after}},
|
|
365
|
-
{:job=>"Joblin::Batching::ConcurrentBatchJob",
|
|
366
|
-
:args=>
|
|
367
|
-
[[{:job=>"CanvasSync::Jobs::SyncProvisioningReportJob", :options=>{:models=>["users"]}},
|
|
368
|
-
{:job=>"CanvasSync::Jobs::SyncTermsJob",
|
|
369
|
-
:args=>[],
|
|
370
|
-
:kwargs=>
|
|
371
|
-
{:term_scope=>nil,
|
|
372
|
-
:sub_jobs=>
|
|
373
|
-
[{:job=>"CanvasSync::Jobs::SyncProvisioningReportJob",
|
|
374
|
-
:options=>{:models=>["enrollments"]}}]}}]],
|
|
375
|
-
:kwargs=>{:description=>"Default Concurrent Batch"}},
|
|
376
|
-
{:job=>"CanvasSync::Jobs::BetaCleanup::DeleteRelatedRecordsJob",
|
|
377
|
-
:options=>{:models=>["users", "enrollments"]}},
|
|
378
|
-
{:job=>"CanvasSync::Jobs::BetaCleanup::DeleteTempTablesJob",
|
|
379
|
-
:options=>{:models=>["users", "enrollments"]}}]],
|
|
380
|
-
:kwargs=>
|
|
381
|
-
{:legacy_support=>false,
|
|
382
|
-
:updated_after=>updated_after,
|
|
383
|
-
:full_sync_every=>nil,
|
|
384
|
-
:batch_genre=>"beta_cleanup"}
|
|
385
|
-
})
|
|
386
|
-
end
|
|
387
347
|
end
|
|
388
348
|
|
|
389
349
|
describe ".sync_scope" do
|
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
canvas_enrollment_id,canvas_course_id,course_id,canvas_user_id,user_id,role,role_id,canvas_section_id,section_id,status,canvas_associated_user_id,associated_user_id,created_by_sis,base_role_type,limit_section_privileges
|
|
2
|
-
1,1,sis_MUSC_2520_834,2,sis_dalvarez_247,teacher,4,1,sis_section_MUSC_2520_752,active,,,false,TeacherEnrollment,false
|
|
3
|
-
2,1,sis_MUSC_2520_834,3,sis_jweaver_720,student,4,1,sis_section_MUSC_2520_752,active,,,false,StudentEnrollment,false
|
|
1
|
+
canvas_enrollment_id,canvas_course_id,course_id,canvas_user_id,user_id,role,role_id,canvas_section_id,section_id,status,canvas_associated_user_id,associated_user_id,created_by_sis,base_role_type,limit_section_privileges,restricted_access
|
|
2
|
+
1,1,sis_MUSC_2520_834,2,sis_dalvarez_247,teacher,4,1,sis_section_MUSC_2520_752,active,,,false,TeacherEnrollment,false,false
|
|
3
|
+
2,1,sis_MUSC_2520_834,3,sis_jweaver_720,student,4,1,sis_section_MUSC_2520_752,active,,,false,StudentEnrollment,false,true
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: canvas_sync
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.27.
|
|
4
|
+
version: 0.27.3
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Instructure CustomDev
|
|
@@ -421,6 +421,26 @@ dependencies:
|
|
|
421
421
|
- - "~>"
|
|
422
422
|
- !ruby/object:Gem::Version
|
|
423
423
|
version: 0.1.0
|
|
424
|
+
- !ruby/object:Gem::Dependency
|
|
425
|
+
name: colorize
|
|
426
|
+
requirement: !ruby/object:Gem::Requirement
|
|
427
|
+
requirements:
|
|
428
|
+
- - "~>"
|
|
429
|
+
- !ruby/object:Gem::Version
|
|
430
|
+
version: '1.0'
|
|
431
|
+
- - "<"
|
|
432
|
+
- !ruby/object:Gem::Version
|
|
433
|
+
version: '2.0'
|
|
434
|
+
type: :runtime
|
|
435
|
+
prerelease: false
|
|
436
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
437
|
+
requirements:
|
|
438
|
+
- - "~>"
|
|
439
|
+
- !ruby/object:Gem::Version
|
|
440
|
+
version: '1.0'
|
|
441
|
+
- - "<"
|
|
442
|
+
- !ruby/object:Gem::Version
|
|
443
|
+
version: '2.0'
|
|
424
444
|
email:
|
|
425
445
|
- pseng@instructure.com
|
|
426
446
|
executables: []
|
|
@@ -534,9 +554,6 @@ files:
|
|
|
534
554
|
- lib/canvas_sync/importers/legacy_importer.rb
|
|
535
555
|
- lib/canvas_sync/job.rb
|
|
536
556
|
- lib/canvas_sync/jobs/begin_sync_chain_job.rb
|
|
537
|
-
- lib/canvas_sync/jobs/beta_cleanup/create_temp_tables_job.rb
|
|
538
|
-
- lib/canvas_sync/jobs/beta_cleanup/delete_related_records_job.rb
|
|
539
|
-
- lib/canvas_sync/jobs/beta_cleanup/delete_temp_tables_job.rb
|
|
540
557
|
- lib/canvas_sync/jobs/canvas_process_waiter.rb
|
|
541
558
|
- lib/canvas_sync/jobs/report_starter.rb
|
|
542
559
|
- lib/canvas_sync/jobs/report_sync_task.rb
|
|
@@ -567,12 +584,11 @@ files:
|
|
|
567
584
|
- lib/canvas_sync/processors/normal_processor.rb
|
|
568
585
|
- lib/canvas_sync/processors/report_processor.rb
|
|
569
586
|
- lib/canvas_sync/record.rb
|
|
587
|
+
- lib/canvas_sync/reports_map.rb
|
|
570
588
|
- lib/canvas_sync/sidekiq_job.rb
|
|
571
589
|
- lib/canvas_sync/version.rb
|
|
590
|
+
- lib/tasks/canvas_sync_tasks.rake
|
|
572
591
|
- spec/canvas_sync/canvas_sync_spec.rb
|
|
573
|
-
- spec/canvas_sync/jobs/beta_cleanup/create_temp_tables_spec.rb
|
|
574
|
-
- spec/canvas_sync/jobs/beta_cleanup/delete_related_records_spec.rb
|
|
575
|
-
- spec/canvas_sync/jobs/beta_cleanup/delete_temp_tables_spec.rb
|
|
576
592
|
- spec/canvas_sync/jobs/canvas_process_waiter_spec.rb
|
|
577
593
|
- spec/canvas_sync/jobs/job_spec.rb
|
|
578
594
|
- spec/canvas_sync/jobs/report_starter_spec.rb
|
|
@@ -766,9 +782,6 @@ specification_version: 4
|
|
|
766
782
|
summary: Gem for generating Canvas models and migrations and syncing data from Canvas
|
|
767
783
|
test_files:
|
|
768
784
|
- spec/canvas_sync/canvas_sync_spec.rb
|
|
769
|
-
- spec/canvas_sync/jobs/beta_cleanup/create_temp_tables_spec.rb
|
|
770
|
-
- spec/canvas_sync/jobs/beta_cleanup/delete_related_records_spec.rb
|
|
771
|
-
- spec/canvas_sync/jobs/beta_cleanup/delete_temp_tables_spec.rb
|
|
772
785
|
- spec/canvas_sync/jobs/canvas_process_waiter_spec.rb
|
|
773
786
|
- spec/canvas_sync/jobs/job_spec.rb
|
|
774
787
|
- spec/canvas_sync/jobs/report_starter_spec.rb
|
|
@@ -1,30 +0,0 @@
|
|
|
1
|
-
module CanvasSync
|
|
2
|
-
module Jobs::BetaCleanup
|
|
3
|
-
# This job creates "temporary" tables that holds the records that were updated since a given date
|
|
4
|
-
# and deletes those records from the main tables.
|
|
5
|
-
# The tables are actual tables and not the temporary tables in the DB sense
|
|
6
|
-
# because they need to be reused in other contexts.
|
|
7
|
-
# They are deleted in a different job at a later point
|
|
8
|
-
class CreateTempTablesJob < CanvasSync::Job
|
|
9
|
-
def perform(options = {})
|
|
10
|
-
canvas_tables = options[:models]
|
|
11
|
-
|
|
12
|
-
return if canvas_tables.empty?
|
|
13
|
-
|
|
14
|
-
canvas_tables.each do |table_name|
|
|
15
|
-
model = table_name.singularize.camelize.constantize
|
|
16
|
-
|
|
17
|
-
updated_after = options[:updated_after] || 2.weeks.ago
|
|
18
|
-
ActiveRecord::Base.connection.drop_table("beta_#{table_name}", if_exists: true)
|
|
19
|
-
ActiveRecord::Base.connection.exec_query(<<~SQL
|
|
20
|
-
CREATE TABLE beta_#{table_name} AS
|
|
21
|
-
SELECT * FROM #{model.quoted_table_name} WHERE updated_at >= '#{updated_after}';
|
|
22
|
-
SQL
|
|
23
|
-
)
|
|
24
|
-
model.where("updated_at >= '#{updated_after}'").delete_all
|
|
25
|
-
end
|
|
26
|
-
end
|
|
27
|
-
end
|
|
28
|
-
end
|
|
29
|
-
end
|
|
30
|
-
|
|
@@ -1,125 +0,0 @@
|
|
|
1
|
-
module CanvasSync
|
|
2
|
-
module Jobs::BetaCleanup
|
|
3
|
-
class DeleteRelatedRecordsJob < CanvasSync::Job
|
|
4
|
-
# This method assumes that the main tables have been dumped into temporary tables (until provided date range, e.g 2 weeks)
|
|
5
|
-
# and that the main tables have been re-synced
|
|
6
|
-
# Considering these two criteria, assuming we have the main table now with
|
|
7
|
-
# records [1, 2, 3] and the temp table with records [1, 2, 3, 4], this means that
|
|
8
|
-
# record '4' needs to be taken out of related tables.
|
|
9
|
-
# This could for example be a record with a 'canvas_user_id = 4' in a table that is not part of CanvasSync models
|
|
10
|
-
# When this is implemented in CanvasSync, we could store the cleanup id and prefix the table name with it
|
|
11
|
-
# e.g: beta_cleanup_1_users
|
|
12
|
-
def delete_matching_records_between_main_and_temp
|
|
13
|
-
@canvas_tables.each do |table_name|
|
|
14
|
-
model = table_name.singularize.camelize.constantize
|
|
15
|
-
|
|
16
|
-
# here we can safely use the id to find the records we want - the primary keys are preserved in the temporary table
|
|
17
|
-
ActiveRecord::Base.connection.exec_query(<<~SQL
|
|
18
|
-
DELETE FROM beta_#{table_name} WHERE canvas_id IN (SELECT canvas_id FROM #{model.quoted_table_name});
|
|
19
|
-
SQL
|
|
20
|
-
)
|
|
21
|
-
end
|
|
22
|
-
end
|
|
23
|
-
|
|
24
|
-
# possible foreign keys per table. Some LTI tables may or may not always use the Canvas ID
|
|
25
|
-
#
|
|
26
|
-
# Some tables like user_observers have multiple user_id in them (e.g 'observed_user_id' and 'observing_user_id')
|
|
27
|
-
# Not sure if this matters here, to investigate
|
|
28
|
-
def foreign_keys
|
|
29
|
-
@foreign_keys ||= @canvas_tables.map do |table_name|
|
|
30
|
-
model_name = table_name.singularize
|
|
31
|
-
fks = ["#{model_name}_id", "canvas_#{model_name}_id"]
|
|
32
|
-
fks.each { |fk| foreign_key_to_table[fk] = table_name }
|
|
33
|
-
fks
|
|
34
|
-
end.flatten
|
|
35
|
-
end
|
|
36
|
-
|
|
37
|
-
# Find better names for these next two methods
|
|
38
|
-
# fk => table_name (1:1)
|
|
39
|
-
def foreign_key_to_table
|
|
40
|
-
@foreign_key_to_table ||= {}.with_indifferent_access
|
|
41
|
-
end
|
|
42
|
-
|
|
43
|
-
# fk => table_name[] (1:many)
|
|
44
|
-
def foreign_key_to_tables
|
|
45
|
-
h = {}.with_indifferent_access
|
|
46
|
-
ActiveRecord::Base.connection.tables.map do |lti_table|
|
|
47
|
-
# ignore the models we just synced or the beta tables
|
|
48
|
-
# This may not be super reliable in case someone calls a table 'beta_xyz'
|
|
49
|
-
next if @canvas_tables.include?(lti_table.gsub(/^beta_/, ''))
|
|
50
|
-
|
|
51
|
-
columns = ActiveRecord::Base.connection.columns(lti_table).map(&:name)
|
|
52
|
-
cols_in_table = foreign_keys & columns
|
|
53
|
-
|
|
54
|
-
next if cols_in_table.empty?
|
|
55
|
-
|
|
56
|
-
cols_in_table.map do |col|
|
|
57
|
-
h[col] ||= []
|
|
58
|
-
h[col] << lti_table
|
|
59
|
-
end
|
|
60
|
-
end
|
|
61
|
-
@foreign_key_to_tables ||= h
|
|
62
|
-
end
|
|
63
|
-
|
|
64
|
-
def active_tables
|
|
65
|
-
# check the count of each "beta_" table
|
|
66
|
-
active_tables_sql = @canvas_tables.map do |table|
|
|
67
|
-
"SELECT 'beta_#{table}' AS table_name FROM beta_#{table} GROUP BY table_name HAVING COUNT(*) > 0"
|
|
68
|
-
end.join(' UNION ALL ')
|
|
69
|
-
|
|
70
|
-
# active tables are tables with at least one record
|
|
71
|
-
# If a beta table has no rows, we can ignore it in the next step
|
|
72
|
-
active_tables = ActiveRecord::Base.connection.exec_query(active_tables_sql).rows.flatten
|
|
73
|
-
end
|
|
74
|
-
|
|
75
|
-
def perform(options = {})
|
|
76
|
-
canvas_tables = options[:models] || []
|
|
77
|
-
@canvas_tables = canvas_tables
|
|
78
|
-
|
|
79
|
-
return [] if canvas_tables.empty?
|
|
80
|
-
|
|
81
|
-
delete_matching_records_between_main_and_temp
|
|
82
|
-
|
|
83
|
-
# every table is cleaned by foreign key to be idempotent in case of job failures (instead of cleaned by table)
|
|
84
|
-
# we can not clean by table because multiple tables may be using the same 'user_id' foreign key for example
|
|
85
|
-
foreign_key_to_tables.each do |fk, table_names|
|
|
86
|
-
temp_related_table = "beta_#{foreign_key_to_table[fk]}"
|
|
87
|
-
next unless active_tables.include?(temp_related_table)
|
|
88
|
-
|
|
89
|
-
pk = fk.include?('canvas') ? 'canvas_id' : 'id'
|
|
90
|
-
|
|
91
|
-
# Ideally this commented CTE is used so this process is idempotent but there is an issue when there are two tables with, for example user_id & canvas_user_id respectively
|
|
92
|
-
# The user_id foreign key will be processed and will delete the temporary records, which means the second foreign key, canvas_user_id, will not be able to retrive any records from it
|
|
93
|
-
# I don't think idempotency is such a big deal here because:
|
|
94
|
-
# 1) this runs only in beta instances
|
|
95
|
-
# 2) it runs for data within (most likely) the last 2 weeks, which means not that many records
|
|
96
|
-
# In the meantime, we can use the CTE that does not delete records
|
|
97
|
-
=begin
|
|
98
|
-
sql = <<~SQL
|
|
99
|
-
WITH stale_records AS (
|
|
100
|
-
DELETE FROM #{temp_related_table}
|
|
101
|
-
WHERE #{pk} IN (
|
|
102
|
-
SELECT #{pk} FROM #{temp_related_table} ORDER BY #{pk} LIMIT 1000
|
|
103
|
-
)
|
|
104
|
-
RETURNING #{pk}
|
|
105
|
-
),
|
|
106
|
-
SQL
|
|
107
|
-
=end
|
|
108
|
-
sql = <<~SQL
|
|
109
|
-
WITH stale_records AS (
|
|
110
|
-
SELECT #{pk} FROM #{temp_related_table} ORDER BY #{pk} LIMIT 1000
|
|
111
|
-
),
|
|
112
|
-
SQL
|
|
113
|
-
|
|
114
|
-
# using CTEs for multiple deletion statements in one query
|
|
115
|
-
# This allows using the same 'stale_records' CTE for all of the delete statements
|
|
116
|
-
sql << table_names.map { |table_name| "#{table_name}_del AS ( DELETE FROM #{table_name} WHERE #{fk} IN (SELECT #{pk} FROM stale_records) )" }.join(",\n")
|
|
117
|
-
sql << 'SELECT count(*) AS deleted_count FROM stale_records'
|
|
118
|
-
ActiveRecord::Base.transaction do # transaction to use the CTE multiple times
|
|
119
|
-
ActiveRecord::Base.connection.exec_query(sql)
|
|
120
|
-
end
|
|
121
|
-
end
|
|
122
|
-
end
|
|
123
|
-
end
|
|
124
|
-
end
|
|
125
|
-
end
|
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
module CanvasSync
|
|
2
|
-
module Jobs::BetaCleanup
|
|
3
|
-
class DeleteTempTablesJob < CanvasSync::Job
|
|
4
|
-
def perform(options = {})
|
|
5
|
-
tables = options[:models] || []
|
|
6
|
-
return if tables.empty?
|
|
7
|
-
|
|
8
|
-
tables.each do |table_name|
|
|
9
|
-
ActiveRecord::Base.connection.drop_table("beta_#{table_name}", if_exists: true)
|
|
10
|
-
end
|
|
11
|
-
end
|
|
12
|
-
end
|
|
13
|
-
end
|
|
14
|
-
end
|
|
15
|
-
|
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
require 'spec_helper'
|
|
2
|
-
|
|
3
|
-
RSpec.describe CanvasSync::Jobs::BetaCleanup::CreateTempTablesJob do
|
|
4
|
-
describe "#perform" do
|
|
5
|
-
it "creates a beta_<table> for each model with the records with an updated_at after the updated_after param " do
|
|
6
|
-
MODELS_TO_SYNC = %w[accounts courses enrollments users]
|
|
7
|
-
|
|
8
|
-
MODELS_TO_SYNC.each do |table_name|
|
|
9
|
-
model = table_name.singularize.to_sym
|
|
10
|
-
FactoryBot.create_list(model, 3, updated_at: 4.weeks.ago)
|
|
11
|
-
FactoryBot.create_list(model, 2, updated_at: 1.week.ago)
|
|
12
|
-
end
|
|
13
|
-
|
|
14
|
-
described_class.new.perform({ models: MODELS_TO_SYNC, updated_after: 2.weeks.ago })
|
|
15
|
-
beta_tables = MODELS_TO_SYNC.map { |table_name| "beta_#{table_name}" }
|
|
16
|
-
expect(ActiveRecord::Base.connection.tables & beta_tables).to match_array(beta_tables)
|
|
17
|
-
expect(beta_tables.all? { |table_name| ActiveRecord::Base.connection.exec_query("SELECT COUNT(*) FROM #{table_name}").rows.flatten.first == 2 }).to be true
|
|
18
|
-
# original tables should no longer have the records that were moved to the beta tables
|
|
19
|
-
expect(MODELS_TO_SYNC.all? { |table_name| ActiveRecord::Base.connection.exec_query("SELECT COUNT(*) FROM #{table_name}").rows.flatten.first == 3 }).to be true
|
|
20
|
-
end
|
|
21
|
-
end
|
|
22
|
-
end
|
|
@@ -1,56 +0,0 @@
|
|
|
1
|
-
require 'spec_helper'
|
|
2
|
-
|
|
3
|
-
RSpec.describe CanvasSync::Jobs::BetaCleanup::DeleteRelatedRecordsJob do
|
|
4
|
-
describe "#perform" do
|
|
5
|
-
let(:models_to_sync) { %w[accounts] }
|
|
6
|
-
let(:conn) { ActiveRecord::Base.connection }
|
|
7
|
-
|
|
8
|
-
before(:each) do
|
|
9
|
-
@accounts = FactoryBot.create_list(:account, 3, updated_at: 1.week.ago)
|
|
10
|
-
@records_to_readd = @accounts[0..1]
|
|
11
|
-
CanvasSync::Jobs::BetaCleanup::CreateTempTablesJob.new.perform({ models: models_to_sync, updated_after: 2.weeks.ago })
|
|
12
|
-
# we pretend a sync happens, which re-adds 2/3 of the previous records
|
|
13
|
-
Account.create!(@records_to_readd.map(&:attributes))
|
|
14
|
-
# This means that records[2] should have its association deleted in any LTI specific tables via its foreign key
|
|
15
|
-
end
|
|
16
|
-
|
|
17
|
-
it "deletes records in associated tables via their foreign key" do
|
|
18
|
-
conn.exec_query(<<~SQL)
|
|
19
|
-
CREATE TABLE purchases (
|
|
20
|
-
id SERIAL PRIMARY KEY,
|
|
21
|
-
canvas_account_id INTEGER
|
|
22
|
-
)
|
|
23
|
-
SQL
|
|
24
|
-
conn.exec_query(<<~SQL)
|
|
25
|
-
INSERT INTO purchases (canvas_account_id) VALUES (#{@accounts[0].canvas_id}), (#{@accounts[1].canvas_id}), (#{@accounts[2].canvas_id});
|
|
26
|
-
SQL
|
|
27
|
-
# the record in this table with canvas_account_id = 3 should be deleted (records[2])
|
|
28
|
-
described_class.new.perform({ models: models_to_sync })
|
|
29
|
-
expect(conn.exec_query("SELECT canvas_account_id FROM purchases").rows.flatten).to match_array(@records_to_readd.map(&:canvas_id))
|
|
30
|
-
end
|
|
31
|
-
|
|
32
|
-
# BACKWARD COMPABILITY WITH THE ID COLUMN
|
|
33
|
-
# In newer versions of CanvasSync, tables no longer have the "id" primary key and only have the "canvas_id" column
|
|
34
|
-
# Older apps may still have tables with an "id" primary key
|
|
35
|
-
context "when the table has an 'id' column as a primary key instead of 'canvas_id'" do
|
|
36
|
-
it "deletes the records via the 'account_id' foreign key" do
|
|
37
|
-
conn.execute(<<~SQL)
|
|
38
|
-
ALTER TABLE accounts ADD COLUMN id SERIAL PRIMARY KEY;
|
|
39
|
-
ALTER TABLE beta_accounts ADD COLUMN id SERIAL PRIMARY KEY;
|
|
40
|
-
SQL
|
|
41
|
-
conn.exec_query(<<~SQL)
|
|
42
|
-
CREATE TABLE customers (
|
|
43
|
-
id SERIAL PRIMARY KEY,
|
|
44
|
-
account_id INTEGER
|
|
45
|
-
)
|
|
46
|
-
SQL
|
|
47
|
-
conn.exec_query(<<~SQL)
|
|
48
|
-
INSERT INTO customers (account_id) VALUES (1), (2), (3);
|
|
49
|
-
SQL
|
|
50
|
-
described_class.new.perform({ models: models_to_sync })
|
|
51
|
-
expect(conn.exec_query("SELECT account_id FROM customers").rows.flatten).to match_array([1, 2])
|
|
52
|
-
end
|
|
53
|
-
end
|
|
54
|
-
end
|
|
55
|
-
|
|
56
|
-
end
|
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
require 'spec_helper'
|
|
2
|
-
|
|
3
|
-
RSpec.describe CanvasSync::Jobs::BetaCleanup::DeleteTempTablesJob do
|
|
4
|
-
describe '#perform' do
|
|
5
|
-
let(:conn) { ActiveRecord::Base.connection }
|
|
6
|
-
let(:models_to_sync) { %w[accounts courses]}
|
|
7
|
-
|
|
8
|
-
before do
|
|
9
|
-
CanvasSync::Jobs::BetaCleanup::CreateTempTablesJob.new.perform({ models: models_to_sync })
|
|
10
|
-
end
|
|
11
|
-
|
|
12
|
-
it 'deletes the specified tables' do
|
|
13
|
-
expect(models_to_sync.all? { |table_name| conn.data_source_exists?("beta_#{table_name}") }).to be true
|
|
14
|
-
described_class.new.perform({ models: models_to_sync })
|
|
15
|
-
expect(models_to_sync.all? { |table_name| conn.data_source_exists?("beta_#{table_name}") }).to be false
|
|
16
|
-
expect(models_to_sync.all? { |table_name| conn.data_source_exists?(table_name) }).to be true
|
|
17
|
-
end
|
|
18
|
-
end
|
|
19
|
-
end
|