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 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.
@@ -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)
@@ -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
@@ -0,0 +1 @@
1
+ require 'ims/lti'
@@ -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&lti_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
+