Skip to content

Commit

Permalink
Merge branch 'new/numbas-integration' into 8.0.x
Browse files Browse the repository at this point in the history
  • Loading branch information
satikaj committed Jul 3, 2024
2 parents 0782918 + 08a0090 commit 835d2b0
Show file tree
Hide file tree
Showing 37 changed files with 1,888 additions and 91 deletions.
6 changes: 6 additions & 0 deletions app/api/api_root.rb
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ class ApiRoot < Grape::API
mount BreaksApi
mount DiscussionCommentApi
mount ExtensionCommentsApi
mount ScormExtensionCommentsApi
mount GroupSetsApi
mount LearningOutcomesApi
mount LearningAlignmentApi
Expand All @@ -78,6 +79,8 @@ class ApiRoot < Grape::API
mount Tii::TiiGroupAttachmentApi
mount Tii::TiiActionApi

mount ScormApi
mount TestAttemptsApi
mount CampusesPublicApi
mount CampusesAuthenticatedApi
mount TutorialsApi
Expand All @@ -98,6 +101,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 @@ -124,6 +128,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
18 changes: 18 additions & 0 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 @@ -368,4 +369,21 @@ class AuthenticationApi < Grape::API

present nil
end

desc 'Get SCORM authentication token'
get '/auth/scorm' do
if authenticated?
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/scorm_entity.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
module Entities
class ScormEntity < Grape::Entity
expose :file_content, documentation: { type: 'string', desc: 'File content' }
expose :content_type, documentation: { type: 'string', desc: 'Content type' }
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?
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
95 changes: 95 additions & 0 deletions app/api/task_definitions_api.rb
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,11 @@ class TaskDefinitionsApi < Grape::API
requires :restrict_status_updates, type: Boolean, desc: 'Restrict updating of the status to staff'
optional :upload_requirements, type: String, desc: 'Task file upload requirements'
requires :plagiarism_warn_pct, type: Integer, desc: 'The percent at which to record and warn about plagiarism'
requires :scorm_enabled, type: Boolean, desc: 'Whether SCORM assessment is enabled for this task'
requires :scorm_allow_review, type: Boolean, desc: 'Whether a student is allowed to review their completed test attempts'
requires :scorm_bypass_test, type: Boolean, desc: 'Whether a student is allowed to upload files before passing SCORM test'
requires :scorm_time_delay_enabled, type: Boolean, desc: 'Whether there is an incremental time delay between SCORM test attempts'
requires :scorm_attempt_limit, type: Integer, desc: 'The number of times a SCORM test can be attempted'
requires :is_graded, type: Boolean, desc: 'Whether or not this task definition is a graded task'
requires :max_quality_pts, type: Integer, desc: 'A range for quality points when quality is assessed'
optional :assessment_enabled, type: Boolean, desc: 'Enable or disable assessment'
Expand Down Expand Up @@ -55,6 +60,11 @@ class TaskDefinitionsApi < Grape::API
:abbreviation,
:restrict_status_updates,
:plagiarism_warn_pct,
:scorm_enabled,
:scorm_allow_review,
:scorm_bypass_test,
:scorm_time_delay_enabled,
:scorm_attempt_limit,
:is_graded,
:max_quality_pts,
:assessment_enabled,
Expand Down Expand Up @@ -106,6 +116,11 @@ class TaskDefinitionsApi < Grape::API
optional :restrict_status_updates, type: Boolean, desc: 'Restrict updating of the status to staff'
optional :upload_requirements, type: String, desc: 'Task file upload requirements'
optional :plagiarism_warn_pct, type: Integer, desc: 'The percent at which to record and warn about plagiarism'
optional :scorm_enabled, type: Boolean, desc: 'Whether or not SCORM test assessment is enabled for this task'
optional :scorm_allow_review, type: Boolean, desc: 'Whether a student is allowed to review their completed test attempts'
optional :scorm_bypass_test, type: Boolean, desc: 'Whether a student is allowed to upload files before passing SCORM test'
optional :scorm_time_delay_enabled, type: Boolean, desc: 'Whether or not there is an incremental time delay between SCORM test attempts'
optional :scorm_attempt_limit, type: Integer, desc: 'The number of times a SCORM test can be attempted'
optional :is_graded, type: Boolean, desc: 'Whether or not this task definition is a graded task'
optional :max_quality_pts, type: Integer, desc: 'A range for quality points when quality is assessed'
optional :assessment_enabled, type: Boolean, desc: 'Enable or disable assessment'
Expand Down Expand Up @@ -134,6 +149,11 @@ class TaskDefinitionsApi < Grape::API
:abbreviation,
:restrict_status_updates,
:plagiarism_warn_pct,
:scorm_enabled,
:scorm_allow_review,
:scorm_bypass_test,
:scorm_time_delay_enabled,
:scorm_attempt_limit,
:is_graded,
:max_quality_pts,
:assessment_enabled,
Expand Down Expand Up @@ -611,4 +631,79 @@ class TaskDefinitionsApi < Grape::API

stream_file path
end

desc 'Upload the SCORM container (zip file) for a task'
params do
requires :unit_id, type: Integer, desc: 'The related unit'
requires :task_def_id, type: Integer, desc: 'The related task definition'
requires :file, type: File, desc: 'The SCORM data container'
end
post '/units/:unit_id/task_definitions/:task_def_id/scorm_data' do
unit = Unit.find(params[:unit_id])

unless authorise? current_user, unit, :add_task_def
error!({ error: 'Not authorised to upload SCORM data for the unit' }, 403)
end

task_def = unit.task_definitions.find(params[:task_def_id])

unless params[:file].present?
error!({ error: "No file uploaded" }, 403)
end

file_path = params[:file][:tempfile].path

check_mime_against_list! file_path, 'zip', ['application/zip', 'multipart/x-gzip', 'multipart/x-zip', 'application/x-gzip', 'application/octet-stream']

# Actually import...
task_def.add_scorm_data(file_path)
true
end

desc 'Download the SCORM test data'
params do
requires :unit_id, type: Integer, desc: 'The unit to modify tasks for'
requires :task_def_id, type: Integer, desc: 'The task definition to get the SCORM test data of'
end
get '/units/:unit_id/task_definitions/:task_def_id/scorm_data' do
unit = Unit.find(params[:unit_id])
task_def = unit.task_definitions.find(params[:task_def_id])

unless authorise? current_user, unit, :get_unit
error!({ error: 'Not authorised to download task details of unit' }, 403)
end

if task_def.has_scorm_data?
path = task_def.task_scorm_data
content_type 'application/octet-stream'
header['Content-Disposition'] = "attachment; filename=#{task_def.abbreviation}-scorm.zip"
else
path = Rails.root.join('public', 'resources', 'FileNotFound.pdf')
content_type 'application/pdf'
header['Content-Disposition'] = 'attachment; filename=FileNotFound.pdf'
end
header['Access-Control-Expose-Headers'] = 'Content-Disposition'

env['api.format'] = :binary
File.read(path)
end

desc 'Remove the SCORM test data for a given task'
params do
requires :unit_id, type: Integer, desc: 'The related unit'
requires :task_def_id, type: Integer, desc: 'The related task definition'
end
delete '/units/:unit_id/task_definitions/:task_def_id/scorm_data' do
unit = Unit.find(params[:unit_id])

unless authorise? current_user, unit, :add_task_def
error!({ error: 'Not authorised to remove task SCORM data of unit' }, 403)
end

task_def = unit.task_definitions.find(params[:task_def_id])

# Actually remove...
task_def.remove_scorm_data
true
end
end
3 changes: 2 additions & 1 deletion app/api/tasks_api.rb
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,8 @@ class TasksApi < Grape::API
task_definition_id: task.task_definition_id,
status: TaskStatus.id_to_key(task.task_status_id),
due_date: task.due_date,
extensions: task.extensions
extensions: task.extensions,
scorm_extensions: task.scorm_extensions
}
end

Expand Down
Loading

0 comments on commit 835d2b0

Please sign in to comment.