scorm-package 0.1.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.
- checksums.yaml +7 -0
- data/.rspec +3 -0
- data/.rubocop.yml +16 -0
- data/LICENSE.txt +21 -0
- data/README.md +24 -0
- data/Rakefile +12 -0
- data/lib/scorm_package/abstract_course.rb +21 -0
- data/lib/scorm_package/abstract_course_module.rb +17 -0
- data/lib/scorm_package/abstract_lesson.rb +17 -0
- data/lib/scorm_package/packaging/common/adlcp_rootv1p2.xsd +110 -0
- data/lib/scorm_package/packaging/common/ims_xml.xsd +1 -0
- data/lib/scorm_package/packaging/common/imscp_rootv1p1p2.xsd +345 -0
- data/lib/scorm_package/packaging/common/imsmd_rootv1p2p1.xsd +573 -0
- data/lib/scorm_package/packaging/common/scormfunctions.js +155 -0
- data/lib/scorm_package/packaging/create_zip.rb +61 -0
- data/lib/scorm_package/packaging/generate.rb +143 -0
- data/lib/scorm_package/version.rb +5 -0
- data/lib/scorm_package.rb +11 -0
- data/sig/scorm_package.rbs +4 -0
- metadata +62 -0
@@ -0,0 +1,155 @@
|
|
1
|
+
/*
|
2
|
+
Source code created by Rustici Software, LLC is licensed under a
|
3
|
+
Creative Commons Attribution 3.0 United States License
|
4
|
+
(http://creativecommons.org/licenses/by/3.0/us/)
|
5
|
+
|
6
|
+
Want to make SCORM easy? See our solutions at http://www.scorm.com.
|
7
|
+
|
8
|
+
This example provides for the bare minimum SCORM run-time calls.
|
9
|
+
It will demonstrate using the API discovery algorithm to find the
|
10
|
+
SCORM API and then calling Initialize and Terminate when the page
|
11
|
+
loads and unloads respectively. This example also demonstrates
|
12
|
+
some basic SCORM error handling.
|
13
|
+
*/
|
14
|
+
|
15
|
+
|
16
|
+
//Include the standard ADL-supplied API discovery algorithm
|
17
|
+
|
18
|
+
|
19
|
+
///////////////////////////////////////////
|
20
|
+
//Begin ADL-provided API discovery algorithm
|
21
|
+
///////////////////////////////////////////
|
22
|
+
var findAPITries = 0;
|
23
|
+
|
24
|
+
function findAPI(win)
|
25
|
+
{
|
26
|
+
// Check to see if the window (win) contains the API
|
27
|
+
// if the window (win) does not contain the API and
|
28
|
+
// the window (win) has a parent window and the parent window
|
29
|
+
// is not the same as the window (win)
|
30
|
+
while ( (win.API == null) &&
|
31
|
+
(win.parent != null) &&
|
32
|
+
(win.parent != win) )
|
33
|
+
{
|
34
|
+
// increment the number of findAPITries
|
35
|
+
findAPITries++;
|
36
|
+
|
37
|
+
// Note: 7 is an arbitrary number, but should be more than sufficient
|
38
|
+
if (findAPITries > 7)
|
39
|
+
{
|
40
|
+
alert("Error finding API -- too deeply nested.");
|
41
|
+
return null;
|
42
|
+
}
|
43
|
+
|
44
|
+
// set the variable that represents the window being
|
45
|
+
// being searched to be the parent of the current window
|
46
|
+
// then search for the API again
|
47
|
+
win = win.parent;
|
48
|
+
}
|
49
|
+
return win.API;
|
50
|
+
}
|
51
|
+
|
52
|
+
function getAPI()
|
53
|
+
{
|
54
|
+
// start by looking for the API in the current window
|
55
|
+
var theAPI = findAPI(window);
|
56
|
+
|
57
|
+
// if the API is null (could not be found in the current window)
|
58
|
+
// and the current window has an opener window
|
59
|
+
if ( (theAPI == null) &&
|
60
|
+
(window.opener != null) &&
|
61
|
+
(typeof(window.opener) != "undefined") )
|
62
|
+
{
|
63
|
+
// try to find the API in the current window�s opener
|
64
|
+
theAPI = findAPI(window.opener);
|
65
|
+
}
|
66
|
+
// if the API has not been found
|
67
|
+
if (theAPI == null)
|
68
|
+
{
|
69
|
+
// Alert the user that the API Adapter could not be found
|
70
|
+
alert("Unable to find an API adapter");
|
71
|
+
}
|
72
|
+
return theAPI;
|
73
|
+
}
|
74
|
+
|
75
|
+
|
76
|
+
///////////////////////////////////////////
|
77
|
+
//End ADL-provided API discovery algorithm
|
78
|
+
///////////////////////////////////////////
|
79
|
+
|
80
|
+
|
81
|
+
//Create function handlers for the loading and unloading of the page
|
82
|
+
|
83
|
+
|
84
|
+
//Constants
|
85
|
+
var SCORM_TRUE = "true";
|
86
|
+
var SCORM_FALSE = "false";
|
87
|
+
|
88
|
+
//Since the Unload handler will be called twice, from both the onunload
|
89
|
+
//and onbeforeunload events, ensure that we only call LMSFinish once.
|
90
|
+
var finishCalled = false;
|
91
|
+
|
92
|
+
//Track whether or not we successfully initialized.
|
93
|
+
var initialized = false;
|
94
|
+
|
95
|
+
var API = null;
|
96
|
+
|
97
|
+
function ScormProcessInitialize(){
|
98
|
+
var result;
|
99
|
+
|
100
|
+
API = getAPI();
|
101
|
+
|
102
|
+
if (API == null){
|
103
|
+
alert("ERROR - Could not establish a connection with the LMS.\n\nYour results may not be recorded.");
|
104
|
+
return;
|
105
|
+
}
|
106
|
+
|
107
|
+
result = API.LMSInitialize("");
|
108
|
+
|
109
|
+
if (result == SCORM_FALSE){
|
110
|
+
var errorNumber = API.LMSGetLastError();
|
111
|
+
var errorString = API.LMSGetErrorString(errorNumber);
|
112
|
+
var diagnostic = API.LMSGetDiagnostic(errorNumber);
|
113
|
+
|
114
|
+
var errorDescription = "Number: " + errorNumber + "\nDescription: " + errorString + "\nDiagnostic: " + diagnostic;
|
115
|
+
|
116
|
+
alert("Error - Could not initialize communication with the LMS.\n\nYour results may not be recorded.\n\n" + errorDescription);
|
117
|
+
return;
|
118
|
+
}
|
119
|
+
|
120
|
+
initialized = true;
|
121
|
+
}
|
122
|
+
|
123
|
+
function ScormProcessFinish(){
|
124
|
+
|
125
|
+
var result;
|
126
|
+
|
127
|
+
//Don't terminate if we haven't initialized or if we've already terminated
|
128
|
+
if (initialized == false || finishCalled == true){return;}
|
129
|
+
|
130
|
+
result = API.LMSFinish("");
|
131
|
+
|
132
|
+
finishCalled = true;
|
133
|
+
|
134
|
+
if (result == SCORM_FALSE){
|
135
|
+
var errorNumber = API.LMSGetLastError();
|
136
|
+
var errorString = API.LMSGetErrorString(errorNumber);
|
137
|
+
var diagnostic = API.LMSGetDiagnostic(errorNumber);
|
138
|
+
|
139
|
+
var errorDescription = "Number: " + errorNumber + "\nDescription: " + errorString + "\nDiagnostic: " + diagnostic;
|
140
|
+
|
141
|
+
alert("Error - Could not terminate communication with the LMS.\n\nYour results may not be recorded.\n\n" + errorDescription);
|
142
|
+
return;
|
143
|
+
}
|
144
|
+
}
|
145
|
+
|
146
|
+
|
147
|
+
/*
|
148
|
+
Assign the processing functions to the page's load and unload
|
149
|
+
events. The onbeforeunload event is included because it can be
|
150
|
+
more reliable than the onunload event and we want to make sure
|
151
|
+
that Terminate is ALWAYS called.
|
152
|
+
*/
|
153
|
+
window.onload = ScormProcessInitialize;
|
154
|
+
window.onunload = ScormProcessFinish;
|
155
|
+
window.onbeforeunload = ScormProcessFinish;
|
@@ -0,0 +1,61 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "zip"
|
4
|
+
|
5
|
+
module ScormPackage
|
6
|
+
module Packaging
|
7
|
+
class CreateZip
|
8
|
+
attr_reader :manifest, :lessons
|
9
|
+
|
10
|
+
root_path = File.expand_path("..", __dir__)
|
11
|
+
|
12
|
+
SCHEMA_FILES = [
|
13
|
+
File.join(root_path, "packaging", "common", "adlcp_rootv1p2.xsd"),
|
14
|
+
File.join(root_path, "packaging", "common", "ims_xml.xsd"),
|
15
|
+
File.join(root_path, "packaging", "common", "imscp_rootv1p1p2.xsd"),
|
16
|
+
File.join(root_path, "packaging", "common", "imsmd_rootv1p2p1.xsd"),
|
17
|
+
File.join(root_path, "packaging", "common", "scormfunctions.js")
|
18
|
+
].freeze
|
19
|
+
|
20
|
+
def initialize(manifest, lessons)
|
21
|
+
@manifest = manifest
|
22
|
+
@lessons = lessons
|
23
|
+
end
|
24
|
+
|
25
|
+
def process
|
26
|
+
create_zip_package
|
27
|
+
end
|
28
|
+
|
29
|
+
private
|
30
|
+
|
31
|
+
def create_zip_package
|
32
|
+
buffer = Zip::OutputStream.write_buffer do |zipfile|
|
33
|
+
add_manifest(zipfile)
|
34
|
+
add_source_files(zipfile)
|
35
|
+
add_lesson_files(zipfile)
|
36
|
+
end
|
37
|
+
buffer.string
|
38
|
+
end
|
39
|
+
|
40
|
+
def add_manifest(zipfile)
|
41
|
+
zipfile.put_next_entry("imsmanifest.xml")
|
42
|
+
zipfile.write(manifest)
|
43
|
+
end
|
44
|
+
|
45
|
+
def add_source_files(zipfile)
|
46
|
+
SCHEMA_FILES.each do |file|
|
47
|
+
filename = File.basename(file)
|
48
|
+
zipfile.put_next_entry(filename)
|
49
|
+
zipfile.write(File.read(file))
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
def add_lesson_files(zipfile)
|
54
|
+
lessons.each do |lesson_path, lesson_content|
|
55
|
+
zipfile.put_next_entry(lesson_path)
|
56
|
+
zipfile.write(lesson_content)
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
@@ -0,0 +1,143 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "create_zip"
|
4
|
+
|
5
|
+
module ScormPackage
|
6
|
+
module Packaging
|
7
|
+
class Generate
|
8
|
+
attr_reader :course, :scorm_token
|
9
|
+
|
10
|
+
def initialize(course, scorm_token)
|
11
|
+
@course = course
|
12
|
+
@scorm_token = scorm_token
|
13
|
+
end
|
14
|
+
|
15
|
+
def process
|
16
|
+
manifest = generate_manifest
|
17
|
+
lessons_html = generate_lesson_contents
|
18
|
+
ScormPackage::Packaging::CreateZip.new(manifest, lessons_html).process
|
19
|
+
end
|
20
|
+
|
21
|
+
private
|
22
|
+
|
23
|
+
def generate_manifest
|
24
|
+
<<~XML
|
25
|
+
<?xml version="1.0" standalone="no" ?>
|
26
|
+
<manifest identifier="#{course.title}" version="1"
|
27
|
+
xmlns="http://www.imsproject.org/xsd/imscp_rootv1p1p2"
|
28
|
+
xmlns:adlcp="http://www.adlnet.org/xsd/adlcp_rootv1p2"
|
29
|
+
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
30
|
+
xsi:schemaLocation="http://www.imsproject.org/xsd/imscp_rootv1p1p2 imscp_rootv1p1p2.xsd
|
31
|
+
http://www.imsglobal.org/xsd/imsmd_rootv1p2p1 imsmd_rootv1p2p1.xsd
|
32
|
+
http://www.adlnet.org/xsd/adlcp_rootv1p2 adlcp_rootv1p2.xsd">
|
33
|
+
<metadata>
|
34
|
+
<schema>ADL SCORM</schema>
|
35
|
+
<schemaversion>1.2</schemaversion>
|
36
|
+
</metadata>
|
37
|
+
<organizations default="default-org">
|
38
|
+
<organization identifier="default-org">
|
39
|
+
<title>#{course.title}</title>
|
40
|
+
#{generate_modules(course.course_modules)}
|
41
|
+
</organization>
|
42
|
+
</organizations>
|
43
|
+
<resources>
|
44
|
+
#{generate_resources(course.course_modules)}
|
45
|
+
<resource identifier="common_files" type="webcontent" adlcp:scormtype="asset">
|
46
|
+
<file href="scormfunctions.js"/>
|
47
|
+
</resource>
|
48
|
+
</resources>
|
49
|
+
</manifest>
|
50
|
+
XML
|
51
|
+
end
|
52
|
+
|
53
|
+
def generate_modules(modules)
|
54
|
+
modules.map.with_index(1) do |mod, mod_index|
|
55
|
+
<<~XML
|
56
|
+
<item identifier="module-#{mod_index}">
|
57
|
+
<title>#{mod.title}</title>
|
58
|
+
#{generate_lessons(mod.lessons, mod_index)}
|
59
|
+
</item>
|
60
|
+
XML
|
61
|
+
end.join
|
62
|
+
end
|
63
|
+
|
64
|
+
def generate_lessons(lessons, mod_index)
|
65
|
+
lessons.map.with_index(1) do |lesson, lesson_index|
|
66
|
+
<<~XML
|
67
|
+
<item identifier="module-#{mod_index}-lesson-#{lesson_index}" identifierref="resource-module-#{mod_index}-lesson-#{lesson_index}">
|
68
|
+
<title>#{lesson.title}</title>
|
69
|
+
</item>
|
70
|
+
XML
|
71
|
+
end.join
|
72
|
+
end
|
73
|
+
|
74
|
+
def generate_resources(modules)
|
75
|
+
modules.flat_map.with_index(1) do |mod, mod_index|
|
76
|
+
mod.lessons.map.with_index(1) do |_, lesson_index|
|
77
|
+
<<~XML
|
78
|
+
<resource identifier="resource-module-#{mod_index}-lesson-#{lesson_index}" type="webcontent" adlcp:scormtype="sco" href="content/module#{mod_index}/lesson#{lesson_index}.html">
|
79
|
+
<file href="content/module#{mod_index}/lesson#{lesson_index}.html" />
|
80
|
+
<dependency identifierref="common_files" />
|
81
|
+
</resource>
|
82
|
+
XML
|
83
|
+
end
|
84
|
+
end.join
|
85
|
+
end
|
86
|
+
|
87
|
+
def generate_lesson_html(lesson)
|
88
|
+
<<~HTML
|
89
|
+
<!DOCTYPE html>
|
90
|
+
<html lang="en">
|
91
|
+
<head>
|
92
|
+
<meta charset="UTF-8">
|
93
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
94
|
+
<script src="../../scormfunctions.js"></script>
|
95
|
+
<title>#{lesson.title}</title>
|
96
|
+
<style>
|
97
|
+
.loader { display: none; position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%); }
|
98
|
+
iframe { display: none; width: 560px; height: 315px; border: none; }
|
99
|
+
</style>
|
100
|
+
</head>
|
101
|
+
<body>
|
102
|
+
<p>Lesson: #{lesson.title}</p>
|
103
|
+
<ul>#{lesson.videos.map do |video|
|
104
|
+
"<li>Language: #{video[:language]}<br>" \
|
105
|
+
"<div id=\"loader-#{video[:id]}\" class=\"loader\">Loading...</div>" \
|
106
|
+
"<iframe id=\"custom-iframe-#{video[:id]}\" data-video-url=\"#{video[:video_url]}\"></iframe></li>"
|
107
|
+
end.join}</ul>
|
108
|
+
<script>
|
109
|
+
const loadIframe = async (iframe) => {
|
110
|
+
const loader = document.getElementById(`loader-${iframe.id.split('-').pop()}`);
|
111
|
+
try {
|
112
|
+
loader.style.display = 'block';
|
113
|
+
const response = await fetch(iframe.dataset.videoUrl, {
|
114
|
+
headers: { 'X-Scorm-Token': '#{scorm_token}' }
|
115
|
+
});
|
116
|
+
iframe.srcdoc = await response.text();
|
117
|
+
iframe.style.display = 'block';
|
118
|
+
} catch (error) { console.error(error); }
|
119
|
+
finally { loader.style.display = 'none'; }
|
120
|
+
};
|
121
|
+
window.addEventListener('load', () =>
|
122
|
+
document.querySelectorAll('iframe[data-video-url]').forEach(loadIframe)
|
123
|
+
);
|
124
|
+
</script>
|
125
|
+
</body>
|
126
|
+
</html>
|
127
|
+
HTML
|
128
|
+
end
|
129
|
+
|
130
|
+
def generate_lesson_contents
|
131
|
+
lessons_hash = {}
|
132
|
+
|
133
|
+
course.course_modules.each_with_index do |mod, mod_index|
|
134
|
+
mod.lessons.each_with_index do |lesson, lesson_index|
|
135
|
+
lesson_path = "content/module#{mod_index + 1}/lesson#{lesson_index + 1}.html"
|
136
|
+
lessons_hash[lesson_path] = generate_lesson_html(lesson)
|
137
|
+
end
|
138
|
+
end
|
139
|
+
lessons_hash
|
140
|
+
end
|
141
|
+
end
|
142
|
+
end
|
143
|
+
end
|
@@ -0,0 +1,11 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "scorm_package/version"
|
4
|
+
require_relative "scorm_package/abstract_course"
|
5
|
+
require_relative "scorm_package/abstract_course_module"
|
6
|
+
require_relative "scorm_package/abstract_lesson"
|
7
|
+
require_relative "scorm_package/packaging/generate"
|
8
|
+
|
9
|
+
module ScormPackage
|
10
|
+
class Error < StandardError; end
|
11
|
+
end
|
metadata
ADDED
@@ -0,0 +1,62 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: scorm-package
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Paul Sojan
|
8
|
+
autorequire:
|
9
|
+
bindir: exe
|
10
|
+
cert_chain: []
|
11
|
+
date: 2025-01-29 00:00:00.000000000 Z
|
12
|
+
dependencies: []
|
13
|
+
description:
|
14
|
+
email:
|
15
|
+
- paul.eliassojan@gmail.com
|
16
|
+
executables: []
|
17
|
+
extensions: []
|
18
|
+
extra_rdoc_files: []
|
19
|
+
files:
|
20
|
+
- ".rspec"
|
21
|
+
- ".rubocop.yml"
|
22
|
+
- LICENSE.txt
|
23
|
+
- README.md
|
24
|
+
- Rakefile
|
25
|
+
- lib/scorm_package.rb
|
26
|
+
- lib/scorm_package/abstract_course.rb
|
27
|
+
- lib/scorm_package/abstract_course_module.rb
|
28
|
+
- lib/scorm_package/abstract_lesson.rb
|
29
|
+
- lib/scorm_package/packaging/common/adlcp_rootv1p2.xsd
|
30
|
+
- lib/scorm_package/packaging/common/ims_xml.xsd
|
31
|
+
- lib/scorm_package/packaging/common/imscp_rootv1p1p2.xsd
|
32
|
+
- lib/scorm_package/packaging/common/imsmd_rootv1p2p1.xsd
|
33
|
+
- lib/scorm_package/packaging/common/scormfunctions.js
|
34
|
+
- lib/scorm_package/packaging/create_zip.rb
|
35
|
+
- lib/scorm_package/packaging/generate.rb
|
36
|
+
- lib/scorm_package/version.rb
|
37
|
+
- sig/scorm_package.rbs
|
38
|
+
homepage: https://github.com/openvitae-tech/scorm-package
|
39
|
+
licenses:
|
40
|
+
- MIT
|
41
|
+
metadata:
|
42
|
+
homepage_uri: https://github.com/openvitae-tech/scorm-package
|
43
|
+
post_install_message:
|
44
|
+
rdoc_options: []
|
45
|
+
require_paths:
|
46
|
+
- lib
|
47
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
48
|
+
requirements:
|
49
|
+
- - ">="
|
50
|
+
- !ruby/object:Gem::Version
|
51
|
+
version: 3.0.0
|
52
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
53
|
+
requirements:
|
54
|
+
- - ">="
|
55
|
+
- !ruby/object:Gem::Version
|
56
|
+
version: '0'
|
57
|
+
requirements: []
|
58
|
+
rubygems_version: 3.5.3
|
59
|
+
signing_key:
|
60
|
+
specification_version: 4
|
61
|
+
summary: Gem to create scorm package.
|
62
|
+
test_files: []
|