Skip to content

Commit 30ccbf6

Browse files
committed
feat: add overseer step model
1 parent b93b276 commit 30ccbf6

File tree

7 files changed

+324
-4
lines changed

7 files changed

+324
-4
lines changed

app/api/api_root.rb

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,7 @@ class ApiRoot < Grape::API
103103
mount WebcalPublicApi
104104
mount MarkingSessionsApi
105105
mount DiscussionPromptsApi
106+
mount OverseerStepsApi
106107

107108
mount Feedback::FeedbackChipApi
108109

@@ -152,6 +153,7 @@ class ApiRoot < Grape::API
152153
AuthenticationHelpers.add_auth_to Feedback::FeedbackChipApi
153154
AuthenticationHelpers.add_auth_to MarkingSessionsApi
154155
AuthenticationHelpers.add_auth_to DiscussionPromptsApi
156+
AuthenticationHelpers.add_auth_to OverseerStepsApi
155157

156158
add_swagger_documentation \
157159
base_path: nil,
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
module Entities
2+
class OverseerStepEntity < Grape::Entity
3+
expose :id
4+
expose :task_definition_id
5+
6+
expose :name
7+
expose :description
8+
9+
expose :display_name
10+
expose :display_description
11+
12+
expose :run_command
13+
14+
expose :timeout_ms
15+
expose :sort_order
16+
expose :step_type
17+
18+
expose :stdin_input_file
19+
expose :expected_output_file
20+
21+
expose :feedback_message
22+
23+
expose :status_on_success do |overseer_step|
24+
overseer_step.status_on_success_id && TaskStatus.find(overseer_step.status_on_success_id).status_key
25+
end
26+
27+
expose :status_on_failure do |overseer_step|
28+
overseer_step.status_on_failure_id && TaskStatus.find(overseer_step.status_on_failure_id).status_key
29+
end
30+
31+
expose :halt_on_success
32+
expose :halt_on_failure
33+
expose :show_expected_output
34+
expose :show_stdin
35+
expose :show_stdout
36+
37+
expose :enabled
38+
end
39+
end

app/api/entities/task_definition_entity.rb

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,5 +61,10 @@ def staff?(my_role)
6161
expose :discussion_prompts_count do |task_def|
6262
task_def.discussion_prompts.size
6363
end
64+
65+
# expose :overseer_steps, using: OverseerStepEntity
66+
expose :overseer_steps do |task_definition|
67+
task_definition.overseer_steps.order(:sort_order)
68+
end
6469
end
6570
end

app/api/overseer_steps_api.rb

Lines changed: 257 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,257 @@
1+
require 'grape'
2+
3+
class OverseerStepsApi < Grape::API
4+
helpers AuthenticationHelpers
5+
helpers AuthorisationHelpers
6+
helpers SidekiqHelper
7+
8+
before do
9+
authenticated?
10+
end
11+
12+
desc 'Add an overseer step'
13+
params do
14+
requires :overseer_step, type: Hash do
15+
requires :name, type: String
16+
optional :description, type: String
17+
optional :display_name, type: String
18+
optional :display_description, type: String
19+
optional :run_command, type: String
20+
optional :timeout_ms, type: Integer
21+
# TODO: rename to execution_order || exec_order?
22+
optional :sort_order, type: Integer
23+
requires :step_type, type: String
24+
optional :stdin_input_file, type: String
25+
optional :expected_output_file, type: String
26+
optional :feedback_message, type: String
27+
optional :status_on_success, type: String
28+
optional :status_on_failure, type: String
29+
optional :halt_on_success, type: Boolean
30+
optional :halt_on_failure, type: Boolean
31+
optional :show_expected_output, type: Boolean
32+
optional :show_stdin, type: Boolean
33+
optional :show_stdout, type: Boolean
34+
optional :enabled, type: Boolean
35+
end
36+
requires :task_def_id, type: Integer
37+
end
38+
post '/units/:unit_id/task_definitions/:task_def_id/overseer_steps' do
39+
# unless authorise? current_user, User, :admin_overseer
40+
# error!({ error: 'Not authorised to create overseer images' }, 403)
41+
# end
42+
# TODO: ensure correct permissions
43+
44+
task_definition = TaskDefinition.find(params[:task_def_id])
45+
46+
unless Doubtfire::Application.config.overseer_enabled
47+
error!({ error: 'Overseer is not enabled. Enable Overseer before updating settings.' }, 403)
48+
end
49+
50+
# status_on_success = TaskStatus.status_for_name(params[:status_on_success])
51+
# status_on_failure = TaskStatus.status_for_name(params[:status_on_failure])
52+
53+
status_on_success_id = params[:status_on_success].present? && params[:status_on_success] != 'no_change' ? TaskStatus.status_for_name(params[:status_on_success])&.id : nil
54+
status_on_failure_id = params[:status_on_failure].present? && params[:status_on_failure] != 'no_change' ? TaskStatus.status_for_name(params[:status_on_failure])&.id : nil
55+
56+
# status_on_success_id = TaskStatus.status_for_name(params[:status_on_success])&.id
57+
# status_on_failure_id = TaskStatus.status_for_name(params[:status_on_failure])&.id
58+
59+
overseer_step_params = ActionController::Parameters.new(params)
60+
.require(:overseer_step)
61+
.permit(
62+
:name,
63+
:description,
64+
:display_name,
65+
:display_description,
66+
:run_command,
67+
:timeout_ms,
68+
:sort_order,
69+
:step_type,
70+
:stdin_input_file,
71+
:expected_output_file,
72+
:feedback_message,
73+
:status_on_success_id,
74+
:status_on_failure_id,
75+
:halt_on_success,
76+
:halt_on_failure,
77+
:show_expected_output,
78+
:show_stdin,
79+
:show_stdout,
80+
:enabled
81+
)
82+
.merge(task_definition_id: task_definition.id,
83+
status_on_success_id: status_on_success_id,
84+
status_on_failure_id: status_on_failure_id)
85+
86+
result = OverseerStep.create!(overseer_step_params)
87+
88+
if result.nil?
89+
error!({ error: 'No overseer step added' }, 403)
90+
else
91+
present result, with: Entities::OverseerStepEntity
92+
end
93+
end
94+
95+
desc 'Update an overseer step'
96+
params do
97+
requires :overseer_step, type: Hash do
98+
optional :name, type: String
99+
optional :description, type: String
100+
optional :display_name, type: String
101+
optional :display_description, type: String
102+
optional :run_command, type: String
103+
optional :timeout_ms, type: Integer
104+
optional :sort_order, type: Integer
105+
optional :step_type, type: String
106+
optional :stdin_input_file, type: String
107+
optional :expected_output_file, type: String
108+
optional :feedback_message, type: String
109+
optional :status_on_success, type: String
110+
optional :status_on_failure, type: String
111+
optional :halt_on_success, type: Boolean
112+
optional :halt_on_failure, type: Boolean
113+
optional :show_expected_output, type: Boolean
114+
optional :show_stdin, type: Boolean
115+
optional :show_stdout, type: Boolean
116+
optional :enabled, type: Boolean
117+
end
118+
requires :task_def_id, type: Integer
119+
end
120+
put '/units/:unit_id/task_definitions/:task_def_id/overseer_steps/:id' do
121+
# unless authorise? current_user, User, :admin_overseer
122+
# error!({ error: 'Not authorised to create overseer images' }, 403)
123+
# end
124+
# TODO: ensure correct permissions
125+
126+
unless Doubtfire::Application.config.overseer_enabled
127+
error!({ error: 'Overseer is not enabled. Enable Overseer before updating settings.' }, 403)
128+
end
129+
130+
overseer_step = OverseerStep.find(params[:id])
131+
132+
puts params.inspect
133+
134+
status_on_success_id = params[:status_on_success].present? ? TaskStatus.status_for_name(params[:status_on_success])&.id : nil
135+
status_on_failure_id = params[:status_on_failure].present? ? TaskStatus.status_for_name(params[:status_on_failure])&.id : nil
136+
137+
overseer_step_params = ActionController::Parameters.new(params)
138+
.require(:overseer_step)
139+
.permit(
140+
:name,
141+
:description,
142+
:display_name,
143+
:display_description,
144+
:run_command,
145+
:timeout_ms,
146+
:sort_order,
147+
:step_type,
148+
:stdin_input_file,
149+
:expected_output_file,
150+
:feedback_message,
151+
:status_on_success_id,
152+
:status_on_failure_id,
153+
:halt_on_success,
154+
:halt_on_failure,
155+
:show_expected_output,
156+
:show_stdin,
157+
:show_stdout,
158+
:enabled
159+
)
160+
.merge(
161+
status_on_success_id: status_on_success_id,
162+
status_on_failure_id: status_on_failure_id
163+
)
164+
# .merge(task_definition_id: task_definition.id)
165+
166+
overseer_step.update!(overseer_step_params)
167+
168+
present overseer_step, with: Entities::OverseerStepEntity
169+
end
170+
171+
# desc 'Update an overseer image'
172+
# params do
173+
# requires :overseer_image, type: Hash do
174+
# optional :name, type: String, desc: 'The name of the overseer image'
175+
# optional :tag, type: String, desc: 'The tag used to receive from container repo'
176+
# end
177+
# end
178+
# put '/admin/overseer_images/:id' do
179+
# unless authorise? current_user, User, :admin_overseer
180+
# error!({ error: 'Not authorised to update an overseer image' }, 403)
181+
# end
182+
# unless Doubtfire::Application.config.overseer_enabled
183+
# error!({ error: 'Overseer is not enabled. Enable Overseer before updating settings.' }, 403)
184+
# end
185+
186+
# overseer_image = OverseerImage.find(params[:id])
187+
188+
# overseer_image_params = ActionController::Parameters.new(params)
189+
# .require(:overseer_image)
190+
# .permit(:name,
191+
# :tag)
192+
193+
# # Clear image status and text when updating
194+
# overseer_image_params[:pulled_image_status] = nil
195+
# overseer_image_params[:pulled_image_text] = nil
196+
# overseer_image_params[:last_pulled_date] = nil
197+
198+
# overseer_image.update!(overseer_image_params)
199+
# present overseer_image, with: Entities::OverseerImageEntity
200+
# end
201+
202+
# desc 'Delete an overseer image'
203+
# delete '/admin/overseer_images/:id' do
204+
# unless authorise? current_user, User, :admin_overseer
205+
# error!({ error: 'Not authorised to delete an overseer image' }, 403)
206+
# end
207+
208+
# overseer_image = OverseerImage.find(params[:id])
209+
# overseer_image.destroy
210+
# error!({ error: overseer_image.errors.full_messages.last }, 403) unless overseer_image.destroyed?
211+
212+
# present overseer_image.destroyed?, with: Grape::Presenters::Presenter
213+
# end
214+
215+
# desc 'Get all overseer images'
216+
# get '/admin/overseer_images' do
217+
# unless authorise? current_user, User, :use_overseer
218+
# error!({ error: 'Not authorised to get overseer images' }, 403)
219+
# end
220+
221+
# if Doubtfire::Application.config.overseer_enabled
222+
# present OverseerImage.all, with: Entities::OverseerImageEntity
223+
# else
224+
# present [], with: Grape::Presenters::Presenter
225+
# end
226+
# end
227+
228+
# desc 'Get all overseer images'
229+
# get '/admin/overseer_images/:id' do
230+
# unless authorise? current_user, User, :use_overseer
231+
# error!({ error: 'Not authorised to get overseer images' }, 403)
232+
# end
233+
234+
# if Doubtfire::Application.config.overseer_enabled
235+
# present OverseerImage.find(params[:id]), with: Entities::OverseerImageEntity
236+
# else
237+
# present [], with: Grape::Presenters::Presenter
238+
# end
239+
# end
240+
241+
# desc 'Get overseer image by id and pull image'
242+
# put '/admin/overseer_images/:id/pull_image' do
243+
# unless authorise? current_user, User, :admin_overseer
244+
# error!({ error: 'Not authorised to pull an overseer image' }, 403)
245+
# end
246+
# unless Doubtfire::Application.config.overseer_enabled
247+
# error!({ error: 'Overseer is not enabled. Enable Overseer before updating settings.' }, 403)
248+
# end
249+
250+
# overseer_image = OverseerImage.find(params[:id])
251+
252+
# job_id = PullDockerImageJob.perform_async(overseer_image.id)
253+
# job = setup_job(job_id)
254+
255+
# present job, with: Entities::SidekiqJobEntity
256+
# end
257+
end

app/models/overseer_step.rb

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,4 @@
11
class OverseerStep < ApplicationRecord
2+
belongs_to :task_definition, optional: false
3+
24
end

app/models/task_definition.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,7 @@ def self.permissions
7171
has_many :tasks, dependent: :destroy # Destroying a task definition will also nuke any instances
7272
has_many :group_submissions, dependent: :destroy # Destroying a task definition will also nuke any group submissions
7373
has_many :learning_outcomes, as: :context, dependent: :destroy
74+
has_many :overseer_steps, dependent: :destroy
7475

7576
has_many :tii_group_attachments, dependent: :destroy # destroy uploaded files to tii - after the tasks
7677
has_many :tii_actions, as: :entity, dependent: :destroy

db/migrate/20251211014916_create_overseer_steps.rb

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,15 +3,22 @@ def change
33
create_table :overseer_steps do |t|
44
t.references :task_definition
55

6+
# Staff only
67
t.string :name, null: false
78
t.text :description
8-
# t.text :script, null: false # This will belong to a file path
9+
10+
# Shown to the student
11+
t.string :display_name, null: false
12+
t.string :display_description, null: false
13+
14+
t.text :run_command
15+
916
# t.string :interpreter, default: "bash"
1017
t.integer :timeout_ms, default: 1000
1118
t.integer :sort_order, default: 0
1219

13-
t.string :step_type, null: false # "status_check", "output_diff", etc.
14-
t.string :visibility, null: false # "public", "masked", "hidden"
20+
t.string :step_type, null: false # "status_check", "output_diff", etc.
21+
# t.string :visibility, null: false # "public", "masked", "hidden"
1522
# public => Student can see the name/desc, input & output logs and feedback message. Sees if it was a pass or fail
1623
# masked => Student can see the name/desc, not the input/output. Sees if it was a pass or fail
1724
# hidden => Student doesn't know this step exists, has no effect on the task status
@@ -23,7 +30,14 @@ def change
2330
t.text :feedback_message # Only shown on fail
2431

2532
t.references :status_on_success
26-
t.references :status_on_failed
33+
t.references :status_on_failure
34+
35+
t.boolean :halt_on_success
36+
t.boolean :halt_on_failure
37+
38+
t.boolean :show_expected_output
39+
t.boolean :show_stdin
40+
t.boolean :show_stdout
2741

2842
t.boolean :enabled, default: true
2943

0 commit comments

Comments
 (0)