ims-lti 1.0
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.
- data/LICENSE +18 -0
- data/README.md +105 -0
- data/ims-lti.gemspec +24 -0
- data/lib/ims.rb +1 -0
- data/lib/ims/lti.rb +71 -0
- data/lib/ims/lti/launch_params.rb +159 -0
- data/lib/ims/lti/outcome_request.rb +185 -0
- data/lib/ims/lti/outcome_response.rb +155 -0
- data/lib/ims/lti/tool_config.rb +221 -0
- data/lib/ims/lti/tool_consumer.rb +95 -0
- data/lib/ims/lti/tool_provider.rb +177 -0
- metadata +74 -0
data/LICENSE
ADDED
@@ -0,0 +1,18 @@
|
|
1
|
+
Copyright (c) 2012 Instructure
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
4
|
+
this software and associated documentation files (the "Software"), to deal in
|
5
|
+
the Software without restriction, including without limitation the rights to use,
|
6
|
+
copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
7
|
+
Software, and to permit persons to whom the Software is furnished to do so,
|
8
|
+
subject to the following conditions:
|
9
|
+
|
10
|
+
The above copyright notice and this permission notice shall be included in all
|
11
|
+
copies or substantial portions of the Software.
|
12
|
+
|
13
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
14
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
15
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
16
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
17
|
+
WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
18
|
+
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,105 @@
|
|
1
|
+
# IMS LTI
|
2
|
+
|
3
|
+
This ruby library is to help create Tool Providers and Tool Consumers for the
|
4
|
+
[IMS LTI standard](http://www.imsglobal.org/lti/index.html).
|
5
|
+
|
6
|
+
## Installation
|
7
|
+
This is packaged as the `ims-lti` rubygem, so you can just add the dependency to
|
8
|
+
your Gemfile or install the gem on your system:
|
9
|
+
|
10
|
+
gem install ims-lti
|
11
|
+
|
12
|
+
To require the library in your project:
|
13
|
+
|
14
|
+
require 'ims/lti'
|
15
|
+
|
16
|
+
To validate the OAuth signatures you need to require the appropriate request
|
17
|
+
proxy for your application. For example:
|
18
|
+
|
19
|
+
# For a sinatra app:
|
20
|
+
require 'oauth/request_proxy/rack_request'
|
21
|
+
|
22
|
+
# For a rails app:
|
23
|
+
require 'oauth/request_proxy/action_controller_request'
|
24
|
+
|
25
|
+
For further information see the [oauth-ruby](https://github.com/oauth/oauth-ruby) project.
|
26
|
+
|
27
|
+
## Usage
|
28
|
+
This readme won't cover the LTI standard, just how to use the library. It will be
|
29
|
+
very helpful to read the [LTI documentation](http://www.imsglobal.org/lti/index.html)
|
30
|
+
|
31
|
+
In LTI there are Tool Providers (TP) and Tool Consumers (TC), this library is
|
32
|
+
useful for implementing both. Here is an overview of the communication process:
|
33
|
+
[LTI 1.1 Introduction](http://www.imsglobal.org/lti/v1p1pd/ltiIMGv1p1pd.html#_Toc309649680)
|
34
|
+
|
35
|
+
### Tool Provider
|
36
|
+
As a TP your app will receive a POST request with a bunch of
|
37
|
+
[LTI launch data](http://www.imsglobal.org/lti/v1p1pd/ltiIMGv1p1pd.html#_Toc309649684)
|
38
|
+
and it will be signed with OAuth using a key/secret that both the TP and TC share.
|
39
|
+
This is covered in the [LTI security model](http://www.imsglobal.org/lti/v1p1pd/ltiIMGv1p1pd.html#_Toc309649685)
|
40
|
+
|
41
|
+
Here is an example of a simple TP Sinatra app using this gem:
|
42
|
+
[LTI Tool Provider](https://github.com/instructure/lti_tool_provider_example)
|
43
|
+
|
44
|
+
This library doesn't help the TP manage the consumer keys and secrets. The POST
|
45
|
+
headers/parameters will contain the `oauth_consumer_key` and your app can use that to look
|
46
|
+
up the appropriate `oauth_consumer_secret`. Once you have the necessary credentials
|
47
|
+
you can initialize a `ToolProvider` object with them and the post parameters:
|
48
|
+
|
49
|
+
```ruby
|
50
|
+
# Initialize TP object with OAuth creds and post parameters
|
51
|
+
provider = IMS::LTI::ToolProvider.new(consumer_key, consumer_secret, params)
|
52
|
+
|
53
|
+
# Verify OAuth signature by passing the request object
|
54
|
+
if provider.valid_request?(request)
|
55
|
+
# success
|
56
|
+
else
|
57
|
+
# handle invalid OAuth
|
58
|
+
end
|
59
|
+
```
|
60
|
+
|
61
|
+
Once your TP object is initialized and verified you can load your tool. All of the
|
62
|
+
[launch data](http://www.imsglobal.org/lti/v1p1pd/ltiIMGv1p1pd.html#_Toc309649684)
|
63
|
+
is available in the TP object along with some convenience methods like `provider.username`
|
64
|
+
which will try to find the name from the 3 potential name launch data attributes.
|
65
|
+
|
66
|
+
#### Returning Results of a Quiz/Assignment
|
67
|
+
If your TP provides some kind of assessment service you can write grades back to
|
68
|
+
the TC. This is documented in the LTI docs [here](http://www.imsglobal.org/lti/v1p1pd/ltiIMGv1p1pd.html#_Toc309649690).
|
69
|
+
|
70
|
+
You can check whether the TC is expecting a grade write-back:
|
71
|
+
|
72
|
+
```ruby
|
73
|
+
if provider.outcome_service?
|
74
|
+
# ready for grade write-back
|
75
|
+
else
|
76
|
+
# normal tool launch without grade write-back
|
77
|
+
end
|
78
|
+
```
|
79
|
+
|
80
|
+
To write the grade back to the TC your tool will do a POST directly back to the
|
81
|
+
URL the TC passed in the launch data. You can use the TP object to do that for you:
|
82
|
+
|
83
|
+
```ruby
|
84
|
+
# post the score to the TC, score should be a float >= 0.0 and <= 1.0
|
85
|
+
# this returns an OutcomeResponse object
|
86
|
+
response = provider.post_replace_result!(score)
|
87
|
+
if response.success?
|
88
|
+
# grade write worked
|
89
|
+
elsif response.processing?
|
90
|
+
elsif response.unsupported?
|
91
|
+
else
|
92
|
+
# failed
|
93
|
+
end
|
94
|
+
```
|
95
|
+
|
96
|
+
You can see the error code documentation
|
97
|
+
[here](http://www.imsglobal.org/gws/gwsv1p0/imsgws_baseProfv1p0.html#1639667).
|
98
|
+
|
99
|
+
### Tool Consumer
|
100
|
+
As a Tool Consumer your app will POST an OAuth-signed launch requests to TPs with the necessary
|
101
|
+
[LTI launch data](http://www.imsglobal.org/lti/v1p1pd/ltiIMGv1p1pd.html#_Toc309649684).
|
102
|
+
This is covered in the [LTI security model](http://www.imsglobal.org/lti/v1p1pd/ltiIMGv1p1pd.html#_Toc309649685)
|
103
|
+
|
104
|
+
Here is an example of a simple TC Sinatra app using this gem:
|
105
|
+
[LTI Tool Consumer](https://github.com/instructure/lti_tool_consumer_example)
|
data/ims-lti.gemspec
ADDED
@@ -0,0 +1,24 @@
|
|
1
|
+
Gem::Specification.new do |s|
|
2
|
+
s.name = %q{ims-lti}
|
3
|
+
s.version = "1.0"
|
4
|
+
|
5
|
+
s.authors = ["Instructure"]
|
6
|
+
s.date = %q{2012-03-10}
|
7
|
+
s.extra_rdoc_files = %W(LICENSE)
|
8
|
+
s.files = %W(
|
9
|
+
LICENSE
|
10
|
+
README.md
|
11
|
+
lib/ims.rb
|
12
|
+
lib/ims/lti.rb
|
13
|
+
lib/ims/lti/launch_params.rb
|
14
|
+
lib/ims/lti/outcome_request.rb
|
15
|
+
lib/ims/lti/outcome_response.rb
|
16
|
+
lib/ims/lti/tool_config.rb
|
17
|
+
lib/ims/lti/tool_consumer.rb
|
18
|
+
lib/ims/lti/tool_provider.rb
|
19
|
+
ims-lti.gemspec
|
20
|
+
)
|
21
|
+
s.homepage = %q{http://github.com/instructure/ims-lti}
|
22
|
+
s.require_paths = %W(lib)
|
23
|
+
s.summary = %q{Ruby library for creating IMS LTI tool providers and consumers}
|
24
|
+
end
|
data/lib/ims.rb
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
require 'ims/lti'
|
data/lib/ims/lti.rb
ADDED
@@ -0,0 +1,71 @@
|
|
1
|
+
require 'oauth'
|
2
|
+
require 'builder'
|
3
|
+
require "rexml/document"
|
4
|
+
require 'uuid'
|
5
|
+
require 'cgi'
|
6
|
+
|
7
|
+
module IMS # :nodoc:
|
8
|
+
|
9
|
+
# :main:IMS::LTI
|
10
|
+
# LTI is a standard defined by IMS for creating eduction Tool Consumers/Providers.
|
11
|
+
# LTI documentation: http://www.imsglobal.org/lti/index.html
|
12
|
+
#
|
13
|
+
# When creating these tools you will work primarily with the ToolProvider and
|
14
|
+
# ToolConsumer classes.
|
15
|
+
#
|
16
|
+
# For validating OAuth request be sure to require the necessary proxy request
|
17
|
+
# object. See valid_request? for more documentation.
|
18
|
+
#
|
19
|
+
# == Installation
|
20
|
+
# This is packaged as the `ims-lti` rubygem, so you can just add the dependency to
|
21
|
+
# your Gemfile or install the gem on your system:
|
22
|
+
#
|
23
|
+
# gem install ims-lti
|
24
|
+
#
|
25
|
+
# To require the library in your project:
|
26
|
+
#
|
27
|
+
# require 'ims/lti'
|
28
|
+
module LTI
|
29
|
+
VERSIONS = %w{1.0 1.1}
|
30
|
+
|
31
|
+
class InvalidLTIConfigError < StandardError
|
32
|
+
end
|
33
|
+
|
34
|
+
# Generates a unique identifier
|
35
|
+
def self.generate_identifier
|
36
|
+
UUID.new
|
37
|
+
end
|
38
|
+
|
39
|
+
# Validates and OAuth request using the OAuth Gem - https://github.com/oauth/oauth-ruby
|
40
|
+
#
|
41
|
+
# To validate the OAuth signatures you need to require the appropriate
|
42
|
+
# request proxy for your application. For example:
|
43
|
+
#
|
44
|
+
# # For a sinatra app:
|
45
|
+
# require 'oauth/request_proxy/rack_request'
|
46
|
+
#
|
47
|
+
# # For a rails app:
|
48
|
+
# require 'oauth/request_proxy/action_controller_request'
|
49
|
+
def self.valid_request?(secret, request, handle_error=true)
|
50
|
+
begin
|
51
|
+
signature = OAuth::Signature.build(request, :consumer_secret => secret)
|
52
|
+
signature.verify() or raise OAuth::Unauthorized
|
53
|
+
true
|
54
|
+
rescue OAuth::Signature::UnknownSignatureMethod, OAuth::Unauthorized
|
55
|
+
if handle_error
|
56
|
+
false
|
57
|
+
else
|
58
|
+
raise $!
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
require 'ims/lti/launch_params'
|
67
|
+
require 'ims/lti/tool_provider'
|
68
|
+
require 'ims/lti/tool_consumer'
|
69
|
+
require 'ims/lti/outcome_request'
|
70
|
+
require 'ims/lti/outcome_response'
|
71
|
+
require 'ims/lti/tool_config'
|
@@ -0,0 +1,159 @@
|
|
1
|
+
module IMS::LTI
|
2
|
+
# Mixin module for managing LTI Launch Data
|
3
|
+
#
|
4
|
+
# Launch data documentation:
|
5
|
+
# http://www.imsglobal.org/lti/v1p1pd/ltiIMGv1p1pd.html#_Toc309649684
|
6
|
+
module LaunchParams
|
7
|
+
|
8
|
+
# List of the standard launch parameters for an LTI launch
|
9
|
+
LAUNCH_DATA_PARAMETERS = %w{
|
10
|
+
context_id
|
11
|
+
context_label
|
12
|
+
context_title
|
13
|
+
context_type
|
14
|
+
launch_presentation_css_url
|
15
|
+
launch_presentation_document_target
|
16
|
+
launch_presentation_height
|
17
|
+
launch_presentation_locale
|
18
|
+
launch_presentation_return_url
|
19
|
+
launch_presentation_width
|
20
|
+
lis_course_offering_sourcedid
|
21
|
+
lis_course_section_sourcedid
|
22
|
+
lis_outcome_service_url
|
23
|
+
lis_person_contact_email_primary
|
24
|
+
lis_person_name_family
|
25
|
+
lis_person_name_full
|
26
|
+
lis_person_name_given
|
27
|
+
lis_person_sourcedid
|
28
|
+
lis_result_sourcedid
|
29
|
+
lti_message_type
|
30
|
+
lti_version
|
31
|
+
oauth_callback
|
32
|
+
oauth_consumer_key
|
33
|
+
oauth_nonce
|
34
|
+
oauth_signature
|
35
|
+
oauth_signature_method
|
36
|
+
oauth_timestamp
|
37
|
+
oauth_version
|
38
|
+
resource_link_description
|
39
|
+
resource_link_id
|
40
|
+
resource_link_title
|
41
|
+
roles
|
42
|
+
tool_consumer_info_product_family_code
|
43
|
+
tool_consumer_info_version
|
44
|
+
tool_consumer_instance_contact_email
|
45
|
+
tool_consumer_instance_description
|
46
|
+
tool_consumer_instance_guid
|
47
|
+
tool_consumer_instance_name
|
48
|
+
tool_consumer_instance_url
|
49
|
+
user_id
|
50
|
+
user_image
|
51
|
+
}
|
52
|
+
|
53
|
+
LAUNCH_DATA_PARAMETERS.each { |p| attr_accessor p }
|
54
|
+
|
55
|
+
# Hash of custom parameters, the keys will be prepended with "custom_" at launch
|
56
|
+
attr_accessor :custom_params
|
57
|
+
|
58
|
+
# Hash of extension parameters, the keys will be prepended with "ext_" at launch
|
59
|
+
attr_accessor :ext_params
|
60
|
+
|
61
|
+
# Hash of parameters to add to the launch. These keys will not be prepended
|
62
|
+
# with any value at launch
|
63
|
+
attr_accessor :non_spec_params
|
64
|
+
|
65
|
+
# Set the roles for the current launch
|
66
|
+
#
|
67
|
+
# Full list of roles can be found here:
|
68
|
+
# http://www.imsglobal.org/LTI/v1p1pd/ltiIMGv1p1pd.html#_Toc309649700
|
69
|
+
#
|
70
|
+
# LIS roles include:
|
71
|
+
# * Student
|
72
|
+
# * Faculty
|
73
|
+
# * Member
|
74
|
+
# * Learner
|
75
|
+
# * Instructor
|
76
|
+
# * Mentor
|
77
|
+
# * Staff
|
78
|
+
# * Alumni
|
79
|
+
# * ProspectiveStudent
|
80
|
+
# * Guest
|
81
|
+
# * Other
|
82
|
+
# * Administrator
|
83
|
+
# * Observer
|
84
|
+
# * None
|
85
|
+
#
|
86
|
+
# @param roles_list [String,Array] An Array or comma-separated String of roles
|
87
|
+
def roles=(roles_list)
|
88
|
+
if roles_list
|
89
|
+
if roles_list.is_a?(Array)
|
90
|
+
@roles = roles_list
|
91
|
+
else
|
92
|
+
@roles = roles_list.split(",").map(&:downcase)
|
93
|
+
end
|
94
|
+
else
|
95
|
+
@roles = nil
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
def set_custom_param(key, val)
|
100
|
+
@custom_params[key] = val
|
101
|
+
end
|
102
|
+
|
103
|
+
def get_custom_param(key)
|
104
|
+
@custom_params[key]
|
105
|
+
end
|
106
|
+
|
107
|
+
def set_non_spec_param(key, val)
|
108
|
+
@non_spec_params[key] = val
|
109
|
+
end
|
110
|
+
|
111
|
+
def get_non_spec_param(key)
|
112
|
+
@non_spec_params[key]
|
113
|
+
end
|
114
|
+
|
115
|
+
def set_ext_param(key, val)
|
116
|
+
@ext_params[key] = val
|
117
|
+
end
|
118
|
+
|
119
|
+
def get_ext_param(key)
|
120
|
+
@ext_params[key]
|
121
|
+
end
|
122
|
+
|
123
|
+
# Create a new Hash with all launch data. Custom/Extension keys will have the
|
124
|
+
# appropriate value prepended to the keys and the roles are set as a comma
|
125
|
+
# separated String
|
126
|
+
def to_params
|
127
|
+
params = launch_data_hash.merge(add_key_prefix(@custom_params, 'custom')).merge(add_key_prefix(@ext_params, 'ext')).merge(@non_spec_params)
|
128
|
+
params["roles"] = @roles.map(&:capitalize).join(",") if @roles
|
129
|
+
params
|
130
|
+
end
|
131
|
+
|
132
|
+
# Populates the launch data from a Hash
|
133
|
+
#
|
134
|
+
# Only keys in LAUNCH_DATA_PARAMETERS and that start with 'custom_' or 'ext_'
|
135
|
+
# will be pulled from the provided Hash
|
136
|
+
def process_params(params)
|
137
|
+
params.each_pair do |key, val|
|
138
|
+
if LAUNCH_DATA_PARAMETERS.member?(key)
|
139
|
+
self.send("#{key}=", val)
|
140
|
+
elsif key =~ /custom_(.*)/
|
141
|
+
@custom_params[$1] = val
|
142
|
+
elsif key =~ /ext_(.*)/
|
143
|
+
@ext_params[$1] = val
|
144
|
+
end
|
145
|
+
end
|
146
|
+
end
|
147
|
+
|
148
|
+
private
|
149
|
+
|
150
|
+
def launch_data_hash
|
151
|
+
LAUNCH_DATA_PARAMETERS.inject({}) { |h, k| h[k] = self.send(k) if self.send(k); h }
|
152
|
+
end
|
153
|
+
|
154
|
+
def add_key_prefix(hash, prefix)
|
155
|
+
hash.keys.inject({}) { |h, k| h["#{prefix}_#{k}"] = hash[k]; h }
|
156
|
+
end
|
157
|
+
|
158
|
+
end
|
159
|
+
end
|
@@ -0,0 +1,185 @@
|
|
1
|
+
module IMS::LTI
|
2
|
+
# Class for consuming/generating LTI Outcome Requests
|
3
|
+
#
|
4
|
+
# Outcome Request documentation: http://www.imsglobal.org/lti/v1p1pd/ltiIMGv1p1pd.html#_Toc309649691
|
5
|
+
#
|
6
|
+
# This class can be used by both Tool Providers and Tool Consumers. Each will
|
7
|
+
# use it a bit differently. The Tool Provider will use it to POST an OAuth-signed
|
8
|
+
# request to a TC. A Tool Consumer will use it to parse such a request from a TP.
|
9
|
+
#
|
10
|
+
# === Tool Provider Usage
|
11
|
+
# An OutcomeRequest will generally be created through a configured ToolProvider
|
12
|
+
# object. See the ToolProvider documentation.
|
13
|
+
#
|
14
|
+
# === Tool Consumer Usage
|
15
|
+
# When an outcome request is sent from a TP the body of the request is XML.
|
16
|
+
# This class parses that XML and provides a simple interface for accessing the
|
17
|
+
# information in the request. Typical usage would be:
|
18
|
+
#
|
19
|
+
# # create an OutcomeRequest from the request object
|
20
|
+
# req = IMS::LTI::OutcomeRequest.from_post_request(request)
|
21
|
+
#
|
22
|
+
# # access the source id to identify the user who's grade you'd like to access
|
23
|
+
# req.lis_result_sourcedid
|
24
|
+
#
|
25
|
+
# # process the request
|
26
|
+
# if req.replace_request?
|
27
|
+
# # set a new score for the user
|
28
|
+
# elsif req.read_request?
|
29
|
+
# # return the score for the user
|
30
|
+
# elsif req.delete_request?
|
31
|
+
# # clear the score for the user
|
32
|
+
# else
|
33
|
+
# # return an unsupported OutcomeResponse
|
34
|
+
# end
|
35
|
+
class OutcomeRequest
|
36
|
+
|
37
|
+
REPLACE_REQUEST = 'replaceResult'
|
38
|
+
DELETE_REQUEST = 'deleteResult'
|
39
|
+
READ_REQUEST = 'readResult'
|
40
|
+
|
41
|
+
attr_accessor :operation, :score, :outcome_response, :message_identifier,
|
42
|
+
:lis_outcome_service_url, :lis_result_sourcedid,
|
43
|
+
:consumer_key, :consumer_secret, :post_request
|
44
|
+
|
45
|
+
# Create a new OutcomeRequest
|
46
|
+
#
|
47
|
+
# @param opts [Hash] initialization hash
|
48
|
+
def initialize(opts={})
|
49
|
+
opts.each_pair do |key, val|
|
50
|
+
self.send("#{key}=", val) if self.respond_to?("#{key}=")
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
# Convenience method for creating a new OutcomeRequest from a request object
|
55
|
+
#
|
56
|
+
# req = IMS::LTI::OutcomeRequest.from_post_request(request)
|
57
|
+
def self.from_post_request(post_request)
|
58
|
+
request = OutcomeRequest.new
|
59
|
+
request.post_request = post_request
|
60
|
+
if post_request.body.respond_to?(:read)
|
61
|
+
xml = post_request.body.read
|
62
|
+
post_request.body.rewind
|
63
|
+
else
|
64
|
+
xml = post_request.body
|
65
|
+
end
|
66
|
+
request.process_xml(xml)
|
67
|
+
request
|
68
|
+
end
|
69
|
+
|
70
|
+
# POSTs the given score to the Tool Consumer with a replaceResult
|
71
|
+
#
|
72
|
+
# @return [OutcomeResponse] The response from the Tool Consumer
|
73
|
+
def post_replace_result!(score)
|
74
|
+
@operation = REPLACE_REQUEST
|
75
|
+
@score = score
|
76
|
+
post_outcome_request
|
77
|
+
end
|
78
|
+
|
79
|
+
# POSTs a deleteResult to the Tool Consumer
|
80
|
+
#
|
81
|
+
# @return [OutcomeResponse] The response from the Tool Consumer
|
82
|
+
def post_delete_result!
|
83
|
+
@operation = DELETE_REQUEST
|
84
|
+
post_outcome_request
|
85
|
+
end
|
86
|
+
|
87
|
+
# POSTs a readResult to the Tool Consumer
|
88
|
+
#
|
89
|
+
# @return [OutcomeResponse] The response from the Tool Consumer
|
90
|
+
def post_read_result!
|
91
|
+
@operation = READ_REQUEST
|
92
|
+
post_outcome_request
|
93
|
+
end
|
94
|
+
|
95
|
+
# Check whether this request is a replaceResult request
|
96
|
+
def replace_request?
|
97
|
+
@operation == REPLACE_REQUEST
|
98
|
+
end
|
99
|
+
|
100
|
+
# Check whether this request is a deleteResult request
|
101
|
+
def delete_request?
|
102
|
+
@operation == DELETE_REQUEST
|
103
|
+
end
|
104
|
+
|
105
|
+
# Check whether this request is a readResult request
|
106
|
+
def read_request?
|
107
|
+
@operation == READ_REQUEST
|
108
|
+
end
|
109
|
+
|
110
|
+
# Check whether the last outcome POST was successful
|
111
|
+
def outcome_post_successful?
|
112
|
+
@outcome_response && @outcome_response.success?
|
113
|
+
end
|
114
|
+
|
115
|
+
# POST an OAuth signed request to the Tool Consumer
|
116
|
+
#
|
117
|
+
# @return [OutcomeResponse] The response from the Tool Consumer
|
118
|
+
def post_outcome_request
|
119
|
+
raise IMS::LTI::InvalidLTIConfigError, "" unless has_required_attributes?
|
120
|
+
|
121
|
+
consumer = OAuth::Consumer.new(@consumer_key, @consumer_secret)
|
122
|
+
token = OAuth::AccessToken.new(consumer)
|
123
|
+
res = token.post(
|
124
|
+
@lis_outcome_service_url,
|
125
|
+
generate_request_xml,
|
126
|
+
'Content-Type' => 'application/xml'
|
127
|
+
)
|
128
|
+
@outcome_response = OutcomeResponse.from_post_response(res)
|
129
|
+
end
|
130
|
+
|
131
|
+
# Parse Outcome Request data from XML
|
132
|
+
def process_xml(xml)
|
133
|
+
doc = REXML::Document.new xml
|
134
|
+
@message_identifier = doc.text("//imsx_POXRequestHeaderInfo/imsx_messageIdentifier")
|
135
|
+
@lis_result_sourcedid = doc.text("//resultRecord/sourcedGUID/sourcedId")
|
136
|
+
|
137
|
+
if REXML::XPath.first(doc, "//deleteResultRequest")
|
138
|
+
@operation = DELETE_REQUEST
|
139
|
+
elsif REXML::XPath.first(doc, "//readResultRequest")
|
140
|
+
@operation = READ_REQUEST
|
141
|
+
elsif REXML::XPath.first(doc, "//replaceResultRequest")
|
142
|
+
@operation = REPLACE_REQUEST
|
143
|
+
@score = doc.get_text("//resultRecord/result/resultScore/textString")
|
144
|
+
end
|
145
|
+
end
|
146
|
+
|
147
|
+
private
|
148
|
+
|
149
|
+
def has_required_attributes?
|
150
|
+
@consumer_key && @consumer_secret && @lis_outcome_service_url && @lis_result_sourcedid && @operation
|
151
|
+
end
|
152
|
+
|
153
|
+
def generate_request_xml
|
154
|
+
builder = Builder::XmlMarkup.new #(:indent=>2)
|
155
|
+
builder.instruct!
|
156
|
+
|
157
|
+
builder.imsx_POXEnvelopeRequest("xmlns" => "http://www.imsglobal.org/lis/oms1p0/pox") do |env|
|
158
|
+
env.imsx_POXHeader do |header|
|
159
|
+
header.imsx_POXRequestHeaderInfo do |info|
|
160
|
+
info.imsx_version "V1.0"
|
161
|
+
info.imsx_messageIdentifier @message_identifier || IMS::LTI::generate_identifier
|
162
|
+
end
|
163
|
+
end
|
164
|
+
env.imsx_POXBody do |body|
|
165
|
+
body.tag!(@operation + 'Request') do |request|
|
166
|
+
request.resultRecord do |record|
|
167
|
+
record.sourcedGUID do |guid|
|
168
|
+
guid.sourcedId @lis_result_sourcedid
|
169
|
+
end
|
170
|
+
if @score
|
171
|
+
record.result do |res|
|
172
|
+
res.resultScore do |res_score|
|
173
|
+
res_score.language "en" # 'en' represents the format of the number
|
174
|
+
res_score.textString @score.to_s
|
175
|
+
end
|
176
|
+
end
|
177
|
+
end
|
178
|
+
end
|
179
|
+
end
|
180
|
+
end
|
181
|
+
end
|
182
|
+
end
|
183
|
+
|
184
|
+
end
|
185
|
+
end
|
@@ -0,0 +1,155 @@
|
|
1
|
+
module IMS::LTI
|
2
|
+
# Class for consuming/generating LTI Outcome Responses
|
3
|
+
#
|
4
|
+
# Response documentation: http://www.imsglobal.org/lti/v1p1pd/ltiIMGv1p1pd.html#_Toc309649691
|
5
|
+
#
|
6
|
+
# Error code documentation: http://www.imsglobal.org/gws/gwsv1p0/imsgws_baseProfv1p0.html#1639667
|
7
|
+
#
|
8
|
+
# This class can be used by both Tool Providers and Tool Consumers. Each will
|
9
|
+
# use it a bit differently. The Tool Provider will use it parse the result of
|
10
|
+
# an OutcomeRequest to the Tool Consumer. A Tool Consumer will use it generate
|
11
|
+
# proper response XML to send back to a Tool Provider
|
12
|
+
#
|
13
|
+
# === Tool Provider Usage
|
14
|
+
# An OutcomeResponse will generally be created when POSTing an OutcomeRequest
|
15
|
+
# through a configured ToolProvider. See the ToolProvider documentation for
|
16
|
+
# typical usage.
|
17
|
+
#
|
18
|
+
# === Tool Consumer Usage
|
19
|
+
# When an outcome request is sent from a Tool Provider the body of the request
|
20
|
+
# is XML. This class parses that XML and provides a simple interface for
|
21
|
+
# accessing the information in the request. Typical usage would be:
|
22
|
+
#
|
23
|
+
# # create a new response and set the appropriate values
|
24
|
+
# res = IMS::LTI::OutcomeResponse.new
|
25
|
+
# res.message_ref_identifier = outcome_request.message_identifier
|
26
|
+
# res.operation = outcome_request.operation
|
27
|
+
# res.code_major = 'success'
|
28
|
+
# res.severity = 'status'
|
29
|
+
#
|
30
|
+
# # set a description (optional) and other information based on the type of response
|
31
|
+
# if outcome_request.replace_request?
|
32
|
+
# res.description = "Your old score of 0 has been replaced with #{outcome_request.score}"
|
33
|
+
# elsif outcome_request.read_request?
|
34
|
+
# res.description = "You score is 50"
|
35
|
+
# res.score = 50
|
36
|
+
# elsif outcome_request.delete_request?
|
37
|
+
# res.description = "You score has been cleared"
|
38
|
+
# else
|
39
|
+
# res.code_major = 'unsupported'
|
40
|
+
# res.severity = 'status'
|
41
|
+
# res.description = "#{outcome_request.operation} is not supported"
|
42
|
+
# end
|
43
|
+
#
|
44
|
+
# # the generated xml is returned to the Tool Provider
|
45
|
+
# res.generate_response_xml
|
46
|
+
#
|
47
|
+
class OutcomeResponse
|
48
|
+
|
49
|
+
attr_accessor :request_type, :score, :message_identifier, :response_code,
|
50
|
+
:post_response, :code_major, :severity, :description, :operation,
|
51
|
+
:message_ref_identifier
|
52
|
+
|
53
|
+
CODE_MAJOR_CODES = %w{success processing failure unsupported}
|
54
|
+
SEVERITY_CODES = %w{status warning error}
|
55
|
+
|
56
|
+
# Create a new OutcomeResponse
|
57
|
+
#
|
58
|
+
# @param opts [Hash] initialization hash
|
59
|
+
def initialize(opts={})
|
60
|
+
opts.each_pair do |key, val|
|
61
|
+
self.send("#{key}=", val) if self.respond_to?("#{key}=")
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
# Convenience method for creating a new OutcomeResponse from a response object
|
66
|
+
#
|
67
|
+
# req = IMS::LTI::OutcomeResponse.from_post_response(response)
|
68
|
+
def self.from_post_response(post_response)
|
69
|
+
response = OutcomeResponse.new
|
70
|
+
response.post_response = post_response
|
71
|
+
response.response_code = post_response.code
|
72
|
+
xml = post_response.body
|
73
|
+
response.process_xml(xml)
|
74
|
+
response
|
75
|
+
end
|
76
|
+
|
77
|
+
def success?
|
78
|
+
@code_major == 'success'
|
79
|
+
end
|
80
|
+
|
81
|
+
def processing?
|
82
|
+
@code_major == 'processing'
|
83
|
+
end
|
84
|
+
|
85
|
+
def failure?
|
86
|
+
@code_major == 'failure'
|
87
|
+
end
|
88
|
+
|
89
|
+
def unsupported?
|
90
|
+
@code_major == 'unsupported'
|
91
|
+
end
|
92
|
+
|
93
|
+
def has_warning?
|
94
|
+
@severity == 'warning'
|
95
|
+
end
|
96
|
+
|
97
|
+
def has_error?
|
98
|
+
@severity == 'error'
|
99
|
+
end
|
100
|
+
|
101
|
+
# Parse Outcome Response data from XML
|
102
|
+
def process_xml(xml)
|
103
|
+
doc = REXML::Document.new xml
|
104
|
+
@message_identifier = doc.text("//imsx_statusInfo/imsx_messageIdentifier").to_s
|
105
|
+
@code_major = doc.text("//imsx_statusInfo/imsx_codeMajor")
|
106
|
+
@code_major.downcase! if @code_major
|
107
|
+
@severity = doc.text("//imsx_statusInfo/imsx_severity")
|
108
|
+
@severity.downcase! if @severity
|
109
|
+
@description = doc.text("//imsx_statusInfo/imsx_description")
|
110
|
+
@description = @description.to_s if @description
|
111
|
+
@message_ref_identifier = doc.text("//imsx_statusInfo/imsx_messageRefIdentifier")
|
112
|
+
@operation = doc.text("//imsx_statusInfo/imsx_operationRefIdentifier")
|
113
|
+
@score = doc.text("//readResultResponse//resultScore/textString")
|
114
|
+
@score = @score.to_s if @score
|
115
|
+
end
|
116
|
+
|
117
|
+
# Generate XML based on the current configuration
|
118
|
+
# @return [String] The response xml
|
119
|
+
def generate_response_xml
|
120
|
+
builder = Builder::XmlMarkup.new
|
121
|
+
builder.instruct!
|
122
|
+
|
123
|
+
builder.imsx_POXEnvelopeResponse("xmlns" => "http://www.imsglobal.org/lis/oms1p0/pox") do |env|
|
124
|
+
env.imsx_POXHeader do |header|
|
125
|
+
header.imsx_POXResponseHeaderInfo do |info|
|
126
|
+
info.imsx_version "V1.0"
|
127
|
+
info.imsx_messageIdentifier @message_identifier || IMS::LTI::generate_identifier
|
128
|
+
info.imsx_statusInfo do |status|
|
129
|
+
status.imsx_codeMajor @code_major
|
130
|
+
status.imsx_severity @severity
|
131
|
+
status.imsx_description @description
|
132
|
+
status.imsx_messageRefIdentifier @message_ref_identifier
|
133
|
+
status.imsx_operationRefIdentifier @operation
|
134
|
+
end
|
135
|
+
end
|
136
|
+
end #/header
|
137
|
+
env.imsx_POXBody do |body|
|
138
|
+
unless unsupported?
|
139
|
+
body.tag!(@operation + 'Response') do |request|
|
140
|
+
if @operation == OutcomeRequest::READ_REQUEST
|
141
|
+
request.result do |res|
|
142
|
+
res.resultScore do |res_score|
|
143
|
+
res_score.language "en" # 'en' represents the format of the number
|
144
|
+
res_score.textString @score.to_s
|
145
|
+
end
|
146
|
+
end #/result
|
147
|
+
end
|
148
|
+
end #/operationResponse
|
149
|
+
end
|
150
|
+
end #/body
|
151
|
+
end
|
152
|
+
end
|
153
|
+
|
154
|
+
end
|
155
|
+
end
|
@@ -0,0 +1,221 @@
|
|
1
|
+
module IMS::LTI
|
2
|
+
# Class used to represent an LTI configuration
|
3
|
+
#
|
4
|
+
# It can create and read the Common Cartridge XML representation of LTI links
|
5
|
+
# as described here: http://www.imsglobal.org/LTI/v1p1pd/ltiIMGv1p1pd.html#_Toc309649689
|
6
|
+
#
|
7
|
+
# == Usage
|
8
|
+
# To generate an XML configuration:
|
9
|
+
#
|
10
|
+
# # Create a config object and set some options
|
11
|
+
# tc = IMS::LTI::ToolConfig.new(:title => "Example Sinatra Tool Provider", :launch_url => url)
|
12
|
+
# tc.description = "This example LTI Tool Provider supports LIS Outcome pass-back."
|
13
|
+
#
|
14
|
+
# # generate the XML
|
15
|
+
# tc.to_xml
|
16
|
+
#
|
17
|
+
# Or to create a config object from an XML String:
|
18
|
+
#
|
19
|
+
# tc = IMS::LTI::ToolConfig.create_from_xml(xml)
|
20
|
+
class ToolConfig
|
21
|
+
attr_reader :custom_params, :extensions
|
22
|
+
|
23
|
+
attr_accessor :title, :description, :launch_url, :secure_launch_url,
|
24
|
+
:icon, :secure_icon, :cartridge_bundle, :cartridge_icon,
|
25
|
+
:vendor_code, :vendor_name, :vendor_description, :vendor_url,
|
26
|
+
:vendor_contact_email, :vendor_contact_name
|
27
|
+
|
28
|
+
# Create a new ToolConfig with the given options
|
29
|
+
#
|
30
|
+
# @param opts [Hash] The initial options for the ToolConfig
|
31
|
+
def initialize(opts={})
|
32
|
+
@custom_params = opts.delete("custom_params") || {}
|
33
|
+
@extensions = opts.delete("extensions") || {}
|
34
|
+
|
35
|
+
opts.each_pair do |key, val|
|
36
|
+
self.send("#{key}=", val) if self.respond_to?("#{key}=")
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
# Create a ToolConfig from the given XML
|
41
|
+
#
|
42
|
+
# @param xml [String]
|
43
|
+
def self.create_from_xml(xml)
|
44
|
+
tc = ToolConfig.new
|
45
|
+
tc.process_xml(xml)
|
46
|
+
|
47
|
+
tc
|
48
|
+
end
|
49
|
+
|
50
|
+
def set_custom_param(key, val)
|
51
|
+
@custom_params[key] = val
|
52
|
+
end
|
53
|
+
|
54
|
+
def get_custom_param(key)
|
55
|
+
@custom_params[key]
|
56
|
+
end
|
57
|
+
|
58
|
+
# Set the extension parameters for a specific vendor
|
59
|
+
#
|
60
|
+
# @param ext_key [String] The identifier for the vendor-specific parameters
|
61
|
+
# @param ext_params [Hash] The parameters, this is allowed to be two-levels deep
|
62
|
+
def set_ext_params(ext_key, ext_params)
|
63
|
+
raise ArgumentError unless ext_params.is_a?(Hash)
|
64
|
+
@extensions[ext_key] = ext_params
|
65
|
+
end
|
66
|
+
|
67
|
+
def get_ext_params(ext_key)
|
68
|
+
@extensions[ext_key]
|
69
|
+
end
|
70
|
+
|
71
|
+
def set_ext_param(ext_key, param_key, val)
|
72
|
+
@extensions[ext_key] ||= {}
|
73
|
+
@extensions[ext_key][param_key] = val
|
74
|
+
end
|
75
|
+
|
76
|
+
def get_ext_param(ext_key, param_key)
|
77
|
+
@extensions[ext_key] && @extensions[ext_key][param_key]
|
78
|
+
end
|
79
|
+
|
80
|
+
# Namespaces used for parsing configuration XML
|
81
|
+
LTI_NAMESPACES = {
|
82
|
+
"xmlns" => 'http://www.imsglobal.org/xsd/imslticc_v1p0',
|
83
|
+
"blti" => 'http://www.imsglobal.org/xsd/imsbasiclti_v1p0',
|
84
|
+
"lticm" => 'http://www.imsglobal.org/xsd/imslticm_v1p0',
|
85
|
+
"lticp" => 'http://www.imsglobal.org/xsd/imslticp_v1p0',
|
86
|
+
}
|
87
|
+
|
88
|
+
# Parse tool configuration data out of the Common Cartridge LTI link XML
|
89
|
+
def process_xml(xml)
|
90
|
+
doc = REXML::Document.new xml
|
91
|
+
if root = REXML::XPath.first(doc, 'xmlns:cartridge_basiclti_link')
|
92
|
+
@title = get_node_text(root, 'blti:title')
|
93
|
+
@description = get_node_text(root, 'blti:description')
|
94
|
+
@launch_url = get_node_text(root, 'blti:launch_url')
|
95
|
+
@secure_launch_url = get_node_text(root, 'blti:secure_launch_url')
|
96
|
+
@icon = get_node_text(root, 'blti:icon')
|
97
|
+
@secure_icon = get_node_text(root, 'blti:secure_icon')
|
98
|
+
@cartridge_bundle = get_node_att(root, 'xmlns:cartridge_bundle', 'identifierref')
|
99
|
+
@cartridge_icon = get_node_att(root, 'xmlns:cartridge_icon', 'identifierref')
|
100
|
+
|
101
|
+
if vendor = REXML::XPath.first(root, 'blti:vendor')
|
102
|
+
@vendor_code = get_node_text(vendor, 'lticp:code')
|
103
|
+
@vendor_description = get_node_text(vendor, 'lticp:description')
|
104
|
+
@vendor_name = get_node_text(vendor, 'lticp:name')
|
105
|
+
@vendor_url = get_node_text(vendor, 'lticp:url')
|
106
|
+
@vendor_contact_email = get_node_text(vendor, '//lticp:contact/lticp:email')
|
107
|
+
@vendor_contact_name = get_node_text(vendor, '//lticp:contact/lticp:name')
|
108
|
+
end
|
109
|
+
|
110
|
+
if custom = REXML::XPath.first(root, 'blti:custom', LTI_NAMESPACES)
|
111
|
+
set_properties(@custom_params, custom)
|
112
|
+
end
|
113
|
+
|
114
|
+
REXML::XPath.each(root, 'blti:extensions', LTI_NAMESPACES) do |vendor_ext_node|
|
115
|
+
platform = vendor_ext_node.attributes['platform']
|
116
|
+
properties = {}
|
117
|
+
set_properties(properties, vendor_ext_node)
|
118
|
+
REXML::XPath.each(vendor_ext_node, 'lticm:options', LTI_NAMESPACES) do |options_node|
|
119
|
+
opt_name = options_node.attributes['name']
|
120
|
+
options = {}
|
121
|
+
set_properties(options, options_node)
|
122
|
+
properties[opt_name] = options
|
123
|
+
end
|
124
|
+
|
125
|
+
self.set_ext_params(platform, properties)
|
126
|
+
end
|
127
|
+
|
128
|
+
end
|
129
|
+
end
|
130
|
+
|
131
|
+
# Generate XML from the current settings
|
132
|
+
def to_xml(opts = {})
|
133
|
+
raise IMS::LTI::InvalidLTIConfigError, "A launch url is required for an LTI configuration." unless self.launch_url || self.secure_launch_url
|
134
|
+
|
135
|
+
builder = Builder::XmlMarkup.new(:indent => opts[:indent] || 0)
|
136
|
+
builder.instruct!
|
137
|
+
builder.cartridge_basiclti_link("xmlns" => "http://www.imsglobal.org/xsd/imslticc_v1p0",
|
138
|
+
"xmlns:blti" => 'http://www.imsglobal.org/xsd/imsbasiclti_v1p0',
|
139
|
+
"xmlns:lticm" => 'http://www.imsglobal.org/xsd/imslticm_v1p0',
|
140
|
+
"xmlns:lticp" => 'http://www.imsglobal.org/xsd/imslticp_v1p0',
|
141
|
+
"xmlns:xsi" => "http://www.w3.org/2001/XMLSchema-instance",
|
142
|
+
"xsi:schemaLocation" => "http://www.imsglobal.org/xsd/imslticc_v1p0 http://www.imsglobal.org/xsd/lti/ltiv1p0/imslticc_v1p0.xsd http://www.imsglobal.org/xsd/imsbasiclti_v1p0 http://www.imsglobal.org/xsd/lti/ltiv1p0/imsbasiclti_v1p0p1.xsd http://www.imsglobal.org/xsd/imslticm_v1p0 http://www.imsglobal.org/xsd/lti/ltiv1p0/imslticm_v1p0.xsd http://www.imsglobal.org/xsd/imslticp_v1p0 http://www.imsglobal.org/xsd/lti/ltiv1p0/imslticp_v1p0.xsd"
|
143
|
+
) do |blti_node|
|
144
|
+
|
145
|
+
%w{title description launch_url secure_launch_url}.each do |key|
|
146
|
+
blti_node.blti key.to_sym, self.send(key) if self.send(key)
|
147
|
+
end
|
148
|
+
|
149
|
+
vendor_keys = %w{name code description url}
|
150
|
+
if vendor_keys.any?{|k|self.send("vendor_#{k}")} || vendor_contact_email
|
151
|
+
blti_node.blti :vendor do |v_node|
|
152
|
+
vendor_keys.each do |key|
|
153
|
+
v_node.lticp key.to_sym, self.send("vendor_#{key}") if self.send("vendor_#{key}")
|
154
|
+
end
|
155
|
+
if vendor_contact_email
|
156
|
+
v_node.lticp :contact do |c_node|
|
157
|
+
c_node.lticp :name, vendor_contact_name
|
158
|
+
c_node.lticp :email, vendor_contact_email
|
159
|
+
end
|
160
|
+
end
|
161
|
+
end
|
162
|
+
end
|
163
|
+
|
164
|
+
if !@custom_params.empty?
|
165
|
+
blti_node.tag!("blti:custom") do |custom_node|
|
166
|
+
@custom_params.each_pair do |key, val|
|
167
|
+
custom_node.lticm :property, val, 'name' => key
|
168
|
+
end
|
169
|
+
end
|
170
|
+
end
|
171
|
+
|
172
|
+
if !@extensions.empty?
|
173
|
+
@extensions.each_pair do |ext_platform, ext_params|
|
174
|
+
blti_node.blti(:extensions, :platform => ext_platform) do |ext_node|
|
175
|
+
ext_params.each_pair do |key, val|
|
176
|
+
if val.is_a?(Hash)
|
177
|
+
ext_node.lticm(:options, :name => key) do |type_node|
|
178
|
+
val.each_pair do |p_key, p_val|
|
179
|
+
type_node.lticm :property, p_val, 'name' => p_key
|
180
|
+
end
|
181
|
+
end
|
182
|
+
else
|
183
|
+
ext_node.lticm :property, val, 'name' => key
|
184
|
+
end
|
185
|
+
end
|
186
|
+
end
|
187
|
+
end
|
188
|
+
end
|
189
|
+
|
190
|
+
blti_node.cartridge_bundle(:identifierref => @cartridge_bundle) if @cartridge_bundle
|
191
|
+
blti_node.cartridge_icon(:identifierref => @cartridge_icon) if @cartridge_icon
|
192
|
+
|
193
|
+
end
|
194
|
+
end
|
195
|
+
|
196
|
+
private
|
197
|
+
|
198
|
+
def get_node_text(node, path)
|
199
|
+
if val = REXML::XPath.first(node, path, LTI_NAMESPACES)
|
200
|
+
val.text
|
201
|
+
else
|
202
|
+
nil
|
203
|
+
end
|
204
|
+
end
|
205
|
+
|
206
|
+
def get_node_att(node, path, att)
|
207
|
+
if val = REXML::XPath.first(node, path, LTI_NAMESPACES)
|
208
|
+
val.attributes[att]
|
209
|
+
else
|
210
|
+
nil
|
211
|
+
end
|
212
|
+
end
|
213
|
+
|
214
|
+
def set_properties(hash, node)
|
215
|
+
REXML::XPath.each(node, 'lticm:property', LTI_NAMESPACES) do |prop|
|
216
|
+
hash[prop.attributes['name']] = prop.text
|
217
|
+
end
|
218
|
+
end
|
219
|
+
|
220
|
+
end
|
221
|
+
end
|
@@ -0,0 +1,95 @@
|
|
1
|
+
module IMS::LTI
|
2
|
+
# Class for implementing an LTI Tool Consumer
|
3
|
+
class ToolConsumer
|
4
|
+
include IMS::LTI::LaunchParams
|
5
|
+
|
6
|
+
attr_accessor :consumer_key, :consumer_secret, :launch_url, :timestamp, :nonce
|
7
|
+
|
8
|
+
# Create a new ToolConsumer
|
9
|
+
#
|
10
|
+
# @param consumer_key [String] The OAuth consumer key
|
11
|
+
# @param consumer_secret [String] The OAuth consumer secret
|
12
|
+
# @param params [Hash] Set the launch parameters as described in LaunchParams
|
13
|
+
def initialize(consumer_key, consumer_secret, params={})
|
14
|
+
@consumer_key = consumer_key
|
15
|
+
@consumer_secret = consumer_secret
|
16
|
+
@custom_params = {}
|
17
|
+
@ext_params = {}
|
18
|
+
@non_spec_params = {}
|
19
|
+
@launch_url = params['launch_url']
|
20
|
+
process_params(params)
|
21
|
+
end
|
22
|
+
|
23
|
+
# Set launch data from a ToolConfig
|
24
|
+
#
|
25
|
+
# @param config [ToolConfig]
|
26
|
+
def set_config(config)
|
27
|
+
@launch_url ||= config.secure_launch_url
|
28
|
+
@launch_url ||= config.launch_url
|
29
|
+
# any parameters already set will take priority
|
30
|
+
@custom_params = config.custom_params.merge(@custom_params)
|
31
|
+
end
|
32
|
+
|
33
|
+
# Check whether the OAuth-signed request is valid
|
34
|
+
#
|
35
|
+
# @return [Bool] Whether the request was valid
|
36
|
+
def valid_request?(request, handle_error=true)
|
37
|
+
IMS::LTI.valid_request?(@consumer_secret, request, handle_error)
|
38
|
+
end
|
39
|
+
|
40
|
+
# Check if the required parameters for a tool launch are set
|
41
|
+
def has_required_params?
|
42
|
+
@consumer_key && @consumer_secret && @resource_link_id && @launch_url
|
43
|
+
end
|
44
|
+
|
45
|
+
# Generate the launch data including the necessary OAuth information
|
46
|
+
#
|
47
|
+
#
|
48
|
+
def generate_launch_data
|
49
|
+
raise IMS::LTI::InvalidLTIConfigError, "Not all required params set for tool launch" unless has_required_params?
|
50
|
+
|
51
|
+
params = self.to_params
|
52
|
+
params['lti_version'] ||= 'LTI-1.0'
|
53
|
+
params['lti_message_type'] ||= 'basic-lti-launch-request'
|
54
|
+
uri = URI.parse(@launch_url)
|
55
|
+
|
56
|
+
if uri.port == uri.default_port
|
57
|
+
host = uri.host
|
58
|
+
else
|
59
|
+
host = "#{uri.host}:#{uri.port}"
|
60
|
+
end
|
61
|
+
|
62
|
+
consumer = OAuth::Consumer.new(@consumer_key, @consumer_secret, {
|
63
|
+
:site => "#{uri.scheme}://#{host}",
|
64
|
+
:signature_method => "HMAC-SHA1"
|
65
|
+
})
|
66
|
+
|
67
|
+
path = uri.path
|
68
|
+
path = '/' if path.empty?
|
69
|
+
if uri.query && uri.query != ''
|
70
|
+
CGI.parse(uri.query).each do |query_key, query_values|
|
71
|
+
unless params[query_key]
|
72
|
+
params[query_key] = query_values.first
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
76
|
+
options = {
|
77
|
+
:scheme => 'body',
|
78
|
+
:timestamp => @timestamp,
|
79
|
+
:nonce => @nonce
|
80
|
+
}
|
81
|
+
request = consumer.create_signed_request(:post, path, nil, options, params)
|
82
|
+
|
83
|
+
# the request is made by a html form in the user's browser, so we
|
84
|
+
# want to revert the escapage and return the hash of post parameters ready
|
85
|
+
# for embedding in a html view
|
86
|
+
hash = {}
|
87
|
+
request.body.split(/&/).each do |param|
|
88
|
+
key, val = param.split(/=/).map { |v| CGI.unescape(v) }
|
89
|
+
hash[key] = val
|
90
|
+
end
|
91
|
+
hash
|
92
|
+
end
|
93
|
+
|
94
|
+
end
|
95
|
+
end
|
@@ -0,0 +1,177 @@
|
|
1
|
+
module IMS::LTI
|
2
|
+
|
3
|
+
# Class for implementing an LTI Tool Provider
|
4
|
+
#
|
5
|
+
# # Initialize TP object with OAuth creds and post parameters
|
6
|
+
# provider = IMS::LTI::ToolProvider.new(consumer_key, consumer_secret, params)
|
7
|
+
#
|
8
|
+
# # Verify OAuth signature by passing the request object
|
9
|
+
# if provider.valid_request?(request)
|
10
|
+
# # success
|
11
|
+
# else
|
12
|
+
# # handle invalid OAuth
|
13
|
+
# end
|
14
|
+
#
|
15
|
+
# if provider.outcome_service?
|
16
|
+
# # ready for grade write-back
|
17
|
+
# else
|
18
|
+
# # normal tool launch without grade write-back
|
19
|
+
# end
|
20
|
+
#
|
21
|
+
# If the tool was launch as an outcome service you can POST a score to the TC.
|
22
|
+
# The POST calls all return an OutcomeResponse object which can be used to
|
23
|
+
# handle the response appropriately.
|
24
|
+
#
|
25
|
+
# # post the score to the TC, score should be a float >= 0.0 and <= 1.0
|
26
|
+
# # this returns an OutcomeResponse object
|
27
|
+
# response = provider.post_replace_result!(score)
|
28
|
+
# if response.success?
|
29
|
+
# # grade write worked
|
30
|
+
# elsif response.processing?
|
31
|
+
# elsif response.unsupported?
|
32
|
+
# else
|
33
|
+
# # failed
|
34
|
+
# end
|
35
|
+
|
36
|
+
class ToolProvider
|
37
|
+
include IMS::LTI::LaunchParams
|
38
|
+
|
39
|
+
# OAuth credentials
|
40
|
+
attr_accessor :consumer_key, :consumer_secret
|
41
|
+
# List of outcome requests made through this instance
|
42
|
+
attr_accessor :outcome_requests
|
43
|
+
# Message to be sent back to the ToolConsumer when the user returns
|
44
|
+
attr_accessor :lti_errormsg, :lti_errorlog, :lti_msg, :lti_log
|
45
|
+
|
46
|
+
# Create a new ToolProvider
|
47
|
+
#
|
48
|
+
# @param consumer_key [String] The OAuth consumer key
|
49
|
+
# @param consumer_secret [String] The OAuth consumer secret
|
50
|
+
# @param params [Hash] Set the launch parameters as described in LaunchParams
|
51
|
+
def initialize(consumer_key, consumer_secret, params={})
|
52
|
+
@consumer_key = consumer_key
|
53
|
+
@consumer_secret = consumer_secret
|
54
|
+
@custom_params = {}
|
55
|
+
@ext_params = {}
|
56
|
+
@non_spec_params = {}
|
57
|
+
@outcome_requests = []
|
58
|
+
process_params(params)
|
59
|
+
end
|
60
|
+
|
61
|
+
# Check whether the OAuth-signed request is valid
|
62
|
+
#
|
63
|
+
# @return [Bool] Whether the request was valid
|
64
|
+
def valid_request?(request, handle_error=true)
|
65
|
+
IMS::LTI.valid_request?(@consumer_secret, request, handle_error)
|
66
|
+
end
|
67
|
+
|
68
|
+
# Check whether the OAuth-signed request is valid and throw error if not
|
69
|
+
#
|
70
|
+
# @return [Bool] Whether the request was valid
|
71
|
+
def valid_request!(request)
|
72
|
+
valid_request?(request, false)
|
73
|
+
end
|
74
|
+
|
75
|
+
# Check whether the Launch Parameters have a role
|
76
|
+
def has_role?(role)
|
77
|
+
@roles && @roles.member?(role.downcase)
|
78
|
+
end
|
79
|
+
|
80
|
+
# Convenience method for checking if the user has 'learner' or 'student' role
|
81
|
+
def student?
|
82
|
+
has_role?('learner') || has_role?('student')
|
83
|
+
end
|
84
|
+
|
85
|
+
# Convenience method for checking if the user has 'instructor' or 'faculty' or 'staff' role
|
86
|
+
def instructor?
|
87
|
+
has_role?('instructor') || has_role?('faculty') || has_role?('staff')
|
88
|
+
end
|
89
|
+
|
90
|
+
# Check if the request was an LTI Launch Request
|
91
|
+
def launch_request?
|
92
|
+
lti_message_type == 'basic-lti-launch-request'
|
93
|
+
end
|
94
|
+
|
95
|
+
# Check if the Tool Launch expects an Outcome Result
|
96
|
+
def outcome_service?
|
97
|
+
!!(lis_outcome_service_url && lis_result_sourcedid)
|
98
|
+
end
|
99
|
+
|
100
|
+
# Return the full, given, or family name if set
|
101
|
+
def username(default=nil)
|
102
|
+
lis_person_name_given || lis_person_name_family || lis_person_name_full || default
|
103
|
+
end
|
104
|
+
|
105
|
+
# POSTs the given score to the Tool Consumer with a replaceResult
|
106
|
+
#
|
107
|
+
# Creates a new OutcomeRequest object and stores it in @outcome_requests
|
108
|
+
#
|
109
|
+
# @return [OutcomeResponse] the response from the Tool Consumer
|
110
|
+
def post_replace_result!(score)
|
111
|
+
new_request.post_replace_result!(score)
|
112
|
+
end
|
113
|
+
|
114
|
+
# POSTs a delete request to the Tool Consumer
|
115
|
+
#
|
116
|
+
# Creates a new OutcomeRequest object and stores it in @outcome_requests
|
117
|
+
#
|
118
|
+
# @return [OutcomeResponse] the response from the Tool Consumer
|
119
|
+
def post_delete_result!
|
120
|
+
new_request.post_delete_result!
|
121
|
+
end
|
122
|
+
|
123
|
+
# POSTs the given score to the Tool Consumer with a replaceResult, the
|
124
|
+
# returned OutcomeResponse will have the score
|
125
|
+
#
|
126
|
+
# Creates a new OutcomeRequest object and stores it in @outcome_requests
|
127
|
+
#
|
128
|
+
# @return [OutcomeResponse] the response from the Tool Consumer
|
129
|
+
def post_read_result!
|
130
|
+
new_request.post_read_result!
|
131
|
+
end
|
132
|
+
|
133
|
+
# Returns the most recent OutcomeRequest
|
134
|
+
def last_outcome_request
|
135
|
+
@outcome_requests.last
|
136
|
+
end
|
137
|
+
|
138
|
+
# Convenience method for whether the last OutcomeRequest was successful
|
139
|
+
def last_outcome_success?
|
140
|
+
last_outcome_request && last_outcome_request.outcome_post_successful?
|
141
|
+
end
|
142
|
+
|
143
|
+
# If the Tool Consumer sent a URL for the user to return to this will add
|
144
|
+
# any set messages to the URL.
|
145
|
+
#
|
146
|
+
# Example:
|
147
|
+
#
|
148
|
+
# tc = IMS::LTI::tc.new
|
149
|
+
# tc.launch_presentation_return_url = "http://example.com/return"
|
150
|
+
# tc.lti_msg = "hi there"
|
151
|
+
# tc.lti_errorlog = "error happens"
|
152
|
+
#
|
153
|
+
# tc.build_return_url # => "http://example.com/return?lti_msg=hi%20there<i_errorlog=error%20happens"
|
154
|
+
def build_return_url
|
155
|
+
return nil unless launch_presentation_return_url
|
156
|
+
messages = []
|
157
|
+
%w{lti_errormsg lti_errorlog lti_msg lti_log}.each do |m|
|
158
|
+
if message = self.send(m)
|
159
|
+
messages << "#{m}=#{URI.escape(message)}"
|
160
|
+
end
|
161
|
+
end
|
162
|
+
q_string = messages.any? ? ("?" + messages.join("&")) : ''
|
163
|
+
launch_presentation_return_url + q_string
|
164
|
+
end
|
165
|
+
|
166
|
+
private
|
167
|
+
|
168
|
+
def new_request
|
169
|
+
@outcome_requests << OutcomeRequest.new(:consumer_key => @consumer_key,
|
170
|
+
:consumer_secret => @consumer_secret,
|
171
|
+
:lis_outcome_service_url => lis_outcome_service_url,
|
172
|
+
:lis_result_sourcedid =>lis_result_sourcedid)
|
173
|
+
@outcome_requests.last
|
174
|
+
end
|
175
|
+
|
176
|
+
end
|
177
|
+
end
|
metadata
ADDED
@@ -0,0 +1,74 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: ims-lti
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
hash: 15
|
5
|
+
prerelease:
|
6
|
+
segments:
|
7
|
+
- 1
|
8
|
+
- 0
|
9
|
+
version: "1.0"
|
10
|
+
platform: ruby
|
11
|
+
authors:
|
12
|
+
- Instructure
|
13
|
+
autorequire:
|
14
|
+
bindir: bin
|
15
|
+
cert_chain: []
|
16
|
+
|
17
|
+
date: 2012-03-10 00:00:00 Z
|
18
|
+
dependencies: []
|
19
|
+
|
20
|
+
description:
|
21
|
+
email:
|
22
|
+
executables: []
|
23
|
+
|
24
|
+
extensions: []
|
25
|
+
|
26
|
+
extra_rdoc_files:
|
27
|
+
- LICENSE
|
28
|
+
files:
|
29
|
+
- LICENSE
|
30
|
+
- README.md
|
31
|
+
- lib/ims.rb
|
32
|
+
- lib/ims/lti.rb
|
33
|
+
- lib/ims/lti/launch_params.rb
|
34
|
+
- lib/ims/lti/outcome_request.rb
|
35
|
+
- lib/ims/lti/outcome_response.rb
|
36
|
+
- lib/ims/lti/tool_config.rb
|
37
|
+
- lib/ims/lti/tool_consumer.rb
|
38
|
+
- lib/ims/lti/tool_provider.rb
|
39
|
+
- ims-lti.gemspec
|
40
|
+
homepage: http://github.com/instructure/ims-lti
|
41
|
+
licenses: []
|
42
|
+
|
43
|
+
post_install_message:
|
44
|
+
rdoc_options: []
|
45
|
+
|
46
|
+
require_paths:
|
47
|
+
- lib
|
48
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
49
|
+
none: false
|
50
|
+
requirements:
|
51
|
+
- - ">="
|
52
|
+
- !ruby/object:Gem::Version
|
53
|
+
hash: 3
|
54
|
+
segments:
|
55
|
+
- 0
|
56
|
+
version: "0"
|
57
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
58
|
+
none: false
|
59
|
+
requirements:
|
60
|
+
- - ">="
|
61
|
+
- !ruby/object:Gem::Version
|
62
|
+
hash: 3
|
63
|
+
segments:
|
64
|
+
- 0
|
65
|
+
version: "0"
|
66
|
+
requirements: []
|
67
|
+
|
68
|
+
rubyforge_project:
|
69
|
+
rubygems_version: 1.8.15
|
70
|
+
signing_key:
|
71
|
+
specification_version: 3
|
72
|
+
summary: Ruby library for creating IMS LTI tool providers and consumers
|
73
|
+
test_files: []
|
74
|
+
|