panda_pal 5.0.0.beta.1 → 5.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +76 -25
- data/app/models/panda_pal/organization.rb +17 -46
- data/app/models/panda_pal/organization/settings_validation.rb +111 -0
- data/app/models/panda_pal/organization/task_scheduling.rb +172 -0
- data/lib/panda_pal/helpers/controller_helper.rb +10 -50
- data/lib/panda_pal/version.rb +1 -1
- data/panda_pal.gemspec +3 -0
- data/spec/dummy/config/application.rb +7 -1
- data/spec/dummy/config/environments/development.rb +0 -14
- data/spec/dummy/config/environments/production.rb +0 -11
- data/spec/models/panda_pal/organization/settings_validation_spec.rb +175 -0
- data/spec/models/panda_pal/organization/task_scheduling_spec.rb +144 -0
- data/spec/models/panda_pal/organization_spec.rb +0 -89
- data/spec/spec_helper.rb +4 -0
- metadata +38 -8
- data/app/views/panda_pal/lti/iframe_cookie_authorize.html.erb +0 -19
- data/app/views/panda_pal/lti/iframe_cookie_fix.html.erb +0 -12
- data/spec/dummy/config/initializers/assets.rb +0 -11
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 8a2c4fed3544a682a44b4e9acfc30742ab7f7bcdf2ec22db97d90df80af31e04
|
4
|
+
data.tar.gz: f0ee1ba262458c9a5fee095c17c1c673e58e90cc821ea92d29b59cee2afcd1e4
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: adcb31a168a7c5eb09c5ac4ac4c28de5f45ba480873210e309bf5ba60c4c29b4f172648ed03ccee54cfc0e585eac0a25c56be3686dadf582d11e8b95c24a2870
|
7
|
+
data.tar.gz: b06936171d9f3424d2de2cac678726e33d081084a0a99195c6c6d4b7d4286d0a88bb91c9e1ea269ccccbb3f3ea9698a60096b3b7baf8b162f5db751f7b45521c
|
data/README.md
CHANGED
@@ -28,6 +28,40 @@ Use one of these 6 options in `PandaPal.lti_options` hash.
|
|
28
28
|
5. Leave this property off, and you will get the dynamic host with the root path ('http://appdomain.com/') by default.
|
29
29
|
6. If you really do not want this property use the option `launch_url: false` for it to be left off.
|
30
30
|
|
31
|
+
### Task Scheduling
|
32
|
+
`PandaPal` includes an integration with `sidekiq-scheduler`. You can define tasks on an Organization class Stub like so:
|
33
|
+
```ruby
|
34
|
+
# <your_app>/app/models/panda_pal/organization.rb
|
35
|
+
require File.expand_path('../../app/models/panda_pal/organization.rb', PandaPal::Engine.called_from)
|
36
|
+
|
37
|
+
module PandaPal
|
38
|
+
class Organization
|
39
|
+
# Will invoke CanvasSyncStarterWorker.perform_async() according to the cron schedule
|
40
|
+
scheduled_task '0 15 05 * * *', :identifier, worker: CanvasSyncStarterWorker
|
41
|
+
|
42
|
+
# Will invoke the method 'organization_method' on the Organization
|
43
|
+
scheduled_task '0 15 05 * * *', :organization_method_and_identifier
|
44
|
+
|
45
|
+
# If you need to invoke the same method on multiple schedules
|
46
|
+
scheduled_task '0 15 05 * * *', :identifier, worker: :organization_method
|
47
|
+
|
48
|
+
# You can also use a block
|
49
|
+
scheduled_task '0 15 05 * * *', :identifier do
|
50
|
+
# Do Stuff
|
51
|
+
end
|
52
|
+
|
53
|
+
# You can use a Proc (called in the context of the Organization) to determine the schedule
|
54
|
+
scheduled_task -> { settings[:cron] }, :identifier
|
55
|
+
|
56
|
+
# You can specify a timezone. If a TZ is not coded and settings[:timezone] is present, it will be appended automatically
|
57
|
+
scheduled_task '0 15 05 * * * America/Denver', :identifier, worker: :organization_method
|
58
|
+
|
59
|
+
# Setting settings[:task_schedules][:identifier] will override the code cron schedule. Setting it to false will disable the Task
|
60
|
+
# :identifer values _must_ be unique, but can be nil, in which case they will be determined by where (lineno etc) scheduled_task is called
|
61
|
+
end
|
62
|
+
end
|
63
|
+
```
|
64
|
+
|
31
65
|
# Organization Attributes
|
32
66
|
id: Primary Key
|
33
67
|
name: Name of the organization. Used to on requests to select the tenant
|
@@ -226,9 +260,43 @@ In your panda_pal initializer (i.e. config/initializers/panda_pal.rb or config/i
|
|
226
260
|
You can specify options that can include a structure for your settings. If specified, PandaPal will
|
227
261
|
enforce this structure on any new / updated organizations.
|
228
262
|
|
263
|
+
```ruby
|
264
|
+
PandaPal.lti_options = {
|
265
|
+
title: 'LBS Gradebook',
|
266
|
+
settings_structure: {
|
267
|
+
allow_additional: true, # Allow additional properties that aren't included in the :properties Hash.
|
268
|
+
allow_additional: { type: 'String' }, # You can also set :allow_additional to a settings specification that will be used to validate each additional setting
|
269
|
+
validate: ->(value, spec, **kwargs) {
|
270
|
+
# kwargs currently includes:
|
271
|
+
# :errors => [An array to push errors to]
|
272
|
+
# :path => [An array representation of the current path in the settings object]
|
273
|
+
|
274
|
+
# To add errors, you may:
|
275
|
+
# Push strings to the kwargs[:errors] Array:
|
276
|
+
kwargs[:errors] << "Your error message at <path>" unless value < 10
|
277
|
+
# Or return a string or string array:
|
278
|
+
value.valid? ? nil : "Your error message at <path>" # <path> will be replaced with the actual path that the error occurred at
|
279
|
+
},
|
280
|
+
properties: {
|
281
|
+
canvas_api_token: { type: 'String', required: true, },
|
282
|
+
catalog: { # :validate, :allow_additional, :properties keys are all supported at this level as well
|
283
|
+
type: 'Hash',
|
284
|
+
required: false,
|
285
|
+
validate: -> (*args) {},
|
286
|
+
allow_additional: false,
|
287
|
+
properties: {
|
288
|
+
|
289
|
+
},
|
290
|
+
}
|
291
|
+
}
|
292
|
+
},
|
293
|
+
}
|
294
|
+
```
|
295
|
+
|
296
|
+
#### Legacy Settings Structure:
|
229
297
|
Here is an example options specification:
|
230
298
|
|
231
|
-
```
|
299
|
+
```ruby
|
232
300
|
PandaPal.lti_options = {
|
233
301
|
title: 'LBS Gradebook',
|
234
302
|
settings_structure: YAML.load("
|
@@ -287,7 +355,7 @@ This will allow `PandaPal` to apply an iframe cookie fix that will allow CSRF va
|
|
287
355
|
It has been a constant struggle to force safari to store and allow
|
288
356
|
access to a rails session while the application is embedded in Canvas.
|
289
357
|
|
290
|
-
As of PandaPal 5, a
|
358
|
+
As of PandaPal 5, a persistent session is no longer required by panda_pal.
|
291
359
|
|
292
360
|
This means that safari will likely refuse to send info about your rails session
|
293
361
|
back to the LTI, and the application will start up a new session each time the
|
@@ -299,29 +367,8 @@ You will want to watch out for a few scenarios:
|
|
299
367
|
and have your PandaPal session_key persisted server side.
|
300
368
|
2) Use the "Authorization" header with "token={session_key}" to send your
|
301
369
|
PandaPal session info into api calls.
|
302
|
-
|
303
|
-
|
304
|
-
PandaPal.lti_options = {
|
305
|
-
require_persistent_session: true
|
306
|
-
}
|
307
|
-
in your config/initializer. With that setting, the user will be required to
|
308
|
-
allow our application to store / access cookies in safari before they can use
|
309
|
-
the LTI.
|
310
|
-
|
311
|
-
# Notes on require_persistent_session
|
312
|
-
|
313
|
-
IF you must have a persistent session this is the logical flow of how panda_pal
|
314
|
-
attempts to set that up.
|
315
|
-
|
316
|
-
1) LTI laumches.
|
317
|
-
2) LTI will attempt to POST message from iframe to top window (canvas) telling
|
318
|
-
canvas to relaunch in full screen so we aren't inhibited by safari.
|
319
|
-
3) LTI will setup session and cookies in full-screen mode. Session will be saved
|
320
|
-
in browser.
|
321
|
-
4) LTI will redirect to an authorization page, that will require user to give
|
322
|
-
access to the session store to our application.
|
323
|
-
5) Once the user gives access to the session store, we will reload the LTI
|
324
|
-
and the cookie should now be persistent.
|
370
|
+
3) If you use link_to and navigate in your LTI (apps that are not single page)
|
371
|
+
make sure you include an encrypted_session_key parameter in your links.
|
325
372
|
|
326
373
|
# Upgrading from PandaPal 4 to 5:
|
327
374
|
|
@@ -361,3 +408,7 @@ class AccountController < ApplicationController
|
|
361
408
|
end
|
362
409
|
```
|
363
410
|
|
411
|
+
## Running Specs:
|
412
|
+
Initialize the Specs DB:
|
413
|
+
`cd spec/dummy; bundle exec rake db:drop; bundle exec rake db:create; bundle exec rake db:schema:load`
|
414
|
+
Then `bundle exec rspec`
|
@@ -1,15 +1,23 @@
|
|
1
|
+
Dir[File.dirname(__FILE__) + "/organization/*.rb"].each { |file| require file }
|
2
|
+
|
1
3
|
module PandaPal
|
4
|
+
module OrganizationConcerns; end
|
2
5
|
|
3
6
|
class Organization < ActiveRecord::Base
|
7
|
+
include OrganizationConcerns::SettingsValidation
|
8
|
+
include OrganizationConcerns::TaskScheduling if defined?(Sidekiq.schedule)
|
9
|
+
|
4
10
|
attribute :settings
|
11
|
+
serialize :settings, Hash
|
5
12
|
attr_encrypted :settings, marshal: true, key: :encryption_key
|
6
13
|
before_save {|a| a.settings = a.settings} # this is a hacky work-around to a bug where attr_encrypted is not saving settings in place
|
14
|
+
|
7
15
|
validates :key, uniqueness: { case_sensitive: false }, presence: true
|
8
16
|
validates :secret, presence: true
|
9
17
|
validates :name, uniqueness: { case_sensitive: false }, presence: true, format: { with: /\A[a-z0-9_]+\z/i }
|
10
18
|
validates :canvas_account_id, presence: true
|
11
19
|
validates :salesforce_id, presence: true, uniqueness: true
|
12
|
-
|
20
|
+
|
13
21
|
after_create :create_schema
|
14
22
|
after_commit :destroy_schema, on: :destroy
|
15
23
|
|
@@ -17,8 +25,6 @@ module PandaPal
|
|
17
25
|
errors.add(:name, 'should not be changed after creation') if name_changed?
|
18
26
|
end
|
19
27
|
|
20
|
-
serialize :settings, Hash
|
21
|
-
|
22
28
|
def encryption_key
|
23
29
|
# production environment might not have loaded secret_key_base yet.
|
24
30
|
# In that case, just read it from env.
|
@@ -29,6 +35,14 @@ module PandaPal
|
|
29
35
|
end
|
30
36
|
end
|
31
37
|
|
38
|
+
def switch_tenant(&block)
|
39
|
+
if block_given?
|
40
|
+
Apartment::Tenant.switch(name, &block)
|
41
|
+
else
|
42
|
+
Apartment::Tenant.switch!(name)
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
32
46
|
private
|
33
47
|
|
34
48
|
def create_schema
|
@@ -38,48 +52,5 @@ module PandaPal
|
|
38
52
|
def destroy_schema
|
39
53
|
Apartment::Tenant.drop name
|
40
54
|
end
|
41
|
-
|
42
|
-
def validate_settings
|
43
|
-
record = self
|
44
|
-
if PandaPal.lti_options && PandaPal.lti_options[:settings_structure]
|
45
|
-
validate_level(record, PandaPal.lti_options[:settings_structure], record.settings, [])
|
46
|
-
end
|
47
|
-
end
|
48
|
-
def validate_level(record, expectation, reality, previous_keys)
|
49
|
-
# Verify that the data elements at this level conform to requirements.
|
50
|
-
if expectation
|
51
|
-
expectation.each do |key, value|
|
52
|
-
is_required = expectation[key].try(:delete, :is_required)
|
53
|
-
data_type = expectation[key].try(:delete, :data_type)
|
54
|
-
value = reality.is_a?(Hash) ? reality.dig(key) : reality
|
55
|
-
|
56
|
-
if is_required && !value
|
57
|
-
record.errors[:settings] << "PandaPal::Organization.settings requires key [:#{previous_keys.push(key).join("][:")}]. It was not found."
|
58
|
-
end
|
59
|
-
if data_type && value
|
60
|
-
if value.class.to_s != data_type
|
61
|
-
record.errors[:settings] << "PandaPal::Organization.settings expected key [:#{previous_keys.push(key).join("][:")}] to be #{data_type} but it was instead #{value.class}."
|
62
|
-
end
|
63
|
-
end
|
64
|
-
end
|
65
|
-
end
|
66
|
-
# Verify that anything that is in the real settings has an expectation.
|
67
|
-
if reality
|
68
|
-
if reality.is_a? Hash
|
69
|
-
reality.each do |key, value|
|
70
|
-
was_expected = expectation.has_key?(key)
|
71
|
-
if !was_expected
|
72
|
-
record.errors[:settings] << "PandaPal::Organization.settings had unexpected key: #{key}. If settings have expanded please update your lti_options accordingly."
|
73
|
-
end
|
74
|
-
end
|
75
|
-
end
|
76
|
-
end
|
77
|
-
# Recursively check out any children settings as well.
|
78
|
-
if expectation
|
79
|
-
expectation.each do |key, value|
|
80
|
-
validate_level(record, expectation[key], (reality.is_a?(Hash) ? reality.dig(key) : nil), previous_keys.deep_dup.push(key))
|
81
|
-
end
|
82
|
-
end
|
83
|
-
end
|
84
55
|
end
|
85
56
|
end
|
@@ -0,0 +1,111 @@
|
|
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
|
@@ -0,0 +1,172 @@
|
|
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
|
@@ -42,29 +42,9 @@ module PandaPal::Helpers::ControllerHelper
|
|
42
42
|
render plain: 'Invalid Credentials, please contact your Administrator.', :status => :unauthorized unless authorized
|
43
43
|
return authorized
|
44
44
|
end
|
45
|
-
if require_persistent_session
|
46
|
-
if cookies_need_iframe_fix?(false)
|
47
|
-
fix_iframe_cookies
|
48
|
-
return false
|
49
|
-
end
|
50
|
-
# For safari we may have been launched temporarily full-screen by canvas. This allows us to set the session cookie.
|
51
|
-
# In this case, we should make sure the session cookie is fixed and redirect back to canvas to properly launch the embedded LTI.
|
52
|
-
if params[:platform_redirect_url]
|
53
|
-
session[:safari_cookie_fixed] = true
|
54
|
-
redirect_to params[:platform_redirect_url]
|
55
|
-
return false
|
56
|
-
end
|
57
|
-
end
|
58
45
|
return authorized
|
59
46
|
end
|
60
47
|
|
61
|
-
def require_persistent_session
|
62
|
-
if PandaPal.lti_options.has_key?(:require_persistent_session) && PandaPal.lti_options[:require_persistent_session] == true
|
63
|
-
return true
|
64
|
-
end
|
65
|
-
return false
|
66
|
-
end
|
67
|
-
|
68
48
|
def switch_tenant(organization = current_organization, &block)
|
69
49
|
return unless organization
|
70
50
|
raise 'This method should be called in an around_action callback' unless block_given?
|
@@ -74,35 +54,8 @@ module PandaPal::Helpers::ControllerHelper
|
|
74
54
|
end
|
75
55
|
end
|
76
56
|
|
77
|
-
# Browsers that prevent 3rd party cookies by default (Safari and IE) run into problems
|
78
|
-
# with CSRF handling because the Rails session cookie isn't set. To fix this, we
|
79
|
-
# redirect the current page to the LTI using JavaScript, which will set the cookie,
|
80
|
-
# and then immediately redirect back to Canvas.
|
81
|
-
def fix_iframe_cookies
|
82
|
-
if params[:safari_cookie_authorized].present?
|
83
|
-
session[:safari_cookie_authorized] = true
|
84
|
-
redirect_to params[:return_to]
|
85
|
-
elsif (session[:safari_cookie_fixed] && !params[:safari_cookie_authorized])
|
86
|
-
render 'panda_pal/lti/iframe_cookie_authorize', layout: false
|
87
|
-
else
|
88
|
-
render 'panda_pal/lti/iframe_cookie_fix', layout: false
|
89
|
-
end
|
90
|
-
end
|
91
|
-
|
92
|
-
def cookies_need_iframe_fix?(check_authorized=true)
|
93
|
-
if check_authorized
|
94
|
-
return browser.safari? && !request.referrer&.include?('sessionless_launch') && !(session[:safari_cookie_fixed] && session[:safari_cookie_authorized]) && !params[:platform_redirect_url]
|
95
|
-
else
|
96
|
-
return browser.safari? && !request.referrer&.include?('sessionless_launch') && !session[:safari_cookie_fixed] && !params[:platform_redirect_url]
|
97
|
-
end
|
98
|
-
end
|
99
|
-
|
100
57
|
def forbid_access_if_lacking_session
|
101
|
-
|
102
|
-
fix_iframe_cookies
|
103
|
-
else
|
104
|
-
render plain: 'You should do an LTI Tool Launch.', status: :unauthorized unless valid_session?
|
105
|
-
end
|
58
|
+
render plain: 'You should do an LTI Tool Launch.', status: :unauthorized unless valid_session?
|
106
59
|
safari_override
|
107
60
|
end
|
108
61
|
|
@@ -129,6 +82,10 @@ module PandaPal::Helpers::ControllerHelper
|
|
129
82
|
end
|
130
83
|
|
131
84
|
def session_key
|
85
|
+
if params[:encrypted_session_key]
|
86
|
+
crypt = ActiveSupport::MessageEncryptor.new(Rails.application.secret_key_base[0..31])
|
87
|
+
return crypt.decrypt_and_verify(params[:encrypted_session_key])
|
88
|
+
end
|
132
89
|
params[:session_key] || session_key_header || flash[:session_key] || session[:session_key]
|
133
90
|
end
|
134
91
|
|
@@ -139,7 +96,8 @@ module PandaPal::Helpers::ControllerHelper
|
|
139
96
|
end
|
140
97
|
|
141
98
|
# Redirect with the session key intact. In production,
|
142
|
-
# handle this by
|
99
|
+
# handle this by encrypting the session key. That way if the
|
100
|
+
# url is logged anywhere, it will all be encrypted data. In dev,
|
143
101
|
# just put it in the URL. Putting it in the URL
|
144
102
|
# is insecure, but is fine in development.
|
145
103
|
# Keeping it in the URL in development means that it plays
|
@@ -162,8 +120,10 @@ module PandaPal::Helpers::ControllerHelper
|
|
162
120
|
end
|
163
121
|
|
164
122
|
def redirect_production_mode(location, params)
|
165
|
-
|
123
|
+
crypt = ActiveSupport::MessageEncryptor.new(Rails.application.secret_key_base[0..31])
|
124
|
+
encrypted_data = crypt.encrypt_and_sign(current_session.session_key)
|
166
125
|
redirect_to send(location, {
|
126
|
+
encrypted_session_key: encrypted_data,
|
167
127
|
organization_id: current_organization.id
|
168
128
|
}.merge(params))
|
169
129
|
end
|
data/lib/panda_pal/version.rb
CHANGED
data/panda_pal.gemspec
CHANGED
@@ -22,6 +22,9 @@ Gem::Specification.new do |s|
|
|
22
22
|
s.add_dependency 'browser', '2.5.0'
|
23
23
|
s.add_dependency 'attr_encrypted', '~> 3.0.0'
|
24
24
|
s.add_dependency 'secure_headers', '~> 6.1.2'
|
25
|
+
|
26
|
+
s.add_development_dependency 'sidekiq'
|
27
|
+
s.add_development_dependency 'sidekiq-scheduler'
|
25
28
|
s.add_development_dependency 'rspec-rails'
|
26
29
|
s.add_development_dependency 'factory_girl_rails'
|
27
30
|
end
|
@@ -1,6 +1,12 @@
|
|
1
1
|
require File.expand_path('../boot', __FILE__)
|
2
2
|
|
3
|
-
require
|
3
|
+
require "active_model/railtie"
|
4
|
+
require "active_job/railtie"
|
5
|
+
require "active_record/railtie"
|
6
|
+
# require "active_storage/engine"
|
7
|
+
require "action_controller/railtie"
|
8
|
+
require "action_mailer/railtie"
|
9
|
+
require "action_view/railtie"
|
4
10
|
|
5
11
|
Bundler.require(*Rails.groups)
|
6
12
|
require "panda_pal"
|
@@ -22,20 +22,6 @@ Rails.application.configure do
|
|
22
22
|
# Raise an error on page load if there are pending migrations.
|
23
23
|
config.active_record.migration_error = :page_load
|
24
24
|
|
25
|
-
# Debug mode disables concatenation and preprocessing of assets.
|
26
|
-
# This option may cause significant delays in view rendering with a large
|
27
|
-
# number of complex assets.
|
28
|
-
config.assets.debug = true
|
29
|
-
|
30
|
-
# Asset digests allow you to set far-future HTTP expiration dates on all assets,
|
31
|
-
# yet still be able to expire them through the digest params.
|
32
|
-
config.assets.digest = true
|
33
|
-
|
34
|
-
# Adds additional error checking when serving assets at runtime.
|
35
|
-
# Checks for improperly declared sprockets dependencies.
|
36
|
-
# Raises helpful error messages.
|
37
|
-
config.assets.raise_runtime_errors = true
|
38
|
-
|
39
25
|
# Raises error for missing translations
|
40
26
|
# config.action_view.raise_on_missing_translations = true
|
41
27
|
end
|
@@ -24,17 +24,6 @@ Rails.application.configure do
|
|
24
24
|
# Apache or NGINX already handles this.
|
25
25
|
config.serve_static_files = ENV['RAILS_SERVE_STATIC_FILES'].present?
|
26
26
|
|
27
|
-
# Compress JavaScripts and CSS.
|
28
|
-
config.assets.js_compressor = :uglifier
|
29
|
-
# config.assets.css_compressor = :sass
|
30
|
-
|
31
|
-
# Do not fallback to assets pipeline if a precompiled asset is missed.
|
32
|
-
config.assets.compile = false
|
33
|
-
|
34
|
-
# Asset digests allow you to set far-future HTTP expiration dates on all assets,
|
35
|
-
# yet still be able to expire them through the digest params.
|
36
|
-
config.assets.digest = true
|
37
|
-
|
38
27
|
# `config.assets.precompile` and `config.assets.version` have moved to config/initializers/assets.rb
|
39
28
|
|
40
29
|
# Specifies the header that your server uses for sending files.
|
@@ -0,0 +1,175 @@
|
|
1
|
+
require 'rails_helper'
|
2
|
+
|
3
|
+
module PandaPal
|
4
|
+
PandaPal::Organization
|
5
|
+
RSpec.describe OrganizationConcerns::SettingsValidation, type: :model do
|
6
|
+
let!(:org) { create :panda_pal_organization }
|
7
|
+
|
8
|
+
def set_test_settings_structure
|
9
|
+
PandaPal.lti_options = {
|
10
|
+
title: 'Test App',
|
11
|
+
settings_structure: structure,
|
12
|
+
}
|
13
|
+
end
|
14
|
+
|
15
|
+
RSpec.shared_examples "shared stuff" do
|
16
|
+
it 'does not perform any validations if settings is not defined' do
|
17
|
+
PandaPal.lti_options = {}
|
18
|
+
expect(org.valid?).to be_truthy
|
19
|
+
end
|
20
|
+
|
21
|
+
it 'does not perform any validations if options is not defined' do
|
22
|
+
PandaPal.lti_options = nil
|
23
|
+
expect(org.valid?).to be_truthy
|
24
|
+
end
|
25
|
+
|
26
|
+
it 'does perform validations if settings_structure is defined' do
|
27
|
+
set_test_settings_structure
|
28
|
+
org.valid?
|
29
|
+
expect(org.valid?).to be_falsey
|
30
|
+
end
|
31
|
+
|
32
|
+
it 'will fail if a required setting is not present' do
|
33
|
+
set_test_settings_structure
|
34
|
+
expect(org.valid?).to be_falsey
|
35
|
+
errors = org.errors.messages[:settings]
|
36
|
+
expect(errors[0]).to eq("Entry [:canvas] is required")
|
37
|
+
end
|
38
|
+
|
39
|
+
it 'will fail if a setting is supplied but data_type is wrong' do
|
40
|
+
set_test_settings_structure
|
41
|
+
org.settings = {canvas: "Dog", reports: {}}
|
42
|
+
expect(org.valid?).to be_falsey
|
43
|
+
errors = org.errors.messages[:settings]
|
44
|
+
expect(errors[0]).to eq("Expected [:canvas] to be a Hash. Was a String")
|
45
|
+
end
|
46
|
+
|
47
|
+
it 'will fail if a required subsetting is missing' do
|
48
|
+
set_test_settings_structure
|
49
|
+
org.settings = {canvas: {base_url: 'http://'}, reports: {}}
|
50
|
+
expect(org.valid?).to be_falsey
|
51
|
+
errors = org.errors.messages[:settings]
|
52
|
+
expect(errors[0]).to eq("Entry [:canvas][:api_token] is required")
|
53
|
+
end
|
54
|
+
|
55
|
+
it 'will fail if extra options are specified' do
|
56
|
+
set_test_settings_structure
|
57
|
+
org.settings = {canvas: {base_url: 'http://', api_token: 'TEST'}, reports: {}, unknown_option: "WHAT IS THIS?"}
|
58
|
+
expect(org.valid?).to be_falsey
|
59
|
+
errors = org.errors.messages[:settings]
|
60
|
+
expect(errors[0]).to eq("Did not expect [:] to contain [unknown_option]")
|
61
|
+
end
|
62
|
+
|
63
|
+
it 'will pass if all structure is maintained' do
|
64
|
+
set_test_settings_structure
|
65
|
+
org.settings = {canvas: {base_url: 'http://', api_token: 'TEST'}, reports: {submissions_report_time_length: 30.minutes, max_recheck_time: 10.hours}}
|
66
|
+
expect(org.valid?).to be_truthy
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
context 'new settings_structure' do
|
71
|
+
let!(:properties) do
|
72
|
+
{
|
73
|
+
canvas: {
|
74
|
+
required: true,
|
75
|
+
type: 'Hash',
|
76
|
+
properties: {
|
77
|
+
api_token: { type: 'String', required: true },
|
78
|
+
base_url: { type: 'String', required: true },
|
79
|
+
},
|
80
|
+
},
|
81
|
+
reports: {
|
82
|
+
required: true,
|
83
|
+
type: 'Hash',
|
84
|
+
allow_additional: true,
|
85
|
+
}
|
86
|
+
}
|
87
|
+
end
|
88
|
+
|
89
|
+
let!(:structure) do
|
90
|
+
{
|
91
|
+
properties: properties,
|
92
|
+
}
|
93
|
+
end
|
94
|
+
|
95
|
+
it_behaves_like("shared stuff")
|
96
|
+
|
97
|
+
describe ':allow_additional' do
|
98
|
+
it 'passes extra properties if allow_additional is a matching Hash' do
|
99
|
+
structure[:allow_additional] = { type: 'String' }
|
100
|
+
set_test_settings_structure
|
101
|
+
org.settings = {canvas: {base_url: 'http://', api_token: 'TEST'}, reports: {}, unknown_option: "WHAT IS THIS?"}
|
102
|
+
expect(org).to be_valid
|
103
|
+
end
|
104
|
+
|
105
|
+
it 'fails extra properties if allow_additional is an unmatching Hash' do
|
106
|
+
structure[:allow_additional] = { type: 'Hash' }
|
107
|
+
set_test_settings_structure
|
108
|
+
org.settings = {canvas: {base_url: 'http://', api_token: 'TEST'}, reports: {}, unknown_option: "WHAT IS THIS?"}
|
109
|
+
expect(org).not_to be_valid
|
110
|
+
end
|
111
|
+
|
112
|
+
it 'passes extra properties if allow_additional is a blank Hash' do
|
113
|
+
structure[:allow_additional] = { }
|
114
|
+
set_test_settings_structure
|
115
|
+
org.settings = {canvas: {base_url: 'http://', api_token: 'TEST'}, reports: {}, unknown_option: "WHAT IS THIS?"}
|
116
|
+
expect(org).to be_valid
|
117
|
+
end
|
118
|
+
end
|
119
|
+
|
120
|
+
describe ':validate' do
|
121
|
+
let!(:properties) do
|
122
|
+
{
|
123
|
+
blah: {
|
124
|
+
type: 'String',
|
125
|
+
validate: -> (v, *args) { v == 'blah' ? nil : '<path> failed validation' },
|
126
|
+
}
|
127
|
+
}
|
128
|
+
end
|
129
|
+
|
130
|
+
it 'supports a custom validation proc' do
|
131
|
+
set_test_settings_structure
|
132
|
+
org.settings = { blah: 'blah' }
|
133
|
+
expect(org).to be_valid
|
134
|
+
end
|
135
|
+
|
136
|
+
it 'replaces <path> with the human readable path' do
|
137
|
+
set_test_settings_structure
|
138
|
+
org.settings = { blah: '' }
|
139
|
+
expect(org.valid?).to be_falsey
|
140
|
+
errors = org.errors.messages[:settings]
|
141
|
+
expect(errors[0]).to eq("[:blah] failed validation")
|
142
|
+
end
|
143
|
+
end
|
144
|
+
end
|
145
|
+
|
146
|
+
context 'old settings_structure' do
|
147
|
+
let!(:structure) do
|
148
|
+
YAML.load("
|
149
|
+
canvas:
|
150
|
+
is_required: true
|
151
|
+
data_type: Hash
|
152
|
+
api_token:
|
153
|
+
is_required: true
|
154
|
+
data_type: String
|
155
|
+
base_url:
|
156
|
+
is_required: true
|
157
|
+
data_type: String
|
158
|
+
reports:
|
159
|
+
is_required: true
|
160
|
+
data_type: Hash
|
161
|
+
active_term_allowance:
|
162
|
+
submissions_report_time_length:
|
163
|
+
is_required: false
|
164
|
+
data_type: ActiveSupport::Duration
|
165
|
+
recheck_wait:
|
166
|
+
data_type: ActiveSupport::Duration
|
167
|
+
max_recheck_time:
|
168
|
+
is_required: false
|
169
|
+
").deep_symbolize_keys
|
170
|
+
end
|
171
|
+
|
172
|
+
it_behaves_like("shared stuff")
|
173
|
+
end
|
174
|
+
end
|
175
|
+
end
|
@@ -0,0 +1,144 @@
|
|
1
|
+
require 'rails_helper'
|
2
|
+
|
3
|
+
module PandaPal
|
4
|
+
PandaPal::Organization
|
5
|
+
|
6
|
+
RSpec.describe OrganizationConcerns::TaskScheduling, type: :model do
|
7
|
+
let!(:organization) { create(:panda_pal_organization) }
|
8
|
+
let(:schedules) { {} }
|
9
|
+
|
10
|
+
before :each do
|
11
|
+
Organization.instance_variable_set(:@_schedule_descriptors, nil)
|
12
|
+
Organization.scheduled_task('0 0 0 * * *', :ident) { }
|
13
|
+
|
14
|
+
allow(Sidekiq).to receive(:remove_schedule)
|
15
|
+
allow(Sidekiq).to receive(:get_schedule) { schedules }
|
16
|
+
end
|
17
|
+
|
18
|
+
def descriptors
|
19
|
+
Organization.instance_variable_get(:@_schedule_descriptors)
|
20
|
+
end
|
21
|
+
|
22
|
+
def descriptor
|
23
|
+
descriptors[descriptors.keys[0]]
|
24
|
+
end
|
25
|
+
|
26
|
+
describe '.scheduled_task' do
|
27
|
+
it 'adds to the set of descriptors' do
|
28
|
+
expect(descriptors).to match(
|
29
|
+
'ident' => {
|
30
|
+
:key=>"ident",
|
31
|
+
:schedule=>"0 0 0 * * *",
|
32
|
+
:worker=>Proc,
|
33
|
+
:queue=>"default",
|
34
|
+
}
|
35
|
+
)
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
describe '.sync_schedules' do
|
40
|
+
it 'adds new schedules' do
|
41
|
+
expect(Sidekiq).to receive(:set_schedule).with(/org:\w+-ident/, anything)
|
42
|
+
Organization.sync_schedules
|
43
|
+
end
|
44
|
+
|
45
|
+
it 'removes schedules for deleted Orgs' do
|
46
|
+
schedules['org:deleted_org-schedule'] = {}
|
47
|
+
expect(Sidekiq).to receive(:remove_schedule).with('org:deleted_org-schedule')
|
48
|
+
Organization.sync_schedules
|
49
|
+
end
|
50
|
+
|
51
|
+
it 'keeps schedules created from other sources' do
|
52
|
+
schedules['other-schedule'] = {}
|
53
|
+
expect(Sidekiq).not_to receive(:remove_schedule).with('other-schedule')
|
54
|
+
Organization.sync_schedules
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
describe '#generate_schedule' do
|
59
|
+
it 'generates the expected schedule' do
|
60
|
+
expect(organization.generate_schedule).to eq({
|
61
|
+
"org:#{organization.name}-ident" => {
|
62
|
+
"cron"=>"0 0 0 * * *",
|
63
|
+
"queue"=>"default",
|
64
|
+
"class"=>"PandaPal::OrganizationConcerns::TaskScheduling::ScheduledTaskExecutor",
|
65
|
+
"args"=>[organization.name, "ident"],
|
66
|
+
}
|
67
|
+
})
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
describe '#schedule_task_cron_time' do
|
72
|
+
before :each do
|
73
|
+
organization.settings = { timezone: 'America/Denver' }
|
74
|
+
end
|
75
|
+
|
76
|
+
it 'includes timezone if in settings' do
|
77
|
+
expect(organization.send(:schedule_task_cron_time, descriptor)).to eq '0 0 0 * * * America/Denver'
|
78
|
+
end
|
79
|
+
|
80
|
+
it 'does not re-append timezone if already present' do
|
81
|
+
descriptor[:schedule] = '1 1 1 * * * America/Chicago'
|
82
|
+
expect(organization.send(:schedule_task_cron_time, descriptor)).to eq '1 1 1 * * * America/Chicago'
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
describe '#sync_schedule' do
|
87
|
+
it 'adds new schedules' do
|
88
|
+
expect(Sidekiq).to receive(:set_schedule).with(/org:\w+-ident/, anything)
|
89
|
+
organization.sync_schedule
|
90
|
+
end
|
91
|
+
|
92
|
+
it 'removes old jobs' do
|
93
|
+
schedules["org:#{organization.name}-old_schedule"] = {}
|
94
|
+
expect(Sidekiq).to receive(:remove_schedule).with("org:#{organization.name}-old_schedule")
|
95
|
+
organization.sync_schedule
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
describe 'SettingsValidation' do
|
100
|
+
before :each do
|
101
|
+
organization.settings = {}
|
102
|
+
end
|
103
|
+
|
104
|
+
it 'passes a valid timezone' do
|
105
|
+
organization.settings[:timezone] = 'America/Denver'
|
106
|
+
expect(organization).to be_valid
|
107
|
+
end
|
108
|
+
|
109
|
+
it 'fails an invalid timezone' do
|
110
|
+
organization.settings[:timezone] = 'Timezone/Blorg'
|
111
|
+
expect(organization).not_to be_valid
|
112
|
+
end
|
113
|
+
|
114
|
+
it 'allows [:task_schedules] entry to be missing' do
|
115
|
+
expect(organization).to be_valid
|
116
|
+
end
|
117
|
+
|
118
|
+
it 'does not require [:task_schedules] sub-entries' do
|
119
|
+
organization.settings[:task_schedules] = {}
|
120
|
+
expect(organization).to be_valid
|
121
|
+
end
|
122
|
+
|
123
|
+
it 'allows task entry to be false' do
|
124
|
+
organization.settings[:task_schedules] = { ident: false }
|
125
|
+
expect(organization).to be_valid
|
126
|
+
end
|
127
|
+
|
128
|
+
it 'allows task entry to be a valid cron string' do
|
129
|
+
organization.settings[:task_schedules] = { ident: '0 0 0 * * * America/Denver' }
|
130
|
+
expect(organization).to be_valid
|
131
|
+
end
|
132
|
+
|
133
|
+
it 'does not allow task entry to be an invalid cron string' do
|
134
|
+
organization.settings[:task_schedules] = { ident: 'blort' }
|
135
|
+
expect(organization).not_to be_valid
|
136
|
+
end
|
137
|
+
|
138
|
+
it 'does not allow sub-entries for unknown tasks' do
|
139
|
+
organization.settings[:task_schedules] = { missing_ident: '0 0 0 * * * America/Denver' }
|
140
|
+
expect(organization).not_to be_valid
|
141
|
+
end
|
142
|
+
end
|
143
|
+
end
|
144
|
+
end
|
@@ -2,35 +2,6 @@ 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
|
-
|
34
5
|
it 'creates a schema upon creation' do
|
35
6
|
expect(Apartment::Tenant).to receive(:create)
|
36
7
|
create :panda_pal_organization
|
@@ -59,65 +30,5 @@ module PandaPal
|
|
59
30
|
org2 = build :panda_pal_organization, salesforce_id: 'salesforce'
|
60
31
|
expect(org2.valid?).to be_falsey
|
61
32
|
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
|
122
33
|
end
|
123
34
|
end
|
data/spec/spec_helper.rb
CHANGED
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: 5.
|
4
|
+
version: 5.1.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-
|
11
|
+
date: 2020-06-10 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: rails
|
@@ -114,6 +114,34 @@ 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'
|
117
145
|
- !ruby/object:Gem::Dependency
|
118
146
|
name: rspec-rails
|
119
147
|
requirement: !ruby/object:Gem::Requirement
|
@@ -163,10 +191,10 @@ files:
|
|
163
191
|
- app/lib/lti_xml/bridge_platform.rb
|
164
192
|
- app/lib/lti_xml/canvas_platform.rb
|
165
193
|
- app/models/panda_pal/organization.rb
|
194
|
+
- app/models/panda_pal/organization/settings_validation.rb
|
195
|
+
- app/models/panda_pal/organization/task_scheduling.rb
|
166
196
|
- app/models/panda_pal/session.rb
|
167
197
|
- app/views/layouts/panda_pal/application.html.erb
|
168
|
-
- app/views/panda_pal/lti/iframe_cookie_authorize.html.erb
|
169
|
-
- app/views/panda_pal/lti/iframe_cookie_fix.html.erb
|
170
198
|
- app/views/panda_pal/lti/launch.html.erb
|
171
199
|
- config/initializers/apartment.rb
|
172
200
|
- config/routes.rb
|
@@ -208,7 +236,6 @@ files:
|
|
208
236
|
- spec/dummy/config/environments/development.rb
|
209
237
|
- spec/dummy/config/environments/production.rb
|
210
238
|
- spec/dummy/config/environments/test.rb
|
211
|
-
- spec/dummy/config/initializers/assets.rb
|
212
239
|
- spec/dummy/config/initializers/backtrace_silencers.rb
|
213
240
|
- spec/dummy/config/initializers/cookies_serializer.rb
|
214
241
|
- spec/dummy/config/initializers/filter_parameter_logging.rb
|
@@ -230,6 +257,8 @@ files:
|
|
230
257
|
- spec/dummy/public/favicon.ico
|
231
258
|
- spec/factories/panda_pal_organizations.rb
|
232
259
|
- spec/factories/panda_pal_sessions.rb
|
260
|
+
- spec/models/panda_pal/organization/settings_validation_spec.rb
|
261
|
+
- spec/models/panda_pal/organization/task_scheduling_spec.rb
|
233
262
|
- spec/models/panda_pal/organization_spec.rb
|
234
263
|
- spec/models/panda_pal/session_spec.rb
|
235
264
|
- spec/rails_helper.rb
|
@@ -249,9 +278,9 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
249
278
|
version: '0'
|
250
279
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
251
280
|
requirements:
|
252
|
-
- - "
|
281
|
+
- - ">="
|
253
282
|
- !ruby/object:Gem::Version
|
254
|
-
version:
|
283
|
+
version: '0'
|
255
284
|
requirements: []
|
256
285
|
rubygems_version: 3.1.2
|
257
286
|
signing_key:
|
@@ -283,7 +312,6 @@ test_files:
|
|
283
312
|
- spec/dummy/config/initializers/filter_parameter_logging.rb
|
284
313
|
- spec/dummy/config/initializers/session_store.rb
|
285
314
|
- spec/dummy/config/initializers/wrap_parameters.rb
|
286
|
-
- spec/dummy/config/initializers/assets.rb
|
287
315
|
- spec/dummy/config/initializers/cookies_serializer.rb
|
288
316
|
- spec/dummy/config/initializers/inflections.rb
|
289
317
|
- spec/dummy/config.ru
|
@@ -298,6 +326,8 @@ test_files:
|
|
298
326
|
- spec/dummy/log/test.log
|
299
327
|
- spec/dummy/log/development.log
|
300
328
|
- spec/dummy/README.rdoc
|
329
|
+
- spec/models/panda_pal/organization/settings_validation_spec.rb
|
330
|
+
- spec/models/panda_pal/organization/task_scheduling_spec.rb
|
301
331
|
- spec/models/panda_pal/session_spec.rb
|
302
332
|
- spec/models/panda_pal/organization_spec.rb
|
303
333
|
- spec/factories/panda_pal_sessions.rb
|
@@ -1,19 +0,0 @@
|
|
1
|
-
<html>
|
2
|
-
<p>Safari requires your consent to access session information when applications are hosted inside of Canvas. Please consent by clicking the following button.</p>
|
3
|
-
<button id="myButton">Authorize application to use browser session</button>
|
4
|
-
<script nonce=<%= content_security_policy_script_nonce %>>
|
5
|
-
function makeRequestWithUserGesture() {
|
6
|
-
var promise = document.requestStorageAccess();
|
7
|
-
promise.then(
|
8
|
-
function () {
|
9
|
-
var referrer = document.referrer;
|
10
|
-
window.location='?safari_cookie_authorized=true&return_to='.concat(encodeURI(window.location));
|
11
|
-
},
|
12
|
-
function () {
|
13
|
-
// If the user doesn't consent, then do nothing.
|
14
|
-
}
|
15
|
-
);
|
16
|
-
}
|
17
|
-
document.getElementById("myButton").addEventListener("click", makeRequestWithUserGesture);
|
18
|
-
</script>
|
19
|
-
</html>
|
@@ -1,12 +0,0 @@
|
|
1
|
-
<script nonce=<%= content_security_policy_script_nonce %>>
|
2
|
-
const mainWindow = window.parent;
|
3
|
-
var url = window.location.href;
|
4
|
-
// Until PLAT-4836 is resolved, we need to make sure our url has a "?" in it.
|
5
|
-
if (!(url.indexOf("?") > -1)) {
|
6
|
-
url = url + "?dummy_param=1"
|
7
|
-
}
|
8
|
-
mainWindow.postMessage({
|
9
|
-
messageType: "requestFullWindowLaunch",
|
10
|
-
data: url
|
11
|
-
}, '*');
|
12
|
-
</script>
|
@@ -1,11 +0,0 @@
|
|
1
|
-
# Be sure to restart your server when you modify this file.
|
2
|
-
|
3
|
-
# Version of your assets, change this if you want to expire all your assets.
|
4
|
-
Rails.application.config.assets.version = '1.0'
|
5
|
-
|
6
|
-
# Add additional assets to the asset load path
|
7
|
-
# Rails.application.config.assets.paths << Emoji.images_path
|
8
|
-
|
9
|
-
# Precompile additional assets.
|
10
|
-
# application.js, application.css, and all non-JS/CSS in app/assets folder are already added.
|
11
|
-
# Rails.application.config.assets.precompile += %w( search.js )
|