panda_pal 5.16.0 → 5.16.2

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: c78eb783dcf56cb075ce23d1218500128e6ac1ac58c3049bcfa13f6409835554
4
- data.tar.gz: bb555a130fdab3e1805013700e04bddfe24a6c3460750ac2201a06f9dfe96c1d
3
+ metadata.gz: 7c39df6be8d76f63ab12597a80696f629aac5da4a400adfa30119f303177251c
4
+ data.tar.gz: 2ab2639ea53d3e698244d65313f94b5ebc6c5edcb5039efc0b7dd801658a1603
5
5
  SHA512:
6
- metadata.gz: 5378ea91ce63e263dc93368f4d87cec35a14273b74f37452cec2b1dea67ff8bbb3a261e609bbc550bbcc4f08dac8c1de09e387e6ec8691e3d4901ac7b2af1e0a
7
- data.tar.gz: aef30aa2d98b20f502a9274ce31c5bf17e66111c8a628cd61ce2765c06ef7fe280af459069a3fafe0a13aa885e9ba915f048553f14714e3ad274537b7bd4cfe3
6
+ metadata.gz: '06705595bcabf1959f7a2d72e7174d6944acc0876770878aa700d926d5b4beefdbb4a8af9462009a253d6eaf4a3fc3870f915291baa6490f5d2ecd756f84a275'
7
+ data.tar.gz: dac25e88efe02b2982e6441506e5d17328b3ee4500627d400523f37da6f7e6093c2a5d85492a73eb5946e616bc0ae7839730620ed66deb8986c952e416fc8f83
data/README.md CHANGED
@@ -166,7 +166,7 @@ PandaPal.stage_placement(:content_selection, {
166
166
  })
167
167
  ```
168
168
 
169
- 2. **Include helpers** in your controller and build content items:
169
+ 2. **Include helpers** in your controller and build content items with **required `launch_type`**:
170
170
  ```ruby
171
171
  class ContentSelectionController < ApplicationController
172
172
  include PandaPal::DeepLinkingHelpers
@@ -174,8 +174,12 @@ class ContentSelectionController < ApplicationController
174
174
  def create
175
175
  content_items = [
176
176
  build_link_content_item(
177
- url: launch_url_for_content(params[:content_id]),
178
- title: 'Selected Content'
177
+ url: panda_pal.v1p3_resource_link_request_url,
178
+ title: 'Selected Content',
179
+ custom: {
180
+ content_id: params[:content_id],
181
+ launch_type: :content_launch # REQUIRED for proper routing
182
+ }
179
183
  )
180
184
  ]
181
185
  render_deep_linking_response(content_items)
@@ -183,6 +187,14 @@ class ContentSelectionController < ApplicationController
183
187
  end
184
188
  ```
185
189
 
190
+ 3. **Add corresponding launch routes**:
191
+ ```ruby
192
+ # config/routes.rb
193
+ lti_nav content_launch: 'content#launch'
194
+ ```
195
+
196
+ **⚠️ IMPORTANT**: All content items **MUST** include `launch_type` in the `custom` hash. This is used by the `resource_link_request` action to determine which route to redirect to when Canvas launches your content.
197
+
186
198
  **For complete configuration options, implementation examples, helper method documentation, and troubleshooting, see [DEEP_LINKING.md](DEEP_LINKING.md).**
187
199
 
188
200
  ## Implementating data segregation
@@ -40,7 +40,7 @@ module PandaPal
40
40
  end
41
41
 
42
42
  def resource_link_request
43
- ltype = @decoded_lti_jwt[LtiConstants::Canvas::PLACEMENT]
43
+ ltype = @decoded_lti_jwt[LtiConstants::Canvas::PLACEMENT] || @decoded_lti_jwt[LtiConstants::Claims::CUSTOM]&.dig("launch_type")
44
44
 
45
45
  if ltype
46
46
  current_session.data.merge!({
@@ -1,7 +1,26 @@
1
1
  module PandaPal
2
2
  module DeepLinkingHelpers
3
3
  # Build a link content item
4
- def build_link_content_item(url:, title: nil, text: nil, icon: nil, thumbnail: nil, **options)
4
+ # @param url [String] The launch URL for the content
5
+ # @param title [String] The title of the content item
6
+ # @param text [String] Optional description text
7
+ # @param icon [String] Optional icon URL
8
+ # @param thumbnail [String] Optional thumbnail URL
9
+ # @param custom [Hash] Custom parameters - MUST include launch_type for proper routing
10
+ # @param options [Hash] Additional options to merge into the content item
11
+ #
12
+ # Example:
13
+ # build_link_content_item(
14
+ # url: panda_pal.v1p3_resource_link_request_url,
15
+ # title: 'My Quiz',
16
+ # custom: {
17
+ # quiz_id: 123,
18
+ # launch_type: :quiz_launch # REQUIRED for proper routing
19
+ # }
20
+ # )
21
+ def build_link_content_item(url:, title: nil, text: nil, icon: nil, thumbnail: nil, custom: {}, **options)
22
+ validate_custom_launch_type!(custom)
23
+
5
24
  content_item = {
6
25
  type: 'link',
7
26
  url: url,
@@ -10,11 +29,21 @@ module PandaPal
10
29
  content_item[:text] = text if text.present?
11
30
  content_item[:icon] = icon if icon.present?
12
31
  content_item[:thumbnail] = thumbnail if thumbnail.present?
32
+ content_item[:custom] = custom if custom.present?
13
33
  content_item.merge(options)
14
34
  end
15
35
 
16
36
  # Build a file content item
17
- def build_file_content_item(url:, title: nil, text: nil, media_type: nil, icon: nil, **options)
37
+ # @param url [String] The file URL
38
+ # @param title [String] The title of the content item
39
+ # @param text [String] Optional description text
40
+ # @param media_type [String] Optional media type
41
+ # @param icon [String] Optional icon URL
42
+ # @param custom [Hash] Custom parameters - MUST include launch_type for proper routing
43
+ # @param options [Hash] Additional options to merge into the content item
44
+ def build_file_content_item(url:, title: nil, text: nil, media_type: nil, icon: nil, custom: {}, **options)
45
+ validate_custom_launch_type!(custom)
46
+
18
47
  content_item = {
19
48
  type: 'file',
20
49
  url: url,
@@ -23,17 +52,26 @@ module PandaPal
23
52
  content_item[:text] = text if text.present?
24
53
  content_item[:mediaType] = media_type if media_type.present?
25
54
  content_item[:icon] = icon if icon.present?
55
+ content_item[:custom] = custom if custom.present?
26
56
  content_item.merge(options)
27
57
  end
28
58
 
29
59
  # Build an HTML content item
30
- def build_html_content_item(html:, title: nil, text: nil, **options)
60
+ # @param html [String] The HTML content
61
+ # @param title [String] The title of the content item
62
+ # @param text [String] Optional description text
63
+ # @param custom [Hash] Custom parameters - MUST include launch_type for proper routing
64
+ # @param options [Hash] Additional options to merge into the content item
65
+ def build_html_content_item(html:, title: nil, text: nil, custom: {}, **options)
66
+ validate_custom_launch_type!(custom)
67
+
31
68
  content_item = {
32
69
  type: 'html',
33
70
  html: html,
34
71
  title: title
35
72
  }
36
73
  content_item[:text] = text if text.present?
74
+ content_item[:custom] = custom if custom.present?
37
75
  content_item.merge(options)
38
76
  end
39
77
 
@@ -68,6 +106,12 @@ module PandaPal
68
106
 
69
107
  private
70
108
 
109
+ def validate_custom_launch_type!(custom)
110
+ unless custom.is_a?(Hash) && custom.key?(:launch_type)
111
+ raise ArgumentError, "custom parameter must include :launch_type for proper routing. Example: custom: { launch_type: :my_content_type }"
112
+ end
113
+ end
114
+
71
115
  def build_deep_link_jwt(content_items, custom_deep_link_data)
72
116
  # Get issuer (client_id) and audience from session
73
117
  launch_params = current_session[:launch_params]
@@ -52,7 +52,7 @@ module PandaPal
52
52
  def self.launch_route(opts, launch_type: nil)
53
53
  if opts.is_a?(Symbol) || opts.is_a?(String)
54
54
  launch_type = opts.to_sym
55
- opts = PandaPal.lti_paths[launch_type]
55
+ opts = PandaPal.lti_paths[launch_type] || {}
56
56
  end
57
57
 
58
58
  if opts[:route_helper_key]
@@ -251,29 +251,6 @@ end
251
251
  ActiveSupport::Cache::Store.send(:prepend, PandaPal::Plugins::ApartmentCache)
252
252
 
253
253
  if defined?(ActionCable)
254
- module ActionCable
255
- module Channel
256
- class Base
257
- def self.broadcasting_for(model)
258
- # Rails 5 #stream_for passes #channel_name as part of model. Rails 6 doesn't and includes it via #broadcasting_for.
259
- model = [channel_name, model] unless model.is_a?(Array)
260
- serialize_broadcasting([ Apartment::Tenant.current, model ])
261
- end
262
-
263
- def self.serialize_broadcasting(object)
264
- case
265
- when object.is_a?(Array)
266
- object.map { |m| serialize_broadcasting(m) }.join(":")
267
- when object.respond_to?(:to_gid_param)
268
- object.to_gid_param
269
- else
270
- object.to_param
271
- end
272
- end
273
- end
274
- end
275
- end
276
-
277
254
  module PandaPal::Plugins::ActionCableApartment
278
255
  module Connection
279
256
  def tenant=(name)
@@ -283,13 +260,81 @@ if defined?(ActionCable)
283
260
  def tenant
284
261
  @tenant || 'public'
285
262
  end
263
+ end
264
+ end
265
+
266
+ if Rails.version < '7.0'
267
+ module ActionCable
268
+ module Channel
269
+ class Base
270
+ def self.broadcasting_for(model)
271
+ # Rails 5 #stream_for passes #channel_name as part of model. Rails 6 doesn't and includes it via #broadcasting_for.
272
+ model = [channel_name, model] unless model.is_a?(Array)
273
+ serialize_broadcasting([ Apartment::Tenant.current, model ])
274
+ end
275
+
276
+ def self.serialize_broadcasting(object)
277
+ case
278
+ when object.is_a?(Array)
279
+ object.map { |m| serialize_broadcasting(m) }.join(":")
280
+ when object.respond_to?(:to_gid_param)
281
+ object.to_gid_param
282
+ else
283
+ object.to_param
284
+ end
285
+ end
286
+ end
287
+ end
288
+ end
286
289
 
290
+ module PandaPal::Plugins::ActionCableApartment::Connection
287
291
  def dispatch_websocket_message(*args, **kwargs)
288
292
  Apartment::Tenant.switch(tenant) do
289
293
  super
290
294
  end
291
295
  end
292
296
  end
297
+ else
298
+ module ActionCable
299
+ module Channel
300
+ class Base
301
+ def self.broadcasting_for(*args)
302
+ cconn = ActionCable.server.worker_pool.connection
303
+ items = [cconn&.tenant || Apartment::Tenant.current, channel_name, *args]
304
+ serialize_broadcasting(items)
305
+ end
306
+ end
307
+ end
308
+ end
309
+
310
+ ActionCable::Server::Worker.set_callback :work, :around do |_, blk|
311
+ pten = Thread.current[:cable_tenant]
312
+ Thread.current[:cable_tenant] = connection.tenant
313
+ blk.call
314
+ ensure
315
+ Thread.current[:cable_tenant] = pten
316
+ end
317
+
318
+ if Apartment::Tenant.adapter.is_a?(Apartment::Adapters::PostgresqlSchemaAdapter)
319
+ module ApartmentConnPoolMixin
320
+ # Allows cable channels to lazy-load the tenant schema on first use
321
+ # (Using `switch` will ping the DB even if no queries are made)
322
+ def acquire_connection(...)
323
+ super.tap do |conn|
324
+ if ct = Thread.current[:cable_tenant]
325
+ Thread.current[:cable_tenant] = nil
326
+ adapter = Apartment::Tenant.adapter
327
+ # Apartment::Tenant.switch!(ct)
328
+ Thread.current[:cable_tenant] = ct
329
+ adapter.instance_variable_set(:@current, ct)
330
+ conn.schema_search_path = adapter.send :full_search_path
331
+ end
332
+ end
333
+ end
334
+ end
335
+
336
+ ActiveRecord::ConnectionAdapters::ConnectionPool.prepend(ApartmentConnPoolMixin)
337
+ end
293
338
  end
294
339
 
295
340
  ActionCable::Connection::Base.prepend(PandaPal::Plugins::ActionCableApartment::Connection)
@@ -1,3 +1,3 @@
1
1
  module PandaPal
2
- VERSION = "5.16.0"
2
+ VERSION = "5.16.2"
3
3
  end
@@ -31,6 +31,28 @@ RSpec.describe PandaPal::Organization, type: :model do
31
31
  end
32
32
  end
33
33
 
34
+ it "creates records on the target shard and schema" do
35
+ org1.switch_tenant
36
+ PandaPal::ApiCall.create!(logic: "from_primary")
37
+ expect(PandaPal::ApiCall.count).to eq(1)
38
+ expect(PandaPal::ApiCall.first.logic).to eq("from_primary")
39
+
40
+ org2.switch_tenant
41
+ PandaPal::ApiCall.create!(logic: "from_alt")
42
+ expect(PandaPal::ApiCall.count).to eq(1)
43
+ expect(PandaPal::ApiCall.first.logic).to eq("from_alt")
44
+
45
+ org1.switch_tenant
46
+ expect(PandaPal::ApiCall.count).to eq(1)
47
+ expect(PandaPal::ApiCall.first.logic).to eq("from_primary")
48
+ expect(PandaPal::ApiCall.connection.exec_query("SELECT current_database()").pluck("current_database")[0]).to eq("panda_pal_test1")
49
+
50
+ org2.switch_tenant
51
+ expect(PandaPal::ApiCall.count).to eq(1)
52
+ expect(PandaPal::ApiCall.first.logic).to eq("from_alt")
53
+ expect(PandaPal::ApiCall.connection.exec_query("SELECT current_database()").pluck("current_database")[0]).to eq("panda_pal_test2")
54
+ end
55
+
34
56
  context "load_async" do
35
57
  it "works across shards" do
36
58
  qs = []
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: panda_pal
3
3
  version: !ruby/object:Gem::Version
4
- version: 5.16.0
4
+ version: 5.16.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Instructure CustomDev