panda_pal 4.1.0.beta3 → 5.0.0

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