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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 569b75049c7212d44318075a8d2ff1680cbd0d7d68a5446a0b94ec09718eef95
4
- data.tar.gz: 911fd5399e951f4badf0dd77baf78badb8e930606e6f031bbda67eee79e05e72
3
+ metadata.gz: 778924d69dbb2a1f29a6e9b5101851ebda4042536949619b1aa5a2ebdd0f1f69
4
+ data.tar.gz: 06ce5ef0c498612f864dd54f817cb24200b9f64d6dcce644ab235ba784bd4d79
5
5
  SHA512:
6
- metadata.gz: 1fc6af99ed8eb1e3f9c4b5be559beff9e390b9913c966641c5fa0ef4e77714494bc33b96b174d4c0b332bf1e288a566b9346569932e52760a7a501b520e65a57
7
- data.tar.gz: b2ad6cbeb8b979ea843fab8ee161a4b7acc82b620fee99467284ff79e150e2625913c4797d057d76e253eb576e85c0c8e4368f9028a0660fcc2b80bfb29ec132
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)
@@ -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
@@ -14,6 +14,7 @@ class CreateEnrollments < ActiveRecord::Migration[5.1]
14
14
  t.string :workflow_state
15
15
  t.string :base_role_type
16
16
  t.datetime :completed_at
17
+ t.boolean :restricted_access
17
18
 
18
19
  t.timestamps
19
20
  end
@@ -59,14 +59,30 @@ module CanvasSync
59
59
  { parameters: params }
60
60
  end
61
61
 
62
- def report_name(n)
63
- define_method(:report_name) { n }
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
- define_method(:process) do |file|
68
- m = m.constantize if m.is_a?(String) || m.is_a?(Symbol)
69
- do_bulk_import(file, m)
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
- raise NotImplementedError
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
- mbits = self.class.to_s.scan(AUTO_MODEL_REGEX)
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
@@ -165,6 +165,9 @@ enrollments:
165
165
  completed_at:
166
166
  database_column_name: completed_at
167
167
  type: datetime
168
+ restricted_access:
169
+ database_column_name: restricted_access
170
+ type: boolean
168
171
 
169
172
  sections:
170
173
  conflict_target: canvas_section_id
@@ -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
@@ -1,3 +1,3 @@
1
1
  module CanvasSync
2
- VERSION = "0.27.1.beta3".freeze
2
+ VERSION = "0.27.3".freeze
3
3
  end
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/**/*.rb"].each { |file| require file }
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
- # wrapper around the default provisioning chain for the weekly beta refresh
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,6 +1,6 @@
1
1
  FactoryBot.define do
2
2
  factory :account do
3
- sequence(:canvas_id) { |n| n }
3
+ canvas_id { SecureRandom.random_number(100_000_000) }
4
4
  sis_id { SecureRandom.hex }
5
5
  canvas_parent_account_id { 1 }
6
6
  sis_parent_account_id { SecureRandom.hex }
@@ -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.1.beta3
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