Rails LTI Tool Provider - Send Scores back to Canvas
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'
Once you share your unique keys and secrets with a tool consumer, it will be able to add your application. Here is an example of the interface that Canvas uses:
If you create a new settings file, you need to create a variable that loads the content from your settings file. Add this line in config/application.rb
config.lti_settings = Rails.application.config_for(:lti_settings)This variable will load the key and secret from your settings file.
You can change the quizkey and FirstSecret in this example to what you like but each key/secret pair should be distinct for each tool consumer. If you knew that 10 different institutes/instructors want to integrate your tool into their courses, you might create 10 key/pairs.
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:
To fix this, add the following line at the beginning of the launch endpoint.
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
The code above checks if the key is present in the settings variable you created in step 2. If the key is not present, then redirect the user to launch_error page. We will create this page later in this tutorial.
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.
- To check if the request is a valid LTI request, we need to check if The POST request contains lti_message_type with a value of basic-lti-launch-request, lti_version with a value of LTI-1p0 for LTI 1, and a resource_link_id. If any of these are missing, then the request is not a valid LTI request.
- If this is a valid LTI request, then we need to validate its authenticity. An authentic LTI request will have oauth_signature along with the oauth_consumer_key . We have already validated in Step 4 if the key is present or not. The next step is to generate a signature from this key and its corresponding secret that we have stored, and compare it with oauth_signature. If the two signatures match, only then the request is valid.
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.
First we have to check whether the tool was launched as an outcome service or not. This means that if the tool was launched as an assignment, then we have to send the final result of an assessment back to the tool consumer. Therefore, in its request it sends lis_outcome_service_url and lis_result_sourcedid. The fist parameter is a URL used to send the score back, and the second parameter identifies a unique row and column within the Tool Consumer's gradebook. If these two parameters are not present, then we redirect the user to an error page. Otherwise we post the result back to the tool consumer. The IMS-LTI gem enables us to accomplish all of this using 'post_extended_replace_result'.
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:
To fix this, you need to tell Rails that it does not need to verify the user before a launch request. Add the following line in the application controller.
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.
To add SSL certificate on localhost, follow the steps mentioned here.
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'.
You see this error because Canvas opens the LTI Tool in an iframe, but Rails does not allow the application to be embedded in an iframe by default. Therefore, you need to add the following code in the questions controller.
after_action :allow_iframe, only: [:show, :result] def allow_iframe response.headers.except! 'X-Frame-Options' end
In the first line of the code, we tell rails to only allow "show" and "result" endpoints to open in an iframe.
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:
Lunch Error
Make sure you have a correct key and a secret to access the quiz application.
Since Canvas will also open this launch error page in an iframe and we redirect to this page within the launch endpoint, we need to allow the launch endpoint to open in an iframe as well. Add the following code in the lti controller.
after_action :allow_iframe, only: [:launch] def allow_iframe response.headers.except! 'X-Frame-Options' end
Now, if the request received is not validated or authenticated, you will see the the following on Canvas:
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:
Here you can download the complete source code for a version of the quiz application with LTI support.