Skip to content

Commit

Permalink
Merge pull request #449 from doubtfire-lms/new/scorm
Browse files Browse the repository at this point in the history
New/scorm
  • Loading branch information
macite authored Oct 25, 2024
2 parents d750c61 + 715ccaf commit 26ac143
Show file tree
Hide file tree
Showing 44 changed files with 2,120 additions and 174 deletions.
6 changes: 6 additions & 0 deletions app/api/api_root.rb
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ class ApiRoot < Grape::API
mount BreaksApi
mount DiscussionCommentApi
mount ExtensionCommentsApi
mount ScormExtensionCommentsApi
mount GroupSetsApi
mount LearningOutcomesApi
mount LearningAlignmentApi
Expand All @@ -81,6 +82,8 @@ class ApiRoot < Grape::API
mount Tii::TiiGroupAttachmentApi
mount Tii::TiiActionApi

mount ScormApi
mount TestAttemptsApi
mount CampusesPublicApi
mount CampusesAuthenticatedApi
mount TutorialsApi
Expand All @@ -101,6 +104,7 @@ class ApiRoot < Grape::API
AuthenticationHelpers.add_auth_to BreaksApi
AuthenticationHelpers.add_auth_to DiscussionCommentApi
AuthenticationHelpers.add_auth_to ExtensionCommentsApi
AuthenticationHelpers.add_auth_to ScormExtensionCommentsApi
AuthenticationHelpers.add_auth_to GroupSetsApi
AuthenticationHelpers.add_auth_to LearningOutcomesApi
AuthenticationHelpers.add_auth_to LearningAlignmentApi
Expand All @@ -127,6 +131,8 @@ class ApiRoot < Grape::API
AuthenticationHelpers.add_auth_to UnitRolesApi
AuthenticationHelpers.add_auth_to UnitsApi
AuthenticationHelpers.add_auth_to WebcalApi
AuthenticationHelpers.add_auth_to ScormApi
AuthenticationHelpers.add_auth_to TestAttemptsApi

add_swagger_documentation \
base_path: nil,
Expand Down
36 changes: 27 additions & 9 deletions app/api/authentication_api.rb
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
class AuthenticationApi < Grape::API
helpers LogHelper
helpers AuthenticationHelpers
helpers AuthorisationHelpers

#
# Sign in - only mounted if AAF auth is NOT used
Expand Down Expand Up @@ -71,7 +72,7 @@ class AuthenticationApi < Grape::API

# Return user details
present :user, user, with: Entities::UserEntity
present :auth_token, user.generate_authentication_token!(remember).authentication_token
present :auth_token, user.generate_authentication_token!(remember: remember).authentication_token
end
end

Expand Down Expand Up @@ -237,18 +238,18 @@ class AuthenticationApi < Grape::API
requires :auth_token, type: String, desc: 'The user\'s temporary auth token'
end
post '/auth' do
error!({ error: 'Invalid token.' }, 404) if params[:auth_token].nil?
logger.info "Get user via auth_token from #{request.ip}"
error!({ error: 'Invalid authentication details.' }, 404) if params[:auth_token].blank? || params[:username].blank?
logger.info "Get user via auth_token from #{request.ip} - #{params[:username]}"

# Authenticate that the token is okay
if authenticated?
if authenticated?(:login)
user = User.find_by(username: params[:username])
token = user.token_for_text?(params[:auth_token]) unless user.nil?
error!({ error: 'Invalid token.' }, 404) if token.nil?
token = user.token_for_text?(params[:auth_token], :login) unless user.nil?
error!({ error: 'Invalid authentication details.' }, 404) if token.nil?

# Invalidate the token and regenrate a new one
token.destroy!
token = user.generate_authentication_token! true
token = user.generate_authentication_token!

logger.info "Login #{params[:username]} from #{request.ip}"

Expand Down Expand Up @@ -324,7 +325,7 @@ class AuthenticationApi < Grape::API

# Find user
user = User.find_by(username: user_param)
token = user.token_for_text?(token_param) unless user.nil?
token = user.token_for_text?(token_param, :general) unless user.nil?
remember = params[:remember] || false

# Token does not match user
Expand Down Expand Up @@ -359,7 +360,7 @@ class AuthenticationApi < Grape::API
}
delete '/auth' do
user = User.find_by(username: headers['username'] || headers['Username'])
token = user.token_for_text?(headers['auth-token'] || headers['Auth-Token']) unless user.nil?
token = user.token_for_text?(headers['auth-token'] || headers['Auth-Token'], :general) unless user.nil?

if token.present?
logger.info "Sign out #{user.username} from #{request.ip}"
Expand All @@ -368,4 +369,21 @@ class AuthenticationApi < Grape::API

present nil
end

desc 'Get SCORM authentication token'
get '/auth/scorm' do
if authenticated?(:general)
unless authorise? current_user, User, :get_scorm_token
error!({ error: 'You cannot get SCORM tokens' }, 403)
end

token = current_user.auth_tokens.find_by(token_type: :scorm)
if token.nil? || token.auth_token_expiry <= Time.zone.now
token&.destroy
token = current_user.generate_scorm_authentication_token!
end

present :scorm_auth_token, token.authentication_token
end
end
end
6 changes: 6 additions & 0 deletions app/api/entities/task_definition_entity.rb
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,12 @@ def staff?(my_role)
expose :has_task_sheet?, as: :has_task_sheet
expose :has_task_resources?, as: :has_task_resources
expose :has_task_assessment_resources?, as: :has_task_assessment_resources, if: ->(unit, options) { staff?(options[:my_role]) }
expose :has_scorm_data?, as: :has_scorm_data
expose :scorm_enabled
expose :scorm_allow_review
expose :scorm_bypass_test
expose :scorm_time_delay_enabled
expose :scorm_attempt_limit
expose :is_graded
expose :max_quality_pts
expose :overseer_image_id, if: ->(unit, options) { staff?(options[:my_role]) }, expose_nil: false
Expand Down
1 change: 1 addition & 0 deletions app/api/entities/task_entity.rb
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ class TaskEntity < Grape::Entity
end

expose :extensions
expose :scorm_extensions

expose :times_assessed
expose :grade, expose_nil: false
Expand Down
12 changes: 12 additions & 0 deletions app/api/entities/test_attempt_entity.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
module Entities
class TestAttemptEntity < Grape::Entity
expose :id
expose :task_id
expose :attempted_time
expose :terminated
expose :success_status
expose :score_scaled
expose :completion_status
expose :cmi_datamodel
end
end
71 changes: 71 additions & 0 deletions app/api/scorm_api.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
require 'grape'
require 'zip'
require 'mime/types'
class ScormApi < Grape::API
# Include the AuthenticationHelpers for authentication functionality
helpers AuthenticationHelpers
helpers AuthorisationHelpers

before do
authenticated? :scorm
end

helpers do
# Method to stream a file from a zip archive at the specified path
# @param zip_path [String] the path to the zip archive
# @param file_path [String] the path of the file within the zip archive
def stream_file_from_zip(zip_path, file_path)
file_stream = nil

logger.debug "Streaming zip file at #{zip_path}"
# Get an input stream for the requested file within the ZIP archive
Zip::File.open(zip_path) do |zip_file|
zip_file.each do |entry|
next unless entry.name == file_path
logger.debug "Found file #{file_path} from SCORM container"
file_stream = entry.get_input_stream
break
end
end

# If the file was not found in the ZIP archive, return a 404 response
unless file_stream
error!({ error: 'File not found' }, 404)
end

# Set the content type based on the file extension
content_type = MIME::Types.type_for(file_path).first.content_type
logger.debug "Content type: #{content_type}"

# Set the content type header
header 'Content-Type', content_type

# Set cache control header to prevent caching
header 'Cache-Control', 'no-cache, no-store, must-revalidate'

# Set the body to the contents of the file_stream and return the response
body file_stream.read
end
end

desc 'Serve SCORM content'
params do
requires :task_def_id, type: Integer, desc: 'Task Definition ID to get SCORM test data for'
end
get '/scorm/:task_def_id/:username/:auth_token/*file_path' do
task_def = TaskDefinition.find(params[:task_def_id])

unless authorise? current_user, task_def.unit, :get_unit
error!({ error: 'You cannot access SCORM tests of unit' }, 403)
end

env['api.format'] = :txt
if task_def.has_scorm_data?
zip_path = task_def.task_scorm_data
content_type 'application/octet-stream'
stream_file_from_zip(zip_path, params[:file_path])
else
error!({ error: 'SCORM data does not exist.' }, 404)
end
end
end
54 changes: 54 additions & 0 deletions app/api/scorm_extension_comments_api.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
require 'grape'

class ScormExtensionCommentsApi < Grape::API
helpers AuthenticationHelpers
helpers AuthorisationHelpers

desc 'Request a scorm extension for a task'
params do
requires :comment, type: String, desc: 'The details of the request'
end
post '/projects/:project_id/task_def_id/:task_definition_id/request_scorm_extension' do
project = Project.find(params[:project_id])
task_definition = project.unit.task_definitions.find(params[:task_definition_id])
task = project.task_for_task_definition(task_definition)

# check permissions using specific permission has with addition of request extension if allowed in unit
unless authorise? current_user, task, :request_scorm_extension
error!({ error: 'Not authorised to request a scorm extension for this task' }, 403)
end

if task_definition.scorm_attempt_limit == 0
error!({ message: 'This task allows unlimited attempts to complete the test' }, 400)
return
end

result = task.apply_for_scorm_extension(current_user, params[:comment])
present result.serialize(current_user), Grape::Presenters::Presenter
end

desc 'Assess a scorm extension for a task'
params do
requires :granted, type: Boolean, desc: 'Assess a scorm extension'
end
put '/projects/:project_id/task_def_id/:task_definition_id/assess_scorm_extension/:task_comment_id' do
project = Project.find(params[:project_id])
task_definition = project.unit.task_definitions.find(params[:task_definition_id])
task = project.task_for_task_definition(task_definition)

unless authorise? current_user, task, :assess_scorm_extension
error!({ error: 'Not authorised to assess a scorm extension for this task' }, 403)
end

task_comment = task.all_comments.find(params[:task_comment_id]).becomes(ScormExtensionComment)

unless task_comment.assess_scorm_extension(current_user, params[:granted])
if task_comment.errors.count >= 1
error!({ error: task_comment.errors.full_messages.first }, 403)
else
error!({ error: 'Error saving scorm extension' }, 403)
end
end
present task_comment.serialize(current_user), Grape::Presenters::Presenter
end
end
Loading

0 comments on commit 26ac143

Please sign in to comment.