Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
41 commits
Select commit Hold shift + click to select a range
b93b276
feat: init overseer step pipeline
b0ink Dec 11, 2025
d7bd321
feat: add overseer step model
b0ink Dec 17, 2025
1558ca9
Merge branch '10.0.x' into feat/overseer-pipeline
b0ink Dec 17, 2025
93b4de2
refactor: correctly expose sorted overseer steps
b0ink Dec 18, 2025
77d6930
feat: expose list of overseer resource files
b0ink Dec 18, 2025
7fb78f7
chore: remove name of zip file from path
b0ink Dec 18, 2025
19459c3
feat: allow partial diff check
b0ink Dec 18, 2025
c904b50
refactor: rerun migration
b0ink Dec 18, 2025
6fd05b7
chore: remove duplicate overseer run
b0ink Dec 18, 2025
ea71ba8
fix: ignore top level zip name
b0ink Dec 18, 2025
8329ba3
feat: delete overseer step
b0ink Dec 18, 2025
b916b98
feat: overseer step result model
b0ink Dec 18, 2025
5576029
refactor: init refactor of new overseer step workflow
b0ink Dec 18, 2025
6962a8d
chore: overseer step result relationships
b0ink Dec 18, 2025
50d3f2d
chore: remove byebug
b0ink Dec 18, 2025
85bc130
refactor: add assessment relationship
b0ink Dec 18, 2025
c7a4015
chore: remove newlines from output
b0ink Dec 18, 2025
3b031da
fix: set correct status
b0ink Dec 19, 2025
941cc82
refactor: store feedback message in step result
b0ink Dec 19, 2025
09fa675
feat: expose minimal overseer step results in overseer assessment com…
b0ink Dec 19, 2025
4dfe60a
refactor: filter out overseer steps from students
b0ink Dec 19, 2025
908724e
refactor: dont expose step results and add endpoint to fetch them
b0ink Dec 19, 2025
0d56b94
chore: expose assessment enabled to students
b0ink Dec 22, 2025
cbf84a6
fix: set timeout on overseer run script
b0ink Dec 22, 2025
abf1dfb
chore: allow for empty output file
b0ink Dec 22, 2025
7bf2156
refactor: expose overseer stdout and stdin results if enabled
b0ink Dec 22, 2025
311dacd
chore: retrieve the last set task status
b0ink Dec 22, 2025
878d6ca
refactor: break logic into separate methods
b0ink Dec 22, 2025
b8702cb
refactor: allow test submissions while overseer is disabled for a task
b0ink Dec 22, 2025
d62ddc0
refactor: combine scheme changes into single migration
b0ink Dec 26, 2025
a6f92a8
refactor: use seconds for overseer timeout
b0ink Dec 26, 2025
356d150
chore: pass in test submission argument
b0ink Dec 26, 2025
65050dd
chore: disable execution script endpoint
b0ink Dec 26, 2025
df5fcba
refactor: clean up overseer job
b0ink Jan 5, 2026
43a22f3
refactor: only update status if halting on step
b0ink Jan 5, 2026
fc022d8
chore: remove todo
b0ink Jan 5, 2026
29373eb
refactor: add overseer step permissions
b0ink Jan 5, 2026
03a36c3
feat: add overseer step validation
b0ink Jan 5, 2026
3b2dfea
fix: get correct models
b0ink Jan 5, 2026
9f60b66
chore: remove todo
b0ink Jan 5, 2026
7c2fb67
refactor: expose statuses to staff only
b0ink Jan 5, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions app/api/api_root.rb
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,7 @@ class ApiRoot < Grape::API
mount WebcalPublicApi
mount MarkingSessionsApi
mount DiscussionPromptsApi
mount OverseerStepsApi

mount Feedback::FeedbackChipApi

Expand Down Expand Up @@ -152,6 +153,7 @@ class ApiRoot < Grape::API
AuthenticationHelpers.add_auth_to Feedback::FeedbackChipApi
AuthenticationHelpers.add_auth_to MarkingSessionsApi
AuthenticationHelpers.add_auth_to DiscussionPromptsApi
AuthenticationHelpers.add_auth_to OverseerStepsApi

add_swagger_documentation \
base_path: nil,
Expand Down
3 changes: 3 additions & 0 deletions app/api/entities/overseer_assessment_entity.rb
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,8 @@ class OverseerAssessmentEntity < Grape::Entity
expose :status
expose :created_at
expose :updated_at

expose :total_steps
expose :passed_steps
end
end
47 changes: 47 additions & 0 deletions app/api/entities/overseer_step_entity.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
module Entities
class OverseerStepEntity < Grape::Entity
expose :id
expose :task_definition_id

def staff?(my_role)
Role.teaching_staff_ids.include?(my_role.id) unless my_role.nil?
end

expose :name, if: ->(_unit, options) { staff?(options[:my_role]) }
expose :description, if: ->(_unit, options) { staff?(options[:my_role]) }

expose :display_name
expose :display_description

expose :run_command, if: ->(_unit, options) { staff?(options[:my_role]) }

expose :timeout, if: ->(_unit, options) { staff?(options[:my_role]) }
expose :sort_order, if: ->(_unit, options) { staff?(options[:my_role]) }

expose :step_type
expose :partial_output_diff, if: ->(_unit, options) { staff?(options[:my_role]) }

expose :stdin_input_file, if: ->(_unit, options) { staff?(options[:my_role]) }
expose :expected_output_file, if: ->(_unit, options) { staff?(options[:my_role]) }

expose :feedback_message, if: ->(_unit, options) { staff?(options[:my_role]) }

expose :status_on_success,
if: ->(_obj, options) { staff?(options[:my_role]) } do |overseer_step|
TaskStatus.find_by(id: overseer_step.status_on_success_id)&.status_key || 'no_change'
end

expose :status_on_failure,
if: ->(_obj, options) { staff?(options[:my_role]) } do |overseer_step|
TaskStatus.find_by(id: overseer_step.status_on_failure_id)&.status_key || 'no_change'
end

expose :halt_on_success, if: ->(_unit, options) { staff?(options[:my_role]) }
expose :halt_on_failure, if: ->(_unit, options) { staff?(options[:my_role]) }
expose :show_expected_output, if: ->(_unit, options) { staff?(options[:my_role]) }
expose :show_stdin, if: ->(_unit, options) { staff?(options[:my_role]) }
expose :show_stdout, if: ->(_unit, options) { staff?(options[:my_role]) }

expose :enabled
end
end
30 changes: 30 additions & 0 deletions app/api/entities/overseer_step_result_entity.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
module Entities
class OverseerStepResultEntity < Grape::Entity

def staff?(my_role)
Role.teaching_staff_ids.include?(my_role.id) unless my_role.nil?
end

expose :id
expose :overseer_step_id
expose :exit_status
expose :pass
expose :feedback_message

expose :stdout, if: lambda { |result, options|
staff?(options[:my_role]) || result.overseer_step&.show_stdout
}

expose :stdin, if: lambda { |result, options|
staff?(options[:my_role]) || result.overseer_step&.show_stdin
}

expose :expected_output, if: lambda { |result, options|
staff?(options[:my_role]) || result.overseer_step&.show_expected_output
}

expose :stdout_sha256
expose :stdin_sha256
expose :expected_output_sha256
end
end
10 changes: 9 additions & 1 deletion app/api/entities/task_definition_entity.rb
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,8 @@ def staff?(my_role)
expose :is_graded
expose :max_quality_pts
expose :overseer_image_id, if: ->(unit, options) { staff?(options[:my_role]) }, expose_nil: false
expose :assessment_enabled, if: ->(unit, options) { staff?(options[:my_role]) }
# expose :assessment_enabled, if: ->(unit, options) { staff?(options[:my_role]) }
expose :assessment_enabled
expose :similarity_language, if: ->(unit, options) { staff?(options[:my_role]) }, expose_nil: false
expose :assess_in_portfolio_only
expose :use_resources_for_jplag_base_code, if: ->(unit, options) { staff?(options[:my_role]) }
Expand All @@ -61,5 +62,12 @@ def staff?(my_role)
expose :discussion_prompts_count do |task_def|
task_def.discussion_prompts.size
end

# expose :overseer_steps, using: OverseerStepEntity, if: ->(unit, options) { staff?(options[:my_role]) }
expose :overseer_steps, using: OverseerStepEntity do |task_def, options|
task_def.overseer_steps # options[:my_role] is still available inside the entity
end
expose :overseer_resource_files, if: ->(task_def, options) { staff?(options[:my_role]) }

end
end
200 changes: 200 additions & 0 deletions app/api/overseer_steps_api.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,200 @@
require 'grape'

class OverseerStepsApi < Grape::API
helpers AuthenticationHelpers
helpers AuthorisationHelpers
helpers SidekiqHelper

before do
authenticated?
end

desc 'Add an overseer step'
params do
requires :overseer_step, type: Hash do
requires :name, type: String
optional :description, type: String
optional :display_name, type: String
optional :display_description, type: String
optional :run_command, type: String
optional :timeout, type: Integer
# TODO: rename to execution_order || exec_order?
optional :sort_order, type: Integer
optional :partial_output_diff, type: Boolean
requires :step_type, type: String
optional :stdin_input_file, type: String
optional :expected_output_file, type: String
optional :feedback_message, type: String
optional :status_on_success, type: String
optional :status_on_failure, type: String
optional :halt_on_success, type: Boolean
optional :halt_on_failure, type: Boolean
optional :show_expected_output, type: Boolean
optional :show_stdin, type: Boolean
optional :show_stdout, type: Boolean
optional :enabled, type: Boolean
end
requires :task_def_id, type: Integer
end
post '/units/:unit_id/task_definitions/:task_def_id/overseer_steps' do
unless Doubtfire::Application.config.overseer_enabled
error!({ error: 'Overseer is not enabled. Enable Overseer before updating settings.' }, 403)
end

task_definition = TaskDefinition.find(params[:task_def_id])

unless authorise? current_user, task_definition, :manage_overseer_steps
error!({ error: 'Not authorised to manage overseer for this task definition' }, 403)
end

status_on_success_param = params[:overseer_step][:status_on_success]
status_on_failure_param = params[:overseer_step][:status_on_failure]

status_on_success_id = status_on_success_param.present? && status_on_success_param != 'no_change' ? TaskStatus.status_for_name(status_on_success_param)&.id : nil
status_on_failure_id = status_on_failure_param.present? && status_on_failure_param != 'no_change' ? TaskStatus.status_for_name(status_on_failure_param)&.id : nil

overseer_step_params = ActionController::Parameters.new(params)
.require(:overseer_step)
.permit(
:name,
:description,
:display_name,
:display_description,
:run_command,
:timeout,
:sort_order,
:step_type,
:partial_output_diff,
:stdin_input_file,
:expected_output_file,
:feedback_message,
:status_on_success_id,
:status_on_failure_id,
:halt_on_success,
:halt_on_failure,
:show_expected_output,
:show_stdin,
:show_stdout,
:enabled
)
.merge(task_definition_id: task_definition.id,
status_on_success_id: status_on_success_id,
status_on_failure_id: status_on_failure_id)

result = OverseerStep.create!(overseer_step_params)

if result.nil?
error!({ error: 'No overseer step added' }, 403)
else
present result, with: Entities::OverseerStepEntity
end
end

desc 'Update an overseer step'
params do
requires :overseer_step, type: Hash do
optional :name, type: String
optional :description, type: String
optional :display_name, type: String
optional :display_description, type: String
optional :run_command, type: String
optional :timeout, type: Integer
optional :sort_order, type: Integer
optional :step_type, type: String
optional :partial_output_diff, type: Boolean
optional :stdin_input_file, type: String
optional :expected_output_file, type: String
optional :feedback_message, type: String
optional :status_on_success, type: String
optional :status_on_failure, type: String
optional :halt_on_success, type: Boolean
optional :halt_on_failure, type: Boolean
optional :show_expected_output, type: Boolean
optional :show_stdin, type: Boolean
optional :show_stdout, type: Boolean
optional :enabled, type: Boolean
end
requires :task_def_id, type: Integer
end
put '/units/:unit_id/task_definitions/:task_def_id/overseer_steps/:id' do
unless Doubtfire::Application.config.overseer_enabled
error!({ error: 'Overseer is not enabled. Enable Overseer before updating settings.' }, 403)
end

unit = Unit.find(params[:unit_id])
task_definition = unit.task_definitions.find(params[:task_def_id])
overseer_step = task_definition.overseer_steps.find(params[:id])

unless authorise? current_user, overseer_step.task_definition, :manage_overseer_steps
error!({ error: 'Not authorised to manage overseer for this task definition' }, 403)
end

status_on_success_param = params[:overseer_step][:status_on_success]
status_on_failure_param = params[:overseer_step][:status_on_failure]

status_on_success_id = status_on_success_param.present? && status_on_success_param != 'no_change' ? TaskStatus.status_for_name(status_on_success_param)&.id : nil
status_on_failure_id = status_on_failure_param.present? && status_on_failure_param != 'no_change' ? TaskStatus.status_for_name(status_on_failure_param)&.id : nil

overseer_step_params = ActionController::Parameters.new(params)
.require(:overseer_step)
.permit(
:name,
:description,
:display_name,
:display_description,
:run_command,
:timeout,
:sort_order,
:step_type,
:partial_output_diff,
:stdin_input_file,
:expected_output_file,
:feedback_message,
:status_on_success_id,
:status_on_failure_id,
:halt_on_success,
:halt_on_failure,
:show_expected_output,
:show_stdin,
:show_stdout,
:enabled
)
.merge(
status_on_success_id: status_on_success_id,
status_on_failure_id: status_on_failure_id
)

overseer_step.update!(overseer_step_params)

present overseer_step, with: Entities::OverseerStepEntity
end

desc 'Delete an overseer step'
delete '/overseer_steps/:id' do
overseer_step = OverseerStep.find(params[:id])

unless authorise? current_user, overseer_step.task_definition, :manage_overseer_steps
error!({ error: 'Not authorised to manage overseer for this task definition' }, 403)
end

overseer_step.destroy!

error!({ error: overseer_step.errors.full_messages.last }, 403) unless overseer_step.destroyed?

present overseer_step.destroyed?, with: Grape::Presenters::Presenter
end

desc 'Get test results for an overseer assessment'
get '/projects/:project_id/task_definitions/:task_def_id/overseer_assessments_results/:id' do
project = Project.find(params[:project_id])

unless authorise? current_user, project, :get_submission
error!({ error: 'Not authorised to view this project' }, 403)
end

unit = project.unit

overseer_assessment = OverseerAssessment.find(params[:id])
present overseer_assessment.overseer_step_results, with: Entities::OverseerStepResultEntity, my_role: unit.role_for(current_user)
end
end
Loading