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
data/.gitignore
ADDED
data/Gemfile
ADDED
data/Rakefile
ADDED
@@ -0,0 +1,25 @@
|
|
1
|
+
require 'bundler'
|
2
|
+
require 'rspec/core/rake_task'
|
3
|
+
require 'cucumber/rake/task'
|
4
|
+
|
5
|
+
Bundler::GemHelper.install_tasks
|
6
|
+
RSpec::Core::RakeTask.new('spec')
|
7
|
+
|
8
|
+
Cucumber::Rake::Task.new do |t|
|
9
|
+
t.cucumber_opts = "--tags ~@apibug"
|
10
|
+
end
|
11
|
+
|
12
|
+
namespace :cucumber do
|
13
|
+
Cucumber::Rake::Task.new('apibugs') do |t|
|
14
|
+
t.cucumber_opts = "--tags @apibug"
|
15
|
+
end
|
16
|
+
Cucumber::Rake::Task.new('wip') do |t|
|
17
|
+
t.cucumber_opts = "--tags @wip"
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
task :test do
|
22
|
+
[:spec, :cucumber].each { |t| Rake::Task[t].execute }
|
23
|
+
end
|
24
|
+
|
25
|
+
task :default => :test
|
data/bin/scorm_cloud
ADDED
@@ -0,0 +1,12 @@
|
|
1
|
+
@apibug
|
2
|
+
Feature: API Bugs and Questions
|
3
|
+
|
4
|
+
Scenario: Deleting a non-existant package should return an error
|
5
|
+
in this case it seems to return deleted=true even when the file
|
6
|
+
does not exist
|
7
|
+
|
8
|
+
When I delete a non-existant package
|
9
|
+
Then I should get an error
|
10
|
+
|
11
|
+
|
12
|
+
# Preview documentation doesn't list redirect URL as a parameter
|
@@ -0,0 +1,19 @@
|
|
1
|
+
Feature: Course Service Interface
|
2
|
+
|
3
|
+
Scenario: A user can import a previously uploaded course
|
4
|
+
When I import a course
|
5
|
+
Then there should be 1 course in the list
|
6
|
+
And the course should be in the course list
|
7
|
+
And the course should exist
|
8
|
+
And I can get a preview URL for the course
|
9
|
+
And I can get the manifest for the course
|
10
|
+
And I can get the attributes for the course
|
11
|
+
|
12
|
+
When I delete the course
|
13
|
+
Then there should be 0 courses in the list
|
14
|
+
Then the course should not be in the course list
|
15
|
+
|
16
|
+
Scenario: A user can update course attributes
|
17
|
+
When I import a course
|
18
|
+
And I update course attributes
|
19
|
+
Then the course attributes should be updated
|
@@ -0,0 +1,12 @@
|
|
1
|
+
Feature: Debug Service Interface
|
2
|
+
|
3
|
+
Scenario: Debug Services Function
|
4
|
+
|
5
|
+
When I ping the server
|
6
|
+
Then the response should be "pong"
|
7
|
+
|
8
|
+
When I authping the server
|
9
|
+
Then the response should be "pong"
|
10
|
+
|
11
|
+
When I get the current time
|
12
|
+
Then the resonse should be the current time
|
@@ -0,0 +1,21 @@
|
|
1
|
+
Feature: Registration Service
|
2
|
+
|
3
|
+
Background: Setup a course
|
4
|
+
Given a registered course
|
5
|
+
|
6
|
+
Scenario: Register a learner
|
7
|
+
|
8
|
+
When I register a learner
|
9
|
+
Then the learner should be in the registration list
|
10
|
+
And I can get the registration results
|
11
|
+
|
12
|
+
When I launch the course
|
13
|
+
Then I will get a valid url
|
14
|
+
|
15
|
+
When I reset the registration
|
16
|
+
Then the learner should be in the registration list
|
17
|
+
And I can get the registration results
|
18
|
+
|
19
|
+
When I delete the registration
|
20
|
+
Then the learner should not be in the registration list
|
21
|
+
|
@@ -0,0 +1,184 @@
|
|
1
|
+
##
|
2
|
+
## Generic
|
3
|
+
##
|
4
|
+
Then /^I will get a valid url$/ do
|
5
|
+
@last_url.should match(/http\:\/\/.+/)
|
6
|
+
end
|
7
|
+
|
8
|
+
|
9
|
+
##
|
10
|
+
## Debug Service
|
11
|
+
##
|
12
|
+
|
13
|
+
When /^I ping the server$/ do
|
14
|
+
@last_response = @c.debug.ping
|
15
|
+
end
|
16
|
+
|
17
|
+
When /^I authping the server$/ do
|
18
|
+
@last_response = @c.debug.auth_ping
|
19
|
+
end
|
20
|
+
|
21
|
+
When /^I get the current time$/ do
|
22
|
+
@last_response = @c.debug.get_time
|
23
|
+
end
|
24
|
+
|
25
|
+
Then /^the response should be "([^"]*)"$/ do |arg1| #"
|
26
|
+
@last_response.should eq(arg1)
|
27
|
+
end
|
28
|
+
|
29
|
+
Then /^the resonse should be the current time$/ do
|
30
|
+
@last_response.should match(/\d+/)
|
31
|
+
end
|
32
|
+
|
33
|
+
##
|
34
|
+
## Upload Service
|
35
|
+
##
|
36
|
+
|
37
|
+
When /^I upload a package$/ do
|
38
|
+
token = @c.upload.get_upload_token
|
39
|
+
token.should_not be_nil
|
40
|
+
path = File.join(File.dirname(__FILE__), '..', '..', 'spec', 'small_scorm_package.zip')
|
41
|
+
@last_upload_path = @c.upload.upload_file(token, path)
|
42
|
+
@last_uploaded_dir, @last_uploaded_file = @last_upload_path.split('/')
|
43
|
+
@last_uploaded_file.should include('.zip')
|
44
|
+
end
|
45
|
+
|
46
|
+
Then /^the package files should be available$/ do
|
47
|
+
list = @c.upload.list_files.map { |f| f[:file] }
|
48
|
+
list.should include(@last_uploaded_file)
|
49
|
+
end
|
50
|
+
|
51
|
+
Then /^the package files should not be available$/ do
|
52
|
+
list = @c.upload.list_files.map { |f| f[:file] }
|
53
|
+
list.should_not include(@last_uploaded_file)
|
54
|
+
end
|
55
|
+
|
56
|
+
When /^I delete the package$/ do
|
57
|
+
@c.upload.delete_files(@last_uploaded_file)
|
58
|
+
end
|
59
|
+
|
60
|
+
When /^I delete a non\-existant package$/ do
|
61
|
+
@last_error = nil
|
62
|
+
begin
|
63
|
+
@c.upload.delete_files("thiscoursedoesnotexist")
|
64
|
+
rescue => e
|
65
|
+
puts e.inspect
|
66
|
+
@last_error = e
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
Then /^I should get an error$/ do
|
71
|
+
@last_error.should_not be_nil
|
72
|
+
end
|
73
|
+
|
74
|
+
|
75
|
+
|
76
|
+
##
|
77
|
+
## Course Service
|
78
|
+
##
|
79
|
+
|
80
|
+
When /^I import a course$/ do
|
81
|
+
@last_uploaded_file.should_not be_nil
|
82
|
+
@last_course_id = "small_scorm_course_#{rand(1000)}"
|
83
|
+
hash = @c.course.import_course(@last_course_id, @last_uploaded_path)
|
84
|
+
hash.should_not be_nil
|
85
|
+
hash[:title].should_not be_nil
|
86
|
+
end
|
87
|
+
|
88
|
+
Given /^a registered course$/ do
|
89
|
+
When "I import a course"
|
90
|
+
end
|
91
|
+
|
92
|
+
Then /^the course should be in the course list$/ do
|
93
|
+
@c.course.get_course_list.find { |c| c.id == @last_course_id}.should_not be_nil
|
94
|
+
end
|
95
|
+
|
96
|
+
Then /^there should be (\d+) courses? in the list$/ do |count|
|
97
|
+
@c.course.get_course_list.size.should eq(count.to_i)
|
98
|
+
end
|
99
|
+
|
100
|
+
When /^I delete the course$/ do
|
101
|
+
@c.course.delete_course(@last_course_id).should eq(true)
|
102
|
+
end
|
103
|
+
|
104
|
+
Then /^the course should not be in the course list$/ do
|
105
|
+
@c.course.get_course_list.find { |c| c.id == @last_course_id}.should be_nil
|
106
|
+
end
|
107
|
+
|
108
|
+
Then /^I can get a preview URL for the course$/ do
|
109
|
+
@c.course.preview(@last_course_id, "http://www.example.com").should match(/http:\/\/.+/)
|
110
|
+
end
|
111
|
+
|
112
|
+
Then /^the course will have a properties url$/ do
|
113
|
+
@c.course.properties(@last_course_id).should match(/http:\/\/.+/)
|
114
|
+
end
|
115
|
+
|
116
|
+
Then /^the course should exist$/ do
|
117
|
+
@c.course.exists(@last_course_id).should be_true
|
118
|
+
end
|
119
|
+
|
120
|
+
Then /^I can get the manifest for the course$/ do
|
121
|
+
xml = @c.course.get_manifest(@last_course_id)
|
122
|
+
doc = REXML::Document.new(xml)
|
123
|
+
doc.elements["//manifest"].should_not be_nil
|
124
|
+
end
|
125
|
+
|
126
|
+
Then /^I can get the attributes for the course$/ do
|
127
|
+
h = @c.course.get_attributes(@last_course_id)
|
128
|
+
h.should be_kind_of(Hash)
|
129
|
+
h[:showProgressBar].should eq("false")
|
130
|
+
h[:showCourseStructure].should eq("false")
|
131
|
+
end
|
132
|
+
|
133
|
+
When /^I update course attributes$/ do
|
134
|
+
updated = @c.course.update_attributes(@last_course_id,
|
135
|
+
{:desiredHeight => "700", :commCommitFrequency => "59999" })
|
136
|
+
updated[:desiredHeight].should eq("700")
|
137
|
+
updated[:commCommitFrequency].should eq("59999")
|
138
|
+
end
|
139
|
+
|
140
|
+
Then /^the course attributes should be updated$/ do
|
141
|
+
h = @c.course.get_attributes(@last_course_id)
|
142
|
+
h.should be_kind_of(Hash)
|
143
|
+
h[:desiredHeight].should eq("700")
|
144
|
+
h[:commCommitFrequency].should eq("59999")
|
145
|
+
end
|
146
|
+
|
147
|
+
|
148
|
+
##
|
149
|
+
## Registration
|
150
|
+
##
|
151
|
+
|
152
|
+
When /^I register a learner$/ do
|
153
|
+
@last_reg_id = "small_scorm_course_#{rand(1000)}"
|
154
|
+
r = @c.registration.create_registration(@last_course_id, @last_reg_id,
|
155
|
+
"fname", "lname", "lid", :email => "lid@example.com")
|
156
|
+
r.should be_true
|
157
|
+
end
|
158
|
+
|
159
|
+
Then /^the learner should be in the registration list$/ do
|
160
|
+
list = @c.registration.get_registration_list
|
161
|
+
list.find { |r| r.id == @last_reg_id }.should_not be_nil
|
162
|
+
end
|
163
|
+
|
164
|
+
Then /^the learner should not be in the registration list$/ do
|
165
|
+
list = @c.registration.get_registration_list
|
166
|
+
list.find { |r| r.id == @last_reg_id }.should be_nil
|
167
|
+
end
|
168
|
+
|
169
|
+
When /^I delete the registration$/ do
|
170
|
+
@c.registration.delete_registration(@last_reg_id).should be_true
|
171
|
+
end
|
172
|
+
|
173
|
+
When /^I reset the registration$/ do
|
174
|
+
@c.registration.reset_registration(@last_reg_id).should be_true
|
175
|
+
end
|
176
|
+
|
177
|
+
When /^I launch the course$/ do
|
178
|
+
@last_url = @c.registration.launch(@last_reg_id, "http://www.example.com/")
|
179
|
+
end
|
180
|
+
|
181
|
+
Then /^I can get the registration results$/ do
|
182
|
+
@reg_results = @c.registration.get_registration_result(@last_reg_id, "full")
|
183
|
+
@reg_results.should include('<rsp stat="ok"><registrationreport')
|
184
|
+
end
|
@@ -0,0 +1,71 @@
|
|
1
|
+
require 'bundler'
|
2
|
+
begin
|
3
|
+
Bundler.setup(:default, :development)
|
4
|
+
rescue Bundler::BundlerError => e
|
5
|
+
$stderr.puts e.message
|
6
|
+
$stderr.puts "Run `bundle install` to install missing gems"
|
7
|
+
exit e.status_code
|
8
|
+
end
|
9
|
+
|
10
|
+
$LOAD_PATH.unshift(File.dirname(__FILE__) + '/../../lib')
|
11
|
+
require File.join(File.dirname(__FILE__), '/../../spec/apikey.rb')
|
12
|
+
require 'scorm_cloud'
|
13
|
+
|
14
|
+
require 'rspec/expectations'
|
15
|
+
|
16
|
+
|
17
|
+
##
|
18
|
+
## Cleanup before testing
|
19
|
+
##
|
20
|
+
Before do
|
21
|
+
|
22
|
+
# Grab a connection
|
23
|
+
unless @c
|
24
|
+
@c = ScormCloud::ScormCloud.new($scorm_cloud_appid,$scorm_cloud_secret)
|
25
|
+
end
|
26
|
+
|
27
|
+
# Cleanup all courses
|
28
|
+
@c.course.get_course_list.each do |course|
|
29
|
+
@c.course.delete_course(course.id)
|
30
|
+
end
|
31
|
+
@c.course.get_course_list.count.should eq(0)
|
32
|
+
|
33
|
+
unless @last_uploaded_file
|
34
|
+
|
35
|
+
# Cleanup all zip packages
|
36
|
+
@c.upload.list_files.each do |file|
|
37
|
+
@c.upload.delete_files(file[:file])
|
38
|
+
end
|
39
|
+
raise "Cannot delete files" unless @c.upload.list_files.length == 0
|
40
|
+
|
41
|
+
# Upload one we can use for testing
|
42
|
+
token = @c.upload.get_upload_token
|
43
|
+
path = File.join(File.dirname(__FILE__), '..', '..', 'spec', 'small_scorm_package.zip')
|
44
|
+
@last_uploaded_path = @c.upload.upload_file(token, path)
|
45
|
+
@last_uploaded_dir, @last_uploaded_file = @last_uploaded_path.split('/')
|
46
|
+
@last_uploaded_file.should include('.zip')
|
47
|
+
|
48
|
+
sleep(5)
|
49
|
+
|
50
|
+
end
|
51
|
+
|
52
|
+
# was a course created?
|
53
|
+
@c.course.get_course_list.count.should eq(0)
|
54
|
+
@c.upload.list_files.count.should eq(1)
|
55
|
+
|
56
|
+
@last_course_id = nil
|
57
|
+
@last_error = nil
|
58
|
+
@last_response = nil
|
59
|
+
|
60
|
+
end
|
61
|
+
|
62
|
+
|
63
|
+
After do
|
64
|
+
|
65
|
+
# Cleanup all courses
|
66
|
+
@c.course.get_course_list.each do |course|
|
67
|
+
@c.course.delete_course(course.id)
|
68
|
+
end
|
69
|
+
|
70
|
+
end
|
71
|
+
|
data/lib/scorm_cloud.rb
ADDED
@@ -0,0 +1,36 @@
|
|
1
|
+
require 'rexml/document'
|
2
|
+
require 'digest/md5'
|
3
|
+
require 'net/http'
|
4
|
+
require 'net/http/post/multipart'
|
5
|
+
require 'uri'
|
6
|
+
require 'scorm_cloud/base'
|
7
|
+
|
8
|
+
require 'scorm_cloud/base_object'
|
9
|
+
require 'scorm_cloud/course'
|
10
|
+
require 'scorm_cloud/registration'
|
11
|
+
|
12
|
+
require 'scorm_cloud/base_service'
|
13
|
+
require 'scorm_cloud/debug_service'
|
14
|
+
require 'scorm_cloud/upload_service'
|
15
|
+
require 'scorm_cloud/course_service'
|
16
|
+
require 'scorm_cloud/registration_service'
|
17
|
+
require 'scorm_cloud/tagging_service'
|
18
|
+
require 'scorm_cloud/reporting_service'
|
19
|
+
require 'scorm_cloud/dispatch_service'
|
20
|
+
require 'scorm_cloud/export_service'
|
21
|
+
|
22
|
+
# Rails 3 Integration
|
23
|
+
require 'scorm_cloud/railtie' if defined?(Rails::Railtie)
|
24
|
+
|
25
|
+
module ScormCloud
|
26
|
+
class ScormCloud < Base
|
27
|
+
add_service :debug => DebugService
|
28
|
+
add_service :upload => UploadService
|
29
|
+
add_service :course => CourseService
|
30
|
+
add_service :registration => RegistrationService
|
31
|
+
add_service :tagging => TaggingService
|
32
|
+
add_service :reporting => ReportingService
|
33
|
+
add_service :dispatch => DispatchService
|
34
|
+
add_service :export => ExportService
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,110 @@
|
|
1
|
+
module ScormCloud
|
2
|
+
class Base
|
3
|
+
|
4
|
+
attr_reader :appid
|
5
|
+
|
6
|
+
def initialize(appid, secret)
|
7
|
+
@appid = appid
|
8
|
+
@secret = secret
|
9
|
+
end
|
10
|
+
|
11
|
+
def call(method, params = {})
|
12
|
+
url = prepare_url(method, params)
|
13
|
+
execute_call_xml(url)
|
14
|
+
end
|
15
|
+
|
16
|
+
def call_raw(method, params = {})
|
17
|
+
url = prepare_url(method, params)
|
18
|
+
execute_call_plain(url)
|
19
|
+
end
|
20
|
+
|
21
|
+
def call_url(url)
|
22
|
+
execute_call_plain(url)
|
23
|
+
end
|
24
|
+
|
25
|
+
def post(method, path, params = {})
|
26
|
+
url = URI.parse(prepare_url(method, params))
|
27
|
+
body = nil
|
28
|
+
File.open(path) do |f|
|
29
|
+
req = Net::HTTP::Post::Multipart.new "#{url.path}?#{url.query}",
|
30
|
+
"file" => UploadIO.new(f, "application/zip", "scorm.zip")
|
31
|
+
res = Net::HTTP.start(url.host, url.port) do |http|
|
32
|
+
http.request(req)
|
33
|
+
end
|
34
|
+
body = res.body
|
35
|
+
end
|
36
|
+
REXML::Document.new(body)
|
37
|
+
end
|
38
|
+
|
39
|
+
def launch_url(method, params = {})
|
40
|
+
prepare_url(method, params)
|
41
|
+
end
|
42
|
+
|
43
|
+
|
44
|
+
private
|
45
|
+
|
46
|
+
# Get the URL for the call
|
47
|
+
def prepare_url(method, params = {})
|
48
|
+
timestamp = Time.now.utc.strftime('%Y%m%d%H%M%S')
|
49
|
+
params[:method] = method
|
50
|
+
params[:appid] = @appid
|
51
|
+
params[:ts] = timestamp
|
52
|
+
html_params = params.map { |k,v| "#{k.to_s}=#{v}" }.join("&")
|
53
|
+
|
54
|
+
raw = @secret + params.keys.
|
55
|
+
sort{ |a,b| a.to_s.downcase <=> b.to_s.downcase }.
|
56
|
+
map{ |k| "#{k.to_s}#{params[k]}" }.
|
57
|
+
join
|
58
|
+
|
59
|
+
sig = Digest::MD5.hexdigest(raw)
|
60
|
+
"http://cloud.scorm.com/api?#{html_params}&sig=#{sig}"
|
61
|
+
end
|
62
|
+
|
63
|
+
|
64
|
+
# Get plain response body and parse the XML doc
|
65
|
+
def execute_call_xml(url)
|
66
|
+
doc = REXML::Document.new(execute_call_plain(url))
|
67
|
+
raise create_error(doc, url) unless doc.elements["rsp"].attributes["stat"] == "ok"
|
68
|
+
doc
|
69
|
+
end
|
70
|
+
|
71
|
+
# Execute the call - returns response body or redirect url
|
72
|
+
def execute_call_plain(url)
|
73
|
+
res = Net::HTTP.get_response(URI.parse(url))
|
74
|
+
case res
|
75
|
+
when Net::HTTPRedirection
|
76
|
+
# Return the new URL
|
77
|
+
res['location']
|
78
|
+
when Net::HTTPSuccess
|
79
|
+
res.body
|
80
|
+
else
|
81
|
+
raise "HTTP Error connecting to scorm cloud: #{res.inspect}"
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
|
86
|
+
# Create an exception with code & message
|
87
|
+
def create_error(doc, url)
|
88
|
+
err = doc.elements["rsp"].elements["err"]
|
89
|
+
code = err.attributes["code"]
|
90
|
+
msg = err.attributes["msg"]
|
91
|
+
"Error In Scorm Cloud: Error=#{code} Message=#{msg} URL=#{url}"
|
92
|
+
end
|
93
|
+
|
94
|
+
|
95
|
+
# Add services
|
96
|
+
def self.add_service(hash)
|
97
|
+
hash.each do |name, klass|
|
98
|
+
define_method(name) do
|
99
|
+
service = instance_variable_get("@#{name.to_s}")
|
100
|
+
unless service
|
101
|
+
service = instance_variable_set("@#{name.to_s}", klass.new(self))
|
102
|
+
end
|
103
|
+
service
|
104
|
+
end
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
|
109
|
+
end
|
110
|
+
end
|