panda_pal 5.8.4 → 5.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: cd953a87fa9112452135474919816d3955cd83240d3814534a9d0cce715ab648
4
- data.tar.gz: 833ce3c958a66074f62f31fe24212bab4937aa08f8b77103aa509d649fd3a641
3
+ metadata.gz: a3cef6069ef4d4c36af2ff4d665e9ae08d712198ea8422b1f60353e117753aec
4
+ data.tar.gz: 4cce12d864ebbab515d5f496dc2371550dd986b920f9fbb6763b9c77db527e95
5
5
  SHA512:
6
- metadata.gz: 434bd73034d184046b7683e0fcccb35cff695ce4efcbc217047b8058ee5d7b5c81c2295973da79b5b64ca395992d864fdcc1f4b1876cb532d1402cefb5781af1
7
- data.tar.gz: 7bfb9934d3caad860a1f141b35c8ab65347803da6c737fbe15fc5a85356044eba6fd43470654fd7bba925611b4c1ef59ce3c1e245a17cb242a956683d708dcc2
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
- As such, `rake panda_pal:clean_sessions` should be run periodically to clean up sessions that haven't been updated in over a week.
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
@@ -49,12 +49,6 @@ module PandaPal
49
49
 
50
50
  redirect_with_session_to(:"#{LaunchUrlHelpers.launch_route(ltype)}_url", route_context: main_app)
51
51
  end
52
- # render json: {
53
- # launch_type: params[:launch_type],
54
- # final_url: LaunchUrlHelpers.launch_url(params[:launch_type]),
55
- # final_route: LaunchUrlHelpers.launch_route(params[:launch_type]),
56
- # decoded_jwt: @decoded_lti_jwt,
57
- # }
58
52
  end
59
53
 
60
54
  def tool_config
@@ -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
@@ -75,7 +143,7 @@ module PandaPal
75
143
 
76
144
  def validate_settings
77
145
  validate_settings_level(settings || {}, settings_structure).each do |err|
78
- errors[:settings] << err
146
+ errors.add(:settings, err)
79
147
  end
80
148
  end
81
149
 
@@ -74,7 +74,7 @@ module PandaPal
74
74
  [ctype, cid]
75
75
  end
76
76
 
77
- def install_lti(host: nil, context: :root_account, version: nil, exists: :error, dedicated_deployment: false)
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
 
@@ -323,7 +329,6 @@ module PandaPal
323
329
  Bearcat::Client.new(
324
330
  prefix: canvas_url,
325
331
  token: canvas_token,
326
- master_rate_limit: (Rails.env.production? && !!defined?(Redis) && ENV['REDIS_URL'].present?),
327
332
  )
328
333
  end
329
334
  end
@@ -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
@@ -9,7 +9,9 @@ module PandaPal::Helpers::RouteHelper
9
9
  path = "#{base_path}/#{nav.to_s}"
10
10
 
11
11
  lti_options = options.delete(:lti_options) || {}
12
+
12
13
  lti_options[:auto_launch] = options.delete(:auto_launch)
14
+ # NB if lti_nav is outside an :organization_id scope, auto_launch defaults differently between 1.1 and 1.3 - 1.1 defaults off, 1.3 defaults on
13
15
  lti_options[:auto_launch] = true if lti_options[:auto_launch].nil? && (@scope[:path] || '').include?(':organization_id')
14
16
 
15
17
  lti_options[:route_helper_key] = path.split('/').reject(&:empty?).join('_')
@@ -1,3 +1,3 @@
1
1
  module PandaPal
2
- VERSION = "5.8.4"
2
+ VERSION = "5.9.0"
3
3
  end
@@ -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.8.4
4
+ version: 5.9.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Instructure CustomDev
8
- autorequire:
8
+ autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2023-08-22 00:00:00.000000000 Z
11
+ date: 2023-11-14 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rails
@@ -282,7 +282,7 @@ dependencies:
282
282
  - - '='
283
283
  - !ruby/object:Gem::Version
284
284
  version: 2.7.1
285
- description:
285
+ description:
286
286
  email:
287
287
  - pseng@instructure.com
288
288
  executables: []
@@ -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
@@ -394,7 +395,7 @@ homepage: http://instructure.com
394
395
  licenses:
395
396
  - MIT
396
397
  metadata: {}
397
- post_install_message:
398
+ post_install_message:
398
399
  rdoc_options: []
399
400
  require_paths:
400
401
  - lib
@@ -410,7 +411,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
410
411
  version: '0'
411
412
  requirements: []
412
413
  rubygems_version: 3.1.6
413
- signing_key:
414
+ signing_key:
414
415
  specification_version: 4
415
416
  summary: LTI mountable engine
416
417
  test_files: