scorm 1.0.0

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/lib/scorm.rb ADDED
@@ -0,0 +1,5 @@
1
+ module Scorm
2
+ VERSION = '1.0.0'
3
+ end
4
+
5
+ require 'scorm/package'
@@ -0,0 +1,63 @@
1
+ require 'scorm/commands/base'
2
+
3
+ Dir["#{File.dirname(__FILE__)}/commands/*.rb"].each { |c| require c }
4
+
5
+ module Scorm
6
+ module Command
7
+ class InvalidCommand < RuntimeError; end
8
+ class CommandFailed < RuntimeError; end
9
+
10
+ class << self
11
+
12
+ def error(msg)
13
+ STDERR.puts(msg)
14
+ exit 1
15
+ end
16
+
17
+ def run(command, args)
18
+ begin
19
+ run_internal(command, args.dup)
20
+ rescue Zip::ZipError => e
21
+ error e.message
22
+ rescue InvalidPackage => e
23
+ error e.message
24
+ rescue InvalidManifest => e
25
+ error e.message
26
+ rescue InvalidCommand
27
+ error "Unknown command. Run 'scorm help' for usage information."
28
+ rescue CommandFailed => e
29
+ error e.message
30
+ rescue Interrupt => e
31
+ error "\n[canceled]"
32
+ end
33
+ end
34
+
35
+ def run_internal(command, args)
36
+ klass, method = parse(command)
37
+ runner = klass.new(args)
38
+ raise InvalidCommand unless runner.respond_to?(method)
39
+ runner.send(method)
40
+ end
41
+
42
+ def parse(command)
43
+ parts = command.split(':')
44
+ case parts.size
45
+ when 1
46
+ begin
47
+ return eval("Scorm::Command::#{command.capitalize}"), :index
48
+ rescue NameError, NoMethodError
49
+ raise InvalidCommand
50
+ end
51
+ when 2
52
+ begin
53
+ return Scorm::Command.const_get(parts[0].capitalize), parts[1]
54
+ rescue NameError
55
+ raise InvalidCommand
56
+ end
57
+ else
58
+ raise InvalidCommand
59
+ end
60
+ end
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,53 @@
1
+ require 'fileutils'
2
+
3
+ module Scorm::Command
4
+ class Base
5
+ attr_accessor :args
6
+ attr_reader :autodetected_package
7
+
8
+ def initialize(args)
9
+ @args = args
10
+ @autodetected_package = false
11
+ end
12
+
13
+ def display(msg, newline=true)
14
+ if newline
15
+ puts(msg)
16
+ else
17
+ print(msg)
18
+ STDOUT.flush
19
+ end
20
+ end
21
+
22
+ def error(msg)
23
+ STDERR.puts(msg)
24
+ exit 1
25
+ end
26
+
27
+ def extract_package(force=true)
28
+ package = extract_option('--package', false)
29
+ raise(CommandFailed, "You must specify a package name after --package") if package == false
30
+ unless package
31
+ raise(CommandFailed, "No package specified.\nRun this command from package folder or set it adding --package <package name>") if force
32
+ @autodetected_package = true
33
+ end
34
+ package
35
+ end
36
+
37
+ def extract_option(options, default=true)
38
+ values = options.is_a?(Array) ? options : [options]
39
+ return unless opt_index = args.select { |a| values.include? a }.first
40
+ opt_position = args.index(opt_index) + 1
41
+ if args.size > opt_position && opt_value = args[opt_position]
42
+ if opt_value.include?('--')
43
+ opt_value = nil
44
+ else
45
+ args.delete_at(opt_position)
46
+ end
47
+ end
48
+ opt_value ||= default
49
+ args.delete(opt_index)
50
+ block_given? ? yield(opt_value) : opt_value
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,29 @@
1
+ module Scorm::Command
2
+ class Bundle < Base
3
+ def index
4
+ name = args.shift.strip rescue '.'
5
+ unless File.exist?(File.join(File.expand_path(name), 'imsmanifest.xml'))
6
+ raise(CommandFailed, "Invalid package, didn't find any imsmanifest.xml file.")
7
+ end
8
+
9
+ outname = File.basename(File.expand_path(name)) + '.zip'
10
+
11
+ require 'zip/zip'
12
+ Zip::ZipFile.open(outname, Zip::ZipFile::CREATE) do |zipfile|
13
+ Scorm::Package.open(name) do |pkg|
14
+ Scorm::Manifest::MANIFEST_FILES.each do |file|
15
+ zipfile.get_output_stream(file) {|f| f.write(pkg.file(file)) }
16
+ display file
17
+ end
18
+ files = pkg.manifest.resources.map {|r| r.files }.flatten.uniq
19
+ files.each do |file|
20
+ zipfile.get_output_stream(file) {|f| f.write(pkg.file(file)) }
21
+ display file
22
+ end
23
+ end
24
+ end
25
+
26
+ display "Created new SCORM package \"#{outname}\"."
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,45 @@
1
+ module Scorm::Command
2
+ class Check < Base
3
+ def index
4
+ package = args.shift.strip rescue ''
5
+ raise(CommandFailed, "Invalid package.") if package == ''
6
+
7
+ Scorm::Package.open(package, :dry_run => true) do |pkg|
8
+ display "Checking package \"#{File.basename(package)}\""
9
+ display ""
10
+ display "== UUID =="
11
+ display "Identifier: #{pkg.manifest.identifier}"
12
+ display ""
13
+ display "== Manifest =="
14
+ Scorm::Manifest::MANIFEST_FILES.each do |file|
15
+ if pkg.exists?(file)
16
+ display "#{file} -> OK"
17
+ else
18
+ display "#{file} -> Missing"
19
+ end
20
+ end
21
+ display ""
22
+ display "== Organizations =="
23
+ pkg.manifest.organizations.each do |id, organization|
24
+ if organization == pkg.manifest.default_organization
25
+ display "#{organization.title} (default)"
26
+ else
27
+ display "#{organization.title}"
28
+ end
29
+ end
30
+ display ""
31
+ display "== Resources =="
32
+ pkg.manifest.resources.each do |resource|
33
+ display "#{resource.href} (#{resource.type}, #{resource.scorm_type}):"
34
+ resource.files.each do |file|
35
+ if pkg.exists?(file)
36
+ display " - #{file} -> OK"
37
+ else
38
+ display " - #{file} -> Missing"
39
+ end
40
+ end
41
+ end
42
+ end
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,15 @@
1
+ module Scorm::Command
2
+ class Create < Base
3
+ def index
4
+ name = args.shift.strip rescue ''
5
+ raise(CommandFailed, "Invalid package name.") if name == ''
6
+
7
+ FileUtils.mkdir_p(name)
8
+ Dir.glob(File.join(File.dirname(File.expand_path(__FILE__)), '../../../skeleton/*')).each do |file|
9
+ FileUtils.cp(file, name)
10
+ display "#{name}/#{File.basename(file)}"
11
+ end
12
+ display "Created new SCORM package \"#{name}\"."
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,12 @@
1
+ module Scorm::Command
2
+ class Extract < Base
3
+ def index
4
+ package = args.shift.strip rescue ''
5
+ raise(CommandFailed, "Invalid package.") if package == ''
6
+
7
+ Scorm::Package.open(package) do |pkg|
8
+ display "Extracted package to #{pkg.path}"
9
+ end
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,80 @@
1
+ module Scorm::Command
2
+ class Help < Base
3
+ class HelpGroup < Array
4
+ attr_reader :title
5
+
6
+ def initialize(title)
7
+ @title = title
8
+ end
9
+
10
+ def command(name, description)
11
+ self << [name, description]
12
+ end
13
+
14
+ def space
15
+ self << ['', '']
16
+ end
17
+ end
18
+
19
+ def self.groups
20
+ @groups ||= []
21
+ end
22
+
23
+ def self.group(title, &block)
24
+ groups << begin
25
+ group = HelpGroup.new(title)
26
+ yield group
27
+ group
28
+ end
29
+ end
30
+
31
+ def self.create_default_groups!
32
+ group 'Commands' do |group|
33
+ group.command 'help', 'show this usage'
34
+ group.command 'version', 'show the gem version'
35
+ group.space
36
+ group.command 'create <name>', 'create a new package skeleton'
37
+ group.command 'bundle [<path to directory>]', 'creates a package from the current directory'
38
+ group.command 'check <path to zip file>', 'runs a test suite against your package'
39
+ group.command 'extract <path to zip file>', 'extracts and checks the specified package'
40
+ end
41
+ end
42
+
43
+ def index
44
+ display usage
45
+ end
46
+
47
+ def usage
48
+ longest_command_length = self.class.groups.map do |group|
49
+ group.map { |g| g.first.length }
50
+ end.flatten.max
51
+
52
+ self.class.groups.inject(StringIO.new) do |output, group|
53
+ output.puts "=== %s" % group.title
54
+ output.puts
55
+
56
+ group.each do |command, description|
57
+ if command.empty?
58
+ output.puts
59
+ else
60
+ output.puts "%-*s # %s" % [longest_command_length, command, description]
61
+ end
62
+ end
63
+
64
+ output.puts
65
+ output
66
+ end.string + <<-EOTXT
67
+ === Example
68
+
69
+ scorm create mypackage
70
+ cd mypackage
71
+ scorm check
72
+ scorm bundle
73
+ scorm extract mypackage.zip
74
+
75
+ EOTXT
76
+ end
77
+ end
78
+ end
79
+
80
+ Scorm::Command::Help.create_default_groups!
@@ -0,0 +1,7 @@
1
+ module Scorm::Command
2
+ class Version < Base
3
+ def index
4
+ display Scorm::VERSION
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,57 @@
1
+ module Scorm
2
+ module Datatypes
3
+
4
+ class Timeinterval
5
+ def initialize(seconds)
6
+ @sec = seconds
7
+ end
8
+
9
+ def self.parse(str)
10
+ case str
11
+ when /(\d{2,4}):(\d{2}):(\d{2})/
12
+ values = str.match(/(\d{2,4}):(\d{2}):(\d{2})/)
13
+ hour = values[1].to_i
14
+ minute = values[2].to_i
15
+ second = values[3].to_i
16
+ else
17
+ date, time = str.split('T')
18
+ if date
19
+ year = date.match(/([0-9]+Y)/)[1].to_i if date.match(/([0-9]+Y)/)
20
+ month = date.match(/([0-9]+M)/)[1].to_i if date.match(/([0-9]+M)/)
21
+ day = date.match(/([0-9]+D)/)[1].to_i if date.match(/([0-9]+D)/)
22
+ end
23
+ if time
24
+ hour = time.match(/([0-9]+H)/)[1].to_i if time.match(/([0-9]+H)/)
25
+ minute = time.match(/([0-9]+M)/)[1].to_i if time.match(/([0-9]+M)/)
26
+ second = time.match(/([0-9\.]+S)/)[1].to_f if time.match(/([0-9\.]+S)/)
27
+ end
28
+ end
29
+ year = year || 0
30
+ month = month || 0
31
+ day = day || 0
32
+ hour = hour || 0
33
+ minute = minute || 0
34
+ second = second || 0
35
+ self.new((year*31557600) + (month*2629800) + (day*86400) + (hour*3600) + (minute*60) + second)
36
+ end
37
+
38
+ def to_i
39
+ @sec.to_i
40
+ end
41
+
42
+ def to_f
43
+ @sec.to_f
44
+ end
45
+
46
+ def to_s
47
+ sec = self.to_i
48
+ hours = (sec/60/60).to_i
49
+ sec -= hours*60*60
50
+ min = (sec/60).to_i
51
+ sec -= min*60
52
+ return "#{hours}:#{min}:#{sec}"
53
+ end
54
+ end
55
+
56
+ end
57
+ end
@@ -0,0 +1,154 @@
1
+ require 'rexml/document'
2
+ require 'scorm/metadata'
3
+ require 'scorm/organization'
4
+ require 'scorm/resource'
5
+
6
+ module Scorm
7
+ class Manifest
8
+
9
+ # Versions of the SCORM standard that is supported when running in
10
+ # strict mode. When not running in strict mode, the library will not
11
+ # care about the version specified in the package manifest and will
12
+ # simply try its best to parse the information that it finds.
13
+ SUPPORTED_VERSIONS = ['2004 3rd Edition', 'CAM 1.3', '1.2']
14
+
15
+ # List of XML and XML Schema files that are part of the manifest for
16
+ # the package.
17
+ MANIFEST_FILES = %w(imsmanifest.xml adlcp_rootv1p2.xsd ims_xml.xsd
18
+ imscp_rootv1p1p2.xsd imsmd_rootv1p2p1.xsd)
19
+
20
+ # Files that might be present in a package, but that should not be
21
+ # interprested as resources. All files starting with a "." (i.e. hidden
22
+ # files) is also implicitly included in this list.
23
+ RESOURCES_BLACKLIST = [
24
+ '__MACOSX', 'desktop.ini', 'Thumbs.db'
25
+ ].concat(MANIFEST_FILES)
26
+
27
+ attr_accessor :identifier
28
+ attr_accessor :metadata
29
+ attr_accessor :organizations
30
+ attr_accessor :default_organization
31
+ attr_accessor :resources
32
+ attr_accessor :base_url
33
+ attr_accessor :schema
34
+ attr_accessor :schema_version
35
+
36
+ def initialize(package, manifest_data)
37
+ @xmldoc = REXML::Document.new(manifest_data)
38
+
39
+ @package = package
40
+ @metadata = Scorm::Metadata.new
41
+ @organizations = Hash.new
42
+ @resources = Hash.new
43
+
44
+ # Manifest identifier
45
+ @identifier = @xmldoc.root.attribute('identifier').to_s
46
+
47
+ # Read metadata
48
+ if metadata_el = REXML::XPath.first(@xmldoc.root, '/manifest/metadata')
49
+ # Read <schema> and <schemaversion>
50
+ schema_el = REXML::XPath.first(metadata_el, 'schema')
51
+ schemaversion_el = REXML::XPath.first(metadata_el, 'schemaversion')
52
+ @schema = schema_el.text.to_s unless schema_el.nil?
53
+ @schema_version = schemaversion_el.text.to_s unless schemaversion_el.nil?
54
+
55
+ if @package.options[:strict]
56
+ if (@schema != 'ADL SCORM') || (!SUPPORTED_VERSIONS.include?(@schema_version))
57
+ raise InvalidManifest, "Sorry, unsupported SCORM-version (#{schema_el.text.to_s} #{schemaversion_el.text.to_s}), try turning strict parsing off."
58
+ end
59
+ end
60
+
61
+ # Find a <lom> element...
62
+ lom_el = nil
63
+ if adlcp_location = REXML::XPath.first(metadata_el, 'adlcp:location')
64
+ # Read external metadata file
65
+ metadata_xmldoc = REXML::Document.new(package.file(adlcp_location.text.to_s))
66
+ if metadata_xmldoc.nil? || (metadata_xmldoc.root.name != 'lom')
67
+ raise InvalidManifest, "Invalid external metadata file (#{adlcp_location.text.to_s})."
68
+ else
69
+ lom_el = metadata_xmldoc.root
70
+ end
71
+ else
72
+ # Read inline metadata
73
+ lom_el = REXML::XPath.first(metadata_el, 'lom') ||
74
+ REXML::XPath.first(metadata_el, 'lom:lom')
75
+ end
76
+
77
+ # Read lom metadata
78
+ if lom_el
79
+ @metadata = Scorm::Metadata.from_xml(lom_el)
80
+ end
81
+ end
82
+
83
+ # Read organizations
84
+ if organizations_el = REXML::XPath.first(@xmldoc.root, '/manifest/organizations')
85
+ default_organization_id = organizations_el.attribute('default').to_s
86
+ REXML::XPath.each(@xmldoc.root, '/manifest/organizations/organization') do |el|
87
+ org = Scorm::Organization.from_xml(el)
88
+ @organizations[org.id.to_s] = org
89
+ end
90
+ # Set the default organization
91
+ @default_organization = @organizations[default_organization_id]
92
+ raise InvalidManifest, "No default organization (#{default_organization_id})." if @default_organization.nil?
93
+ else
94
+ raise InvalidManifest, 'Missing organizations element.'
95
+ end
96
+
97
+ # Read resources
98
+ REXML::XPath.each(@xmldoc.root, '/manifest/resources/resource') do |el|
99
+ res = Scorm::Resource.from_xml(el)
100
+ @resources[res.id] = res
101
+ end
102
+
103
+ # Read additional resources as assets (this is a fix for packages that
104
+ # don't correctly specify all resource dependencies in the manifest).
105
+ @package.files.each do |file|
106
+ next if File.directory?(file)
107
+ next if RESOURCES_BLACKLIST.include?(File.basename(file))
108
+ next if File.basename(file) =~ /^\./
109
+ next unless self.resources(:with_file => file).empty?
110
+ next unless self.resources(:href => file).empty?
111
+
112
+ res = Scorm::Resource.new(file, 'webcontent', 'asset', file, nil, [file])
113
+ @resources[file] = res
114
+ end
115
+
116
+ # Read (optional) base url for resources
117
+ resources_el = REXML::XPath.first(@xmldoc.root, '/manifest/resources')
118
+ @base_url = (resources_el.attribute('xml:base') || '').to_s
119
+
120
+ # Read sub-manifests
121
+ #REXML::XPath.
122
+ end
123
+
124
+ def resources(options = nil)
125
+ if (options.nil?) || (!options.is_a?(Hash))
126
+ @resources.values
127
+ else
128
+ subset = @resources.values
129
+ if options[:id]
130
+ subset = subset.find_all {|r| r.id == options[:id].to_s }
131
+ end
132
+ if options[:type]
133
+ subset = subset.find_all {|r| r.type == options[:type].to_s }
134
+ end
135
+ if options[:scorm_type]
136
+ subset = subset.find_all {|r| r.scorm_type == options[:scorm_type].to_s }
137
+ end
138
+ if options[:href]
139
+ subset = subset.find_all {|r| r.href == options[:href].to_s }
140
+ end
141
+ if options[:with_file]
142
+ subset = subset.find_all {|r| r.files.include?(options[:with_file].to_s) }
143
+ end
144
+ subset
145
+ end
146
+ end
147
+
148
+ def sco(item, attribute = nil)
149
+ resource = self.resources(:id => item.resource_id).first
150
+ resource = (resource && resource.scorm_type == 'sco') ? resource : nil
151
+ return (resource && attribute) ? resource.send(attribute) : resource
152
+ end
153
+ end
154
+ end