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:
parent
e16ff48b23
commit
312e013300
|
@ -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'
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
Loading…
Reference in New Issue