canvas_sync 0.24.0 → 0.25.1

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: 3391d91c2c45a7bbd04099137bfc94d896b703407c71b8b04c71b0e586bd4219
4
- data.tar.gz: 581742ed82c4b5cdfcbd8d2c0c7ef2d97ab6c86833aebceeb4cd1bdab881aea1
3
+ metadata.gz: 796a4b260f5d15302e824852208d22bfceedcb1a7695f8246a4263af91df368f
4
+ data.tar.gz: 1cce9fdc47ad3b93df2523943dd9a7366ae6123d62338f05cf21df02444a14f6
5
5
  SHA512:
6
- metadata.gz: 8dd6fb7e8585626f31564edd76f67227e306bc4d29449f154c197d3ef289ca69d2c7371659b4a479854e9a57f0f9c3a61e68a275370a965da0f192bb97c747e8
7
- data.tar.gz: a31a167d26a44df1837038db87d9584c5f7f0e2964f142c34f08efc353a2cb2f713cf8002df681d79237ece0540f97eb35d2ab9eef931033bc1e264d847c9e15
6
+ metadata.gz: 179dcc7dc1f1456f7614788e5af8bd79adaaa7b9623af11293da320125024f8783b8453fd136d6d9385043e4a6e84067e26902e94b09c4356f79023253c889e2
7
+ data.tar.gz: 38b5630b0a9da79c707bb73a736bf90ae195dff08655aa2c62c76336b5f6eea2a86ac9dc5994e8d302ec2664e9006c6993ad6d92ce5cdeb25aa71fefb8caea6d
@@ -73,8 +73,12 @@ module CanvasSync::Concerns
73
73
  end
74
74
 
75
75
  def canvas_super_user?
76
- panda_pal_session.cache(:canvas_super_user?) do
77
- panda_pal_session.canvas_site_admin? || (panda_pal_session.canvas_account_role_labels(:root) & ["AccountAdmin", "Account Admin"]).present?
76
+ canvas_account_admin?(:root)
77
+ end
78
+
79
+ def canvas_account_admin?(account = launch_account)
80
+ panda_pal_session.cache([:canvas_account_admin?, account]) do
81
+ panda_pal_session.canvas_site_admin? || (panda_pal_session.canvas_account_role_labels(account) & ["AccountAdmin", "Account Admin"]).present?
78
82
  end
79
83
  end
80
84
 
@@ -0,0 +1,47 @@
1
+ module CanvasSync::Concerns
2
+ module User
3
+ module ThroughPseudonyms
4
+ extend ActiveSupport::Concern
5
+ include CanvasSync::Record
6
+
7
+ CanvasSync::Record.define_feature self, default: true
8
+
9
+ class_methods do
10
+ def has_many_via_pseudonyms(rel_name, *args, using_sis_id: false, **kwargs)
11
+ kwargs[:class_name] ||= rel_name.to_s.classify
12
+ kwargs[:primary_key] ||= using_sis_id ? :sis_id : :canvas_id
13
+
14
+ # TODO Try and guess the foreign key?
15
+ # kwargs[:foreign_key] ||= using_sis_id ? :"#{rel_name}_sis_id" : :"#{rel_name}_pseudonym_canvas_id"
16
+
17
+ through_rel = using_sis_id ? :active_pseudonyms : :all_pseudonyms
18
+
19
+ pseudo_rel = :"_user_#{rel_name}_via_pseudonym"
20
+ Pseudonym.has_many(pseudo_rel, *args, **kwargs)
21
+
22
+ class_eval <<~RUBY
23
+ has_many(:#{rel_name}, through: :#{through_rel}, source: :#{pseudo_rel}, inverse_of: :student) do
24
+ def scope_for_create(...)
25
+ # @association.reflection.inverse_of
26
+ super.tap do |scope|
27
+ user = @association.owner
28
+ scope[:#{kwargs[:foreign_key]}] = user.load_pseudonym_for_relation!(any: #{!using_sis_id}).canvas_id
29
+ end
30
+ end
31
+ end
32
+ RUBY
33
+ end
34
+ end
35
+
36
+ def load_pseudonym_for_relation!(any: false)
37
+ return @pseudonym_for_relation if defined?(@pseudonym_for_relation)
38
+
39
+ @pseudonym_for_relation = self.active_pseudonyms[0] || Pseudonym.find_by(user: self)
40
+ raise ActiveRecord::RecordNotFound, "No Pseudonym for User #{self.canvas_id}" unless @pseudonym_for_relation.present?
41
+ raise ActiveRecord::RecordNotFound, "No Active Pseudonym for User #{self.canvas_id}" unless @pseudonym_for_relation.workflow_state != "deleted" || any
42
+
43
+ @pseudonym_for_relation
44
+ end
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,141 @@
1
+ module CanvasSync::Concerns
2
+ module UserViaPseudonym
3
+ extend ActiveSupport::Concern
4
+
5
+ module RelationHook
6
+ extend ActiveSupport::Concern
7
+
8
+ def where!(*args)
9
+ return super unless self.respond_to?(:users_via_pseudonym)
10
+
11
+ super.tap do |q|
12
+ args.select { |a| a.is_a?(Hash) }.each do |hash|
13
+ hash.each do |key, value|
14
+ next unless users_via_pseudonym.key?(key)
15
+ q.joins!(key).references!(Arel::Nodes::SqlLiteral.new(key.to_s))
16
+ end
17
+ end
18
+ end
19
+ end
20
+ end
21
+
22
+ ActiveRecord::Relation.include(RelationHook)
23
+
24
+ class RefreshUserCachesJob < CanvasSync::Job
25
+ def perform
26
+ ActiveRecord::Base.subclasses.each do |model|
27
+ next unless model.respond_to?(:update_cached_user_ids!)
28
+ model.update_cached_user_ids!
29
+ end
30
+ end
31
+ end
32
+
33
+ included do
34
+ class_hash :users_via_pseudonym
35
+ end
36
+
37
+ class_methods do
38
+ def class_hash(key)
39
+ class_eval <<~RUBY
40
+ def self.#{key}
41
+ @#{key} ||= superclass.try(:#{key})&.dup || {}
42
+ end
43
+ RUBY
44
+ end
45
+
46
+ def belongs_to_user(rel_name = :user, using_sis_id: false)
47
+ rel_name = rel_name.to_sym
48
+
49
+ pseudonym_rel_name = :"#{rel_name}_pseudonym"
50
+ if using_sis_id
51
+ belongs_to pseudonym_rel_name, -> { active }, primary_key: :sis_id, foreign_key: :"#{rel_name}_sis_id", class_name: "Pseudonym"
52
+ else
53
+ belongs_to pseudonym_rel_name, primary_key: :canvas_id, foreign_key: :"#{rel_name}_pseudonym_canvas_id", class_name: "Pseudonym"
54
+ end
55
+
56
+ config = users_via_pseudonym[rel_name] = {
57
+ user_relation: rel_name,
58
+ pseudonym_relation: pseudonym_rel_name,
59
+ by_sis_id: using_sis_id,
60
+ }
61
+
62
+ cache_column = :"#{rel_name}_cached_user_canvas_id"
63
+ if column_names.include?(cache_column.to_s)
64
+ belongs_to(rel_name, primary_key: :canvas_id, foreign_key: cache_column, class_name: "User")
65
+ config[:user_cache_column] = cache_column
66
+ else
67
+ has_one(rel_name, through: pseudonym_rel_name, source: :user)
68
+ end
69
+
70
+ class_eval <<~RUBY
71
+ def #{rel_name}=(user)
72
+ return if self.#{rel_name} == user
73
+ association(:#{pseudonym_rel_name}).writer(user&.load_pseudonym_for_relation!(any: #{!using_sis_id}))
74
+ association(:#{rel_name}).writer(user) if association(:#{rel_name}).class <= ActiveRecord::Associations::BelongsToAssociation
75
+ end
76
+
77
+ def #{pseudonym_rel_name}=(pseudonym)
78
+ return if self.#{pseudonym_rel_name} == pseudonym
79
+ association(:#{pseudonym_rel_name}).writer(pseudonym)
80
+ association(:#{rel_name}).writer(pseudonym&.user) if association(:#{rel_name}).class <= ActiveRecord::Associations::BelongsToAssociation
81
+ end
82
+ RUBY
83
+ end
84
+
85
+ def update_cached_user_ids!
86
+ cached_configs = users_via_pseudonym.values.select { |c| c[:user_cache_column].present? }
87
+ return if cached_configs.empty?
88
+
89
+ query = all
90
+
91
+ # Add JOINs
92
+ query = cached_configs.reduce(query) do |clause, config|
93
+ clause.joins(config[:pseudonym_relation]).references([ config[:pseudonym_relation], config[:user_relation] ].map { |x| Arel.sql(x.to_s) })
94
+ end
95
+
96
+ # Add OR clauses for filtering to only records with stale caches
97
+ query = cached_configs.reduce(query.where("1=0")) do |clause, config|
98
+ where_clause = query
99
+ if (updated_after = CanvasSync::JobBatches::Batch.current_context[:updated_after]).present?
100
+ where_clause = where_clause.where("#{config[:pseudonym_relation]}.updated_at >= ?", updated_after)
101
+ end
102
+ where_clause = where_clause.where("#{table_name}.#{config[:user_cache_column]} IS DISTINCT FROM #{config[:pseudonym_relation]}.canvas_user_id")
103
+ clause.or(where_clause)
104
+ end
105
+
106
+ query.in_batches do |batch|
107
+ batch = batch.to_a
108
+
109
+ cacheable_pseudonym_ids = cached_configs.map { |config| :"#{config[:pseudonym_relation]}_canvas_id" }.map { |col| batch.map(&col) }.flatten
110
+ user_id_map = Pseudonym.where(canvas_id: cacheable_pseudonym_ids).pluck(:canvas_id, :canvas_user_id).to_h
111
+
112
+ transaction do
113
+ where(id: batch.map(&:id)).delete_all
114
+
115
+ cached_configs.each do |config|
116
+ batch.each do |record|
117
+ record[config[:user_cache_column]] = user_id_map[record[:"#{config[:pseudonym_relation]}_canvas_id"]]
118
+ end
119
+ end
120
+
121
+ import_results = import(
122
+ batch,
123
+ validate: false,
124
+ timestamps: false,
125
+ on_duplicate_key_ignore: true,
126
+ )
127
+
128
+ failures = import_results.failed_instances
129
+ if failures.present?
130
+ handle_duplicated_cache_failures(failures)
131
+ end
132
+ end
133
+ end
134
+ end
135
+
136
+ def handle_user_cache_refresh_failures(records)
137
+
138
+ end
139
+ end
140
+ end
141
+ end
@@ -8,4 +8,6 @@ class Pseudonym < ApplicationRecord
8
8
 
9
9
  validates :canvas_id, uniqueness: true, presence: true
10
10
  belongs_to :user, primary_key: :canvas_id, foreign_key: :canvas_user_id
11
+
12
+ scope :active, -> { where(workflow_state: "active") }
11
13
  end
@@ -17,6 +17,8 @@ class User < ApplicationRecord
17
17
 
18
18
  validates :canvas_id, uniqueness: true, presence: true
19
19
  has_many :pseudonyms, primary_key: :canvas_id, foreign_key: :canvas_user_id
20
+ has_many :active_pseudonyms, ->() { active }, primary_key: :canvas_id, foreign_key: :canvas_user_id, class_name: "Pseudonym"
21
+ has_many :all_pseudonyms, primary_key: :canvas_id, foreign_key: :canvas_user_id, class_name: "Pseudonym"
20
22
  has_many :enrollments, primary_key: :canvas_id, foreign_key: :canvas_user_id
21
23
  has_many :admins, primary_key: :canvas_id, foreign_key: :canvas_user_id
22
24
  has_many :admin_roles, through: :admins, source: :role
@@ -1,3 +1,3 @@
1
1
  module CanvasSync
2
- VERSION = "0.24.0".freeze
2
+ VERSION = "0.25.1".freeze
3
3
  end
data/lib/canvas_sync.rb CHANGED
@@ -177,6 +177,8 @@ module CanvasSync
177
177
  term_scope = term_scope.to_s if term_scope
178
178
  options = options.deep_symbolize_keys!
179
179
 
180
+ given_models = models.dup
181
+
180
182
  model_job_map = {
181
183
  terms: CanvasSync::Jobs::SyncTermsJob,
182
184
  accounts: CanvasSync::Jobs::SyncAccountsJob,
@@ -255,6 +257,10 @@ module CanvasSync
255
257
  # Wrap it all up
256
258
  ###############################
257
259
 
260
+ if given_models.include?('pseudonyms')
261
+ root_chain << Concerns::UserViaPseudonym::RefreshUserCachesJob
262
+ end
263
+
258
264
  root_chain
259
265
  end
260
266
 
@@ -113,7 +113,6 @@ RSpec.describe CanvasSync::Processors::ProvisioningReportProcessor do
113
113
  subject.process('spec/support/fixtures/reports/grading_periods.csv', { models: ['grading_periods'] }, 1)
114
114
  }.to change { GradingPeriod.count }.by(2)
115
115
  obj = GradingPeriod.find_by(canvas_id: 1)
116
- puts obj.inspect
117
116
  expect(obj.title).to eq 'Period 1'
118
117
  expect(obj.weight).to eq 0.2
119
118
  expect(obj.workflow_state).to eq 'active'
@@ -124,7 +123,6 @@ RSpec.describe CanvasSync::Processors::ProvisioningReportProcessor do
124
123
  subject.process('spec/support/fixtures/reports/grading_period_groups.csv', { models: ['grading_period_groups'] }, 1)
125
124
  }.to change { GradingPeriodGroup.count }.by(1)
126
125
  obj = GradingPeriodGroup.find_by(canvas_id: 1)
127
- puts obj.inspect
128
126
  expect(obj.title).to eq 'Test Group'
129
127
  expect(obj.workflow_state).to eq 'active'
130
128
  end
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.24.0
4
+ version: 0.25.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Instructure CustomDev
@@ -453,6 +453,8 @@ files:
453
453
  - lib/canvas_sync/concerns/live_event_sync.rb
454
454
  - lib/canvas_sync/concerns/role/base.rb
455
455
  - lib/canvas_sync/concerns/sync_mapping.rb
456
+ - lib/canvas_sync/concerns/user/through_pseudonyms.rb
457
+ - lib/canvas_sync/concerns/user_via_pseudonym.rb
456
458
  - lib/canvas_sync/config.rb
457
459
  - lib/canvas_sync/engine.rb
458
460
  - lib/canvas_sync/generators/install_generator.rb