panda_pal 5.8.5 → 5.9.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 +28 -1
- data/app/models/panda_pal/organization.rb +1 -0
- data/app/models/panda_pal/organization_concerns/organization_builder.rb +163 -0
- data/app/models/panda_pal/organization_concerns/settings_validation.rb +68 -0
- data/app/models/panda_pal/platform/canvas.rb +7 -1
- data/lib/panda_pal/helpers/console_helpers.rb +129 -0
- data/lib/panda_pal/version.rb +1 -1
- data/lib/tasks/panda_pal_tasks.rake +37 -0
- metadata +3 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: a3cef6069ef4d4c36af2ff4d665e9ae08d712198ea8422b1f60353e117753aec
|
4
|
+
data.tar.gz: 4cce12d864ebbab515d5f496dc2371550dd986b920f9fbb6763b9c77db527e95
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 0102ced164298cd5c56d9bd62bfbad019301fbc78d150eca69c0b90c142c038febe13a04c7d801a3d357a91cef3533110af26f537fba61725a0033bf5a42b9bd
|
7
|
+
data.tar.gz: d188c8bfcbe5cfc73b3d5570c8a5252c0589f4443e3dd364e182f7252a09495c5888d87b515e00e5cd6907f392dda15e10ef14e346c5f7507cefedeff1812bfc
|
data/README.md
CHANGED
@@ -275,11 +275,38 @@ and a custom implementation can be supplemented.
|
|
275
275
|
|
276
276
|
### Session cleanup
|
277
277
|
Over time, the sessions table will become bloated with sessions that are no longer active.
|
278
|
-
|
278
|
+
If `sidekiq-scheduler` and `TaskScheduling` are enabled, cleanup will be performed automatically. Otherwise you should schedule, `rake panda_pal:clean_sessions` to run periodically (recommend weekly).
|
279
279
|
|
280
280
|
## Organizations and `current_organization`
|
281
281
|
Similar to `current_session`, `current_organization` can be returned with a number of methods, shown below
|
282
282
|
|
283
|
+
## Rake Tasks
|
284
|
+
|
285
|
+
### `panda_pal:org:new`
|
286
|
+
Opens `$EDITOR` and creates a new `Organization`. The opened editor is prefilled with all of the available Settings with optional settings pre-commented.
|
287
|
+
|
288
|
+
### `panda_pal:org:dev`
|
289
|
+
Similar to `panda_pal:org:new`, but is somewhat preconfigured for development environments.
|
290
|
+
|
291
|
+
You can create a `~/pandapalrc.yml` file to specify additional defaults:
|
292
|
+
```yml
|
293
|
+
lti_host: http://localhost:5000
|
294
|
+
settings:
|
295
|
+
canvas:
|
296
|
+
base_url: http://localhost:3000
|
297
|
+
api_token: Uo5yckRQuPl96qVxgt2IVxDAa5oSUo1dqbb2dBVOnpId18aTmjSXdYkn4BN9JF2k
|
298
|
+
```
|
299
|
+
NB: `settings` entries present in `~/pandapalrc.yml` that are not defined in the `settings_structure` will be ignored.
|
300
|
+
|
301
|
+
### `panda_pal:org:edit[name]`
|
302
|
+
Similar to `panda_pal:org:new` but for editing an existing Organization.
|
303
|
+
|
304
|
+
### `panda_pal:org:install[orgname, http://url.of.lti]`
|
305
|
+
Install the LTI into Canvas. Must specify the Organization and the host of the LTI
|
306
|
+
|
307
|
+
### `panda_pal:org:reinstall[orgname, http://url.of.lti]`
|
308
|
+
Same as `panda_pal:org:install`, but first deletes an existing installation
|
309
|
+
|
283
310
|
## Security
|
284
311
|
|
285
312
|
### Tool Launches
|
@@ -38,6 +38,7 @@ module PandaPal
|
|
38
38
|
include SkipSymmetricEncAttrEncrypted if defined?(SkipSymmetricEncAttrEncrypted)
|
39
39
|
|
40
40
|
include OrganizationConcerns::SettingsValidation
|
41
|
+
include OrganizationConcerns::OrganizationBuilder if $stdin&.tty?
|
41
42
|
include OrganizationConcerns::TaskScheduling if defined?(Sidekiq.schedule)
|
42
43
|
|
43
44
|
attr_encrypted :settings, marshal: true, key: :encryption_key, marshaler: SettingsMarshaler
|
@@ -0,0 +1,163 @@
|
|
1
|
+
module PandaPal
|
2
|
+
module OrganizationConcerns
|
3
|
+
module OrganizationBuilder
|
4
|
+
extend ActiveSupport::Concern
|
5
|
+
include OrganizationConcerns::SettingsValidation
|
6
|
+
|
7
|
+
class InteractiveSessionError < StandardError
|
8
|
+
attr_reader :source_code
|
9
|
+
|
10
|
+
def initialize(message, source_code)
|
11
|
+
super(message)
|
12
|
+
@source_code = source_code
|
13
|
+
end
|
14
|
+
|
15
|
+
def print
|
16
|
+
puts cause&.message, cause&.backtrace
|
17
|
+
puts ""
|
18
|
+
puts message
|
19
|
+
puts "Code is below for tweaking and/or using it in a Rails console"
|
20
|
+
puts ""
|
21
|
+
puts source_code
|
22
|
+
puts ""
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
class_methods do
|
27
|
+
def _interactive_save!(org)
|
28
|
+
code = org.generate_orgbuilder_ruby
|
29
|
+
|
30
|
+
tries = 0
|
31
|
+
result = nil
|
32
|
+
|
33
|
+
loop do
|
34
|
+
begin
|
35
|
+
code = ConsoleHelpers.open_string_editor(code, name: "organization.rb", require_save: tries == 0)
|
36
|
+
rescue => ex
|
37
|
+
raise InteractiveSessionError.new("Failed to open interactive editor", code)
|
38
|
+
end
|
39
|
+
|
40
|
+
if code == :aborted
|
41
|
+
puts "Aborted"
|
42
|
+
return
|
43
|
+
end
|
44
|
+
|
45
|
+
begin
|
46
|
+
result = eval(code, TOPLEVEL_BINDING)
|
47
|
+
break
|
48
|
+
rescue => ex
|
49
|
+
puts "Failed to save Organization: #{ex}"
|
50
|
+
if !ConsoleHelpers.prompt_yes_no("Retry?")
|
51
|
+
raise InteractiveSessionError.new("Failed to save Organization", code)
|
52
|
+
end
|
53
|
+
tries += 1
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
result
|
58
|
+
end
|
59
|
+
|
60
|
+
def interactive_create!(org = PandaPal::Organization.new)
|
61
|
+
result = _interactive_save!(org)
|
62
|
+
|
63
|
+
if result&.persisted? && ConsoleHelpers.prompt_yes_no("Organization created. Install in Canvas?", default: false)
|
64
|
+
result.interactive_install!
|
65
|
+
end
|
66
|
+
|
67
|
+
result
|
68
|
+
end
|
69
|
+
|
70
|
+
if Rails.env.development?
|
71
|
+
def development_create!
|
72
|
+
if PandaPal::Organization.count > 0
|
73
|
+
return unless ConsoleHelpers.prompt_yes_no("An Organization is already set up. Continue?")
|
74
|
+
end
|
75
|
+
|
76
|
+
org = PandaPal::Organization.new(
|
77
|
+
name: "local",
|
78
|
+
canvas_account_id: "1",
|
79
|
+
key: "#{_panda_pal_console_app_name}-local",
|
80
|
+
secret: "#{_panda_pal_console_app_name}-local",
|
81
|
+
salesforce_id: "1",
|
82
|
+
)
|
83
|
+
|
84
|
+
rcsettings = ConsoleHelpers.pandapalrc["settings"] || {}
|
85
|
+
cleaned_settings = PandaPal::Organization.remove_undeclared_settings(rcsettings)
|
86
|
+
org.settings = cleaned_settings
|
87
|
+
|
88
|
+
result = interactive_create!(org)
|
89
|
+
|
90
|
+
return unless result
|
91
|
+
|
92
|
+
puts "Created Development Organization: #{result.name}"
|
93
|
+
|
94
|
+
result.switch_tenant
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
def interactive_update!
|
100
|
+
saved = self.class._interactive_save!(self)
|
101
|
+
puts "Organization Updated" if saved
|
102
|
+
saved
|
103
|
+
end
|
104
|
+
|
105
|
+
def interactive_install!(host: nil, exists: :error)
|
106
|
+
first = true
|
107
|
+
loop do
|
108
|
+
# If a host is explicitly passed, don't prompt for it on the first try
|
109
|
+
unless first && host.present?
|
110
|
+
if Rails.env.development?
|
111
|
+
host ||= ConsoleHelpers.pandapalrc["lti_host"].presence || "http://localhost:5000"
|
112
|
+
end
|
113
|
+
host = ConsoleHelpers.prompt("Specify LTI host:", default: host)
|
114
|
+
end
|
115
|
+
|
116
|
+
begin
|
117
|
+
install_lti(host: host, exists: exists)
|
118
|
+
break
|
119
|
+
rescue => ex
|
120
|
+
puts "Failed to install in Canvas: #{ex}"
|
121
|
+
raise ex unless ConsoleHelpers.prompt_pry_retry
|
122
|
+
first = false
|
123
|
+
end
|
124
|
+
end
|
125
|
+
end
|
126
|
+
|
127
|
+
def generate_orgbuilder_ruby
|
128
|
+
code = ConsoleHelpers::CodeBuilder.new
|
129
|
+
|
130
|
+
columns = %w[
|
131
|
+
canvas_account_id
|
132
|
+
key
|
133
|
+
secret
|
134
|
+
salesforce_id
|
135
|
+
]
|
136
|
+
|
137
|
+
if self.persisted?
|
138
|
+
code << "PandaPal::Organization.find_by(name: \"#{self.name}\").update!("
|
139
|
+
else
|
140
|
+
code << "PandaPal::Organization.create!("
|
141
|
+
columns.unshift("name")
|
142
|
+
end
|
143
|
+
|
144
|
+
code << "\n"
|
145
|
+
|
146
|
+
code.block do
|
147
|
+
columns.each do |col|
|
148
|
+
code << "#{col}: \"#{self.send(col)}\","
|
149
|
+
code << "\n"
|
150
|
+
end
|
151
|
+
|
152
|
+
code << "settings: "
|
153
|
+
code << PandaPal::Organization.generate_settings_ruby(value: settings)
|
154
|
+
code << ",\n"
|
155
|
+
end
|
156
|
+
|
157
|
+
code << ")"
|
158
|
+
|
159
|
+
code.to_s
|
160
|
+
end
|
161
|
+
end
|
162
|
+
end
|
163
|
+
end
|
@@ -65,8 +65,76 @@ module PandaPal
|
|
65
65
|
nstruc[:required] = struc.delete(:is_required) if struc.key?(:is_required)
|
66
66
|
nstruc[:properties] = struc.map { |k, sub| [k, normalize_settings_structure(sub)] }.to_h if struc.present?
|
67
67
|
|
68
|
+
nstruc[:type] = "Hash" if !nstruc[:type] && nstruc.key?(:properties)
|
69
|
+
|
68
70
|
nstruc
|
69
71
|
end
|
72
|
+
|
73
|
+
def remove_undeclared_settings(value, setting: settings_structure)
|
74
|
+
if setting[:type] == "Hash"
|
75
|
+
value.dup.tap do |value|
|
76
|
+
value.keys.each do |key|
|
77
|
+
if setting[:properties].key?(key.to_sym)
|
78
|
+
value[key] = remove_undeclared_settings(value[key], setting: setting[:properties][key.to_sym])
|
79
|
+
elsif !setting[:allow_additional]
|
80
|
+
value.delete(key)
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
84
|
+
else
|
85
|
+
value
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
def generate_settings_ruby(indent: 0, setting: settings_structure, value: { }, exclude_extra: false)
|
90
|
+
builder = ConsoleHelpers::CodeBuilder.new(indent: indent)
|
91
|
+
|
92
|
+
value = setting[:default] || setting.dig(:json_schema, :default) if value == :not_given
|
93
|
+
|
94
|
+
if setting[:type] == "Hash"
|
95
|
+
builder << "{"
|
96
|
+
builder << "\n"
|
97
|
+
builder.indent!
|
98
|
+
setting[:properties].each do |key, sub|
|
99
|
+
if sub[:description]
|
100
|
+
builder << sub[:description].lines.map{|l| "# #{l}"}
|
101
|
+
builder.ensure_line
|
102
|
+
end
|
103
|
+
|
104
|
+
sub_val = value&.key?(key) ? value[key] : :not_given
|
105
|
+
commented = sub_val == :not_given && !sub[:required]
|
106
|
+
|
107
|
+
builder.indent!("# ") if commented
|
108
|
+
builder << "#{key}: "
|
109
|
+
builder << generate_settings_ruby(setting: sub, value: sub_val)
|
110
|
+
builder << ",\n"
|
111
|
+
builder.dedent! if commented
|
112
|
+
end
|
113
|
+
|
114
|
+
# If we're editing an existing org, we may have extra keys that aren't in the schema
|
115
|
+
# But if we're creating a new org, we don't want to include undocumented entries from pandapalrc.yml
|
116
|
+
unless exclude_extra
|
117
|
+
extra_keys = (value&.keys || []) - setting[:properties].keys.map(&:to_s)
|
118
|
+
extra_keys.each do |key|
|
119
|
+
builder << "# Undocumented\n" unless setting[:allow_additional]
|
120
|
+
builder << "#{key}: #{value[key].inspect},\n"
|
121
|
+
end
|
122
|
+
end
|
123
|
+
|
124
|
+
builder.dedent!
|
125
|
+
builder << "}"
|
126
|
+
elsif setting[:type] == "Array"
|
127
|
+
builder << "#{value&.inspect || '[]'}"
|
128
|
+
elsif setting[:type] == "Integer"
|
129
|
+
builder << "#{value || 0}"
|
130
|
+
elsif setting[:type] == "Numeric"
|
131
|
+
builder << "#{value || 1.0}"
|
132
|
+
else setting[:type] == "String"
|
133
|
+
builder << "\"#{value}\""
|
134
|
+
end
|
135
|
+
|
136
|
+
builder.to_s
|
137
|
+
end
|
70
138
|
end
|
71
139
|
|
72
140
|
def settings_structure
|
@@ -74,7 +74,7 @@ module PandaPal
|
|
74
74
|
[ctype, cid]
|
75
75
|
end
|
76
76
|
|
77
|
-
def
|
77
|
+
def _install_lti(host: nil, context: :root_account, version: nil, exists: :error, dedicated_deployment: false)
|
78
78
|
raise "Automatically installing this LTI requires Bearcat." unless defined?(Bearcat)
|
79
79
|
|
80
80
|
version = version || PandaPal.lti_options[:lti_spec_version] || 'v1p0'
|
@@ -138,6 +138,12 @@ module PandaPal
|
|
138
138
|
save!
|
139
139
|
end
|
140
140
|
|
141
|
+
def install_lti(*args, **kwargs)
|
142
|
+
switch_tenant do
|
143
|
+
_install_lti(*args, **kwargs)
|
144
|
+
end
|
145
|
+
end
|
146
|
+
|
141
147
|
def _ensure_lti_v1p3_key(exists:, host:)
|
142
148
|
current_client_id = self.key.split('/')[0]
|
143
149
|
|
@@ -23,5 +23,134 @@ module PandaPal
|
|
23
23
|
"\033[1;#{30 + value}m#{text}\033[0m"
|
24
24
|
end
|
25
25
|
end
|
26
|
+
|
27
|
+
def pandapalrc
|
28
|
+
# TODO Consider searching app and parent dirs before ~/
|
29
|
+
@pandapalrc ||= YAML.load(File.read(File.expand_path("~/pandapalrc.yml"))) rescue {}
|
30
|
+
end
|
31
|
+
|
32
|
+
def prompt(prompt = "", default: nil)
|
33
|
+
prompt = prompt + " (#{default})" if default.present?
|
34
|
+
puts prompt
|
35
|
+
print "> "
|
36
|
+
v = gets.chomp.downcase
|
37
|
+
return default if v == ""
|
38
|
+
v
|
39
|
+
end
|
40
|
+
|
41
|
+
def prompt_options(options, prompt = "", default: nil)
|
42
|
+
options = options.map(&:to_s)
|
43
|
+
prompt = prompt + " (" + options.map { |o| o == default ? o.capitalize : o }.join("/") + ")"
|
44
|
+
i = 0
|
45
|
+
loop do
|
46
|
+
puts prompt
|
47
|
+
print "> "
|
48
|
+
i += 1
|
49
|
+
v = gets.chomp.downcase
|
50
|
+
return v if options.include?(v)
|
51
|
+
return default if v == ""
|
52
|
+
return nil if i > 3
|
53
|
+
puts "Invalid Input."
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
def prompt_yes_no(prompt = "", default: true)
|
58
|
+
result = prompt_options(["y", "n"], prompt, default: default ? "y" : "n")
|
59
|
+
return true if result == "y"
|
60
|
+
return false if result == "n"
|
61
|
+
result
|
62
|
+
end
|
63
|
+
|
64
|
+
def prompt_pry_retry(prompt = "Retry?", default: false)
|
65
|
+
default = default ? "y" : "n" unless default.is_a?(String)
|
66
|
+
result = prompt_options(["y", "n", "pry"], prompt, default: default ? "y" : "n")
|
67
|
+
if result == "pry"
|
68
|
+
binding.pry
|
69
|
+
return true
|
70
|
+
end
|
71
|
+
return true if result == "y"
|
72
|
+
return false if result == "n"
|
73
|
+
result
|
74
|
+
end
|
75
|
+
|
76
|
+
def open_editor(file_path)
|
77
|
+
raise "EDITOR environment variable not set" unless ENV["EDITOR"].present?
|
78
|
+
|
79
|
+
args = Shellwords.shellwords(ENV['EDITOR'])
|
80
|
+
args << file_path
|
81
|
+
Kernel::system(*args)
|
82
|
+
end
|
83
|
+
|
84
|
+
def open_string_editor(string, file: nil, name: nil, require_save: true)
|
85
|
+
file_obj = file.present? ? File.new(file) : Tempfile.new([File.basename(name, File.extname(name)), File.extname(name)])
|
86
|
+
File.open(file_obj.path, 'w') { |f| f.write(string) }
|
87
|
+
|
88
|
+
mtime = File.stat(file_obj.path).mtime
|
89
|
+
|
90
|
+
path = file_obj.path
|
91
|
+
file_obj.close rescue nil
|
92
|
+
open_editor(path)
|
93
|
+
|
94
|
+
return :aborted unless !require_save || mtime < File.stat(file_obj.path).mtime
|
95
|
+
|
96
|
+
File.read(path)
|
97
|
+
end
|
98
|
+
|
99
|
+
class CodeBuilder
|
100
|
+
def initialize(indent: 0)
|
101
|
+
@code = ""
|
102
|
+
@line_prefix = [" "] * indent
|
103
|
+
end
|
104
|
+
|
105
|
+
def <<(line)
|
106
|
+
if line.is_a?(Array)
|
107
|
+
line.each do |l|
|
108
|
+
self << l
|
109
|
+
end
|
110
|
+
else
|
111
|
+
bits = line.split("\n", -1)
|
112
|
+
|
113
|
+
push_bit(bits.shift)
|
114
|
+
|
115
|
+
bits.each do |bit|
|
116
|
+
@code << "\n"
|
117
|
+
push_bit(bit)
|
118
|
+
end
|
119
|
+
end
|
120
|
+
end
|
121
|
+
|
122
|
+
def ensure_line
|
123
|
+
return if @code.ends_with?("\n")
|
124
|
+
@code << "\n"
|
125
|
+
end
|
126
|
+
|
127
|
+
def block(char = nil)
|
128
|
+
indent!(char)
|
129
|
+
yield
|
130
|
+
ensure
|
131
|
+
dedent!
|
132
|
+
end
|
133
|
+
|
134
|
+
def indent!(char = nil)
|
135
|
+
@line_prefix << (char || " ")
|
136
|
+
end
|
137
|
+
|
138
|
+
def dedent!
|
139
|
+
@line_prefix.pop
|
140
|
+
end
|
141
|
+
|
142
|
+
def to_s
|
143
|
+
@code
|
144
|
+
end
|
145
|
+
|
146
|
+
protected
|
147
|
+
|
148
|
+
def push_bit(bit)
|
149
|
+
return unless bit.present?
|
150
|
+
|
151
|
+
@code << @line_prefix.join if @code.ends_with?("\n")
|
152
|
+
@code << bit
|
153
|
+
end
|
154
|
+
end
|
26
155
|
end
|
27
156
|
end
|
data/lib/panda_pal/version.rb
CHANGED
@@ -3,4 +3,41 @@ namespace :panda_pal do
|
|
3
3
|
task clean_sessions: :environment do
|
4
4
|
PandaPal::Session.where('updated_at < ?', 1.week.ago).delete_all
|
5
5
|
end
|
6
|
+
|
7
|
+
namespace :org do
|
8
|
+
desc "Interactively Create a new PandaPal::Organization"
|
9
|
+
task :new, [:name] => :environment do |t, args|
|
10
|
+
org = PandaPal::Organization.new(name: args[:name])
|
11
|
+
PandaPal::Organization.interactive_create!(org)
|
12
|
+
rescue PandaPal::OrganizationConcerns::OrganizationBuilder::InteractiveSessionError => ex
|
13
|
+
ex.print
|
14
|
+
end
|
15
|
+
|
16
|
+
desc "Interactively Update a PandaPal::Organization"
|
17
|
+
task :edit, [:name] => :environment do |t, args|
|
18
|
+
org = PandaPal::Organization.find_by!(name: args[:name])
|
19
|
+
org.interactive_update!
|
20
|
+
rescue PandaPal::OrganizationConcerns::OrganizationBuilder::InteractiveSessionError => ex
|
21
|
+
ex.print
|
22
|
+
end
|
23
|
+
|
24
|
+
desc "Install/update the LTI in Canvas for the given Organization"
|
25
|
+
task :install, [:name, :host] => :environment do |t, args|
|
26
|
+
org = PandaPal::Organization.find_by!(name: args[:name])
|
27
|
+
org.interactive_install!(host: args[:host], exists: :update)
|
28
|
+
end
|
29
|
+
|
30
|
+
desc "Reinstall the LTI in Canvas for the given Organization"
|
31
|
+
task :reinstall, [:name, :host] => :environment do |t, args|
|
32
|
+
org = PandaPal::Organization.find_by!(name: args[:name])
|
33
|
+
org.interactive_install!(host: args[:host], exists: :replace)
|
34
|
+
end
|
35
|
+
|
36
|
+
if Rails.env.development?
|
37
|
+
desc "Creates a new PandaPal::Organization for development"
|
38
|
+
task dev: :environment do
|
39
|
+
PandaPal::Organization.development_create!
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
6
43
|
end
|
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.9.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Instructure CustomDev
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2023-
|
11
|
+
date: 2023-11-14 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: rails
|
@@ -311,6 +311,7 @@ files:
|
|
311
311
|
- app/lib/panda_pal/lti_jwt_validator.rb
|
312
312
|
- app/models/panda_pal/api_call.rb
|
313
313
|
- app/models/panda_pal/organization.rb
|
314
|
+
- app/models/panda_pal/organization_concerns/organization_builder.rb
|
314
315
|
- app/models/panda_pal/organization_concerns/settings_validation.rb
|
315
316
|
- app/models/panda_pal/organization_concerns/task_scheduling.rb
|
316
317
|
- app/models/panda_pal/panda_pal_record.rb
|