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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: b99a6e3fecfa610524f2b960b7f45ea9b2a337493662a8e6bde4894670c8ded1
4
- data.tar.gz: 6362fd1bd34723195353ddd9a5288ea98d13acf1ddc390291e16f35715a1e2da
3
+ metadata.gz: a3cef6069ef4d4c36af2ff4d665e9ae08d712198ea8422b1f60353e117753aec
4
+ data.tar.gz: 4cce12d864ebbab515d5f496dc2371550dd986b920f9fbb6763b9c77db527e95
5
5
  SHA512:
6
- metadata.gz: 82513c2b4a82fce30288b38b9e1edc9f2071c1d174a9b5eedc337b8f18c4c72214b4731f0df91ab6ce3ab83a52b47bed1e753c1454c81bbe1bd4a68d7c14db34
7
- data.tar.gz: 8bdc013083dfd97b5e3cbfce9b62c111ac31f1018fb773a9fef091e1ba7d94a22284930b231d2a8b4803436aae7b1ca0ee44ca49d75fad6ebd059d08dfb7b148
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
@@ -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 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
 
@@ -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
@@ -1,3 +1,3 @@
1
1
  module PandaPal
2
- VERSION = "5.8.5"
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.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-10-27 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
@@ -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