panda_pal 4.1.0.beta2 → 5.0.0.beta.4

Sign up to get free protection for your applications and to get access to all the features.
File without changes
@@ -2,6 +2,35 @@ require 'rails_helper'
2
2
 
3
3
  module PandaPal
4
4
  RSpec.describe Organization, type: :model do
5
+
6
+ def set_test_settings_structure
7
+ PandaPal.lti_options = {
8
+ title: 'Test App',
9
+ settings_structure: YAML.load("
10
+ canvas:
11
+ is_required: true
12
+ data_type: Hash
13
+ api_token:
14
+ is_required: true
15
+ data_type: String
16
+ base_url:
17
+ is_required: true
18
+ data_type: String
19
+ reports:
20
+ is_required: true
21
+ data_type: Hash
22
+ active_term_allowance:
23
+ submissions_report_time_length:
24
+ is_required: true
25
+ data_type: ActiveSupport::Duration
26
+ recheck_wait:
27
+ data_type: ActiveSupport::Duration
28
+ max_recheck_time:
29
+ is_required: true
30
+ ").deep_symbolize_keys
31
+ }
32
+ end
33
+
5
34
  it 'creates a schema upon creation' do
6
35
  expect(Apartment::Tenant).to receive(:create)
7
36
  create :panda_pal_organization
@@ -30,5 +59,65 @@ module PandaPal
30
59
  org2 = build :panda_pal_organization, salesforce_id: 'salesforce'
31
60
  expect(org2.valid?).to be_falsey
32
61
  end
62
+
63
+ context 'settings validation' do
64
+ let!(:org) { create :panda_pal_organization }
65
+
66
+ it 'does not perform any validations if settings is not defined' do
67
+ PandaPal.lti_options = {}
68
+ expect_any_instance_of(PandaPal::Organization).not_to receive(:validate_level)
69
+ expect(org.valid?).to be_truthy
70
+ end
71
+
72
+ it 'does not perform any validations if options is not defined' do
73
+ PandaPal.lti_options = nil
74
+ expect_any_instance_of(PandaPal::Organization).not_to receive(:validate_level)
75
+ expect(org.valid?).to be_truthy
76
+ end
77
+
78
+ it 'does perform validations if settings_structure is defined' do
79
+ set_test_settings_structure
80
+ org.valid?
81
+ expect(org.valid?).to be_falsey
82
+ end
83
+
84
+ it 'will fail if a required setting is not present' do
85
+ set_test_settings_structure
86
+ expect(org.valid?).to be_falsey
87
+ errors = org.errors.messages[:settings]
88
+ expect(errors[0]).to eq("PandaPal::Organization.settings requires key [:canvas]. It was not found.")
89
+ end
90
+
91
+ it 'will fail if a setting is supplied but data_type is wrong' do
92
+ set_test_settings_structure
93
+ org.settings = {canvas: "Dog"}
94
+ expect(org.valid?).to be_falsey
95
+ errors = org.errors.messages[:settings]
96
+ expect(errors[0]).to eq("PandaPal::Organization.settings expected key [:canvas] to be Hash but it was instead String.")
97
+ end
98
+
99
+ it 'will fail if a required subsetting is missing' do
100
+ set_test_settings_structure
101
+ org.settings = {canvas: {base_url: 'http://'}, reports: {}}
102
+ expect(org.valid?).to be_falsey
103
+ errors = org.errors.messages[:settings]
104
+ expect(errors[0]).to eq("PandaPal::Organization.settings requires key [:canvas][:api_token]. It was not found.")
105
+ end
106
+
107
+ it 'will fail if extra options are specified' do
108
+ set_test_settings_structure
109
+ org.settings = {canvas: {base_url: 'http://'}, reports: {}, unknown_option: "WHAT IS THIS?"}
110
+ expect(org.valid?).to be_falsey
111
+ errors = org.errors.messages[:settings]
112
+ expect(errors[0]).to eq("PandaPal::Organization.settings had unexpected key: unknown_option. If settings have expanded please update your lti_options accordingly.")
113
+ end
114
+
115
+ it 'will pass if all structure is maintained' do
116
+ set_test_settings_structure
117
+ org.settings = {canvas: {base_url: 'http://', api_token: 'TEST'}, reports: {submissions_report_time_length: 30.minutes, max_recheck_time: 10.hours}}
118
+ expect(org.valid?).to be_truthy
119
+ end
120
+
121
+ end
33
122
  end
34
123
  end
@@ -1,8 +1,4 @@
1
1
  ENV["RAILS_ENV"] ||= 'test'
2
-
3
- require 'sidekiq/testing'
4
- require 'sidekiq-scheduler'
5
-
6
2
  require File.expand_path("../dummy/config/environment.rb", __FILE__)
7
3
  require 'rspec/rails'
8
4
  require 'rspec/autorun'
@@ -44,10 +40,6 @@ RSpec.configure do |config|
44
40
  title: 'Test App',
45
41
  settings_structure: {}
46
42
  }
47
-
48
- allow_any_instance_of(PandaPal::Organization).to receive(:switch_tenant) do |inst, &block|
49
- block.call if block.present?
50
- end
51
43
  end
52
44
 
53
45
  # The settings below are suggested to provide a good initial experience
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: panda_pal
3
3
  version: !ruby/object:Gem::Version
4
- version: 4.1.0.beta2
4
+ version: 5.0.0.beta.4
5
5
  platform: ruby
6
6
  authors:
7
7
  - Instructure ProServe
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2020-05-04 00:00:00.000000000 Z
11
+ date: 2020-06-08 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rails
@@ -114,34 +114,6 @@ dependencies:
114
114
  - - "~>"
115
115
  - !ruby/object:Gem::Version
116
116
  version: 6.1.2
117
- - !ruby/object:Gem::Dependency
118
- name: sidekiq
119
- requirement: !ruby/object:Gem::Requirement
120
- requirements:
121
- - - ">="
122
- - !ruby/object:Gem::Version
123
- version: '0'
124
- type: :development
125
- prerelease: false
126
- version_requirements: !ruby/object:Gem::Requirement
127
- requirements:
128
- - - ">="
129
- - !ruby/object:Gem::Version
130
- version: '0'
131
- - !ruby/object:Gem::Dependency
132
- name: sidekiq-scheduler
133
- requirement: !ruby/object:Gem::Requirement
134
- requirements:
135
- - - ">="
136
- - !ruby/object:Gem::Version
137
- version: '0'
138
- type: :development
139
- prerelease: false
140
- version_requirements: !ruby/object:Gem::Requirement
141
- requirements:
142
- - - ">="
143
- - !ruby/object:Gem::Version
144
- version: '0'
145
117
  - !ruby/object:Gem::Dependency
146
118
  name: rspec-rails
147
119
  requirement: !ruby/object:Gem::Requirement
@@ -191,14 +163,15 @@ files:
191
163
  - app/lib/lti_xml/bridge_platform.rb
192
164
  - app/lib/lti_xml/canvas_platform.rb
193
165
  - app/models/panda_pal/organization.rb
194
- - app/models/panda_pal/organization/settings_validation.rb
195
- - app/models/panda_pal/organization/task_scheduling.rb
196
166
  - app/models/panda_pal/session.rb
197
167
  - app/views/layouts/panda_pal/application.html.erb
198
- - app/views/panda_pal/lti/iframe_cookie_fix.html.erb
199
168
  - app/views/panda_pal/lti/launch.html.erb
200
169
  - config/initializers/apartment.rb
201
170
  - config/routes.rb
171
+ - db/618eef7c0380ba654ad16f867a919e72.sqlite3
172
+ - db/9ff93d4f7e0e9dc80a43f68997caf4a1.sqlite3
173
+ - db/a3fda4044a7215bc2c9eb01a4b9e517a.sqlite3
174
+ - db/daa0e6378a5ec76fcce83b7070dad219.sqlite3
202
175
  - db/migrate/20160412205931_create_panda_pal_organizations.rb
203
176
  - db/migrate/20160413135653_create_panda_pal_sessions.rb
204
177
  - db/migrate/20160425130344_add_panda_pal_organization_to_session.rb
@@ -233,6 +206,7 @@ files:
233
206
  - spec/dummy/config/environments/development.rb
234
207
  - spec/dummy/config/environments/production.rb
235
208
  - spec/dummy/config/environments/test.rb
209
+ - spec/dummy/config/initializers/assets.rb
236
210
  - spec/dummy/config/initializers/backtrace_silencers.rb
237
211
  - spec/dummy/config/initializers/cookies_serializer.rb
238
212
  - spec/dummy/config/initializers/filter_parameter_logging.rb
@@ -243,15 +217,17 @@ files:
243
217
  - spec/dummy/config/locales/en.yml
244
218
  - spec/dummy/config/routes.rb
245
219
  - spec/dummy/config/secrets.yml
220
+ - spec/dummy/db/development.sqlite3
246
221
  - spec/dummy/db/schema.rb
222
+ - spec/dummy/db/test.sqlite3
223
+ - spec/dummy/log/development.log
224
+ - spec/dummy/log/test.log
247
225
  - spec/dummy/public/404.html
248
226
  - spec/dummy/public/422.html
249
227
  - spec/dummy/public/500.html
250
228
  - spec/dummy/public/favicon.ico
251
229
  - spec/factories/panda_pal_organizations.rb
252
230
  - spec/factories/panda_pal_sessions.rb
253
- - spec/models/panda_pal/organization/settings_validation_spec.rb
254
- - spec/models/panda_pal/organization/task_scheduling_spec.rb
255
231
  - spec/models/panda_pal/organization_spec.rb
256
232
  - spec/models/panda_pal/session_spec.rb
257
233
  - spec/rails_helper.rb
@@ -275,8 +251,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
275
251
  - !ruby/object:Gem::Version
276
252
  version: 1.3.1
277
253
  requirements: []
278
- rubyforge_project:
279
- rubygems_version: 2.6.11
254
+ rubygems_version: 3.1.2
280
255
  signing_key:
281
256
  specification_version: 4
282
257
  summary: LTI mountable engine
@@ -306,6 +281,7 @@ test_files:
306
281
  - spec/dummy/config/initializers/filter_parameter_logging.rb
307
282
  - spec/dummy/config/initializers/session_store.rb
308
283
  - spec/dummy/config/initializers/wrap_parameters.rb
284
+ - spec/dummy/config/initializers/assets.rb
309
285
  - spec/dummy/config/initializers/cookies_serializer.rb
310
286
  - spec/dummy/config/initializers/inflections.rb
311
287
  - spec/dummy/config.ru
@@ -315,9 +291,11 @@ test_files:
315
291
  - spec/dummy/public/500.html
316
292
  - spec/dummy/public/404.html
317
293
  - spec/dummy/db/schema.rb
294
+ - spec/dummy/db/test.sqlite3
295
+ - spec/dummy/db/development.sqlite3
296
+ - spec/dummy/log/test.log
297
+ - spec/dummy/log/development.log
318
298
  - spec/dummy/README.rdoc
319
- - spec/models/panda_pal/organization/settings_validation_spec.rb
320
- - spec/models/panda_pal/organization/task_scheduling_spec.rb
321
299
  - spec/models/panda_pal/session_spec.rb
322
300
  - spec/models/panda_pal/organization_spec.rb
323
301
  - spec/factories/panda_pal_sessions.rb
@@ -1,115 +0,0 @@
1
- module PandaPal
2
- module OrganizationConcerns
3
- module SettingsValidation
4
- extend ActiveSupport::Concern
5
-
6
- included do
7
- validate :validate_settings
8
- end
9
-
10
- class_methods do
11
- def settings_structure
12
- if PandaPal.lti_options&.[](:settings_structure).present?
13
- normalize_settings_structure(PandaPal.lti_options[:settings_structure])
14
- else
15
- {
16
- type: Hash,
17
- allow_additional: true,
18
- properties: {},
19
- }
20
- end
21
- end
22
-
23
- def normalize_settings_structure(struc)
24
- return {} unless struc.present?
25
- return struc if struc[:properties] || struc[:type] || struc.key?(:required)
26
-
27
- struc = struc.dup
28
- nstruc = {}
29
-
30
- nstruc[:type] = struc.delete(:data_type) if struc.key?(:data_type)
31
- nstruc[:required] = struc.delete(:is_required) if struc.key?(:is_required)
32
- nstruc[:properties] = struc.map { |k, sub| [k, normalize_settings_structure(sub)] }.to_h if struc.present?
33
-
34
- nstruc
35
- end
36
- end
37
-
38
- def settings_structure
39
- self.class.settings_structure
40
- end
41
-
42
- def validate_settings
43
- switch_tenant do
44
- validate_settings_level(settings || {}, settings_structure).each do |err|
45
- errors[:settings] << err
46
- end
47
- end
48
- end
49
-
50
- private
51
-
52
- def validate_settings_level(settings, spec, path: [], errors: [])
53
- human_path = "[:#{path.join('][:')}]"
54
-
55
- if settings.nil?
56
- errors << "Entry #{human_path} is required" if spec[:required]
57
- return
58
- end
59
-
60
- if spec[:type]
61
- resolved_type = spec[:type]
62
- resolved_type = resolved_type.constantize if resolved_type.is_a?(String)
63
- unless settings.is_a?(resolved_type)
64
- errors << "Expected #{human_path} to be a #{spec[:type]}. Was a #{settings.class.to_s}"
65
- return
66
- end
67
- end
68
-
69
- if spec[:validate].present?
70
- val_errors = []
71
- if spec[:validate].is_a?(Symbol)
72
- proc_result = send(spec[:validate], settings, spec, path: path, errors: val_errors)
73
- elsif spec[:validate].is_a?(String)
74
- split_val = spec[:validate].split?('.')
75
- split_val << 'validate_settings' if split_val.count == 1
76
- resolved_module = split_val[0].constantize
77
- proc_result = resolved_module.send(split_val[1].to_sym, settings, spec, path: path, errors: val_errors)
78
- elsif spec[:validate].is_a?(Proc)
79
- proc_result = instance_eval do
80
- spec[:validate].call(settings, spec, path: path, errors: val_errors)
81
- end
82
- end
83
- val_errors << proc_result unless val_errors.present? || proc_result == val_errors
84
- val_errors = val_errors.flatten.uniq.compact.map do |ve|
85
- ve.gsub('<path>', human_path)
86
- end
87
- errors.concat(val_errors)
88
- end
89
-
90
- if settings.is_a?(Hash)
91
- if spec[:properties] != nil
92
- spec[:properties].each do |key, pspec|
93
- validate_settings_level(settings[key], pspec, path: [*path, key], errors: errors)
94
- end
95
- end
96
-
97
- if spec[:properties] != nil || spec[:allow_additional] != nil
98
- extra_keys = settings.keys - (spec[:properties]&.keys || [])
99
- if extra_keys.present?
100
- if spec[:allow_additional].is_a?(Hash)
101
- extra_keys.each do |key|
102
- validate_settings_level(settings[key], spec[:allow_additional], path: [*path, key], errors: errors)
103
- end
104
- elsif !spec[:allow_additional]
105
- errors << "Did not expect #{human_path} to contain [#{extra_keys.join(', ')}]"
106
- end
107
- end
108
- end
109
- end
110
-
111
- errors
112
- end
113
- end
114
- end
115
- end
@@ -1,164 +0,0 @@
1
- return unless defined?(Sidekiq.schedule)
2
-
3
- require_relative 'settings_validation'
4
-
5
- module PandaPal
6
- module OrganizationConcerns
7
- module TaskScheduling
8
- extend ActiveSupport::Concern
9
- include OrganizationConcerns::SettingsValidation
10
-
11
- included do
12
- after_commit :sync_schedule, on: [:create, :update]
13
- after_commit :unschedule_tasks, on: :destroy
14
- end
15
-
16
- class_methods do
17
- def _schedule_descriptors
18
- @_schedule_descriptors ||= {}
19
- end
20
-
21
- def settings_structure
22
- return super unless _schedule_descriptors.present?
23
-
24
- super.tap do |struc|
25
- struc[:properties] ||= {}
26
-
27
- struc[:properties][:task_schedules] = {
28
- type: 'Hash',
29
- required: false,
30
- properties: _schedule_descriptors.keys.reduce({}) do |hash, k|
31
- hash.tap do |hash|
32
- hash[k.to_sym] = hash[k.to_s] = {
33
- required: false,
34
- validate: ->(value, *args, errors:, **kwargs) {
35
- begin
36
- Rufus::Scheduler.parse(value) if value
37
- nil
38
- rescue ArgumentError
39
- errors << "<path> must be false or a Crontab string"
40
- end
41
- }
42
- }
43
- end
44
- end,
45
- }
46
- end
47
- end
48
-
49
- def scheduled_task(cron_time, name_or_method = nil, worker: nil, queue: nil, &block)
50
- task_key = (name_or_method.presence || "scheduled_task_#{caller_locations[0].lineno}").to_s
51
- raise "Task key '#{task_key}' already taken!" if _schedule_descriptors.key?(task_key)
52
-
53
- _schedule_descriptors[task_key] = {
54
- key: task_key,
55
- schedule: cron_time,
56
- worker: worker || block || name_or_method.to_sym,
57
- queue: queue || 'default',
58
- }
59
- end
60
-
61
- def sync_schedules
62
- # Ensure deleted Orgs are removed
63
- existing_orgs = pluck(:name)
64
- old_schedules = Sidekiq.get_schedule.select do |k, v|
65
- m = k.match(/^org:([a-z0-9_]+)\-/i)
66
- m.present? && !existing_orgs.include?(m[1])
67
- end
68
- old_schedules.keys.each do |k|
69
- Sidekiq.remove_schedule(k)
70
- end
71
-
72
- find_each(&:sync_schedule)
73
- end
74
- end
75
-
76
- def generate_schedule
77
- schedule = {}
78
- self.class._schedule_descriptors.values.each do |desc|
79
- cron_time = schedule_task_cron_time(desc)
80
- next unless cron_time.present?
81
-
82
- schedule["org:#{name}-#{desc[:key]}"] = {
83
- 'cron' => cron_time,
84
- 'queue' => desc[:queue],
85
- 'class' => ScheduledTaskExecutor.to_s,
86
- 'args' => [name, desc[:key]],
87
- }
88
- end
89
- schedule
90
- end
91
-
92
- def sync_schedule
93
- new_schedules = generate_schedule
94
- unschedule_tasks(new_schedules.keys)
95
- new_schedules.each do |k, v|
96
- Sidekiq.set_schedule(k, v)
97
- end
98
- end
99
-
100
- private
101
-
102
- def unschedule_tasks(new_task_keys = nil)
103
- current_schedules = Sidekiq.get_schedule.select { |k,v| k.starts_with?("org:#{name}-") }
104
- del_tasks = current_schedules.keys
105
- del_tasks -= new_task_keys if new_task_keys
106
- del_tasks.each do |k|
107
- Sidekiq.remove_schedule(k)
108
- end
109
- end
110
-
111
- def schedule_task_cron_time(desc)
112
- cron_time = nil
113
- cron_time = settings&.dig(:task_schedules, desc[:key].to_s) if cron_time.nil?
114
- cron_time = settings&.dig(:task_schedules, desc[:key].to_sym) if cron_time.nil?
115
- cron_time = desc[:schedule] if cron_time.nil?
116
-
117
- return nil unless cron_time.present?
118
-
119
- cron_time = instance_eval(&cron_time) if cron_time.is_a?(Proc)
120
- if !Rufus::Scheduler.parse(cron_time).zone.present? && settings && settings[:timezone]
121
- cron_time += " #{settings[:timezone]}"
122
- end
123
-
124
- cron_time
125
- end
126
-
127
- class ScheduledTaskExecutor
128
- include Sidekiq::Worker
129
-
130
- def perform(org_name, task_key)
131
- org = Organization.find_by!(name: org_name)
132
- task = Organization._schedule_descriptors[task_key]
133
- worker = task[:worker]
134
-
135
- Apartment::Tenant.switch(org.name) do
136
- if worker.is_a?(Proc)
137
- org.instance_eval(&worker)
138
- elsif worker.is_a?(Symbol)
139
- org.send(worker)
140
- elsif worker.is_a?(String)
141
- worker.constantize.perform_async
142
- elsif worker.is_a?(Class)
143
- worker.perform_async
144
- end
145
- end
146
- end
147
- end
148
- end
149
- end
150
- end
151
-
152
- SidekiqScheduler::Scheduler.instance.dynamic = true
153
-
154
- module SidekiqScheduler
155
- module Schedule
156
- original_schedule_setter = instance_method(:schedule=)
157
-
158
- define_method :schedule= do |sched|
159
- original_schedule_setter.bind(self).(sched).tap do
160
- PandaPal::Organization.sync_schedules
161
- end
162
- end
163
- end
164
- end