In our previous tutorial,
we showed how to create a basic LTI Tool Provider such as an
eBook or another application that works with a Tool Consumer
like Canvas.
But one of the major benefits of LTI is that it enables
Instructors to add different kinds of assessments (such as an
exercise of a unique type) that are not otherwise available in
Tool Consumers like Canvas.
In this tutorial, we show how to make your assessment
application LTI compatible, and send the scores back
to a Tool Consumer.
We have created a basic Ruby on Rails quiz application.
By following the steps below, you can take
the code
for the stand-alone quiz application, or your own Rails
application, and make it LTI compatible, sending the results of
student's assessment back to any Tool Consumer.
This tutorial assumes that you have read through the basic steps for creating an LTI-compliant application, and that you have at least a little familiarity with Ruby on Rails.
Step 1:
Add this Ruby
Gem to the Gemfile of your Rails
application.
It helps in performing most LTI tasks,
such as validating and authenticating LTI requests.
gem 'ims-lti', '~> 1.1.8'
Step 2: Before tool consumers can send a request to your tool, they will have to add your app. To do so, they need a key and a secret. Create a new config file config/lti_settings.yml, and add a key and a secret to that file. The lti_settings.yml file should contain the following:
production: quizkey: 'FirstSecret' development: quizkey: 'FirstSecret'
config.lti_settings = Rails.application.config_for(:lti_settings)This variable will load the key and secret from your settings file.
Step 3: Along with a key and a secret, the tool consumer will also need a url where it can send a request. For that purpose, create a controller named lti with a launch endpoint (so the url can say '..../lti/launch' for readability). This launch endpoint will receive the post request from the tool consumer, and we will validate and authenticate the LTI requests within this endpoint.
Step 4: We need to keep a check if our application is launched as an LTI application or not. If it is launched as an LTI application, then we will have to make a few changes to the normal behavior of our app. Typically, we might need to hide the header and footer that we display to other users, because the application will load in an iframe. In that context, it should look like a part of a Tool Consumer. Also, we will have to send scores back to the Tool Consumer. This might imply that we don't want to show the results to the user ourselves, such as displaying a results page. For example, our simple Quiz application will look like this if you do not remove the header from an LTI launch:
session[:isLTI]=trueTo hide the header, add the following in views/layout/application.html.erb after moving header code in the _header partial.
render "layouts/header" unless session[:isLTI]Now, when your application is launched inside a tool provider like Canvas, it will look like this:
Step 5: When your app is launched from a Tool Consumer (such as Canvas), it will send a post request to your launch endpoint with a bunch of parameters. One of the received parameters in the request will be oauth_consumer_key. This key should be exactly the same as the one we defined in the settings.yml file. The first step in a request validation is to check whether this received key is present in your system. If the key is not present, then throw an error. Add the following code inside the launch endpoint for key validation:
if not Rails.configuration.lti_settings[params[:oauth_consumer_key]] render :launch_error, status: 401 return end
Step 6:
If the key is present, then we move to the second step of
validation, which is to (1) check whether the request is a valid LTI
request, and (2) verify the authenticity of the Tool Consumer.
require 'oauth/request_proxy/action_controller_request' @provider = IMS::LTI::ToolProvider.new( params[:oauth_consumer_key], Rails.configuration.lti_settings[params[:oauth_consumer_key]], params ) if not @provider.valid_request?(request) # the request wasn't validated render :launch_error, status: 401 return end
Step 7: At this point, you have a valid and authentic LTI request. Now, our quiz application requires a user to be logged in order to take a quiz. But this application will be launched through Canvas, and a key goal of LTI is to provide a seamless experience to the user. Therefore, we will have to create a user account and log her in automatically. To do so, add the following code.
@@launch_params=params; email = params[:lis_person_contact_email_primary] @user = User.where(email: email).first if @user.blank? @user = User.new(:username => email, :email => email, :password => email, :password_confirmation => email) if !@user.save puts @user.errors.full_messages.first end end #Login the user and create his session. authorized_user = User.authenticate(email,email) session[:user_id] = authorized_user.id #redirect the user to give quiz starting from question id 1 redirect_to(:controller => "questions", :action => "show", :id => 1)In the first line we save request parameters, because we will need those to submit scores back to the Tool Consumer. Then we check if any user with this email already exists in our database. If not, then we create a new user. After that, we login the user and create his session. Finally, we redirect the user to the quiz, starting from question id 1.
Step 8: Our quiz application normally redirects the user to a results page once she finishes all the questions. But when launched via LTI from within a tool consumer, we instead want to submit the score back without launching the page. T do this, modify the code at the end of the submitQuestion endpoint in the Questions controller to the following.
if session[:isLTI] @@result = @@count.to_f/(@@count+@@falseCount) @@count = 0 @@falseCount = 0 redirect_to(:controller => "lti", :action => "submitscore", :result => @@result) else @@result = @@count @@count = 0 @@falseCount = 0 redirect_to(:action => "result") endIn this code, we check if the isLTI session variable is set. If so, then submit the score back to tool consumer, otherwise redirect the user to the results page. The score that we pass back must be in range of 0 to 1, which is why we divide total correct answers with total questions.
Step 9: Now create a new end point submitscore to submit the score back to tool consumer. Add the following code in the lti controller.
def submitscore @tp = IMS::LTI::ToolProvider.new( @@launch_params[:oauth_consumer_key], Rails.configuration.lti_settings[@@launch_params [:oauth_consumer_key]], @@launch_params) # add extension @tp.extend IMS::LTI::Extensions::OutcomeData::ToolProvider if !@tp.outcome_service? @message = "This tool wasn't lunched as an outcome service" puts "This tool wasn't lunched as an outcome service" render(:launch_error) end res = @tp.post_extended_replace_result!(score: params[:result]) if res.success? puts "Score Submitted" else puts "Error during score submission" end endThis code creates a tool provider object using the parameters we received in our request. We extend the tool provider object with the OutcomeData extension provided by the IMS-LTI gem.
Step 10: The Tool Consumer also tells us in the request where we should redirect the user once he completes the assessment in launch_presentation_return_url. Therefore, we redirect the user back to launch_presentation_return_url once we submit the scores. Add the following line at the end of the submitscore endpoint
redirect_to @@launch_params[:launch_presentation_return_url]
Step 11: If you want an instructor to be able to register your application in Tool Consumer, you will need XML configuration for your app. Therefore, you should create a page that provides the XML configuration for your app. Following is the XML configuration for our quiz app.
Quiz
Quiz LTI Application
http://localhost:3000/lti/launch
public
Step 12:
Now you have all of the things required to register an application
in Tool Consumer: key, secret and XML configuration.
After entering a URL of your XML file (In Canvas, you can also paste your
XML content) along with the key and a secret
in Tool Consumer, an instructor will be able see your application as an
"External Tool" while creating an assignment or quiz.
Step 13:
If you launch your application now to take an assessment, you
will receive the following error:
skip_before_action :verify_authenticity_token, only: :launch
Step 14:
If you launch your application once again, you should see the
following warning if your application is not running on
HTTPS.
Step 15: Now, delete your application from Canvas, update your config file with the https launch URL, and add your application again.
Step 16: Even now, if you launch your application, you will not see anything, but rather you will see the following error in your browser console:
Refused to display 'https://localhost:3000/questions/1' in a frame because it set 'X-Frame-Options' to 'sameorigin'.
after_action :allow_iframe, only: [:show, :result] def allow_iframe response.headers.except! 'X-Frame-Options' end
Step 17: The last step is to update routes and also to create a launch_error page, because this is where we are redirecting the user if the request is not validated or authenticated. Create views/lit/launch_error.html.erb and add the following code to it:
Make sure you have a correct key and a secret to access the quiz application.
after_action :allow_iframe, only: [:launch] def allow_iframe response.headers.except! 'X-Frame-Options' end
Rails.application.routes.draw do get 'quiz/index' resources :questions post 'questions/submitQuestion'=>'questions#submitQuestion', as: :submit_question root 'sessions#login' get "signup", :to => "users#new" get "login", :to => "sessions#login" get "logout", :to => "sessions#logout" get "home", :to => "sessions#home" get "profile", :to => "sessions#profile" get "setting", :to => "sessions#setting" post "signup", :to => "users#new" post "login", :to => "sessions#login" post "logout", :to => "sessions#logout" post "login_attempt", :to => "sessions#login_attempt" get "login_attempt", :to => "sessions#login_attempt" post "user_create", :to => "users#create" get "all", :to => "questions#index" get "result", :to => "questions#result" get 'lti/launch' post 'lti/launch' get 'lti/submitscore' post 'lti/submitscore' end
At this point you should have a working LTI-enabled quiz application. It will work as-is for non LTI launches (on your own domain), and will also work as an LTI tool when launched from within a tool consumer. If it is launched from within a tool consumer, then once a user submits an assessment, his scores can be recorded in the tool consumer's gradebook. On Canvas it will look like this: