scorm_cloud 0.0.3
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +8 -0
- data/Gemfile +7 -0
- data/Rakefile +25 -0
- data/bin/scorm_cloud +6 -0
- data/features/api_bugs.feature +12 -0
- data/features/course_service.feature +19 -0
- data/features/debug_service.feature +12 -0
- data/features/registration_service.feature +21 -0
- data/features/step_definitions/scorm_cloud_steps.rb +184 -0
- data/features/support/env.rb +71 -0
- data/features/upload_service.feature +9 -0
- data/lib/scorm_cloud.rb +36 -0
- data/lib/scorm_cloud/base.rb +110 -0
- data/lib/scorm_cloud/base_object.rb +23 -0
- data/lib/scorm_cloud/base_service.rb +27 -0
- data/lib/scorm_cloud/connection.rb +67 -0
- data/lib/scorm_cloud/course.rb +13 -0
- data/lib/scorm_cloud/course_service.rb +53 -0
- data/lib/scorm_cloud/debug_service.rb +25 -0
- data/lib/scorm_cloud/dispatch_service.rb +9 -0
- data/lib/scorm_cloud/export_service.rb +7 -0
- data/lib/scorm_cloud/railtie.rb +13 -0
- data/lib/scorm_cloud/registration.rb +19 -0
- data/lib/scorm_cloud/registration_service.rb +50 -0
- data/lib/scorm_cloud/reporting_service.rb +7 -0
- data/lib/scorm_cloud/scorm_rails_helpers.rb +15 -0
- data/lib/scorm_cloud/tagging_service.rb +9 -0
- data/lib/scorm_cloud/upload_service.rb +37 -0
- data/lib/scorm_cloud/version.rb +3 -0
- data/readme.textile +53 -0
- data/scorm_cloud.gemspec +24 -0
- data/spec/apikey_template.rb +3 -0
- data/spec/course_service_spec.rb +31 -0
- data/spec/debug_service_spec.rb +26 -0
- data/spec/dispatch_service_spec.rb +21 -0
- data/spec/export_service_spec.rb +13 -0
- data/spec/registration_service_spec.rb +28 -0
- data/spec/reporting_service_spec.rb +11 -0
- data/spec/small_scorm_package.zip +0 -0
- data/spec/spec_helper.rb +4 -0
- data/spec/tagging_service_spec.rb +20 -0
- data/spec/upload_service_spec.rb +18 -0
- metadata +139 -0
@@ -0,0 +1,23 @@
|
|
1
|
+
module ScormCloud
|
2
|
+
class BaseObject
|
3
|
+
|
4
|
+
def set_attributes(attributes)
|
5
|
+
attributes.each { |k,v| set_attr(k,v) }
|
6
|
+
end
|
7
|
+
|
8
|
+
def set_attr(k,v)
|
9
|
+
f = "#{to_underscore(k)}=".to_sym
|
10
|
+
if respond_to?(f)
|
11
|
+
send(f, v)
|
12
|
+
else
|
13
|
+
raise "Object #{self.class} does not respond to #{to_underscore(k)}"
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
def to_underscore(s)
|
18
|
+
s.gsub(/(.)([A-Z])/,'\1_\2').downcase
|
19
|
+
end
|
20
|
+
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
@@ -0,0 +1,27 @@
|
|
1
|
+
module ScormCloud
|
2
|
+
class BaseService
|
3
|
+
|
4
|
+
def initialize(connection)
|
5
|
+
@connection = connection
|
6
|
+
end
|
7
|
+
|
8
|
+
def connection
|
9
|
+
@connection
|
10
|
+
end
|
11
|
+
|
12
|
+
def self.not_implemented(*methods)
|
13
|
+
methods.each do |method|
|
14
|
+
define_method(method) { raise "Not Implemented: #{method.to_s}" }
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
# Convert xml attributes to hash { :name => value }
|
19
|
+
def xml_to_attributes(xml)
|
20
|
+
xml.elements["/rsp/attributes"].inject({}) { |h,e|
|
21
|
+
h[e.attributes["name"].to_sym] = e.attributes["value"]
|
22
|
+
h
|
23
|
+
}
|
24
|
+
end
|
25
|
+
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,67 @@
|
|
1
|
+
module ScormCloud
|
2
|
+
class Base
|
3
|
+
|
4
|
+
def initialize(appid, secret)
|
5
|
+
@appid = appid
|
6
|
+
@secret = secret
|
7
|
+
end
|
8
|
+
|
9
|
+
def call(method, params = {})
|
10
|
+
url = prepare_url(method, params)
|
11
|
+
execute_call_xml(url)
|
12
|
+
end
|
13
|
+
|
14
|
+
def call_raw(method, params = {})
|
15
|
+
url = prepare_url(method, params)
|
16
|
+
execute_call_plain(url)
|
17
|
+
end
|
18
|
+
|
19
|
+
# Get plain response body and parse the XML doc
|
20
|
+
def execute_call_xml(url)
|
21
|
+
doc = REXML::Document.new(execute_call_plain(url))
|
22
|
+
raise create_error(doc) unless doc.elements["rsp"].attributes["stat"] == "ok"
|
23
|
+
doc
|
24
|
+
end
|
25
|
+
|
26
|
+
# Execute the call - returns response body or redirect url
|
27
|
+
def execute_call_plain(url)
|
28
|
+
res = Net::HTTP.get_response(URI.parse(url))
|
29
|
+
case res
|
30
|
+
when Net::HTTPRedirection
|
31
|
+
# Return the new URL
|
32
|
+
res['location']
|
33
|
+
when Net::HTTPSuccess
|
34
|
+
res.body
|
35
|
+
else
|
36
|
+
raise "HTTP Error connecting to scorm cloud: #{res.inspect}"
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
# Get the URL for the call
|
41
|
+
def prepare_url(method, params = {})
|
42
|
+
timestamp = Time.now.utc.strftime('%Y%m%d%H%M%S')
|
43
|
+
params[:method] = method
|
44
|
+
params[:appid] = @appid
|
45
|
+
params[:ts] = timestamp
|
46
|
+
html_params = params.map { |k,v| "#{k.to_s}=#{v}" }.join("&")
|
47
|
+
|
48
|
+
raw = @secret + params.keys.
|
49
|
+
sort{ |a,b| a.to_s.downcase <=> b.to_s.downcase }.
|
50
|
+
map{ |k| "#{k.to_s}#{params[k]}" }.
|
51
|
+
join
|
52
|
+
|
53
|
+
sig = Digest::MD5.hexdigest(raw)
|
54
|
+
"http://cloud.scorm.com/api?#{html_params}&sig=#{sig}"
|
55
|
+
end
|
56
|
+
|
57
|
+
|
58
|
+
# Create an exception with code & message
|
59
|
+
def create_error(doc)
|
60
|
+
err = doc.elements["rsp"].elements["err"]
|
61
|
+
code = err.attributes["code"]
|
62
|
+
msg = err.attributes["msg"]
|
63
|
+
"Error In Scorm Cloud: Error=#{code} Message=#{msg}"
|
64
|
+
end
|
65
|
+
|
66
|
+
end
|
67
|
+
end
|
@@ -0,0 +1,53 @@
|
|
1
|
+
module ScormCloud
|
2
|
+
class CourseService < BaseService
|
3
|
+
|
4
|
+
not_implemented :import_cours_async, :get_async_import_result,
|
5
|
+
:properties, :get_assets, :update_assets,
|
6
|
+
:get_file_structure, :delete_files, :get_metadata
|
7
|
+
|
8
|
+
# TODO: Handle Warnings
|
9
|
+
def import_course(course_id, path)
|
10
|
+
xml = connection.call("rustici.course.importCourse", :courseid => course_id, :path => path)
|
11
|
+
if xml.elements['//rsp/importresult'].attributes["successful"] == "true"
|
12
|
+
title = xml.elements['//rsp/importresult/title'].text
|
13
|
+
{ :title => title, :warnings => [] }
|
14
|
+
else
|
15
|
+
nil
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
def exists(course_id)
|
20
|
+
connection.call_raw("rustici.course.exists", :courseid => course_id).include?("<result>true</result>")
|
21
|
+
end
|
22
|
+
|
23
|
+
def get_attributes(course_id)
|
24
|
+
xml = connection.call("rustici.course.getAttributes", :courseid => course_id)
|
25
|
+
xml_to_attributes(xml)
|
26
|
+
end
|
27
|
+
|
28
|
+
def delete_course(course_id)
|
29
|
+
connection.call("rustici.course.deleteCourse", :courseid => course_id)
|
30
|
+
true
|
31
|
+
end
|
32
|
+
|
33
|
+
def get_manifest(course_id)
|
34
|
+
connection.call_raw("rustici.course.getManifest", :courseid => course_id)
|
35
|
+
end
|
36
|
+
|
37
|
+
def get_course_list(options = {})
|
38
|
+
xml = connection.call("rustici.course.getCourseList", options)
|
39
|
+
xml.elements["/rsp/courselist"].map { |e| Course.from_xml(e) }
|
40
|
+
end
|
41
|
+
|
42
|
+
def preview(course_id, redirect_url)
|
43
|
+
connection.launch_url("rustici.course.preview", :courseid => course_id, :redirecturl => redirect_url)
|
44
|
+
end
|
45
|
+
|
46
|
+
def update_attributes(course_id, attributes)
|
47
|
+
xml = connection.call("rustici.course.updateAttributes", attributes.merge({:courseid => course_id}))
|
48
|
+
xml_to_attributes(xml)
|
49
|
+
end
|
50
|
+
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
@@ -0,0 +1,25 @@
|
|
1
|
+
module ScormCloud
|
2
|
+
class DebugService < BaseService
|
3
|
+
|
4
|
+
def ping()
|
5
|
+
url = "http://cloud.scorm.com/api?method=rustici.debug.ping"
|
6
|
+
data = connection.call_url(url)
|
7
|
+
raise "Bad Server Response" unless data.include?("pong")
|
8
|
+
"pong"
|
9
|
+
end
|
10
|
+
|
11
|
+
def auth_ping()
|
12
|
+
xml = connection.call("rustici.debug.authPing")
|
13
|
+
raise "Bad Server Response" unless xml.elements["/rsp/pong"]
|
14
|
+
"pong"
|
15
|
+
end
|
16
|
+
|
17
|
+
def get_time()
|
18
|
+
xml = connection.call("rustici.debug.getTime")
|
19
|
+
time = xml.elements["//currenttime"]
|
20
|
+
raise "Bad Server Response" unless time
|
21
|
+
time.text
|
22
|
+
end
|
23
|
+
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,9 @@
|
|
1
|
+
module ScormCloud
|
2
|
+
class DispatchService < BaseService
|
3
|
+
|
4
|
+
not_implemented :get_destination_list, :get_destination_info, :create_destination,
|
5
|
+
:update_destination, :delete_destination, :get_dispatch_list, :get_dispatch_info,
|
6
|
+
:create_dispatch, :update_dispatches, :download_dispatches, :delete_dispatches
|
7
|
+
|
8
|
+
end
|
9
|
+
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
require 'rails'
|
2
|
+
require 'scorm_cloud/scorm_rails_helpers'
|
3
|
+
|
4
|
+
class ScormCloud::Railtie < Rails::Railtie
|
5
|
+
|
6
|
+
config.scorm_cloud = ActiveSupport::OrderedOptions.new
|
7
|
+
|
8
|
+
initializer "scorm_cloud.scorm_rails_helpers" do
|
9
|
+
ActionController::Base.send :include, ScormCloud::ScormRailsHelpers
|
10
|
+
end
|
11
|
+
|
12
|
+
end
|
13
|
+
|
@@ -0,0 +1,19 @@
|
|
1
|
+
module ScormCloud
|
2
|
+
class Registration < ScormCloud::BaseObject
|
3
|
+
|
4
|
+
attr_accessor :id, :courseid, :app_id, :registration_id, :course_id,
|
5
|
+
:course_title, :learner_id, :learner_first_name, :learner_last_name,
|
6
|
+
:email, :create_date, :first_access_date, :last_access_date,
|
7
|
+
:completed_date, :instances
|
8
|
+
|
9
|
+
def self.from_xml(element)
|
10
|
+
r = Registration.new
|
11
|
+
r.set_attributes(element.attributes)
|
12
|
+
element.children.each do |element|
|
13
|
+
r.set_attr(element.name, element.text)
|
14
|
+
end
|
15
|
+
r
|
16
|
+
end
|
17
|
+
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,50 @@
|
|
1
|
+
module ScormCloud
|
2
|
+
class RegistrationService < BaseService
|
3
|
+
|
4
|
+
not_implemented :get_registration_list_results,
|
5
|
+
:get_launch_history, :get_launch_info, :reset_global_objectives,
|
6
|
+
:update_learner_info, :test_registration_post_url
|
7
|
+
|
8
|
+
def create_registration(course_id, reg_id, first_name, last_name, learner_id, options = {})
|
9
|
+
params = options.merge({
|
10
|
+
:courseid => course_id,
|
11
|
+
:regid => reg_id,
|
12
|
+
:fname => first_name,
|
13
|
+
:lname => last_name,
|
14
|
+
:learnerid => learner_id
|
15
|
+
})
|
16
|
+
xml = connection.call("rustici.registration.createRegistration", params)
|
17
|
+
!xml.elements["/rsp/success"].nil?
|
18
|
+
end
|
19
|
+
|
20
|
+
def delete_registration(reg_id)
|
21
|
+
xml = connection.call("rustici.registration.deleteRegistration", {:regid => reg_id })
|
22
|
+
!xml.elements["/rsp/success"].nil?
|
23
|
+
end
|
24
|
+
|
25
|
+
def get_registration_list(options = {})
|
26
|
+
xml = connection.call("rustici.registration.getRegistrationList", options)
|
27
|
+
xml.elements["/rsp/registrationlist"].map { |e| Registration.from_xml(e) }
|
28
|
+
end
|
29
|
+
|
30
|
+
def get_registration_result(reg_id, format="course")
|
31
|
+
raise "Illegal format argument: #{format}" unless ["course","activity","full"].include?(format)
|
32
|
+
connection.call_raw("rustici.registration.getRegistrationResult", { :regid => reg_id, :format => format })
|
33
|
+
end
|
34
|
+
|
35
|
+
def launch(reg_id, redirect_url, options = {})
|
36
|
+
params = options.merge({
|
37
|
+
:regid => reg_id,
|
38
|
+
:redirecturl => redirect_url
|
39
|
+
})
|
40
|
+
connection.launch_url("rustici.registration.launch", params)
|
41
|
+
end
|
42
|
+
|
43
|
+
def reset_registration(reg_id)
|
44
|
+
xml = connection.call("rustici.registration.resetRegistration", {:regid => reg_id })
|
45
|
+
!xml.elements["/rsp/success"].nil?
|
46
|
+
end
|
47
|
+
|
48
|
+
|
49
|
+
end
|
50
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
module ScormCloud
|
2
|
+
module ScormRailsHelpers
|
3
|
+
|
4
|
+
def scorm_cloud
|
5
|
+
unless @scorm_cloud
|
6
|
+
@scorm_cloud = ::ScormCloud::ScormCloud.new(
|
7
|
+
::Rails.configuration.scorm_cloud.appid,
|
8
|
+
::Rails.configuration.scorm_cloud.secretkey
|
9
|
+
)
|
10
|
+
end
|
11
|
+
@scorm_cloud
|
12
|
+
end
|
13
|
+
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,9 @@
|
|
1
|
+
module ScormCloud
|
2
|
+
class TaggingService < BaseService
|
3
|
+
not_implemented :get_course_tags, :set_course_tags, :add_course_tag,
|
4
|
+
:remove_course_tag, :get_learner_tags, :set_learner_tags, :add_learner_tag,
|
5
|
+
:remove_learner_tag, :get_registration_tags, :set_registration_tags,
|
6
|
+
:add_registration_tag, :remove_registration_tag
|
7
|
+
end
|
8
|
+
end
|
9
|
+
|
@@ -0,0 +1,37 @@
|
|
1
|
+
module ScormCloud
|
2
|
+
class UploadService < BaseService
|
3
|
+
|
4
|
+
not_implemented :get_upload_progress
|
5
|
+
|
6
|
+
def get_upload_token
|
7
|
+
xml = connection.call("rustici.upload.getUploadToken")
|
8
|
+
xml.elements["/rsp/token/id"].text
|
9
|
+
end
|
10
|
+
|
11
|
+
def upload_file(token, path)
|
12
|
+
xml = connection.post("rustici.upload.uploadFile", path, :token => token)
|
13
|
+
xml.elements["/rsp/location"].text
|
14
|
+
end
|
15
|
+
|
16
|
+
def list_files
|
17
|
+
xml = connection.call("rustici.upload.listFiles")
|
18
|
+
xml.elements["//rsp/dir"].map do |f|
|
19
|
+
{
|
20
|
+
:dir => f.parent.attributes["name"],
|
21
|
+
:file => f.attributes["name"],
|
22
|
+
:modified => f.attributes["modified"],
|
23
|
+
:size => f.attributes["modified"].to_i
|
24
|
+
}
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
def delete_files files
|
29
|
+
if !files.is_a?(String) && files.is_a?(Enumerable)
|
30
|
+
files = files.map { |f| "file=#{f}"}.join('&')
|
31
|
+
end
|
32
|
+
result = connection.call("rustici.upload.deleteFiles", { :file => files })
|
33
|
+
!result.to_s.include?("deleted='false'")
|
34
|
+
end
|
35
|
+
|
36
|
+
end
|
37
|
+
end
|
data/readme.textile
ADDED
@@ -0,0 +1,53 @@
|
|
1
|
+
h1. Rustici SCORM Cloud Ruby Client
|
2
|
+
|
3
|
+
This ruby gem is provides a ruby interface to the Rustici Scorm Cloud.
|
4
|
+
|
5
|
+
h2. Shell CLI Interface
|
6
|
+
|
7
|
+
$ gem install 'scorm_cloud'
|
8
|
+
$ scorm_cloud rustici.course.getCourseList --appid myappid --secret mysecret
|
9
|
+
|
10
|
+
h2. Standard Ruby Use
|
11
|
+
|
12
|
+
bc. require 'scorm_cloud'
|
13
|
+
sc = ScormCloud::ScormCloud.new("my_app_id","my_secret_key")
|
14
|
+
sc.course.get_course_list.each { |c| puts "#{c.id} #{c.title}"}
|
15
|
+
|
16
|
+
h2. Ruby on Rails Use
|
17
|
+
|
18
|
+
_Place the following in: Gemfile_
|
19
|
+
|
20
|
+
bc. require 'scorm_cloud', :git => 'git@github.com:aeseducation/scorm-cloud.git'
|
21
|
+
|
22
|
+
_Place the following in: config/initializers/scorm_cloud.rb_
|
23
|
+
|
24
|
+
bc. # Change MyApplication to the name of your application
|
25
|
+
MyApplication::Application.configure do |config|
|
26
|
+
config.scorm_cloud.appid = "my_app_id"
|
27
|
+
config.scorm_cloud.secretkey = "my_secret_key"
|
28
|
+
end
|
29
|
+
|
30
|
+
_Place the following in: /app/controllers.course_controller.rb_
|
31
|
+
|
32
|
+
bc. # app/controllers/course_controller.rb
|
33
|
+
class CourseController < ApplicationController
|
34
|
+
def index
|
35
|
+
@courses = scorm_cloud.course.get_course_list
|
36
|
+
end
|
37
|
+
def launch
|
38
|
+
return_url = course_index_url
|
39
|
+
reg = scorm_cloud.registrations.create_registration(...)
|
40
|
+
redirect_to scorm_cloud.registrations.launch(...)
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
_Place the following in: /app/views/course/index.html.erb_
|
45
|
+
|
46
|
+
bq. # app/views/course/index.html.erb
|
47
|
+
<ul>
|
48
|
+
<%= @courses.each |course| %>
|
49
|
+
<li>
|
50
|
+
<%= link_to course_launch_path(course.title,course.id) %>
|
51
|
+
</li>
|
52
|
+
<% end %>
|
53
|
+
</ul>
|