xapi: create page view and asset user access

test plan:
  * play Minecraft
  * look at your user's most recent page view and asset access
  * verify that entries were added for the LTI tool

Change-Id: I422a188ed80cbedbaa8b4ecafcb3262c6f2d1e14
Reviewed-on: https://gerrit.instructure.com/42783
Tested-by: Jenkins <jenkins@instructure.com>
Reviewed-by: Brad Humphrey <brad@instructure.com>
Reviewed-by: Bracken Mosbacker <bracken@instructure.com>
Product-Review: Bracken Mosbacker <bracken@instructure.com>
QA-Review: Braden Anderson <braden@instructure.com>
This commit is contained in:
Braden Anderson 2014-10-15 01:39:45 -06:00 committed by Braden Anderson
parent e16ff48b23
commit 312e013300
3 changed files with 155 additions and 3 deletions

View File

@ -80,6 +80,7 @@ gem 'ritex', '1.0.1'
gem 'rotp', '1.6.1'
gem 'rqrcode', '0.4.2'
gem 'net-ldap', '0.3.1', :require => 'net/ldap'
gem 'ruby-duration', '3.2.0'
gem 'ruby-saml-mod', '0.1.30'
gem 'rubycas-client', '2.3.9'
gem 'rubyzip', '1.1.1', :require => 'zip'

View File

@ -53,6 +53,33 @@ class LtiApiController < ApplicationController
render :text => e.to_s, :status => 401
end
# examples: https://github.com/adlnet/xAPI-Spec/blob/master/xAPI.md#AppendixA
#
# {
# id: "12345678-1234-5678-1234-567812345678",
# actor: {
# account: {
# homePage: "http://www.instructure.com/",
# name: source_id
# }
# },
# verb: {
# id: "http://adlnet.gov/expapi/verbs/interacted",
# display: {
# "en-US" => "interacted"
# }
# },
# object: {
# id: "http://example.com/"
# },
# result: {
# duration: "PT10M0S"
# }
# }
#
# * actor.account.name must be a source_id
# * result.duration must be an ISO 8601 duration
# * object.id will be logged as url
def xapi
verify_oauth
@ -64,12 +91,26 @@ class LtiApiController < ApplicationController
course, assignment, user = BasicLTI::BasicOutcomes.decode_source_id(@tool, source_id)
duration = params[:result]['duration']
seconds = duration.match(/PT(\d+)S/)[1].to_i
seconds = Duration.new(duration).to_i
# TODO: This should create an asset user access and page view as well.
course.enrollments.where(:user_id => user).update_all(['total_activity_time = total_activity_time + ?', seconds])
course.enrollments.where(:user_id => user).update_all(['total_activity_time = COALESCE(total_activity_time, 0) + ?', seconds])
access = AssetUserAccess.where(user_id: user, asset_code: @tool.asset_string).first_or_initialize
access.log(course, group_code: "external_tools", category: "external_tools")
if PageView.page_views_enabled?
PageView.new(user: user, context: course, account: course.account).tap { |p|
p.request_id = CanvasUUID.generate
p.url = params[:object][:id]
# TODO: override 10m cap?
p.interaction_seconds = seconds
}.save
end
return render :text => '', :status => 200
rescue BasicLTI::BasicOutcomes::Unauthorized => e
render :text => e.to_s, :status => 401
end
def logout_service

110
spec/apis/lti/xapi_spec.rb Normal file
View File

@ -0,0 +1,110 @@
require File.expand_path(File.dirname(__FILE__) + '/../api_spec_helper')
# https://github.com/adlnet/xAPI-Spec/blob/master/xAPI.md
describe LtiApiController, type: :request do
before :once do
course_with_student(:active_all => true)
@student = @user
@course.enroll_teacher(user_with_pseudonym(:active_all => true))
@tool = @course.context_external_tools.create!(:shared_secret => 'test_secret', :consumer_key => 'test_key', :name => 'my xapi test tool', :domain => 'example.com')
assignment_model(:course => @course, :name => 'tool assignment', :submission_types => 'external_tool', :points_possible => 20, :grading_type => 'points')
tag = @assignment.build_external_tool_tag(:url => "http://example.com/one")
tag.content_type = 'ContextExternalTool'
tag.save!
end
def make_call(opts = {})
opts['path'] ||= "/api/lti/v1/tools/#{@tool.id}/xapi"
opts['key'] ||= @tool.consumer_key
opts['secret'] ||= @tool.shared_secret
opts['content-type'] ||= 'application/json'
consumer = OAuth::Consumer.new(opts['key'], opts['secret'], :site => "https://www.example.com", :signature_method => "HMAC-SHA1")
req = consumer.create_signed_request(:post, opts['path'], nil, :scheme => 'header', :timestamp => opts['timestamp'], :nonce => opts['nonce'])
req.body = JSON.generate(opts['body']) if opts['body']
post "https://www.example.com#{req.path}",
req.body,
{ "CONTENT_TYPE" => opts['content-type'], "HTTP_AUTHORIZATION" => req['Authorization'] }
end
def source_id
@tool.shard.activate do
payload = [@tool.id, @course.id, @assignment.id, @student.id].join('-')
"#{payload}-#{Canvas::Security.hmac_sha1(payload, @tool.shard.settings[:encryption_key])}"
end
end
it "should require a content-type of application/json" do
make_call('content-type' => 'application/xml')
assert_status(415)
end
it "should require the correct shared secret" do
make_call('secret' => 'bad secret is bad')
assert_status(401)
end
def xapi_body
# https://github.com/adlnet/xAPI-Spec/blob/master/xAPI.md#AppendixA
{
id: "12345678-1234-5678-1234-567812345678",
actor: {
account: {
homePage: "http://www.instructure.com/",
name: source_id
}
},
verb: {
id: "http://adlnet.gov/expapi/verbs/interacted",
display: {
"en-US" => "interacted"
}
},
object: {
id: "http://example.com/"
},
result: {
duration: "PT10M0S"
}
}
end
it "should increment activity time" do
e = Enrollment.where(user_id: @student, course_id: @course).first
previous_time = e.total_activity_time
make_call('body' => xapi_body)
expect(response).to be_success
expect(e.reload.total_activity_time).to eq previous_time + 600
end
it "should create an asset user access" do
accesses = AssetUserAccess.where(user_id: @student)
previous_count = accesses.count
make_call('body' => xapi_body)
expect(accesses.reload.count).to eq previous_count + 1
end
describe "page view creation" do
before { Setting.set 'enable_page_views', 'db' }
it "should include url and interaction_seconds" do
page_views = PageView.where(user_id: @student, context_id: @course, context_type: 'Course')
previous_count = page_views.count
body = xapi_body
make_call('body' => body)
expect(page_views.reload.count).to eq previous_count + 1
page_view = page_views.last
expect(page_view.url).to eq body[:object][:id]
expect(page_view.interaction_seconds).to eq 600
end
after { Setting.set 'enable_page_views', 'false' }
end
end