panda_pal 4.1.0.beta2 → 5.0.0.beta.4

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.
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