panda_pal 5.6.12 → 5.7.0.beta1

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: 29034766a62fed97c43d0e3ef2754e1f04a4d925b8a467bd78dda5a2f5d87f1e
4
- data.tar.gz: d4c3c29aa255a8311bbe00d1dbe0358f247425fa8ed1f7eddfca1ec7315897d5
3
+ metadata.gz: c17abc690e56e5bedffa292170eee88041e9d3214200a9bcf75571fc18b10a69
4
+ data.tar.gz: 40c41edfe6b016d6f9c8db8316fa44f1d7c9e7cff03ab9eb88fda74e6d695712
5
5
  SHA512:
6
- metadata.gz: e4ca709472f506cb84cfc9020b254e3ddafdf756f6dd83325f314910914878adfc1ea3363c8bca938f3933a27852205ccb9e1832df7a67a501bc442d4e3e3b80
7
- data.tar.gz: 72fe59902312a2da78350ab1a095808e8c1e902ca02e208474413fa06989f6bbb0f8c1174e929555b501674754b21f4ebdbfce7d73404f7ffe0d27595b7546ad
6
+ metadata.gz: 48ea51186371b9941434937ed23cfcf8b44f0726fd8d89cf280e7e65edc132702ccaec6e98917194c0c29d320f77f1218287906d9a3489b4efcf8fc8546d0332
7
+ data.tar.gz: 2005bc3182ffdf509da90ae5e29363a6bc79964e71a558386fb8c9ab0de8bf16a33f071ea0e40d7ab44bf5c9eb1f74549491b37eb12010bf6d0820b1acffe4eb
data/README.md CHANGED
@@ -29,7 +29,7 @@ org.install_lti(
29
29
  context: "account/self", # (Optional) Or "account/3", "course/1", etc
30
30
  exists: :error, # (Optional) Action to take if an LTI with the same Key already exists. Options are :error, :replace, :duplicate
31
31
  version: "v1p3", # (Optional, default `v1p3`) LTI Version. Accepts `v1p0` or `v1p3`.
32
- dedicated_deployment: false, # (Optional) If true, the Organization will be updated to link to a single deployment rather then to the general LTI Key. (experimental)
32
+ dedicated_deployment: false, # (Optional) If true, the Organization will be updated to link to a single deployment rather then to the general LTI Key. (experimental, LTI 1.3 only)
33
33
  )
34
34
  ```
35
35
 
@@ -1,7 +1,7 @@
1
1
  require 'jwt'
2
2
 
3
3
  module PandaPal
4
- class ApiCall < ActiveRecord::Base
4
+ class ApiCall < PandaPalRecord
5
5
  # TODO Add Rate Limiting?
6
6
 
7
7
  def self.decode_jwt(jwt)
@@ -1,7 +1,7 @@
1
1
  module PandaPal
2
2
  module OrganizationConcerns; end
3
3
 
4
- # Nwer versions of SymmetricEncryption introduced it's own version of attr_encrypted
4
+ # Newer versions of SymmetricEncryption introduced it's own version of attr_encrypted
5
5
  # that is completely incompatible with the attr_encrypted Gem that PandaPal uses.
6
6
  if defined?(::SymmetricEncryption::ActiveRecord::AttrEncrypted)
7
7
  module SkipSymmetricEncAttrEncrypted
@@ -21,16 +21,34 @@ module PandaPal
21
21
  end
22
22
  end
23
23
 
24
- class Organization < ActiveRecord::Base
24
+ class SettingsMarshaler
25
+ def self.load(data)
26
+ return nil unless data.present?
27
+ loaded = Marshal.load(data)
28
+ loaded = loaded.with_indifferent_access if loaded.is_a?(Hash) && !loaded.is_a?(HashWithIndifferentAccess)
29
+ loaded
30
+ end
31
+
32
+ def self.dump(obj)
33
+ Marshal.dump(obj)
34
+ end
35
+ end
36
+
37
+ class Organization < PandaPalRecord
25
38
  include SkipSymmetricEncAttrEncrypted if defined?(SkipSymmetricEncAttrEncrypted)
26
39
 
27
40
  include OrganizationConcerns::SettingsValidation
28
41
  include OrganizationConcerns::TaskScheduling if defined?(Sidekiq.schedule)
29
42
 
30
- serialize :settings, Hash
31
- attr_encrypted :settings, marshal: true, key: :encryption_key
43
+ attr_encrypted :settings, marshal: true, key: :encryption_key, marshaler: SettingsMarshaler
32
44
  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
33
45
 
46
+ alias_method "settings_panda_pal_super=", "settings="
47
+ def settings=(settings)
48
+ settings = settings.with_indifferent_access if settings.is_a?(Hash) && !settings.is_a?(HashWithIndifferentAccess)
49
+ self.settings_panda_pal_super = settings
50
+ end
51
+
34
52
  validates :key, uniqueness: { case_sensitive: false }, presence: true
35
53
  validates :secret, presence: true
36
54
  validates :name, uniqueness: { case_sensitive: false }, presence: true, format: { with: /\A[a-z0-9_]+\z/i }
@@ -84,10 +102,6 @@ module PandaPal
84
102
  end
85
103
  end
86
104
 
87
- def self.resolve_platform(hint)
88
- iss = hint["iss"]
89
- end
90
-
91
105
  def rename!(new_name)
92
106
  do_switch = Apartment::Tenant.current == name
93
107
  ActiveRecord::Base.connection.execute(
@@ -98,23 +112,56 @@ module PandaPal
98
112
  switch_tenant if do_switch
99
113
  end
100
114
 
101
- def platform_extensions(platform_type)
102
- return self unless defined?(platform_type::OrgExtension)
103
- return self if self.class < platform_type::OrgExtension
104
-
105
- scl = platform_type.org_subclass
106
- scl ? becomes(scl) : self
115
+ if !PandaPal.lti_options[:platform].present? || PandaPal.lti_options[:platform].is_a?(String)
116
+ CONST_PLATFORM_TYPE = Platform.resolve_platform_class(nil) rescue nil
117
+ else
118
+ CONST_PLATFORM_TYPE = nil
107
119
  end
108
120
 
109
- if !PandaPal.lti_options[:platform].present? || PandaPal.lti_options[:platform].is_a?(String)
110
- platform_cls = Platform.resolve_platform_class(nil) rescue nil
111
- if platform_cls && defined?(platform_cls::OrgExtension)
112
- include platform_cls::OrgExtension
121
+ # Include the Platform API...
122
+ if CONST_PLATFORM_TYPE
123
+ # ... directly if this is a single-platform tool
124
+ include CONST_PLATFORM_TYPE::OrgExtension if defined?(CONST_PLATFORM_TYPE::OrgExtension)
125
+ else
126
+ # ... via method_missing/delegation if this is a multi-platform tool
127
+ def respond_to_missing?(name, include_private = false)
128
+ if (self.class == PandaPal::Organization) && (plat_api = platform_api).present?
129
+ plat_api.respond_to?(name, include_private)
130
+ else
131
+ super
132
+ end
133
+ end
134
+
135
+ def method_missing(method, *args, **kwargs, &block)
136
+ if (self.class == PandaPal::Organization) && (plat_api = platform_api).present?
137
+ plat_api.send(method, *args, **kwargs, &block)
138
+ else
139
+ super
140
+ end
113
141
  end
114
142
  end
115
143
 
116
- PandaPal.resolved_extensions_for(self).each do |ext|
117
- include ext
144
+ # Extend a particular type of Platform API.
145
+ def self.extend_platform_api(platform_type = CONST_PLATFORM_TYPE, &blk)
146
+ scl = platform_type.organization_api
147
+ scl.class_eval(&blk)
148
+ end
149
+
150
+ # Retrieve the specified Platform API for this Organization.
151
+ # If only a single Platform is used (when lti_options[:platform] is a constant string or nil), passing the platform_type is unnecessary
152
+ #
153
+ # The API is currently an Organization subclass (using `becomes()`), but such may change.
154
+ def platform_api(platform_type = primary_platform)
155
+ scl = platform_type.organization_api
156
+ return self if scl == self.class
157
+ becomes(scl)
158
+ end
159
+
160
+ protected
161
+
162
+ # OrgExtension-Overridable method to allow multi-platform tool Orgs to implicitly include a Platform API
163
+ def primary_platform
164
+ CONST_PLATFORM_TYPE
118
165
  end
119
166
 
120
167
  private
@@ -126,5 +173,11 @@ module PandaPal
126
173
  def destroy_schema
127
174
  Apartment::Tenant.drop name
128
175
  end
176
+
177
+ public
178
+
179
+ PandaPal.resolved_extensions_for(self).each do |ext|
180
+ include ext
181
+ end
129
182
  end
130
183
  end
@@ -155,7 +155,12 @@ module PandaPal
155
155
  end
156
156
 
157
157
  if spec[:properties] != nil || spec[:allow_additional] != nil
158
- extra_keys = settings.keys - (spec[:properties]&.keys || [])
158
+ set_keys = settings.keys
159
+ expected_keys = spec[:properties]&.keys || []
160
+ expected_keys = expected_keys.map(&:to_s) if settings.is_a?(HashWithIndifferentAccess)
161
+
162
+ extra_keys = set_keys - expected_keys
163
+
159
164
  if extra_keys.present?
160
165
  if spec[:allow_additional].is_a?(Hash)
161
166
  extra_keys.each do |key|
@@ -42,7 +42,7 @@ module PandaPal
42
42
 
43
43
  hash.tap do |hash|
44
44
  kl = ' ' * (k.to_s.length - 4)
45
- hash[k.to_sym] = hash[k.to_s] = PandaPal::OrganizationConcerns::TaskScheduling.build_settings_entry(desc)
45
+ hash[k.to_sym] = PandaPal::OrganizationConcerns::TaskScheduling.build_settings_entry(desc)
46
46
  end
47
47
  end,
48
48
  }
@@ -0,0 +1,5 @@
1
+ module PandaPal
2
+ class PandaPalRecord < ActiveRecord::Base
3
+ self.abstract_class = true
4
+ end
5
+ end
@@ -44,13 +44,11 @@ module PandaPal
44
44
  def install_lti(host: nil, context: :root_account, version: 'v1p3', exists: :error, dedicated_deployment: false)
45
45
  raise "Automatically installing this LTI requires Bearcat." unless defined?(Bearcat)
46
46
 
47
- version = version.to_s
48
-
49
47
  run_callbacks :lti_install do
50
48
  ctype, cid = _parse_lti_context(context)
51
49
 
52
50
  if version == 'v1p0'
53
- existing_installs = _find_existing_installs(context, exists: exists) do |lti|
51
+ existing_installs = _find_existing_installs(exists: exists) do |lti|
54
52
  lti[:consumer_key] == self.key
55
53
  end
56
54
 
@@ -112,9 +110,6 @@ module PandaPal
112
110
  end
113
111
 
114
112
  ekey = existing_keys[0]
115
- # if ekey && exists == :error
116
- # raise "Tool with key #{self.key} already installed"
117
- # end
118
113
 
119
114
  lti_json_url = PandaPal::LaunchUrlHelpers.resolve_route(:v1p3_config_url, host: host)
120
115
  lti_json = JSON.parse(HTTParty.get(lti_json_url, format: :plain).body)
@@ -268,6 +263,7 @@ module PandaPal
268
263
 
269
264
  if defined?(Bearcat)
270
265
  def bearcat_client
266
+ # Less than ideal, but `canvas_sync_client` has been the long-adopted tradition so we check for it so that we can continue to drop-in new versions of PandaPal
271
267
  return canvas_sync_client if defined?(canvas_sync_client)
272
268
 
273
269
  Bearcat::Client.new(
@@ -72,11 +72,13 @@ module PandaPal
72
72
  raise "Unknown platform '#{platform}'"
73
73
  end
74
74
 
75
- def self.org_subclass
76
- return nil unless defined?(self::OrgExtension)
75
+ def self.organization_api
76
+ return PandaPal::Organization unless defined?(self::OrgExtension)
77
+ return PandaPal::Organization if PandaPal::Organization < self::OrgExtension
78
+
77
79
  oext = self::OrgExtension
78
80
 
79
- @org_subclass ||= Class.new(PandaPal::Organization) do
81
+ @organization_api ||= self::OrganizationApi ||= Class.new(PandaPal::Organization) do
80
82
  include oext
81
83
  end
82
84
  end
@@ -1,5 +1,5 @@
1
1
  module PandaPal
2
- class Session < ActiveRecord::Base
2
+ class Session < PandaPalRecord
3
3
  belongs_to :panda_pal_organization, class_name: 'PandaPal::Organization', optional: true
4
4
 
5
5
  after_initialize do
@@ -69,41 +69,105 @@ module PandaPal
69
69
  end
70
70
  end
71
71
 
72
- initializer "panda_pal.autoswitch" do |app|
72
+ initializer "panda_pal.prompts" do |app|
73
73
  class ::Object
74
- def _panda_pal_console_autoswitch_tenant
75
- org = nil
76
- if Rails.env.development?
77
- org = PandaPal::Organization.find_by(name: 'local') || PandaPal::Organization.first
78
- elsif PandaPal::Organization.count == 1
79
- org = PandaPal::Organization.first
74
+ def _panda_pal_console_app_name
75
+ app_class = Rails.application.class
76
+ app_name = app_class.respond_to?(:parent) ? app_class.parent : app_class.module_parent
77
+ end
78
+ end
79
+
80
+ if defined? IRB
81
+ module PandaPalIrbTimePrompt
82
+ def prompt(prompt, ltype, indent, line_no)
83
+ formatted = super(prompt, ltype, indent, line_no)
84
+ app_bit = Pry::Helpers::Text.cyan("#{_panda_pal_console_app_name}-#{Apartment::Tenant.current}")
85
+ "[#{app_bit}] #{formatted}"
80
86
  end
87
+ end
81
88
 
82
- if org
83
- org.switch_tenant
84
- puts "PandaPal: Auto Switched to tenant '#{org.name}'"
89
+ module ::IRB
90
+ class Irb
91
+ prepend PandaPalIrbTimePrompt
85
92
  end
86
- rescue => err
87
- puts "PadaPal: Error occurred auto-switching tenant: #{err}"
88
93
  end
89
94
  end
90
95
 
91
- module Rails::ConsoleMethods
92
- def self.included(base)
93
- _panda_pal_console_autoswitch_tenant
96
+ if defined?(Pry)
97
+ default_prompt = Pry::Prompt[:default]
98
+ env = Pry::Helpers::Text.red(Rails.env.upcase)
99
+
100
+ app_name = _panda_pal_console_app_name
101
+
102
+ Pry.config.prompt = Pry::Prompt.new(
103
+ 'custom',
104
+ 'my custom prompt',
105
+ [
106
+ ->(*args) {
107
+ app_bit = Pry::Helpers::Text.cyan("#{app_name}-#{Apartment::Tenant.current}")
108
+ "#{app_bit}#{default_prompt.wait_proc.call(*args)}"
109
+ },
110
+ ->(*args) {
111
+ app_bit = Pry::Helpers::Text.cyan("#{app_name}-#{Apartment::Tenant.current}")
112
+ "#{app_bit}#{default_prompt.incomplete_proc.call(*args)}"
113
+ },
114
+ ],
115
+ )
116
+ end
117
+
118
+ if defined? Rails::Console
119
+ module PandaPal::ConsoleExtPrefix
120
+ extend ActiveSupport::Concern
121
+
122
+ def start(*args)
123
+ print "\033];#{_panda_pal_console_app_name} Rails Console\007"
124
+ super
125
+ end
94
126
  end
127
+
128
+ Rails::Console.prepend(PandaPal::ConsoleExtPrefix)
95
129
  end
130
+ end
96
131
 
97
- if defined?(Pry)
98
- Pry.hooks.add_hook(:before_session, "switch PandaPal tenant") do |output, binding, pry|
99
- _panda_pal_console_autoswitch_tenant
132
+ initializer "panda_pal.autoswitch" do |app|
133
+ if defined? Rails::Console
134
+ module PandaPal::ConsoleExtAutoSwitch
135
+ extend ActiveSupport::Concern
136
+
137
+ def start(*args)
138
+ begin
139
+ org = nil
140
+ if Rails.env.development?
141
+ org = PandaPal::Organization.find_by(name: 'local') || PandaPal::Organization.first
142
+ elsif PandaPal::Organization.count == 1
143
+ org = PandaPal::Organization.first
144
+ end
145
+
146
+ if org
147
+ org.switch_tenant
148
+ puts "PandaPal: Auto Switched to tenant '#{org.name}'"
149
+ end
150
+ rescue => err
151
+ puts "PandaPal: Error occurred auto-switching tenant: #{err}"
152
+ end
153
+
154
+ super
155
+ end
100
156
  end
157
+
158
+ Rails::Console.prepend(PandaPal::ConsoleExtAutoSwitch)
101
159
  end
102
160
  end
103
161
 
104
162
  initializer "panda_pal.serialze_symbols" do |app|
105
163
  app.config.active_record.yaml_column_permitted_classes ||= []
106
- app.config.active_record.yaml_column_permitted_classes |= [Symbol, ActiveSupport::Duration]
164
+ app.config.active_record.yaml_column_permitted_classes |= [
165
+ Symbol,
166
+ ActiveSupport::Duration,
167
+ ActiveSupport::TimeWithZone,
168
+ ActiveSupport::TimeZone,
169
+ Time,
170
+ ]
107
171
  rescue
108
172
  end
109
173
 
@@ -1,3 +1,3 @@
1
1
  module PandaPal
2
- VERSION = "5.6.12"
2
+ VERSION = "5.7.0.beta1"
3
3
  end
@@ -30,5 +30,21 @@ module PandaPal
30
30
  org2 = build :panda_pal_organization, salesforce_id: 'salesforce'
31
31
  expect(org2.valid?).to be_falsey
32
32
  end
33
+
34
+ it 'stores setting as an indifferent Hash' do
35
+ org = create :panda_pal_organization
36
+ org.update!(settings: { a: 1 })
37
+
38
+ expect(Organization.last.settings).to be_a (ActiveSupport::HashWithIndifferentAccess)
39
+
40
+ org.settings = {}
41
+ expect(org.settings).to be_a (ActiveSupport::HashWithIndifferentAccess)
42
+ end
43
+
44
+ it 'stores settings' do
45
+ org = create :panda_pal_organization
46
+ org.update!(settings: { a: 1 })
47
+ expect(Organization.last.settings[:a]).to eql(1)
48
+ end
33
49
  end
34
50
  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.6.12
4
+ version: 5.7.0.beta1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Instructure ProServe
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2023-05-02 00:00:00.000000000 Z
11
+ date: 2023-04-19 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rails
@@ -313,6 +313,7 @@ files:
313
313
  - app/models/panda_pal/organization.rb
314
314
  - app/models/panda_pal/organization_concerns/settings_validation.rb
315
315
  - app/models/panda_pal/organization_concerns/task_scheduling.rb
316
+ - app/models/panda_pal/panda_pal_record.rb
316
317
  - app/models/panda_pal/platform.rb
317
318
  - app/models/panda_pal/platform/canvas.rb
318
319
  - app/models/panda_pal/session.rb
@@ -403,9 +404,9 @@ required_ruby_version: !ruby/object:Gem::Requirement
403
404
  version: '0'
404
405
  required_rubygems_version: !ruby/object:Gem::Requirement
405
406
  requirements:
406
- - - ">="
407
+ - - ">"
407
408
  - !ruby/object:Gem::Version
408
- version: '0'
409
+ version: 1.3.1
409
410
  requirements: []
410
411
  rubygems_version: 3.1.6
411
412
  signing_key: