scorm 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- data/LICENSE +19 -0
- data/bin/scorm +13 -0
- data/examples/example/README +15 -0
- data/examples/example/adlcp_rootv1p2.xsd +110 -0
- data/examples/example/example.html +10 -0
- data/examples/example/images/large/example.png +0 -0
- data/examples/example/ims_xml.xsd +1 -0
- data/examples/example/imscp_rootv1p1p2.xsd +345 -0
- data/examples/example/imsmanifest.xml +45 -0
- data/examples/example/imsmd_rootv1p2p1.xsd +573 -0
- data/lib/scorm.rb +5 -0
- data/lib/scorm/command.rb +63 -0
- data/lib/scorm/commands/base.rb +53 -0
- data/lib/scorm/commands/bundle.rb +29 -0
- data/lib/scorm/commands/check.rb +45 -0
- data/lib/scorm/commands/create.rb +15 -0
- data/lib/scorm/commands/extract.rb +12 -0
- data/lib/scorm/commands/help.rb +80 -0
- data/lib/scorm/commands/version.rb +7 -0
- data/lib/scorm/datatypes.rb +57 -0
- data/lib/scorm/manifest.rb +154 -0
- data/lib/scorm/metadata.rb +100 -0
- data/lib/scorm/organization.rb +77 -0
- data/lib/scorm/package.rb +210 -0
- data/lib/scorm/resource.rb +49 -0
- data/skeleton/adlcp_rootv1p2.xsd +110 -0
- data/skeleton/ims_xml.xsd +1 -0
- data/skeleton/imscp_rootv1p1p2.xsd +345 -0
- data/skeleton/imsmanifest.xml +45 -0
- data/skeleton/imsmd_rootv1p2p1.xsd +573 -0
- metadata +111 -0
data/lib/scorm.rb
ADDED
@@ -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,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
|