Skip to content
Open
3 changes: 3 additions & 0 deletions app/api/api_root.rb
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,8 @@ class ApiRoot < Grape::API
mount UsersApi
mount WebcalApi
mount WebcalPublicApi
mount StudentTutorialEnrolmentApi


#
# Add auth details to all end points
Expand Down Expand Up @@ -122,6 +124,7 @@ class ApiRoot < Grape::API
AuthenticationHelpers.add_auth_to UnitRolesApi
AuthenticationHelpers.add_auth_to UnitsApi
AuthenticationHelpers.add_auth_to WebcalApi
AuthenticationHelpers.add_auth_to StudentTutorialEnrolmentApi

add_swagger_documentation \
base_path: nil,
Expand Down
5 changes: 5 additions & 0 deletions app/api/entities/task_definition_entity.rb
Original file line number Diff line number Diff line change
Expand Up @@ -44,5 +44,10 @@ def staff?(my_role)
expose :overseer_image_id, if: ->(unit, options) { staff?(options[:my_role]) }
expose :assessment_enabled, if: ->(unit, options) { staff?(options[:my_role]) }
expose :moss_language, if: ->(unit, options) { staff?(options[:my_role]) }
expose :tutorial_self_enrolment_enabled
expose :tutorial_self_enrolment_stream_id
expose :tutorial_self_enrolment_stream_abbr, expose_nil: true do |task_definition, _options|
task_definition.tutorial_self_enrolment_stream&.abbreviation
end
end
end
94 changes: 94 additions & 0 deletions app/api/student_tutorial_enrolment_api.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
require 'grape'

class StudentTutorialEnrolmentApi < Grape::API
helpers AuthenticationHelpers
helpers AuthorisationHelpers

before do
authenticated?
end

resource :projects do
route_param :project_id do
resource :tutorial_enrolments do
desc 'Enrol student in tutorial for self-enrolment task'
params do
requires :task_definition_id, type: Integer, desc: 'Task definition with self enrolment enabled'
requires :tutorial_id, type: Integer, desc: 'Tutorial to enrol in'
end
post do
project = Project.find(params[:project_id])

# Authorization check
error!('Unauthorized', 401) unless project.student == current_user

task_def = TaskDefinition.find(params[:task_definition_id])
tutorial = Tutorial.find(params[:tutorial_id])

# Validation checks
error!('Task does not allow self enrolment', 400) unless task_def.tutorial_self_enrolment_enabled?
error!('Tutorial not available for this task', 400) unless task_def.available_tutorials_for_self_enrolment.include?(tutorial)
if tutorial.respond_to?(:at_capacity?) && tutorial.at_capacity?
error!('Tutorial is full', 400)
end

# Check if already enrolled in this stream
existing_enrolment = project.tutorial_enrolments.joins(:tutorial)
.where(tutorials: { tutorial_stream_id: task_def.tutorial_self_enrolment_stream_id })
.first

if existing_enrolment
# Update existing enrolment
existing_enrolment.update!(tutorial: tutorial)
message = 'Tutorial enrolment updated successfully'
else
# Create new enrolment
project.enrol_in(tutorial)
message = 'Tutorial enrolment created successfully'
end

{
success: true,
message: message,
tutorial: {
id: tutorial.id,
abbreviation: tutorial.abbreviation,
campus_id: tutorial.campus_id
}
}
end

desc 'Get current tutorial enrolment for task'
params do
requires :task_definition_id, type: Integer, desc: 'Task definition with self enrolment enabled'
end
get do
project = Project.find(params[:project_id])

# Authorization check
error!('Unauthorized', 401) unless project.student == current_user || authorise?(current_user, project, :get)

task_def = TaskDefinition.find(params[:task_definition_id])

# Find enrolment in the relevant stream
enrolment = project.tutorial_enrolments.joins(:tutorial)
.where(tutorials: { tutorial_stream_id: task_def.tutorial_self_enrolment_stream_id })
.first

if enrolment
{
enrolled: true,
tutorial: {
id: enrolment.tutorial.id,
abbreviation: enrolment.tutorial.abbreviation,
campus_id: enrolment.tutorial.campus_id
}
}
else
{ enrolled: false }
end
end
end
end
end
end
56 changes: 52 additions & 4 deletions app/api/task_definitions_api.rb
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@ class TaskDefinitionsApi < Grape::API
optional :assessment_enabled, type: Boolean, desc: 'Enable or disable assessment'
optional :overseer_image_id, type: Integer, desc: 'The id of the Docker image for overseer'
optional :moss_language, type: String, desc: 'The language to use for code similarity checks'
optional :tutorial_self_enrolment_enabled, type: Boolean, desc: 'Enable tutorial self enrolment'
optional :tutorial_self_enrolment_stream_abbr, type: String, desc: 'Tutorial stream abbreviation for self enrolment'
end
end
post '/units/:unit_id/task_definitions/' do
Expand Down Expand Up @@ -61,11 +63,24 @@ class TaskDefinitionsApi < Grape::API
:max_quality_pts,
:assessment_enabled,
:overseer_image_id,
:moss_language
:moss_language,
:tutorial_self_enrolment_enabled,
upload_requirements: []
)

task_params[:unit_id] = unit.id
task_params[:upload_requirements] = JSON.parse(params[:task_def][:upload_requirements]) unless params[:task_def][:upload_requirements].nil?
# Handle upload_requirements - check if it's already an array or needs parsing
if params[:task_def][:upload_requirements].present?
if params[:task_def][:upload_requirements].is_a?(Array)
task_params[:upload_requirements] = params[:task_def][:upload_requirements]
elsif params[:task_def][:upload_requirements].is_a?(String)
begin
task_params[:upload_requirements] = JSON.parse(params[:task_def][:upload_requirements])
rescue JSON::ParserError
error!({ error: 'Invalid upload_requirements format' }, 400)
end
end
end

task_def = TaskDefinition.new(task_params)

Expand All @@ -76,6 +91,12 @@ class TaskDefinitionsApi < Grape::API
task_def.tutorial_stream = tutorial_stream
end

# Handle tutorial self-enrolment stream
if params[:task_def][:tutorial_self_enrolment_stream_abbr].present?
stream = unit.tutorial_streams.find_by!(abbreviation: params[:task_def][:tutorial_self_enrolment_stream_abbr])
task_def.tutorial_self_enrolment_stream = stream
end

#
# Link in group set if specified
#
Expand Down Expand Up @@ -111,6 +132,8 @@ class TaskDefinitionsApi < Grape::API
optional :assessment_enabled, type: Boolean, desc: 'Enable or disable assessment'
optional :overseer_image_id, type: Integer, desc: 'The id of the Docker image name for overseer'
optional :moss_language, type: String, desc: 'The language to use for code similarity checks'
optional :tutorial_self_enrolment_enabled, type: Boolean, desc: 'Enable tutorial self enrolment'
optional :tutorial_self_enrolment_stream_abbr, type: String, desc: 'Tutorial stream abbreviation for self enrolment'
end
end
put '/units/:unit_id/task_definitions/:id' do
Expand Down Expand Up @@ -138,10 +161,23 @@ class TaskDefinitionsApi < Grape::API
:max_quality_pts,
:assessment_enabled,
:overseer_image_id,
:moss_language
:moss_language,
:tutorial_self_enrolment_enabled,
upload_requirements: []
)

task_params[:upload_requirements] = JSON.parse(params[:task_def][:upload_requirements]) unless params[:task_def][:upload_requirements].nil?
# Handle upload_requirements - check if it's already an array or needs parsing
if params[:task_def][:upload_requirements].present?
if params[:task_def][:upload_requirements].is_a?(Array)
task_params[:upload_requirements] = params[:task_def][:upload_requirements]
elsif params[:task_def][:upload_requirements].is_a?(String)
begin
task_params[:upload_requirements] = JSON.parse(params[:task_def][:upload_requirements])
rescue JSON::ParserError
error!({ error: 'Invalid upload_requirements format' }, 400)
end
end
end

# Ensure changes to a TD defined as a "draft task definition" are validated
if unit.draft_task_definition_id == params[:id]
Expand All @@ -163,6 +199,18 @@ class TaskDefinitionsApi < Grape::API
task_def.save!
end

# Handle tutorial self-enrolment stream updates
if params[:task_def].key?(:tutorial_self_enrolment_stream_abbr)
if params[:task_def][:tutorial_self_enrolment_stream_abbr].present?
stream = task_def.unit.tutorial_streams.find_by!(abbreviation: params[:task_def][:tutorial_self_enrolment_stream_abbr])
task_def.tutorial_self_enrolment_stream = stream
task_def.save!
else
task_def.tutorial_self_enrolment_stream = nil
task_def.save!
end
end

#
# Link in group set if specified
#
Expand Down
19 changes: 19 additions & 0 deletions app/models/task_definition.rb
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ class TaskDefinition < ApplicationRecord
belongs_to :group_set, optional: true
belongs_to :tutorial_stream, optional: true
belongs_to :overseer_image, optional: true
belongs_to :tutorial_self_enrolment_stream, class_name: "TutorialStream", optional: true

has_many :tasks, dependent: :destroy # Destroying a task definition will also nuke any instances
has_many :group_submissions, dependent: :destroy # Destroying a task definition will also nuke any group submissions
Expand Down Expand Up @@ -41,6 +42,8 @@ class TaskDefinition < ApplicationRecord

validates :weighting, presence: true

validate :self_enrolment_stream_unit_must_match

include TaskDefinitionTiiModule
include TaskDefinitionSimilarityModule

Expand All @@ -56,6 +59,22 @@ def tutorial_stream_present?
end
end

def tutorial_self_enrolment_enabled?
tutorial_self_enrolment_enabled
end

def available_tutorials_for_self_enrolment
return Tutorial.none unless tutorial_self_enrolment_enabled? && tutorial_self_enrolment_stream

tutorial_self_enrolment_stream.tutorials.where(unit: unit)
end

def self_enrolment_stream_unit_must_match
if tutorial_self_enrolment_enabled? && tutorial_self_enrolment_stream.present? && tutorial_self_enrolment_stream.unit != unit
errors.add(:tutorial_self_enrolment_stream, "must belong to the same unit")
end
end

# In the rollover process, copy this definition into another unit
# Copy this task into the other unit
def copy_to(other_unit)
Expand Down
2 changes: 2 additions & 0 deletions app/models/tutorial_stream.rb
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ class TutorialStream < ApplicationRecord
has_many :tutorials, dependent: :destroy
has_many :task_definitions, -> { order 'start_date ASC, abbreviation ASC' }

has_many :self_enrolment_task_definitions, class_name: "TaskDefinition", foreign_key: :tutorial_self_enrolment_stream_id, dependent: :nullify

validates :unit, presence: true
validates :activity_type, presence: true

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
class AddTutorialSelfEnrolmentToTaskDefinitions < ActiveRecord::Migration[7.1]
def change
unless column_exists?(:task_definitions, :tutorial_self_enrolment_enabled)
add_column :task_definitions, :tutorial_self_enrolment_enabled, :boolean, default: false, null: false
add_index :task_definitions, :tutorial_self_enrolment_enabled
end

unless column_exists?(:task_definitions, :tutorial_self_enrolment_stream_id)
add_reference :task_definitions, :tutorial_self_enrolment_stream, foreign_key: { to_table: :tutorial_streams }, null: true
add_index :task_definitions, :tutorial_self_enrolment_stream_id
end
end
end
Loading