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.
Files changed (43) hide show
  1. data/.gitignore +8 -0
  2. data/Gemfile +7 -0
  3. data/Rakefile +25 -0
  4. data/bin/scorm_cloud +6 -0
  5. data/features/api_bugs.feature +12 -0
  6. data/features/course_service.feature +19 -0
  7. data/features/debug_service.feature +12 -0
  8. data/features/registration_service.feature +21 -0
  9. data/features/step_definitions/scorm_cloud_steps.rb +184 -0
  10. data/features/support/env.rb +71 -0
  11. data/features/upload_service.feature +9 -0
  12. data/lib/scorm_cloud.rb +36 -0
  13. data/lib/scorm_cloud/base.rb +110 -0
  14. data/lib/scorm_cloud/base_object.rb +23 -0
  15. data/lib/scorm_cloud/base_service.rb +27 -0
  16. data/lib/scorm_cloud/connection.rb +67 -0
  17. data/lib/scorm_cloud/course.rb +13 -0
  18. data/lib/scorm_cloud/course_service.rb +53 -0
  19. data/lib/scorm_cloud/debug_service.rb +25 -0
  20. data/lib/scorm_cloud/dispatch_service.rb +9 -0
  21. data/lib/scorm_cloud/export_service.rb +7 -0
  22. data/lib/scorm_cloud/railtie.rb +13 -0
  23. data/lib/scorm_cloud/registration.rb +19 -0
  24. data/lib/scorm_cloud/registration_service.rb +50 -0
  25. data/lib/scorm_cloud/reporting_service.rb +7 -0
  26. data/lib/scorm_cloud/scorm_rails_helpers.rb +15 -0
  27. data/lib/scorm_cloud/tagging_service.rb +9 -0
  28. data/lib/scorm_cloud/upload_service.rb +37 -0
  29. data/lib/scorm_cloud/version.rb +3 -0
  30. data/readme.textile +53 -0
  31. data/scorm_cloud.gemspec +24 -0
  32. data/spec/apikey_template.rb +3 -0
  33. data/spec/course_service_spec.rb +31 -0
  34. data/spec/debug_service_spec.rb +26 -0
  35. data/spec/dispatch_service_spec.rb +21 -0
  36. data/spec/export_service_spec.rb +13 -0
  37. data/spec/registration_service_spec.rb +28 -0
  38. data/spec/reporting_service_spec.rb +11 -0
  39. data/spec/small_scorm_package.zip +0 -0
  40. data/spec/spec_helper.rb +4 -0
  41. data/spec/tagging_service_spec.rb +20 -0
  42. data/spec/upload_service_spec.rb +18 -0
  43. metadata +139 -0
@@ -0,0 +1,8 @@
1
+ *.gem
2
+ .bundle
3
+ Gemfile.lock
4
+ pkg/*
5
+ spec/apikey.rb
6
+ .rspec
7
+
8
+
data/Gemfile ADDED
@@ -0,0 +1,7 @@
1
+ source "http://rubygems.org"
2
+
3
+ gemspec
4
+
5
+ gem 'rspec'
6
+ gem 'cucumber'
7
+
@@ -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
@@ -0,0 +1,6 @@
1
+ #!/usr/bin/env ruby
2
+ $:.unshift(File.expand_path('../../lib', __FILE__))
3
+ require 'scorm_cloud'
4
+ require 'scorm_cloud/CLI'
5
+ ScormCloud::CLI.start
6
+
@@ -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
+
@@ -0,0 +1,9 @@
1
+ Feature: Upload Service Interface
2
+
3
+ Scenario: Upload a course
4
+
5
+ When I upload a package
6
+ Then the package files should be available
7
+
8
+ When I delete the package
9
+ Then the package files should not be available
@@ -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