scorm_cloud 0.0.3
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/.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
|