panda_pal 5.19.0 → 5.19.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.
@@ -0,0 +1,355 @@
1
+ # Deep Linking Configuration
2
+
3
+ This document explains how to configure and implement LTI Deep Linking in your PandaPal application.
4
+
5
+ ## Configuration Options
6
+
7
+ ### 1. Placement-Specific Configuration (Recommended)
8
+
9
+ Configure redirect URLs for specific deep linking placements:
10
+
11
+ ```ruby
12
+ # config/initializers/lti.rb
13
+
14
+ # Content selection with explicit redirect URL
15
+ PandaPal.stage_placement(:content_selection, {
16
+ deep_linking: true,
17
+ deep_link_redirect_url: :content_selection_url,
18
+ text: 'Select Content',
19
+ enabled: true
20
+ })
21
+
22
+ # Module selection using default placement route
23
+ PandaPal.stage_placement(:module_selection, {
24
+ deep_linking: true,
25
+ text: 'Select Module',
26
+ enabled: true,
27
+ url: :module_selection_url # Will be used as fallback redirect
28
+ })
29
+ ```
30
+
31
+ ### 2. Global Configuration
32
+
33
+ Set a global default for all deep linking requests:
34
+
35
+ ```ruby
36
+ # config/initializers/lti.rb
37
+
38
+ PandaPal.lti_options = {
39
+ title: 'My LTI Tool',
40
+ deep_link_redirect_url: :deep_link_selection_url
41
+ }
42
+ ```
43
+
44
+ ## Implementation
45
+
46
+ ### 1. Create Deep Linking Controller
47
+
48
+ Create a controller in your main application to handle content selection:
49
+
50
+ ```ruby
51
+ # app/controllers/content_selection_controller.rb
52
+ class ContentSelectionController < ApplicationController
53
+ include PandaPal::Helpers::ControllerHelper
54
+ include PandaPal::DeepLinkingHelpers
55
+
56
+ def index
57
+ render component: 'ContentSelection', layout: 'application', prerender: false
58
+ end
59
+
60
+ def create
61
+ # Handle the content selection and build response
62
+ selected_content = params[:selected_content_ids]
63
+ content_items = build_content_items_for_selection(selected_content)
64
+
65
+ # Render the deep linking response (this will redirect back to Canvas)
66
+ render_deep_linking_response(content_items)
67
+ end
68
+
69
+ private
70
+
71
+ def build_content_items_for_selection(selected_ids)
72
+ content_items = []
73
+
74
+ selected_ids.each do |content_id|
75
+ content = find_content(content_id)
76
+
77
+ content_items << build_link_content_item(
78
+ url: panda_pal.v1p3_resource_link_request_url,
79
+ title: content.title,
80
+ text: content.description,
81
+ icon: content.icon_url,
82
+ thumbnail: content.thumbnail_url,
83
+ custom: {
84
+ content_id: content.id,
85
+ launch_type: :content_launch # REQUIRED: Used for routing in resource_link_request
86
+ }
87
+ )
88
+ end
89
+
90
+ content_items
91
+ end
92
+ end
93
+ ```
94
+
95
+ ### 2. Add Routes
96
+
97
+ ```ruby
98
+ # config/routes.rb
99
+ Rails.application.routes.draw do
100
+ get '/content-selection', to: 'content_selection#index', as: :content_selection
101
+ post '/content-selection', to: 'content_selection#create'
102
+
103
+ # For simple single content selection
104
+ post '/quiz-selection', to: 'quiz_selection#create', as: :quiz_selection
105
+
106
+ # Add launch routes for your content types
107
+ lti_nav content_launch: 'content#launch'
108
+ lti_nav quiz_launch: 'quiz#launch'
109
+ end
110
+ ```
111
+
112
+ ### 3. Create Views
113
+
114
+ #### ERB Template Example
115
+
116
+ ```erb
117
+ <!-- app/views/content_selection/index.html.erb -->
118
+ <h2>Select Content to Add</h2>
119
+
120
+ <%= form_with url: content_selection_path, method: :post do |form| %>
121
+ <% @available_content.each do |content| %>
122
+ <div class="content-item">
123
+ <%= form.check_box :selected_content_ids, { multiple: true }, content.id, nil %>
124
+ <%= form.label "selected_content_ids_#{content.id}", content.title %>
125
+ <p><%= content.description %></p>
126
+ </div>
127
+ <% end %>
128
+
129
+ <%= form.submit "Add Selected Content" %>
130
+ <% end %>
131
+ ```
132
+
133
+ #### React Component Example
134
+
135
+ ```jsx
136
+ // app/javascript/components/ContentSelection.jsx
137
+ import React, { useState } from 'react';
138
+
139
+ const ContentSelection = ({ availableContent, submitUrl, csrfToken }) => {
140
+ const [selectedContentIds, setSelectedContentIds] = useState([]);
141
+
142
+ const handleContentToggle = (contentId) => {
143
+ setSelectedContentIds(prev =>
144
+ prev.includes(contentId)
145
+ ? prev.filter(id => id !== contentId)
146
+ : [...prev, contentId]
147
+ );
148
+ };
149
+
150
+ const handleSubmit = (e) => {
151
+ e.preventDefault();
152
+
153
+ if (selectedContentIds.length === 0) {
154
+ alert('Please select at least one content item');
155
+ return;
156
+ }
157
+
158
+ // Submit the form programmatically
159
+ e.target.submit();
160
+ };
161
+
162
+ return (
163
+ <div className="content-selection">
164
+ <h2>Select Content to Add</h2>
165
+
166
+ <form action={submitUrl} method="POST" onSubmit={handleSubmit}>
167
+ <input type="hidden" name="authenticity_token" value={csrfToken} />
168
+
169
+ {availableContent.map(content => (
170
+ <div key={content.id} className="content-item">
171
+ <label>
172
+ <input
173
+ type="checkbox"
174
+ checked={selectedContentIds.includes(content.id)}
175
+ onChange={() => handleContentToggle(content.id)}
176
+ />
177
+ <span>{content.title}</span>
178
+ <p>{content.description}</p>
179
+ </label>
180
+ </div>
181
+ ))}
182
+
183
+ {selectedContentIds.map(id => (
184
+ <input key={id} type="hidden" name="selected_content_ids[]" value={id} />
185
+ ))}
186
+
187
+ <button type="submit" disabled={selectedContentIds.length === 0}>
188
+ Add {selectedContentIds.length} Selected Item{selectedContentIds.length !== 1 ? 's' : ''}
189
+ </button>
190
+ </form>
191
+ </div>
192
+ );
193
+ };
194
+
195
+ export default ContentSelection;
196
+ ```
197
+
198
+ ## Helper Methods
199
+
200
+ PandaPal provides several helper methods to make deep linking easier:
201
+
202
+ ### Content Item Builders
203
+
204
+ **IMPORTANT: All content items MUST include `launch_type` in the `custom` hash for proper routing.**
205
+
206
+ ```ruby
207
+ # Link content item - REQUIRED: launch_type in custom hash
208
+ build_link_content_item(
209
+ url: panda_pal.v1p3_resource_link_request_url,
210
+ title: 'My Content',
211
+ text: 'Description of the content',
212
+ icon: 'https://example.com/icon.png',
213
+ thumbnail: 'https://example.com/thumb.jpg',
214
+ custom: {
215
+ content_id: 123,
216
+ launch_type: :content_launch # REQUIRED for proper routing
217
+ }
218
+ )
219
+
220
+ # File content item - REQUIRED: launch_type in custom hash
221
+ build_file_content_item(
222
+ url: 'https://example.com/files/document.pdf',
223
+ title: 'Important Document',
224
+ text: 'A very important PDF document',
225
+ media_type: 'application/pdf',
226
+ icon: 'https://example.com/pdf-icon.png',
227
+ custom: {
228
+ document_id: 456,
229
+ launch_type: :document_launch # REQUIRED for proper routing
230
+ }
231
+ )
232
+
233
+ # HTML content item - REQUIRED: launch_type in custom hash
234
+ build_html_content_item(
235
+ html: '<h1>Hello World</h1><p>This is embedded HTML content.</p>',
236
+ title: 'Custom HTML Content',
237
+ text: 'Custom HTML that will be embedded',
238
+ custom: {
239
+ template_id: 789,
240
+ launch_type: :html_launch # REQUIRED for proper routing
241
+ }
242
+ )
243
+ ```
244
+
245
+ ### Launch Type Requirement
246
+
247
+ The `launch_type` parameter in the `custom` hash is **mandatory** because:
248
+
249
+ 1. When Canvas launches your content, it calls the `resource_link_request` action
250
+ 2. This action uses the `launch_type` from custom parameters to determine which route to redirect to
251
+ 3. Without `launch_type`, the launch will fail to route properly
252
+
253
+ Make sure to:
254
+ - Define corresponding `lti_nav` routes for each `launch_type`
255
+ - Use descriptive, unique `launch_type` values (e.g., `:quiz_launch`, `:assignment_launch`, `:content_launch`)
256
+
257
+ ### Response Builder
258
+
259
+ ```ruby
260
+ # Simply pass content items to render the deep linking response
261
+ # The helper will automatically use current_session, current_organization, and build custom data
262
+ render_deep_linking_response(content_items)
263
+
264
+ # You can also pass optional custom data if needed
265
+ render_deep_linking_response(content_items, custom_data: {
266
+ selection_time: Time.now,
267
+ selected_by_user_id: current_user.id
268
+ })
269
+ ```
270
+
271
+ ## Priority Order
272
+
273
+ The deep linking redirect URL is determined in this order:
274
+
275
+ 1. **Placement-specific** `deep_link_redirect_url` (highest priority)
276
+ 2. **Global** `deep_link_redirect_url` from `PandaPal.lti_options`
277
+
278
+ If neither is configured, an error will be returned.
279
+
280
+ ## Auto-Submit Form
281
+
282
+ The `render_deep_linking_response` method automatically builds the JWT response and renders the PandaPal auto-submit form partial (`panda_pal/partials/auto_submit_form`) which will immediately redirect the user back to Canvas with the selected content. All the necessary session data, organization info, and JWT signing is handled automatically.
283
+
284
+ ## Example: Simple Single Content Selection
285
+
286
+ For a simple case where you always return the same content:
287
+
288
+ ```ruby
289
+ class QuizController < ApplicationController
290
+ include PandaPal::Helpers::ControllerHelper
291
+ include PandaPal::DeepLinkingHelpers
292
+
293
+ def create
294
+ # Build a single content item
295
+ content_item = build_link_content_item(
296
+ url: panda_pal.v1p3_resource_link_request_url,
297
+ title: "Quiz: #{@quiz.title}",
298
+ text: @quiz.description,
299
+ custom: {
300
+ quiz_id: params[:quiz_id],
301
+ launch_type: :quiz_launch # REQUIRED for proper routing
302
+ }
303
+ )
304
+
305
+ # Render response immediately
306
+ render_deep_linking_response([content_item])
307
+ end
308
+ end
309
+ ```
310
+
311
+ ## Complete Example: Group Quiz Selection
312
+
313
+ ```ruby
314
+ class GroupQuizSelectionController < ApplicationController
315
+ include PandaPal::DeepLinkingHelpers
316
+
317
+ def create
318
+ group_quiz = GroupQuiz.find(params[:group_quiz_id])
319
+
320
+ content_item = build_link_content_item(
321
+ url: panda_pal.v1p3_resource_link_request_url,
322
+ title: "Group Quiz: #{group_quiz.title}",
323
+ text: group_quiz.description,
324
+ custom: {
325
+ group_quiz_id: group_quiz.id,
326
+ launch_type: :deep_link_group_quiz # REQUIRED for proper routing
327
+ }
328
+ )
329
+
330
+ render_deep_linking_response([content_item])
331
+ end
332
+ end
333
+ ```
334
+
335
+ And the corresponding route configuration:
336
+
337
+ ```ruby
338
+ # config/routes.rb
339
+ Rails.application.routes.draw do
340
+ # Deep linking selection routes
341
+ post '/group-quiz-selection', to: 'group_quiz_selection#create', as: :group_quiz_selection
342
+
343
+ # Launch routes (required for the launch_type values used above)
344
+ lti_nav deep_link_group_quiz: 'group_quiz#launch'
345
+ end
346
+ ```
347
+
348
+ ## Troubleshooting
349
+
350
+ - **Missing launch_type error**: Ensure all content items include `launch_type` in the `custom` hash
351
+ - **Route not found after launch**: Make sure you have corresponding `lti_nav` routes for each `launch_type`
352
+ - Ensure your deep linking URLs are properly configured in the LTI placement
353
+ - Make sure to include the `PandaPal::DeepLinkingHelpers` module in your controllers
354
+ - The auto-submit form requires JavaScript to be enabled in the browser
355
+ - Check that your content URLs are accessible and properly formatted
@@ -0,0 +1,31 @@
1
+ # Developing PandaPal.
2
+
3
+ PandaPal is a gem, so it has no app of its own to run. The specs use the
4
+ [Combustion](https://github.com/pat/combustion) gem's Rails app in `spec/internal`, which only has a
5
+ `test` database config.
6
+
7
+ ## Requirements.
8
+
9
+ - Ruby 3.0 or later. `mise.toml` pins the local version.
10
+ - Postgres and Redis. Run them however you like, for example `docker run postgres:17` and
11
+ `docker run redis`.
12
+
13
+ ## Running specs.
14
+
15
+ ```bash
16
+ bundle install
17
+ bundle exec rspec spec # Default bundle (Rails version depends on your Ruby)
18
+ bundle exec appraisal install # Generate gemfiles/rails_*.gemfile
19
+ bundle exec appraisal rspec spec # Every Rails version your Ruby supports
20
+ COVERAGE=1 bundle exec rspec spec # SimpleCov report in coverage/
21
+ ```
22
+
23
+ CI runs Ruby 3.0 and 3.4 against Rails 6.1, 7.1, and 8.0, wherever the Ruby version supports that
24
+ Rails version. See `Appraisals` and `.github/workflows/ci.yml`.
25
+
26
+ `Gemfile.lock` is gitignored on purpose, because this is a library.
27
+
28
+ ## Specs that change global config.
29
+
30
+ Tag specs that call `PandaPal.define_tool`, `stage_placement`, or other global LTI config with
31
+ `:isolated_lti_config`, so the changes don't leak into other specs.
data/docs/frontend.md ADDED
@@ -0,0 +1,129 @@
1
+ # Frontend with `@inst_proserv/toolkit`.
2
+
3
+ `@inst_proserv/toolkit` is the CustomDev frontend package for PandaPal apps. It reads the session
4
+ from the page, sends it with API calls, mints limited-use panda tokens in the browser, and opens
5
+ authenticated ActionCable connections. This page covers the parts that talk to PandaPal. The
6
+ package also has InstUI-based components (tables, trays, alerts, prompts, and more).
7
+
8
+ Modules are imported by path, for example `@inst_proserv/toolkit/cable`. Many apps alias
9
+ `@toolkit` to `@inst_proserv/toolkit` in webpack and `tsconfig.json`.
10
+
11
+ ## Passing the environment to the page.
12
+
13
+ Toolkit loads app-wide values from a `js-env` meta tag into `env`
14
+ (`@inst_proserv/toolkit/environment`). It also reads `csrf_token` from Rails' `csrf-token` meta tag.
15
+
16
+ ```erb
17
+ <%# app/views/layouts/application.html.erb %>
18
+ <%= csrf_meta_tags %>
19
+ <meta name="js-env" content="<%= js_env.to_json %>">
20
+ ```
21
+
22
+ PandaPal doesn't define `js_env`. Each app defines its own helper. A typical one:
23
+
24
+ ```ruby
25
+ # app/controllers/application_controller.rb
26
+ def js_env
27
+ @js_env ||= {
28
+ csrf_token: form_authenticity_token,
29
+ organization_id: current_organization&.id,
30
+ base_path: url_for(path: nil, only_path: true),
31
+ canvas_url: current_organization&.canvas_url,
32
+ }.tap do |env|
33
+ if current_session.present?
34
+ env[:session_key] = current_session.session_key # Full-key panda token
35
+ env[:launch_params] = current_session.launch_params
36
+ # Lets toolkit components talk to the Canvas window via postMessage
37
+ env[:post_message_token] = current_session.get_lti_cust_param(:canvas_post_message_token)
38
+ end
39
+ end
40
+ end
41
+ helper_method :js_env
42
+ ```
43
+
44
+ These are the keys toolkit reads:
45
+
46
+ | Key | Used by |
47
+ | --- | --- |
48
+ | `session_key` | `APIBase`, `panda_tokens`, and `cable`. It must be the full-key panda token from `current_session.session_key`. |
49
+ | `csrf_token` | `APIBase` sends it as `X-CSRF-Token`. |
50
+ | `post_message_token` | `AlertManager` and `ScrollHacksManager`, for Canvas `postMessage` calls. Add `custom_canvas_post_message_token` to `PandaPal.lti_custom_params` to get it from the launch. |
51
+
52
+ Add your own keys with `setEnvVars({...})`, or extend the `Environment` TypeScript interface.
53
+
54
+ ## API calls.
55
+
56
+ `APIBase` (`@inst_proserv/toolkit/api`) is an Axios instance preconfigured for PandaPal:
57
+
58
+ - It sends `Authorization: token=<session_key>` and `X-CSRF-Token` on every request. PandaPal still
59
+ accepts this legacy header form (see [sessions.md](sessions.md#passing-the-token-to-the-frontend)).
60
+ - `create_href(path)` builds a link with `?session_key=<session_key>`.
61
+ - `download_file(url)` signs same-origin URLs with a `T-URL` panda token that expires after 30
62
+ seconds, then downloads the file (or opens it in a tab with `in_tab: true`).
63
+ - `protectedRequest` shows the `error` or `message` from a 4xx response as an alert, maps
64
+ validation `errors` onto a form, and reports anything else to Sentry.
65
+
66
+ ```ts
67
+ import { APIBase } from "@inst_proserv/toolkit/api";
68
+
69
+ export const api = new APIBase({ subURL: `/organizations/${env.organization_id}/api/v1` });
70
+ const { data } = await api.get("/reports");
71
+ ```
72
+
73
+ ## Minting panda tokens in the browser.
74
+
75
+ `@inst_proserv/toolkit/panda_tokens` signs URLs with the session secret from `env.session_key`, so
76
+ the browser can make `GET` links (downloads, images, and new tabs) without a round trip to the
77
+ server.
78
+
79
+ ```ts
80
+ import { signURL } from "@inst_proserv/toolkit/panda_tokens";
81
+
82
+ const url = await signURL("/organizations/1/reports/5.csv", {
83
+ params: { format: "csv" }, // Added to the URL before signing
84
+ claims: { exp: 60 }, // Seconds. Defaults to 15 minutes. Pass false for no expiration.
85
+ });
86
+ window.open(url);
87
+ ```
88
+
89
+ `signURL` creates a `T-URL` token, which PandaPal accepts as `T-URI`. The token covers the URL's path
90
+ and query, so changing either breaks the signature. It only works for `GET` requests.
91
+
92
+ Call `configure({ default_expiration, session_secret, ... })` to change the defaults.
93
+
94
+ ## ActionCable.
95
+
96
+ `@inst_proserv/toolkit/cable` opens an ActionCable connection to `/cable`, authenticated with a
97
+ signed `T-URL` panda token. It works with PandaPal's `connection.panda_session` (see
98
+ [action-cable.md](action-cable.md)).
99
+
100
+ - The token lasts about six minutes and refreshes every five.
101
+ - Before every connect or reconnect (including after the computer wakes from sleep), toolkit checks
102
+ the token and refreshes it first if it's stale, so it never connects with an expired token.
103
+ - Subscriptions with the same channel and params share one ActionCable subscription.
104
+
105
+ ```ts
106
+ import { subscribe_channel, useChannelSubscription, useModelSubscription } from "@inst_proserv/toolkit/cable";
107
+
108
+ // Plain subscription. Returns an unsubscribe function.
109
+ const unsubscribe = subscribe_channel("BackgroundTaskChannel", { id: task.id }, (data) => {
110
+ console.log(data);
111
+ });
112
+
113
+ // React hook version
114
+ useChannelSubscription("BackgroundTaskChannel", { id: task.id }, (data) => setProgress(data.progress));
115
+
116
+ // Keep a MobX model up to date. Each message is merged into the instance.
117
+ useModelSubscription(report, "ReportChannel"); // Subscribes with { id: report.id }
118
+ ```
119
+
120
+ `SubscribeChannel` and `SubscribeModel` are component versions of the hooks. To connect somewhere
121
+ other than `/cable`, or without a panda token, pass your own consumer to
122
+ `setDefaultCableConsumer(...)`, or create a `new CableSubscriptionManager(consumer)`.
123
+
124
+ ## Routing inside the iframe.
125
+
126
+ `createLtiHistory` (`@inst_proserv/toolkit/extra/LtiHistory`) is a React Router (v5) history that
127
+ keeps the app's location in `history.state` instead of the URL. The iframe URL, and its panda token,
128
+ stay the same while the user moves around the app, and the browser back button still works.
129
+ `<LtiRouter>` from the same module wraps it as a ready-made `<Router>`.
@@ -0,0 +1,27 @@
1
+ # LTI 1.1 grade passback.
2
+
3
+ For LTI 1.0/1.1 launches, `PandaPal::Jobs::GradePassbackJob` posts a score back to Canvas with the
4
+ LTI Basic Outcomes service, signed with the Organization's key and secret. For LTI 1.3, use
5
+ [AGS](ags.md) instead.
6
+
7
+ ```ruby
8
+ PandaPal::Jobs::GradePassbackJob.perform_later(current_organization, {
9
+ passback_guid: launch_params['lis_result_sourcedid'],
10
+ passback_url: launch_params['lis_outcome_service_url'],
11
+ score: 0.8,
12
+ })
13
+ ```
14
+
15
+ | Option | Required | Notes |
16
+ | --- | --- | --- |
17
+ | `passback_guid` | Yes | The launch's `lis_result_sourcedid`. |
18
+ | `passback_url` | Yes | The launch's `lis_outcome_service_url`. |
19
+ | `score` and/or `total_score` | Yes, at least one | |
20
+ | `text`, `cdata_text`, `url`, `lti_launch_url`, `submitted_at` | No | Canvas submission extensions. |
21
+
22
+ The job raises `PandaPal::Jobs::MissingGradePassbackParams` when a required option is missing, and
23
+ `PandaPal::Jobs::GradePassbackFailure` when Canvas rejects the result. On Sidekiq, it retries five
24
+ times.
25
+
26
+ See the [Canvas assignment tools docs](https://canvas.instructure.com/doc/api/file.assignment_tools.html)
27
+ for what each option does.
@@ -0,0 +1,129 @@
1
+ # Installing into Canvas.
2
+
3
+ PandaPal can create an Organization and install the tool into Canvas for it, from the console or
4
+ with rake tasks. Automatic installs need the `bearcat` gem and a Canvas API token in the
5
+ Organization's settings (`settings[:canvas][:api_token]` and `settings[:canvas][:base_url]`).
6
+
7
+ ## From the console.
8
+
9
+ ```ruby
10
+ org.install_lti(
11
+ host: "https://your_lti.herokuapp.com",
12
+ context: :org_account, # (Optional) Where to install. See "Contexts" below.
13
+ exists: :error, # (Optional) :error, :replace, :duplicate, or :update
14
+ version: "v1p3", # (Optional) "v1p3" or "v1p0"
15
+ dedicated_deployment: false, # (Optional, LTI 1.3 only, experimental) Link the Organization to one deployment instead of the whole Developer Key
16
+ tool: :default, # (Optional) Which tool to install. See multiple-tools.md.
17
+ )
18
+ ```
19
+
20
+ `org.reinstall_lti!(...)` is the same as `install_lti(..., exists: :replace)`.
21
+ `org.lti_installations(context)` lists the matching external tools already in Canvas.
22
+
23
+ ### Contexts.
24
+
25
+ | Value | Installs into |
26
+ | --- | --- |
27
+ | `:org_account` (default) | The Organization's `canvas_account_id` |
28
+ | `:root_account` | `account/self`, the root account of the API token |
29
+ | `"account/3"`, `"course/1"` | That account or course |
30
+
31
+ ### What `exists:` does when the tool is already installed.
32
+
33
+ | Value | Behavior |
34
+ | --- | --- |
35
+ | `:error` (default) | Raise an error. |
36
+ | `:update` | Update the existing install. |
37
+ | `:replace` | Delete the existing install, then create a new one. |
38
+ | `:duplicate` | Create another install alongside it. |
39
+
40
+ ### Versions.
41
+
42
+ `version:` defaults to the tool's `lti_spec_version` option, then `v1p3`. It also accepts `"1.3"`,
43
+ `"1.1"`, and `"v1p1"`. LTI 1.1 installs use the `v1p0` endpoints.
44
+
45
+ On LTI 1.3, `install_lti`:
46
+
47
+ - Creates (or reuses) a Developer Key, and sets the Organization's `key` and `secret` right away, so
48
+ a failed install retries against the same key instead of making another one.
49
+ - Turns the key's account binding on.
50
+ - Turns off `lock_deploying` on the key's LTI Registration during the install if Canvas turned it on, then
51
+ turns it back on, even if the install fails. The API token needs permission to update LTI
52
+ Registrations for this to work.
53
+
54
+ ## Install callbacks.
55
+
56
+ `before_lti_install`, `around_lti_install`, and `after_lti_install` callbacks run around every
57
+ install. Inside them, `current_lti_install` describes the install in progress: `tool`, `host`,
58
+ `context`, `version`, `exists`, `dedicated_deployment`, and, after the install, the Canvas
59
+ `installation`.
60
+
61
+ ```ruby
62
+ # app/models/organization_extension.rb
63
+ module OrganizationExtension
64
+ extend ActiveSupport::Concern
65
+
66
+ included do
67
+ after_lti_install :configure_canvas
68
+ end
69
+
70
+ def configure_canvas
71
+ puts "Installed #{current_lti_install.tool} as external tool #{current_lti_install.installation[:id]}"
72
+ end
73
+ end
74
+ ```
75
+
76
+ The interactive install (`interactive_install!`, used by the rake tasks) also runs
77
+ `panda_pal_interactive_install` callbacks around the whole prompt-and-install loop, inside the
78
+ Organization's tenant:
79
+
80
+ ```ruby
81
+ included do
82
+ after_panda_pal_interactive_install :seed_defaults
83
+ end
84
+ ```
85
+
86
+ ## Rake tasks.
87
+
88
+ | Task | What it does |
89
+ | --- | --- |
90
+ | `panda_pal:org:new[name]` | Opens `$EDITOR` to create an Organization. The file is prefilled with every setting, with optional ones commented out. |
91
+ | `panda_pal:org:edit[name]` | Same as `new`, for an existing Organization. |
92
+ | `panda_pal:org:install[name,host]` | Installs the tool into Canvas, or updates an existing install (`exists: :update`). |
93
+ | `panda_pal:org:reinstall[name,host]` | Deletes the existing install, then installs again (`exists: :replace`). |
94
+ | `panda_pal:org:dev` | Development only. Creates a `local` Organization with development defaults. |
95
+ | `panda_pal:clean_sessions` | Deletes sessions not updated in the last week. See [sessions.md](sessions.md#session-cleanup). |
96
+
97
+ Both `install` tasks prompt for the host when it isn't given, and offer to retry if the install
98
+ fails.
99
+
100
+ ### `~/pandapalrc.yml`.
101
+
102
+ `panda_pal:org:dev` and the interactive install read defaults from `~/pandapalrc.yml`:
103
+
104
+ ```yml
105
+ lti_host: http://localhost:5000
106
+ settings:
107
+ canvas:
108
+ base_url: http://localhost:3000
109
+ api_token: <your local Canvas API token>
110
+ ```
111
+
112
+ Entries under `settings` that aren't in the `settings_structure` are ignored.
113
+
114
+ ## LTI 1.3 checklist.
115
+
116
+ 1. Configure a private key. See [configuration.md](configuration.md#lti-13-private-key).
117
+ This is optional if you're only interested in LTI launching / Canvas initiates all LTI messages
118
+ 2. In production, use a `Rails.cache` store that every app process shares, such as Redis. LTI 1.3
119
+ replay protection and the JWKS cache depend on it. See
120
+ [architecture.md](architecture.md#shared-state-that-needs-a-shared-cache).
121
+ 3. If you run Canvas locally, make sure `config/redis.yml`, `config/dynamic_settings.yml`, and
122
+ `config/security.yml` exist in Canvas, and set `development.lti_iss` in `security.yml` to
123
+ `'http://localhost:3000'` (your Canvas port).
124
+ 4. Install with `install_lti`, or set it up by hand. When you set it up by hand, the Organization's
125
+ `key` is `CLIENT_ID/DEPLOYMENT_ID` (both are in Canvas). Leave off `/DEPLOYMENT_ID` to accept every
126
+ deployment of the key (the most common approach for CDev).
127
+
128
+ The tool configuration is served at `<mount>/v1p3/config` (LTI 1.3 JSON) and `<mount>/v1p0/config`
129
+ (LTI 1.0 XML). `<mount>/config` is a legacy alias for the LTI 1.0 XML.