panda_pal 4.1.0.beta3 → 5.0.0

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'
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.beta3
4
+ version: 5.0.0
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
@@ -271,12 +247,11 @@ required_ruby_version: !ruby/object:Gem::Requirement
271
247
  version: '0'
272
248
  required_rubygems_version: !ruby/object:Gem::Requirement
273
249
  requirements:
274
- - - ">"
250
+ - - ">="
275
251
  - !ruby/object:Gem::Version
276
- version: 1.3.1
252
+ version: '0'
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,111 +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
- validate_settings_level(settings || {}, settings_structure).each do |err|
44
- errors[:settings] << err
45
- end
46
- end
47
-
48
- private
49
-
50
- def validate_settings_level(settings, spec, path: [], errors: [])
51
- human_path = "[:#{path.join('][:')}]"
52
-
53
- if settings.nil?
54
- errors << "Entry #{human_path} is required" if spec[:required]
55
- return
56
- end
57
-
58
- if spec[:type]
59
- resolved_type = spec[:type]
60
- resolved_type = resolved_type.constantize if resolved_type.is_a?(String)
61
- unless settings.is_a?(resolved_type)
62
- errors << "Expected #{human_path} to be a #{spec[:type]}. Was a #{settings.class.to_s}"
63
- return
64
- end
65
- end
66
-
67
- if spec[:validate].present?
68
- val_errors = []
69
- if spec[:validate].is_a?(Symbol)
70
- proc_result = send(spec[:validate], settings, spec, path: path, errors: val_errors)
71
- elsif spec[:validate].is_a?(String)
72
- split_val = spec[:validate].split?('.')
73
- split_val << 'validate_settings' if split_val.count == 1
74
- resolved_module = split_val[0].constantize
75
- proc_result = resolved_module.send(split_val[1].to_sym, settings, spec, path: path, errors: val_errors)
76
- elsif spec[:validate].is_a?(Proc)
77
- proc_result = instance_exec(settings, spec, path: path, errors: val_errors, &spec[:validate])
78
- end
79
- val_errors << proc_result unless val_errors.present? || proc_result == val_errors
80
- val_errors = val_errors.flatten.uniq.compact.map do |ve|
81
- ve.gsub('<path>', human_path)
82
- end
83
- errors.concat(val_errors)
84
- end
85
-
86
- if settings.is_a?(Hash)
87
- if spec[:properties] != nil
88
- spec[:properties].each do |key, pspec|
89
- validate_settings_level(settings[key], pspec, path: [*path, key], errors: errors)
90
- end
91
- end
92
-
93
- if spec[:properties] != nil || spec[:allow_additional] != nil
94
- extra_keys = settings.keys - (spec[:properties]&.keys || [])
95
- if extra_keys.present?
96
- if spec[:allow_additional].is_a?(Hash)
97
- extra_keys.each do |key|
98
- validate_settings_level(settings[key], spec[:allow_additional], path: [*path, key], errors: errors)
99
- end
100
- elsif !spec[:allow_additional]
101
- errors << "Did not expect #{human_path} to contain [#{extra_keys.join(', ')}]"
102
- end
103
- end
104
- end
105
- end
106
-
107
- errors
108
- end
109
- end
110
- end
111
- end
@@ -1,172 +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][:timezone] ||= {
28
- type: 'String',
29
- required: false,
30
- validate: ->(timezone, *args) {
31
- ActiveSupport::TimeZone[timezone].present? ? nil : "<path> Invalid Timezone '#{timezone}'"
32
- },
33
- }
34
-
35
- struc[:properties][:task_schedules] = {
36
- type: 'Hash',
37
- required: false,
38
- properties: _schedule_descriptors.keys.reduce({}) do |hash, k|
39
- hash.tap do |hash|
40
- hash[k.to_sym] = hash[k.to_s] = {
41
- required: false,
42
- validate: ->(value, *args, errors:, **kwargs) {
43
- begin
44
- Rufus::Scheduler.parse(value) if value
45
- nil
46
- rescue ArgumentError
47
- errors << "<path> must be false or a Crontab string"
48
- end
49
- }
50
- }
51
- end
52
- end,
53
- }
54
- end
55
- end
56
-
57
- def scheduled_task(cron_time, name_or_method = nil, worker: nil, queue: nil, &block)
58
- task_key = (name_or_method.presence || "scheduled_task_#{caller_locations[0].lineno}").to_s
59
- raise "Task key '#{task_key}' already taken!" if _schedule_descriptors.key?(task_key)
60
-
61
- _schedule_descriptors[task_key] = {
62
- key: task_key,
63
- schedule: cron_time,
64
- worker: worker || block || name_or_method.to_sym,
65
- queue: queue || 'default',
66
- }
67
- end
68
-
69
- def sync_schedules
70
- # Ensure deleted Orgs are removed
71
- existing_orgs = pluck(:name)
72
- old_schedules = Sidekiq.get_schedule.select do |k, v|
73
- m = k.match(/^org:([a-z0-9_]+)\-/i)
74
- m.present? && !existing_orgs.include?(m[1])
75
- end
76
- old_schedules.keys.each do |k|
77
- Sidekiq.remove_schedule(k)
78
- end
79
-
80
- find_each(&:sync_schedule)
81
- end
82
- end
83
-
84
- def generate_schedule
85
- schedule = {}
86
- self.class._schedule_descriptors.values.each do |desc|
87
- cron_time = schedule_task_cron_time(desc)
88
- next unless cron_time.present?
89
-
90
- schedule["org:#{name}-#{desc[:key]}"] = {
91
- 'cron' => cron_time,
92
- 'queue' => desc[:queue],
93
- 'class' => ScheduledTaskExecutor.to_s,
94
- 'args' => [name, desc[:key]],
95
- }
96
- end
97
- schedule
98
- end
99
-
100
- def sync_schedule
101
- new_schedules = generate_schedule
102
- unschedule_tasks(new_schedules.keys)
103
- new_schedules.each do |k, v|
104
- Sidekiq.set_schedule(k, v)
105
- end
106
- end
107
-
108
- private
109
-
110
- def unschedule_tasks(new_task_keys = nil)
111
- current_schedules = Sidekiq.get_schedule.select { |k,v| k.starts_with?("org:#{name}-") }
112
- del_tasks = current_schedules.keys
113
- del_tasks -= new_task_keys if new_task_keys
114
- del_tasks.each do |k|
115
- Sidekiq.remove_schedule(k)
116
- end
117
- end
118
-
119
- def schedule_task_cron_time(desc)
120
- cron_time = nil
121
- cron_time = settings&.dig(:task_schedules, desc[:key].to_s) if cron_time.nil?
122
- cron_time = settings&.dig(:task_schedules, desc[:key].to_sym) if cron_time.nil?
123
- cron_time = desc[:schedule] if cron_time.nil?
124
-
125
- return nil unless cron_time.present?
126
-
127
- cron_time = instance_exec(&cron_time) if cron_time.is_a?(Proc)
128
- if !Rufus::Scheduler.parse(cron_time).zone.present? && settings && settings[:timezone]
129
- cron_time += " #{settings[:timezone]}"
130
- end
131
-
132
- cron_time
133
- end
134
-
135
- class ScheduledTaskExecutor
136
- include Sidekiq::Worker
137
-
138
- def perform(org_name, task_key)
139
- org = Organization.find_by!(name: org_name)
140
- task = Organization._schedule_descriptors[task_key]
141
- worker = task[:worker]
142
-
143
- Apartment::Tenant.switch(org.name) do
144
- if worker.is_a?(Proc)
145
- org.instance_exec(&worker)
146
- elsif worker.is_a?(Symbol)
147
- org.send(worker)
148
- elsif worker.is_a?(String)
149
- worker.constantize.perform_async
150
- elsif worker.is_a?(Class)
151
- worker.perform_async
152
- end
153
- end
154
- end
155
- end
156
- end
157
- end
158
- end
159
-
160
- SidekiqScheduler::Scheduler.instance.dynamic = true
161
-
162
- module SidekiqScheduler
163
- module Schedule
164
- original_schedule_setter = instance_method(:schedule=)
165
-
166
- define_method :schedule= do |sched|
167
- original_schedule_setter.bind(self).(sched).tap do
168
- PandaPal::Organization.sync_schedules
169
- end
170
- end
171
- end
172
- end