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 +4 -4
- data/lib/canvas_sync/concerns/ability_helper.rb +6 -2
- data/lib/canvas_sync/concerns/user/through_pseudonyms.rb +47 -0
- data/lib/canvas_sync/concerns/user_via_pseudonym.rb +141 -0
- data/lib/canvas_sync/generators/templates/models/pseudonym.rb +2 -0
- data/lib/canvas_sync/generators/templates/models/user.rb +2 -0
- data/lib/canvas_sync/version.rb +1 -1
- data/lib/canvas_sync.rb +6 -0
- data/spec/canvas_sync/processors/provisioning_report_processor_spec.rb +0 -2
- metadata +3 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 796a4b260f5d15302e824852208d22bfceedcb1a7695f8246a4263af91df368f
|
|
4
|
+
data.tar.gz: 1cce9fdc47ad3b93df2523943dd9a7366ae6123d62338f05cf21df02444a14f6
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
-
|
|
77
|
-
|
|
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
|
|
@@ -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
|
data/lib/canvas_sync/version.rb
CHANGED
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.
|
|
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
|