scorm 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
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