Skip to content

Support instruction steps - #953

Merged
zetter-rpf merged 2 commits into
mainfrom
instruction-steps
Aug 10, 2026
Merged

Support instruction steps#953
zetter-rpf merged 2 commits into
mainfrom
instruction-steps

Conversation

@zetter-rpf

@zetter-rpf zetter-rpf commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

Status

What's changed?

  • Added support for instructions to contain multiple steps
  • Simplified how we were preventing students from updating instructions

The new instructions will be in a format such as:

[
  {markdown_content: 'step 1'},
  {markdown_content: 'step 2'},
]

This has been added to a new instruction_steps column that will eventually replace instructions.

This change is backwards compatible with current versions of editor since sole strings are allowed in the JSONB column. It won't be at the point that we start storing instruction steps instead of strings in that column.

Created with help from Claude Code

@github-actions

github-actions Bot commented Aug 7, 2026

Copy link
Copy Markdown

Test coverage

92.18% line coverage reported by SimpleCov.
Run: https://github.com/RaspberryPiFoundation/editor-api/actions/runs/31367652328

@zetter-rpf zetter-rpf changed the title Instruction steps Support instruction steps Aug 7, 2026
@zetter-rpf
zetter-rpf marked this pull request as ready for review August 7, 2026 10:08
Copilot AI review requested due to automatic review settings August 7, 2026 10:08
@raspberrypiherokubot
raspberrypiherokubot temporarily deployed to editor-api-p-instructio-tqy3iv August 7, 2026 10:10 Inactive

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds support for storing project instructions as multi-step content (JSON) while adjusting update behavior so student attempts to change instructions are ignored rather than rejected. This fits into the Rails REST API layer (controllers + Jbuilder) and the Project::Update domain operation.

Changes:

  • Convert projects.instructions from text to jsonb to support either a plain markdown string or an array of instruction steps.
  • Update request handling so students cannot submit instructions via strong params (making instruction updates a no-op for students).
  • Extend request specs to cover reading/writing the instruction-steps format and update unit specs for the new Project::Update signature.

Reviewed changes

Copilot reviewed 12 out of 12 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
spec/requests/projects/update_spec.rb Adds coverage for saving/returning instruction steps; updates student behavior expectation to “ignore” instruction changes.
spec/requests/projects/show_spec.rb Adds coverage for returning instruction steps when stored as an array.
spec/concepts/project/update_spec.rb Updates unit specs for Project::Update signature change; removes student/teacher branching in operation tests.
spec/concepts/project/update_invalid_spec.rb Updates unit spec call signature for Project::Update.
spec/concepts/project/update_delete_components_spec.rb Updates unit spec call signature for Project::Update.
spec/concepts/project/update_default_component_spec.rb Updates unit spec call signature for Project::Update.
lib/concepts/project/operations/update.rb Removes current_user-based validation and always assigns :instructions if present in update_hash.
db/schema.rb Reflects projects.instructions as jsonb.
db/migrate/20260807120000_change_projects_instructions_to_jsonb.rb Migration to convert instructions from text to jsonb (and back).
config/locales/en.yml Removes now-unused “student_update_instructions” translation.
app/controllers/api/projects/remixes_controller.rb Permits instruction-step shape in remix request params.
app/controllers/api/projects_controller.rb Moves instruction-update prevention for students into strong params; refactors permitted attributes list.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread db/migrate/20260807120000_change_projects_instructions_to_jsonb.rb Outdated
@zetter-rpf
zetter-rpf marked this pull request as draft August 7, 2026 13:12
@zetter-rpf
zetter-rpf temporarily deployed to editor-api-p-instructio-tqy3iv August 7, 2026 13:16 Inactive
Previously project instructions could only be a single markdown
string, with no way to break guidance into discrete, orderable
steps.

Changing the existing instructions column type in place was tried
first, but that is risky against a live database: a server process
that queried the table before the migration ran keeps the old
column type cached and keeps sending data the database no longer
accepts, causing errors until it is restarted. Converting existing
data to the new type can also take a while on a large table and
holds a lock for that duration, causing errors on any concurrent
read or write.

This change avoids both problems by adding a new instruction_steps
jsonb column instead, leaving the existing instructions text column
untouched - it never needs to change type or be locked for a bulk
conversion. Project#instructions reads instruction_steps when
present and falls back to the legacy text column otherwise; the
controller permits either a plain string or an array of
{markdown_content} steps under the same instructions param, so no
other call site needs to know about the split.

Existing rows keep instruction_steps nil until a project is saved
through the new format. A follow-up task can backfill
instruction_steps for the remaining rows from the legacy column, at
which point the instructions column can be dropped entirely.
Previously Project::Update compared old and new instructions to
detect and reject a student trying to change them, needing a
current_user argument and a JSON-normalising comparison just to
tell Parameters and plain Ruby values apart.

This change drops :instructions from the permitted params entirely
when the current user is a student, so their attempt is filtered
out the same way Rails treats any other unpermitted param. The
request now succeeds with the change silently ignored, instead of
returning a 422. Project::Update no longer needs to know who the
current user is.
@zetter-rpf
zetter-rpf temporarily deployed to editor-api-p-instructio-tqy3iv August 10, 2026 07:52 Inactive
@zetter-rpf
zetter-rpf marked this pull request as ready for review August 10, 2026 07:54
@jamiebenstead
jamiebenstead requested a balanced review from Copilot August 10, 2026 08:40

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 14 out of 14 changed files in this pull request and generated no new comments.

Suppressed comments (2)

app/models/project.rb:83

  • The setter writes every non-array value into the JSONB column, and the getter then returns it without the legacy text column's string coercion. Because :instructions is permitted as a scalar, values such as numbers or booleans now round-trip as JSON numbers/booleans even though the supported contract is a string or an array of { markdown_content: ... } steps; nested parameters can also produce an object. Add format validation (including each array element) or normalize/reject unsupported values before assigning instruction_steps.
  def instructions=(value)
    self[:instructions] = value unless value.is_a?(Array)
    self[:instruction_steps] = value

app/controllers/api/projects/remixes_controller.rb:86

  • This newly permits structured instructions on remix requests, but Project::CreateRemix never reads params[:instructions]; format_project only duplicates the original instructions and applies name/user/remix fields. A submitted steps array is therefore silently ignored. Either apply the permitted instructions when formatting the remix (with the intended student restriction) or remove this accepted parameter if remixes must always inherit the original.
                              instructions: [[:markdown_content]]


class AddInstructionStepsToProjects < ActiveRecord::Migration[8.1]
def change
add_column :projects, :instruction_steps, :jsonb

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What made you decide to add the extra column rather than modify and use the current instructions column?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good question, there's some details in the commit message:

Changing the existing instructions column type in place was tried
first, but that is risky against a live database: a server process
that queried the table before the migration ran keeps the old
column type cached and keeps sending data the database no longer
accepts, causing errors until it is restarted. Converting existing
data to the new type can also take a while on a large table and
holds a lock for that duration, causing errors on any concurrent
read or write.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This isn't true for all type changes, but was for this one when I tested it because Rails treats jsonb differently.

class Update
class << self
def call(project:, update_hash:, current_user:)
def call(project:, update_hash:)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm curious, why are we removing the current_user everywhere here?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's detail in the commit message:

Previously Project::Update compared old and new instructions to
detect and reject a student trying to change them, needing a
current_user argument and a JSON-normalising comparison just to
tell Parameters and plain Ruby values apart.

I think it's simpler to do this in the controller and let Rails manage unexpected parameters rather than special casing this. Having it in the update required more complexity to see if the instructions had changed which was added in the first commit.

@jamiebenstead jamiebenstead left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM. Left a few comments on things I am curious about

@zetter-rpf
zetter-rpf merged commit 62e6553 into main Aug 10, 2026
7 checks passed
@zetter-rpf
zetter-rpf deleted the instruction-steps branch August 10, 2026 10:02
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants