# PandaPal ## LTI Configuration A standard Canvas LTI may have a configuration similar to the one below ```ruby # config/initializers/lti.rb PandaPal.lti_options = { title: 'Teacher Reports' } PandaPal.lti_properties = {oauth_compliant: 'true'} # Environments reflect those used by Canvas PandaPal.lti_environments = { domain: ENV['PROD_DOMAIN'], beta_domain: ENV['BETA_DOMAIN'], test_domain: ENV['TEST_DOMAIN'] } PandaPal.lti_custom_params = { custom_canvas_role: '$Canvas.membership.roles' } # :user_navigation, :course_navigation for user context and course context endpoints respectively PandaPal::stage_navigation(:account_navigation, { enabled: true, # url: :account_index, # Optional if using lti_nav text: 'Teacher Reports', visibility: 'admins', }) ``` Configuration data for an installation can be set by creating a `PandaPal::Organization` record. Due to the nature of the data segregation, once created, the name of the organization should not be changed (and will raise a validation error). ### Launch URL property LTI Spec: `The launch_url contains the URL to which the LTI Launch is to be sent. The secure_launch_url is the URL to use if secure http is required. One of either the launch_url or the secure_launch_url must be specified.` Bridge is now validating that this launch URL exists and requires it per the LTI spec. As of PandaPal 3.1.0, you now have 6 options to use this. Use one of these 6 options in `PandaPal.lti_options` hash. 1. Use option `secure_launch_url: 'http://domain.com/path'` when a full static secure domain is needed. 2. Use option `secure_launch_path: '/path'` when you need the secure options and want the host to be dynamic and just need to provide a path. 3. Use option `launch_url: 'http://domain.com/path'` when a full static domain is needed. 4. Use option `launch_path: '/path'` when you don't need the secure options and want the host to be dynamic and just need to provide a path. 5. Leave this property off, and you will get the dynamic host with the root path ('http://appdomain.com/') by default. 6. If you really do not want this property use the option `launch_url: false` for it to be left off. ### Task Scheduling `PandaPal` includes an integration with `sidekiq-scheduler`. You can define tasks on an Organization class Stub like so: ```ruby # /app/models/panda_pal/organization.rb require File.expand_path('../../app/models/panda_pal/organization.rb', PandaPal::Engine.called_from) module PandaPal class Organization # Will invoke CanvasSyncStarterWorker.perform_async() according to the cron schedule scheduled_task '0 15 05 * * *', :identifier, worker: CanvasSyncStarterWorker # Will invoke the method 'organization_method' on the Organization scheduled_task '0 15 05 * * *', :organization_method_and_identifier # If you need to invoke the same method on multiple schedules scheduled_task '0 15 05 * * *', :identifier, worker: :organization_method # You can also use a block scheduled_task '0 15 05 * * *', :identifier do # Do Stuff end # You can use a Proc (called in the context of the Organization) to determine the schedule scheduled_task -> { settings[:cron] }, :identifier # You can specify a timezone. If a TZ is not coded and settings[:timezone] is present, it will be appended automatically scheduled_task '0 15 05 * * * America/Denver', :identifier, worker: :organization_method # Setting settings[:task_schedules][:identifier] will override the code cron schedule. Setting it to false will disable the Task # :identifer values _must_ be unique, but can be nil, in which case they will be determined by where (lineno etc) scheduled_task is called end end ``` ### Organization Attributes `id`: Primary Key `name`: Name of the organization. Used to on requests to select the tenant `key`: Key field from CanvasLMS `secret`: Secret field from CanvasLMS `canvas_account_id`: ID of the corresponding Canvas account. `settings`: Hash of settings for this Organization `salesforce_id`: ID of this organization in Salesforce XML for an installation can be generated by visiting `/lti/config` in your application. ### Routing The following routes should be added to the routes.rb file of the implementing LTI. (substituting `account_navigation` with the other staged navigation routes, if necessary) ```ruby # config/routes.rb mount PandaPal::Engine, at: '/lti' lti_nav account_navigation: 'accounts#launch' # Use lti_nav to provide a custom Launch implementation, otherwise use the url: param of stage_navigation to let PandaPal handle launch. root to: 'panda_pal/lti#launch' ``` ## Implementating data segregation This engine uses Apartment to keep data segregated between installations of the implementing LTI tool. By default, it does this by inspecting the path of the request, and matching URLs containing `orgs` or `organizations`, followed by the numeric ID of the organization (path such as `/organizations/123/accounts`). As such, all URLs should be built on top of a routing scope, such as the example below. ```ruby scope '/organizations/:organization_id' do resources :accounts, only: :index do collection do get 'teachers' => 'accounts#teachers' end end end ``` This can be overriden by creating an initializer, and adding a custom elevator ```ruby Rails.application.config.middleware.delete 'Apartment::Elevators::Generic' Rails.application.config.middleware.use 'Apartment::Elevators::Generic', lambda { |request| if match = request.path.match(/\/(?:orgs|organizations)\/(\d+)/) PandaPal::Organization.find_by(id: match[1]).try(:name) end } ``` It is also possible to switch tenants by implementing `around_action :switch_tenant` in the controller. However, it should be noted that calling `switch_tenant` must be used in an `around_action` hook (or given a block), and is only intended to be used for LTI launches, and not subsequent requests. ### Rake tasks and jobs **Delayed Job Support has been removed. This allows each project to assess it's need to handle background jobs.** The Apartment Gem makes it so background jobs need to be run within the current tenant. If using sidekiq, there is a gem that does this automatically called `apartment-sidkiq`. If Delayed Jobs, see how it's done in version 1 of PandaPal. For rake tasks, the current tenant will be set to `public`, and thus special handling must be taken when running the task or queueing jobs as part of it. One potential solution is to create a method with switches between tenants, and invoke the job for each individual organization as necessary. ```ruby # config/initializers/global_methods.rb def switch_tenant(tenant, &block) if block_given? Apartment::Tenant.switch(tenant, &block) else Apartment::Tenant.switch! tenant end end ``` ```ruby # lib/tasks/lti_tasks.rake namespace :lti_tasks do desc 'some rake task' task some_rake_task: :environment do PandaPal::Organization.all.each do |org| switch_tenant(org.name) do SomeJob.perform_later end end end end ``` ## Controller helper methods Controllers will automatically have access to a number of methods that can be helpful to implement. A list with descriptions is provided below. * `validate_launch!` - should be used in any tool launch points, and will verify that the launch request received from Canvas is legitimate based on the saved key and secret. * `current_session` - PandaPal provides support to use database persisted sessions, rather than the standard rails variety, as these play better when the tool is launched in an `iframe`. The `session_key` attribute from the returned object needs to be served to the client at some point during the launch and during subsequent requests. The benefit from this is that sessions will exist entirely in the frame used to launch the tool, and will not be visible should the user be accessing the tool in other contexts simultaneously. See section below for more information about this method. * `current_session_data` - Shortcut method to access the `data` hash given by the above method. * `current_organization` - Used to return the organization related to the currently selected tenant. See section below for more information about this method. * `session_changed?` - returns true if the session given by `current_session` has been modified during the request. * `save_session` - Saves the session given by `current_session` to the database. ## Sessions and `current_session` Because of the ever-increasing security around `