Skip to content

Instantly share code, notes, and snippets.

@reginadiana
Last active August 11, 2024 12:04
Show Gist options
  • Save reginadiana/5122fe30f7d8066f449475bd709a3e94 to your computer and use it in GitHub Desktop.
Save reginadiana/5122fe30f7d8066f449475bd709a3e94 to your computer and use it in GitHub Desktop.

SCORM

É uma forma de compartilhar recursos de e-learning entre plataformas e empresas. A versão 1.2 é a mais utilizada, mesmo após a sua atualização em 2004. É algo antigo, sendo uma forma de compartilhar cursos de forma padronizada. Precisamos ler o pacote SCORM,. Fazer um código ruby que le o manifesto e monta o iframe servido no controller do rails

A primeira etapa é a leitura do arquivo, a segunda é leitura do data model, na terceira podemos fazer dados serem retornados para o SCORM. Guardar dados em local storage. 80% dos problemas está no zip, pois nem todos seguem a risca o protocolo.

Não é possivel abrir o index do pacote SCORM manualmente e exeutá-lo. Isso não vai funcionar por conta do CORS. O que vai funcionar é executar o pacote dentro de um http, dentro de um servidor.

O SCORM especifica que o conteúdo deve:

=> Ser empacotado em um arquivo .zip/pif => Ser descrito em um arquivo XML => Comunique-se por meio de javascript => Sequencie conteúdo via XML => Não conter pastas vazias

content packaging especifica como o conteúdo deve ser empacotado e descrito (XML)

run-time especifica como o conteúdo deve ser entregue e como se comunica com o LMS

sequenciamento especifica como o aluno pode navegar entre as partes do curso, também conhecidas como SCO

O LMS só pode lançar um SCO por vez.

Dados que podem ser resgatados pós interação

=> Tempo em que o aluno passou em cada SCO => Status de aprendizado (aprovado/reprovado/etc) => Pontuação do aluno

A principal diferença entre um SCO e um ativo é que um SCO pode se comunicar com um LMS, enquanto um ativo é simplesmente um arquivo estático que é apresentado ao usuário.

content aggregation model (CAM) especifica como o curso deve ser empacotado para ser importado no LMS

A comunicação do curso com o LMS são feitas através de funções do javascript localizadas no mesma janela/iframe em que o pacote SCORM é chamado, e se essa condição não for satisfeita, as funções não serão chamadas. Existem 8 funções que o LMS deve usar como callback.

Os dados sobre os estados do curso estão armazenados nos cookies, e isso é feito pela própria execução do pacote SCORM. Se o limparmos, todo o progesso do curso será perdido.

O parametro src do iframe deve estar igual ao que é especificado no arquivo XLM.

SCORM Run-time

Controla como o LMS lança o conteúdo e como este vai se comunicar com o LMS.

A navegação entre SCO (partes do curso) acontece de acordo com o xml. Apenas ele pode iniciar a comunicação.

LMS

É uma entidade passiva que simplesmente responde as chamadas de API feitas pelo SCO (conteúdo). Ele não pode inicializar nenhuma comunicação, ele simplesmente lança o conteúdo e responde as solicitações.

Funções LMS

O LMS fornece 8 funções que podem ser chamadas/invocadas pelo SCO.

Função Descrição
LMSInitialize Primeira comunicação
LMSGetValue Permite que o SCO receba dados do LMS. Os dados recebidos são sempre os definidos em data modal elements
LMSSetValue Permite que o SCO persista dados no LMS. Os dados são sempre armazenados nas chaves correspondentes e definidas em data modal elements
LMSCommit Indica que o dado foi persistido em data model
LMSGetLastError É chamado apenas para verificar se um erro aconteceu. Se o status/código for diferente do 0, as funções LMSGetErrorString e LMSGetDiagnostic poderão ser chamadas para sabermos o que ocorreu
LMSFinish Ultima chamada

Data Modals

São parametros que podem ser definidos para receber e enviar dados.

Data Model Descrição
cmi.success_status Indica que o usuário terminou um curso com sucesso e que "passou" no teste.

Iframe

image

Caminho:

  • Criar uma conta no SCORM Cloud do tipo Free Trial (ela fornece o limite de 10 registros e 100MB por curso (upload)).
  • Criar curso via interface.
  • Criar aluno via interface.
  • Realizar a autenticação via Oauth2 usando token
  • Realizar a autenticação via HTTP Basic Auth usando as chaves APP_ID e SECRET_KEY (elas serão equivalentes ao login e senha). Podemos "ativar" a secret key pela chave verde.

image

image

image

image

  • "Bater" em qualquer endpont do SCORM Cloud API. Vale lembrar, que em todos os endpoints é obrigatório estar autenticado.

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

  • Listar cursos e pegar informações do curso criado via interface
  • Listar registros
  • Criar novo registro

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

image

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.

  • Criar aluno

  • Acessar execução pacote SCORM em pop-up.

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

  • Excluir um curso. Ao excluir o curso, não é mais possível acessá-lo e o evento é registrado na interface:

🐺 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.

Referencias

📚 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.

CallBacks do SCORM

(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);

Controller do SCORM

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

Data Model

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

@reginadiana
Copy link
Author

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

<iframe
  src=<%= @scorm_package_path %>
  title="Iframe Example"
  name="API"
></iframe>

<!-- Adicionei esse botão para inicializar a comunicação, 
pois diretamente no no script o API Wrapper 
não encontra as funções do RTE de imediato -->
<button onclick="initializeLMS()">Init</button>

<script 
  type="text/javascript" 
  src=<%= @api_wrapper_path %>
>
</script>

<script>
/* Precisa ser difinido para o API Wrapper saber quais funções callback chamar */
pipwerks.SCORM.version = "1.2";
/* Precisa ser setada como verdadeira para permitir o debug com trace dentro do arquivo SCORM API Wrapper*/
pipwerks.debug.isActive = true;

function initializeLMS() {
  // Inicia comunicação com LMS, chama as funções injetadas no pacote
  pipwerks.SCORM.init();

  // Tentando capturar uma informação
  pipwerks.SCORM.data.get('cmi.core.lesson_location');
}
</script>

<style>
  iframe {
    width: 100vw;
    height: 100vh;
  }
</style>

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment