panda_pal 4.0.11 → 4.1.0.beta2
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +72 -1
- data/app/lib/lti_xml/base_platform.rb +2 -2
- data/app/models/panda_pal/organization/settings_validation.rb +115 -0
- data/app/models/panda_pal/organization/task_scheduling.rb +164 -0
- data/app/models/panda_pal/organization.rb +17 -46
- 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 +134 -0
- data/spec/models/panda_pal/organization_spec.rb +0 -89
- data/spec/spec_helper.rb +8 -0
- metadata +38 -6
- data/spec/dummy/config/initializers/assets.rb +0 -11
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 2c6ac3694136d009439988eff1703cd4df593166
|
4
|
+
data.tar.gz: 0d41152d271eb99d7d248062a4f3500c8c146af1
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: b253820d23c08f25c61de72d420da969647e6c2cda98d3957bfd076c7e26b1752d383ea7c3d38018945eadb4b36fb880ccd2f0e5fdf5d0e38ce808aa8e3cf23f
|
7
|
+
data.tar.gz: b9bae0c7b68314c457beeefc7f78d7f97fc6810be4aa94eb826779e9856c62b8ec9f80eb5fae43edb3870256773e2e5a30c3e56e7e29cab981ac0246cd28544a
|
data/README.md
CHANGED
@@ -28,6 +28,38 @@ 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/organization.rb
|
35
|
+
module PandaPal
|
36
|
+
class Organization
|
37
|
+
# Will invoke CanvasSyncStarterWorker.perform_async() according to the cron schedule
|
38
|
+
scheduled_task '0 15 05 * * *', :identifier, worker: CanvasSyncStarterWorker
|
39
|
+
|
40
|
+
# Will invoke the method 'organization_method' on the Organization
|
41
|
+
scheduled_task '0 15 05 * * *', :organization_method_and_identifier
|
42
|
+
|
43
|
+
# If you need to invoke the same method on multiple schedules
|
44
|
+
scheduled_task '0 15 05 * * *', :identifier, worker: :organization_method
|
45
|
+
|
46
|
+
# You can also use a block
|
47
|
+
scheduled_task '0 15 05 * * *', :identifier do
|
48
|
+
# Do Stuff
|
49
|
+
end
|
50
|
+
|
51
|
+
# You can use a Proc (called in the context of the Organization) to determine the schedule
|
52
|
+
scheduled_task -> { settings[:cron] }, :identifier
|
53
|
+
|
54
|
+
# You can specify a timezone. If a TZ is not coded and settings[:timezone] is present, it will be appended automatically
|
55
|
+
scheduled_task '0 15 05 * * * America/Denver', :identifier, worker: :organization_method
|
56
|
+
|
57
|
+
# Setting settings[:task_schedules][:identifier] will override the code cron schedule. Setting it to false will disable the Task
|
58
|
+
# :identifer values _must_ be unique, but can be nil, in which case they will be determined by where (lineno etc) scheduled_task is called
|
59
|
+
end
|
60
|
+
end
|
61
|
+
```
|
62
|
+
|
31
63
|
# Organization Attributes
|
32
64
|
id: Primary Key
|
33
65
|
name: Name of the organization. Used to on requests to select the tenant
|
@@ -226,9 +258,43 @@ In your panda_pal initializer (i.e. config/initializers/panda_pal.rb or config/i
|
|
226
258
|
You can specify options that can include a structure for your settings. If specified, PandaPal will
|
227
259
|
enforce this structure on any new / updated organizations.
|
228
260
|
|
261
|
+
```ruby
|
262
|
+
PandaPal.lti_options = {
|
263
|
+
title: 'LBS Gradebook',
|
264
|
+
settings_structure: {
|
265
|
+
allow_additional: true, # Allow additional properties that aren't included in the :properties Hash.
|
266
|
+
allow_additional: { type: 'String' }, # You can also set :allow_additional to a settings specification that will be used to validate each additional setting
|
267
|
+
validate: ->(value, spec, **kwargs) {
|
268
|
+
# kwargs currently includes:
|
269
|
+
# :errors => [An array to push errors to]
|
270
|
+
# :path => [An array representation of the current path in the settings object]
|
271
|
+
|
272
|
+
# To add errors, you may:
|
273
|
+
# Push strings to the kwargs[:errors] Array:
|
274
|
+
kwargs[:errors] << "Your error message at <path>" unless value < 10
|
275
|
+
# Or return a string or string array:
|
276
|
+
value.valid? ? nil : "Your error message at <path>" # <path> will be replaced with the actual path that the error occurred at
|
277
|
+
},
|
278
|
+
properties: {
|
279
|
+
canvas_api_token: { type: 'String', required: true, },
|
280
|
+
catalog: { # :validate, :allow_additional, :properties keys are all supported at this level as well
|
281
|
+
type: 'Hash',
|
282
|
+
required: false,
|
283
|
+
validate: -> (*args) {},
|
284
|
+
allow_additional: false,
|
285
|
+
properties: {
|
286
|
+
|
287
|
+
},
|
288
|
+
}
|
289
|
+
}
|
290
|
+
},
|
291
|
+
}
|
292
|
+
```
|
293
|
+
|
294
|
+
#### Legacy Settings Structure:
|
229
295
|
Here is an example options specification:
|
230
296
|
|
231
|
-
```
|
297
|
+
```ruby
|
232
298
|
PandaPal.lti_options = {
|
233
299
|
title: 'LBS Gradebook',
|
234
300
|
settings_structure: YAML.load("
|
@@ -280,3 +346,8 @@ Safari is weird, and you'll potentially run into issues getting `POST` requests
|
|
280
346
|
- Make sure your other controller calls `before_action :forbid_access_if_lacking_session`
|
281
347
|
|
282
348
|
This will allow `PandaPal` to apply an iframe cookie fix that will allow CSRF validation to work.
|
349
|
+
|
350
|
+
## Running Specs:
|
351
|
+
Initialize the Specs DB:
|
352
|
+
`cd spec/dummy; bundle exec rake db:drop; bundle exec rake db:create; bundle exec rake db:schema:load`
|
353
|
+
Then `bundle exec rspec`
|
@@ -38,9 +38,9 @@ module LtiXml
|
|
38
38
|
elsif PandaPal.lti_options[:launch_url].present? # Assumes full URL is provided
|
39
39
|
ims_lti_config[:launch_url] = PandaPal.lti_options[:launch_url]
|
40
40
|
elsif PandaPal.lti_options[:launch_path].present? # Assumes path is provided
|
41
|
-
ims_lti_config[:launch_url] =
|
41
|
+
ims_lti_config[:launch_url] = "#{parsed_request_url.host}#{PandaPal.lti_options[:launch_path]}"
|
42
42
|
else
|
43
|
-
ims_lti_config[:launch_url] = parsed_request_url.
|
43
|
+
ims_lti_config[:launch_url] = parsed_request_url.host
|
44
44
|
end
|
45
45
|
@tc = IMS::LTI::Services::ToolConfig.new(ims_lti_config)
|
46
46
|
end
|
@@ -0,0 +1,115 @@
|
|
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
|
@@ -0,0 +1,164 @@
|
|
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
|
@@ -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
|
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,134 @@
|
|
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 'allows [:task_schedules] entry to be missing' do
|
105
|
+
expect(organization).to be_valid
|
106
|
+
end
|
107
|
+
|
108
|
+
it 'does not require [:task_schedules] sub-entries' do
|
109
|
+
organization.settings[:task_schedules] = {}
|
110
|
+
expect(organization).to be_valid
|
111
|
+
end
|
112
|
+
|
113
|
+
it 'allows task entry to be false' do
|
114
|
+
organization.settings[:task_schedules] = { ident: false }
|
115
|
+
expect(organization).to be_valid
|
116
|
+
end
|
117
|
+
|
118
|
+
it 'allows task entry to be a valid cron string' do
|
119
|
+
organization.settings[:task_schedules] = { ident: '0 0 0 * * * America/Denver' }
|
120
|
+
expect(organization).to be_valid
|
121
|
+
end
|
122
|
+
|
123
|
+
it 'does not allow task entry to be an invalid cron string' do
|
124
|
+
organization.settings[:task_schedules] = { ident: 'blort' }
|
125
|
+
expect(organization).not_to be_valid
|
126
|
+
end
|
127
|
+
|
128
|
+
it 'does not allow sub-entries for unknown tasks' do
|
129
|
+
organization.settings[:task_schedules] = { missing_ident: '0 0 0 * * * America/Denver' }
|
130
|
+
expect(organization).not_to be_valid
|
131
|
+
end
|
132
|
+
end
|
133
|
+
end
|
134
|
+
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
@@ -1,4 +1,8 @@
|
|
1
1
|
ENV["RAILS_ENV"] ||= 'test'
|
2
|
+
|
3
|
+
require 'sidekiq/testing'
|
4
|
+
require 'sidekiq-scheduler'
|
5
|
+
|
2
6
|
require File.expand_path("../dummy/config/environment.rb", __FILE__)
|
3
7
|
require 'rspec/rails'
|
4
8
|
require 'rspec/autorun'
|
@@ -40,6 +44,10 @@ RSpec.configure do |config|
|
|
40
44
|
title: 'Test App',
|
41
45
|
settings_structure: {}
|
42
46
|
}
|
47
|
+
|
48
|
+
allow_any_instance_of(PandaPal::Organization).to receive(:switch_tenant) do |inst, &block|
|
49
|
+
block.call if block.present?
|
50
|
+
end
|
43
51
|
end
|
44
52
|
|
45
53
|
# 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.0.
|
4
|
+
version: 4.1.0.beta2
|
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-04
|
11
|
+
date: 2020-05-04 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,6 +191,8 @@ 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
198
|
- app/views/panda_pal/lti/iframe_cookie_fix.html.erb
|
@@ -203,7 +233,6 @@ files:
|
|
203
233
|
- spec/dummy/config/environments/development.rb
|
204
234
|
- spec/dummy/config/environments/production.rb
|
205
235
|
- spec/dummy/config/environments/test.rb
|
206
|
-
- spec/dummy/config/initializers/assets.rb
|
207
236
|
- spec/dummy/config/initializers/backtrace_silencers.rb
|
208
237
|
- spec/dummy/config/initializers/cookies_serializer.rb
|
209
238
|
- spec/dummy/config/initializers/filter_parameter_logging.rb
|
@@ -221,6 +250,8 @@ files:
|
|
221
250
|
- spec/dummy/public/favicon.ico
|
222
251
|
- spec/factories/panda_pal_organizations.rb
|
223
252
|
- 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
|
224
255
|
- spec/models/panda_pal/organization_spec.rb
|
225
256
|
- spec/models/panda_pal/session_spec.rb
|
226
257
|
- spec/rails_helper.rb
|
@@ -240,9 +271,9 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
240
271
|
version: '0'
|
241
272
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
242
273
|
requirements:
|
243
|
-
- - "
|
274
|
+
- - ">"
|
244
275
|
- !ruby/object:Gem::Version
|
245
|
-
version:
|
276
|
+
version: 1.3.1
|
246
277
|
requirements: []
|
247
278
|
rubyforge_project:
|
248
279
|
rubygems_version: 2.6.11
|
@@ -275,7 +306,6 @@ test_files:
|
|
275
306
|
- spec/dummy/config/initializers/filter_parameter_logging.rb
|
276
307
|
- spec/dummy/config/initializers/session_store.rb
|
277
308
|
- spec/dummy/config/initializers/wrap_parameters.rb
|
278
|
-
- spec/dummy/config/initializers/assets.rb
|
279
309
|
- spec/dummy/config/initializers/cookies_serializer.rb
|
280
310
|
- spec/dummy/config/initializers/inflections.rb
|
281
311
|
- spec/dummy/config.ru
|
@@ -286,6 +316,8 @@ test_files:
|
|
286
316
|
- spec/dummy/public/404.html
|
287
317
|
- spec/dummy/db/schema.rb
|
288
318
|
- spec/dummy/README.rdoc
|
319
|
+
- spec/models/panda_pal/organization/settings_validation_spec.rb
|
320
|
+
- spec/models/panda_pal/organization/task_scheduling_spec.rb
|
289
321
|
- spec/models/panda_pal/session_spec.rb
|
290
322
|
- spec/models/panda_pal/organization_spec.rb
|
291
323
|
- spec/factories/panda_pal_sessions.rb
|
@@ -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 )
|