Ao bater em qualquer endpont, o seguinte log é apresentado no console:
ETHON: performed EASY effective_url=https://cloud.scorm.com/api/v2/registrations/2/launchLink response_code=200 return_code=ok total_time=0.400103
O registro que está sendo mostrado no gráfico foi criado via Rails. O ID do registro (REGISTRATION_ID
) precisa ser único. O ID do aluno (LEARNER_ID
) foi resgatado da URL do aluno via interface, pegando o parâmetro personUserId
Um ponto importante sobre a API é que ela não gerencia os ID's, ou seja, toda vez que formos criar algo na nossa aplicação precisamos passar o ID como parâmetro. Ela pode ser do tipo string, inteiro ou qualquer outro.
Ao chamar o endpoint https://cloud.scorm.com/api/v2/registrations/2/launchLink
levando como parâmetro o ID do registro, é devolvido um link que, ao acessá-lo, é possível ver a execução do SCORM. Toda vez que esse endpont é chamado um novo link é criado e o anterior (se existir) passa a ser inválido. Exemplo: https://cloud.scorm.com/api/cloud/registration/launch/a9dcccce-2e43-40f1-b887-83788e5844ff
🐺 Package/Couse: Representa o conteúdo que queremos distribuir.
🐺 Dispatch: Representa o arquivo .zip
🐺 Registration: Representa o registro. É uma conexão feita entre o pacote e o aluno. Para que o aluno possa ter acesso ao curso, um registro deve ser criado antes, como uma forma de matricula.
🐺 SCOs: Representa uma parte de um curso.
🐺 Manifest: Arquivo XML chamado de imsmanifest.xml que armazena as informações de como será montado o SCORM.
🐺 LMS:
🐺 launch_link: Representa o link que, ao acessar, será possível ver o pacote SCORM em execução/lançamento.
📚 Documentação da API SCORM Cloud
📚 Gem do SCORM Cloud API para lidar com o Ruby
📚 Getting Started with SCORM: How does SCORM really work?
📚 Getting Started With SCORM: The basics of initializing/closing a SCO and sending/receiving data
📚 Getting Started with SCORM: Tracking Course Specific Data
📚 SCORM Run-Time Reference Guide
require 'rustici_software_cloud_v2'
require 'open-uri'
include RusticiSoftwareCloudV2
# ScormCloud API credentials
# Note: These are not the same credentials used to log in to ScormCloud
APP_ID = 'AXC9X4CEO8'
SECRET_KEY = 'LLSsC4OvkcDaJUZHvsLMF0wmEq7E0JMNU6vGIV1i'
# Sample values for data
COURSE_PATH = '/'
COURSE_ID = 'CEA_COURSE_ID' # ID do curso submetido via interface e gerado pelo site Easy Generator
LEARNER_ID = 'LEANER_ID_CEA' # ID do 'Aluno Test'
REGISTRATION_ID = 'REGISTRATION_ID_CEA' # ID de um registro, ele pode ser uma string também
# String used for output formatting
OUTPUT_BORDER = "---------------------------------------------------------\n\n"
# This sample will consist of:
# 1. Creating a course.
# 2. Registering a learner for the course.
# 3. Building a link for the learner to take the course.
# 4. Getting the learner's progress after having taken the course.
# 5. Viewing all courses and registrations.
# 6. Deleting all of the data created via this sample.
#
# All input variables used in this sample are defined up above.
class ScormCloud
# Sets the default OAuth token passed with all calls to the API.
#
# If a token is created with limited scope (i.e. read:registration),
# calls that require a different permission set will error. Either a
# new token needs to be generated with the correct scope, or the
# default access token can be reset to nil. This would cause the
# request to be made with basic auth credentials (appId/ secret key)
# instead.
#
# Additionally, you could create a new configuration object and set
# the token on that object instead of the default access token. This
# configuration would then be passed into the Api object:
#
# config = Configuration.new
# token_request = TokenRequestSchema.new({
# permissions: PermissionsSchema.new({
# scopes: [ "write:course", "read:course" ]
# }),
# expiry: (Time.now + 2 * 60).iso8601
# })
# config.access_token = app_management_api.create_token(token_request).result
# course_api = CourseApi.new(ApiClient.new(config))
#
# Any calls that would use this CourseApi instance would then have the
# write:course and read:course permissions passed automatically, but
# other instances would be unaffected and continue to use other means
# of authorization.
#
# @param [Array<String>] :scopes List of permissions for calls made with the token.
private def configure_oauth(scopes)
app_management_api = ApplicationManagementApi.new
# Set permissions and expiry time of the token
# The expiry expected for token request must be in ISO-8601 format
expiry = (Time.now + 2 * 60).iso8601
permissions = PermissionsSchema.new({ scopes: scopes })
# Make the request to get the OAuth token
token_request = TokenRequestSchema.new({ permissions: permissions, expiry: expiry })
token_result = app_management_api.create_token(token_request)
# Set the default access token used with further API requests.
# To remove the token, reset config.access_token back to nil
# before the next call.
RusticiSoftwareCloudV2.configure do |config|
config.access_token = token_result.result
end
nil
end
def authenticate
RusticiSoftwareCloudV2.configure do |config|
# Configure HTTP basic authorization: APP_NORMAL
config.username = APP_ID
config.password = SECRET_KEY
end
end
def registration_progress
authenticate
sc = ScormCloud.new
sc.get_result_for_registration(REGISTRATION_ID)
end
def init()
authenticate
sc = ScormCloud.new
# Get information about all the courses in ScormCloud
course_list = sc.get_all_courses()
# Show details of the courses
puts OUTPUT_BORDER
puts 'Course List: '
course_list.each do |course|
puts course
end
# Get information about all the registrations in ScormCloud
registration_list = sc.get_all_registrations()
# Show details of the registrations
puts OUTPUT_BORDER
puts 'Registration List: '
registration_list.each do |registration|
puts registration
end
# Create a registration
# A cada novo registro, o id precisa ser diferente, unico
# vsc.create_registration(COURSE_ID, LEARNER_ID, REGISTRATION_ID)
# Create the registration launch link
# Toda vez que o build for executado um novo link será gerado
# Ao clicar no link, um pop-up é aberto com o SCORM sendo executado
# ATENÇÃO: toda vez que um novo link é gerado, o anterior passa a ser INVALIDO
# Precisa ter um registro antes de gerar o link
launch_link = sc.build_launch_link(REGISTRATION_ID)
# Show the launch link
puts OUTPUT_BORDER
puts "Launck Link: #{launch_link}"
puts 'Navigate to the url above to take the course. Hit enter once complete.'
# Get the results for the registration
registration_progress = sc.get_result_for_registration(REGISTRATION_ID)
# Show details of the registration progress
puts OUTPUT_BORDER
puts 'Registration Progress: '
puts registration_progress
# Delete all the data created by this sample
# CUIDADO, isso irá excluir todos os dados
# sc.clean_up(COURSE_ID, REGISTRATION_ID)
# Create a course
# course_details = sc.create_course(1, path)
launch_link
end
# Creates a course by uploading the course from your local machine.
# Courses are a package of content for a learner to consume.
#
# Other methods for importing a course exist. Check the documentation
# for additional ways of importing a course.
#
# @param [String] :course_id Id that will be used to identify the course.
# @param [String] :course_path Path to the course being uploaded.
# @return [CourseSchema] Detailed information about the newly uploaded course.
def create_course(course_id, course_path)
# (Optional) Further authenticate via OAuth token access
# configure_oauth([ "write:course", "read:course" ])
# This call will use OAuth with the "write:course" scope
# if configured. Otherwise the basic auth credentials will be used
course_api = CourseApi.new
job_id = course_api.create_upload_and_import_course_job(course_id, { file: open(course_path) })
# This call will use OAuth with the "read:course" scope
# if configured. Otherwise the basic auth credentials will be used
job_result = course_api.get_import_job_status(job_id.result)
while job_result.status == 'RUNNING'
sleep(1)
job_result = course_api.get_import_job_status(job_id.result)
end
if job_result.status == 'ERROR'
raise ArgumentError.new('Course is not properly formatted: ' + job_result.message)
end
job_result.import_result.course
end
# Creates a registration allowing the learner to consume the course
# content. A registration is the link between a learner and a single
# course.
#
# @param [String] :course_id Id of the course to register the learner for.
# @param [String] :learner_id Id that will be used to identify the learner.
# @param [String] :registration_id Id that will be used to identify the registration.
def create_registration(course_id, learner_id, registration_id)
# (Optional) Further authenticate via OAuth token access
# configure_oauth([ "write:registration" ])
registration_api = RegistrationApi.new
learner = LearnerSchema.new({ id: learner_id })
registration = CreateRegistrationSchema.new({ courseId: course_id, learner: learner, registrationId: registration_id })
registration_api.create_registration(registration)
nil
end
# Builds a url allowing the learner to access the course.
#
# This sample will build the launch link and print it out. It will then
# pause and wait for user input, allowing you to navigate to the course
# to generate sample learner progress. Once this step has been reached,
# hitting the enter key will continue program execution.
#
# @param [String] :registration_id Id of the registration the link is being built for.
# @return [String] Link for the learner to launch the course.
def build_launch_link(registration_id)
# (Optional) Further authenticate via OAuth token access
# configure_oauth([ "read:registration" ])
registration_api = RegistrationApi.new
settings = LaunchLinkRequestSchema.new({ redirectOnExitUrl: 'Message' })
launch_link = registration_api.build_registration_launch_link(registration_id, settings)
launch_link.launch_link
end
# Gets information about the progress of the registration.
#
# For the most up-to-date results, you should implement our postback
# mechanism. The basic premise is that any update to the registration
# would cause us to send the updated results to your system.
#
# More details can be found in the documentation:
# https://cloud.scorm.com/docs/v2/guides/postback/
#
# @param [String] :registration_id Id of the registration to get results for.
# @return [RegistrationSchema] Detailed information about the registration's progress.
def get_result_for_registration(registration_id)
# (Optional) Further authenticate via OAuth token access
# configure_oauth([ "read:registration" ])
registration_api = RegistrationApi.new
progress = registration_api.get_registration_progress(registration_id)
progress
end
# Gets information about all courses. The result received from the API
# call is a paginated list, meaning that additional calls are required
# to retrieve all the information from the API. This has already been
# accounted for in the sample.
#
# @return [Array<CourseSchema>] List of detailed information about all of the courses.
def get_all_courses()
# (Optional) Further authenticate via OAuth token access
# configure_oauth([ "read:course" ])
# Additional filters can be provided to this call to get a subset
# of all courses.
course_api = CourseApi.new
response = course_api.get_courses()
# This call is paginated, with a token provided if more results exist
course_list = response.courses
until response.more.nil?
response = course_api.get_courses({ more: response.more })
course_list += response.courses
end
course_list
end
# Gets information about the registration progress for all
# registrations. The result received from the API call is a paginated
# list, meaning that additional calls are required to retrieve all the
# information from the API. This has already been accounted for in the
# sample.
#
# This call can be quite time-consuming and tedious with lots of
# registrations. If you find yourself making lots of calls to this
# endpoint, it might be worthwhile to look into registration postbacks.
#
# More details can be found in the documentation:
# https://cloud.scorm.com/docs/v2/guides/postback/
#
# @return [Array<RegistrationSchema>] List of detailed information about all of the registrations.
def get_all_registrations()
# (Optional) Further authenticate via OAuth token access
# configure_oauth([ "read:registration" ])
# Additional filters can be provided to this call to get a subset
# of all registrations.
registration_api = RegistrationApi.new
response = registration_api.get_registrations()
# This call is paginated, with a token provided if more results exist
registration_list = response.registrations
until response.more.nil?
response = registration_api.get_registrations({ more: response.more })
registration_list += response.registrations
end
registration_list
end
# Deletes all of the data generated by this sample.
# This code is run even if the program has errored out, providing a
# "clean slate" for every run of this sample.
#
# It is not necessary to delete registrations if the course
# they belong to has been deleted. Deleting the course will
# automatically queue deletion of all registrations associated with
# the course. There will be a delay between when the course is deleted
# and when the registrations for the course have been removed. The
# registration deletion has been handled here to prevent scenarios
# where the registration hasn't been deleted yet by the time the
# sample has been rerun.
#
# @param [String] :course_id Id of the course to delete.
# @param [String] :registration_id Id of the registration to delete.
def clean_up(course_id, registration_id)
# (Optional) Further authenticate via OAuth token access
# configure_oauth([ "delete:course", "delete:registration" ])
# This call will use OAuth with the "delete:course" scope
# if configured. Otherwise the basic auth credentials will be used
course_api = CourseApi.new
course_api.delete_course(course_id)
# The code below is to prevent race conditions if the
# sample is run in quick successions.
# This call will use OAuth with the "delete:registration" scope
# if configured. Otherwise the basic auth credentials will be used
registration_api = RegistrationApi.new
registration_api.delete_registration(registration_id)
nil
end
end
if __FILE__ == $0
main
end
Every SCO has its own set of run-time data. Each of these data model elements has a separate value for each SCO within a course, data model elements are not shared across SCOs. Furthermore, each “attempt” on a SCO has it’s own set of run-time data. When the learner starts a new attempt on a SCO, the data model values will be reset for the start of the new attempt.
(function(window) {
values = {
'cmi.core.student_id': '1',
'cmi.core.student_name': 'leandronsp',
'cmi.core.lesson_status': 'completed',
'cmi.core.lesson_mode': 'review',
'cmi.core.exit': 'suspend'
}
window.API = {
LMSInitialize: function() {
console.log('LMSInitialize')
return true;
},
LMSGetValue: function(param) {
value = values[param] || ''
console.log(LMSGetValue: ${param} = ${value}
)
return value
},
LMSSetValue: function(param, value) {
console.log(`LMSSetValue: ${param} = ${value}`)
values[param] = value;
//console.log("Quantidade de lições:", value.split(':**l').length - 1);
return true;
},
LMSGetLastError: function() {
// console.log("LMSGetLastError called");
return '0';
},
LMSGetErrorString: function(errorCode) {
alert("LMSGetErrorString called", errorCode);
return '';
},
LMSGetDiagnostic: function(errorCode) {
alert("LMSGetDiagnostic called", errorCode);
return '';
},
LMSCommit: function() {
// console.log("LMSCommit called");
return true;
},
LMSFinish: function() {
console.log('LMSFinish');
return true;
},
};
})(window);
require 'zip'
require 'open-uri'
class ScormController < ApplicationController
def index
# Pega o blob do pdf do ultimo treinamento (whatever)
# training = Training.find(params[:id])
# blob = training.file.blob
# # Abre arquivo na pasta public
# FileUtils.mkdir_p('public/uploads/')
# my_zip = Rails.root.join('public', 'uploads', 'my_zip.zip')
# # Abre arquivo com permissão de edição
# File.open(my_zip, 'wb+') do |file|
# # Faz o download do blob dentro do arquivo
# blob.download { |chunk| file.write(chunk) }
# end
# extract_zip(my_zip, 'public/scorm_package')
# Link para um .zip na aws
# https://storageawsv1.s3.amazonaws.com/cea.zip
# Tentando descompactar .zip do bucket para a pasta publica
# extract_zip('http://localhost:9000/bucket/cea.zip', 'public/scorm_package/')
# Puxando da pasta publica. Renderiza e chama callbacks
@scorm_package_path = "/scorm/#{params[:id]}/course/index.html?SCORM=true"
# Puxando do bucket, diretório aberto. Retorna bloqueio de cross origin
# @scorm_package_path = 'http://localhost:9000/bucket/CEA081_202103291844/index.html?SCORM=true'
# Puxando do s3. Retorna bloqueio de cross origin
# @scorm_package_path = 'https://storageawsv1.s3.amazonaws.com/CEA081_202103291844/index.html'
# Redireciona, executa pacote, mas não chama callbacks
# redirect_to 'http://localhost:9000/bucket/CEA081_202103291844/index.html?SCORM=true'
# Redireciona para o s3, executa pacote, mas não chama callbacks
# redirect_to 'https://storageawsv1.s3.amazonaws.com/CEA081_202103291844/index.html', allow_other_host: true
end
def extract_zip(file, destination)
FileUtils.mkdir_p(destination)
Zip::File.open(file) do |zip_file|
zip_file.each do |f|
fpath = File.join(destination, f.name)
zip_file.extract(f, fpath) unless File.exist?(fpath)
end
end
end
end
cmi.core.student_id
"cmi.suspend_data": "{Posicao:{pagina:11,licao:01,modulo:01}}",
comunicação.js
cmi.core.score.raw, cmi.core.lesson_status, cmi.core.student_name, cmi.suspend_data, cmi.core.student_id
https://cloud.scorm.com/docs/v2/tutorials/getting_started/client_libraries/
https://cloud.scorm.com/api/cloud/registration/launch/a9dcccce-2e43-40f1-b887-83788e5844ff
Estava investigando sobre o SCORM e o software rustici (scorm cloud) e fiquei com algumas duvidas que queria ver com voce antes de passar para o Pedro, principalmente sobre o que tange aos custos.
Acho que a primeira duvida é: vamos bater o martelo sobre usar o rustici ou vamos considerar procurar outros serviços?
A Academia da Moda também serve o pacote SCORM para os alunos. O time de desenvolvimento usa alguma plataforma externa (paga ou não) para isso? Como é que funciona lá dentro?
Sobre o Rustici, os planos mensais disponíveis são esses da foto. Criei uma conta usando o Free Tial mesmo só pra teste, mas ele é bem limitado em termos de Registrations e Storage (tamanho do zip que pode ser submetido no upload).
Registro é o recurso que vai conectar o aluno ao curso, como se fosse uma especie de matricula. Sem ele, não conseguimos "ouvir" os estados do pacote (onde o aluno parou, quando abriu o curso, quanto tempo ficou lá, pontuação, etc).
Toda vez que um aluno se matricula em um curso, um registro, ou seja, matricula, é gerada. Exemplo: se temos 5 alunos e 1 curso cadastrado, precisamos de 5 registros. Se temos 5 alunos e 5 cursos, precisamos de 25 registros.
Só aparece o alerta quando a API não é encontrada