From de6f46f289484c6b34e6cbdaf6bbf69c9c5e928d Mon Sep 17 00:00:00 2001 From: Matthew Trew Date: Mon, 9 Mar 2026 17:45:46 +0000 Subject: [PATCH 01/13] Add Scratch component model Rather than using the existing Component model, this adds a new Scratch component model. Projects expect to be able to update project metadata and component data at the same time, however for Scratch the lifecycle of project.json is managed independently by scratch-gui so it's best to keep that separate. --- app/models/scratch_component.rb | 3 +++ .../20260309104851_create_scratch_components.rb | 10 ++++++++++ db/schema.rb | 11 ++++++++++- spec/factories/scratch_components.rb | 6 ++++++ spec/models/scratch_component_spec.rb | 5 +++++ 5 files changed, 34 insertions(+), 1 deletion(-) create mode 100644 app/models/scratch_component.rb create mode 100644 db/migrate/20260309104851_create_scratch_components.rb create mode 100644 spec/factories/scratch_components.rb create mode 100644 spec/models/scratch_component_spec.rb diff --git a/app/models/scratch_component.rb b/app/models/scratch_component.rb new file mode 100644 index 000000000..cb92b6236 --- /dev/null +++ b/app/models/scratch_component.rb @@ -0,0 +1,3 @@ +class ScratchComponent < ApplicationRecord + belongs_to :project +end diff --git a/db/migrate/20260309104851_create_scratch_components.rb b/db/migrate/20260309104851_create_scratch_components.rb new file mode 100644 index 000000000..594e7e345 --- /dev/null +++ b/db/migrate/20260309104851_create_scratch_components.rb @@ -0,0 +1,10 @@ +class CreateScratchComponents < ActiveRecord::Migration[7.2] + def change + create_table :scratch_components, id: :uuid do |t| + t.jsonb :content + t.references :project, null: false, foreign_key: true, type: :uuid, index: { unique: true } + + t.timestamps + end + end +end diff --git a/db/schema.rb b/db/schema.rb index 356389a32..afa3525a9 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema[7.2].define(version: 2026_01_26_130135) do +ActiveRecord::Schema[7.2].define(version: 2026_03_09_104851) do # These are extensions that must be enabled in order to support this database enable_extension "pgcrypto" enable_extension "plpgsql" @@ -331,6 +331,14 @@ t.index ["school_roll_number"], name: "index_schools_on_school_roll_number", unique: true, where: "(rejected_at IS NULL)" end + create_table "scratch_components", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t| + t.jsonb "content" + t.uuid "project_id", null: false + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.index ["project_id"], name: "index_scratch_components_on_project_id", unique: true + end + create_table "teacher_invitations", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t| t.string "email_address" t.uuid "school_id", null: false @@ -380,6 +388,7 @@ add_foreign_key "school_project_transitions", "school_projects" add_foreign_key "school_projects", "projects" add_foreign_key "school_projects", "schools" + add_foreign_key "scratch_components", "projects" add_foreign_key "teacher_invitations", "schools" add_foreign_key "user_jobs", "good_jobs" end diff --git a/spec/factories/scratch_components.rb b/spec/factories/scratch_components.rb new file mode 100644 index 000000000..dfcada218 --- /dev/null +++ b/spec/factories/scratch_components.rb @@ -0,0 +1,6 @@ +FactoryBot.define do + factory :scratch_component do + content { "" } + project { nil } + end +end diff --git a/spec/models/scratch_component_spec.rb b/spec/models/scratch_component_spec.rb new file mode 100644 index 000000000..75bf6dcec --- /dev/null +++ b/spec/models/scratch_component_spec.rb @@ -0,0 +1,5 @@ +require 'rails_helper' + +RSpec.describe ScratchComponent, type: :model do + pending "add some examples to (or delete) #{__FILE__}" +end From 9eeece33e729076dc2a35e1b3a3cea874878a6c9 Mon Sep 17 00:00:00 2001 From: Matthew Trew Date: Mon, 9 Mar 2026 18:31:10 +0000 Subject: [PATCH 02/13] Add Scratch project creation and loading --- app/controllers/api/lessons_controller.rb | 3 ++- app/controllers/api/projects_controller.rb | 1 + app/controllers/api/scratch/projects_controller.rb | 3 ++- app/controllers/api/scratch/scratch_controller.rb | 5 +++++ app/graphql/mutations/create_project.rb | 3 ++- app/models/project.rb | 6 ++++++ lib/concepts/project/operations/create.rb | 3 ++- spec/factories/scratch_components.rb | 6 ++++-- spec/features/scratch/showing_a_scratch_project_spec.rb | 9 ++++++++- 9 files changed, 32 insertions(+), 7 deletions(-) diff --git a/app/controllers/api/lessons_controller.rb b/app/controllers/api/lessons_controller.rb index 0d381e2ef..dc51acd79 100644 --- a/app/controllers/api/lessons_controller.rb +++ b/app/controllers/api/lessons_controller.rb @@ -119,7 +119,8 @@ def base_params :name, :project_type, :locale, - { components: %i[id name extension content index default] } + { components: %i[id name extension content index default] }, + { scratch_component: {} } ] } ) diff --git a/app/controllers/api/projects_controller.rb b/app/controllers/api/projects_controller.rb index 272c2cf2a..f5d8218f1 100644 --- a/app/controllers/api/projects_controller.rb +++ b/app/controllers/api/projects_controller.rb @@ -117,6 +117,7 @@ def base_params { components: %i[id name extension content index default] }, + scratch_component: {}, parent: {}, image_list: [] ) diff --git a/app/controllers/api/scratch/projects_controller.rb b/app/controllers/api/scratch/projects_controller.rb index 4904724d3..ec9d46e0a 100644 --- a/app/controllers/api/scratch/projects_controller.rb +++ b/app/controllers/api/scratch/projects_controller.rb @@ -5,9 +5,10 @@ module Scratch class ProjectsController < ScratchController skip_before_action :authorize_user, only: [:show] skip_before_action :check_scratch_feature, only: [:show] + before_action :load_project, only: %i[show update] def show - render :show, formats: [:json] + render json: @project.scratch_component.content.to_json, formats: [:json] end def update diff --git a/app/controllers/api/scratch/scratch_controller.rb b/app/controllers/api/scratch/scratch_controller.rb index 2500b8495..c1b792141 100644 --- a/app/controllers/api/scratch/scratch_controller.rb +++ b/app/controllers/api/scratch/scratch_controller.rb @@ -16,6 +16,11 @@ def check_scratch_feature raise ActiveRecord::RecordNotFound, 'Not Found' end + + def load_project + project_loader = ProjectLoader.new(params[:id], [params[:locale]]) + @project = project_loader.load + end end end end diff --git a/app/graphql/mutations/create_project.rb b/app/graphql/mutations/create_project.rb index 07c98c3f7..6ee9247ce 100644 --- a/app/graphql/mutations/create_project.rb +++ b/app/graphql/mutations/create_project.rb @@ -10,7 +10,8 @@ class CreateProject < BaseMutation def resolve(**input) project_hash = input.merge( user_id: context[:current_user]&.id, - components: input[:components]&.map(&:to_h) + components: input[:components]&.map(&:to_h), + scratch_component: input[:scratch_component]&.to_h ) response = Project::Create.call(project_hash:, current_user: context[:current_user]) diff --git a/app/models/project.rb b/app/models/project.rb index f74dbcdb4..50eb1d0d7 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -13,6 +13,7 @@ module Types belongs_to :parent, optional: true, class_name: :Project, foreign_key: :remixed_from_id, inverse_of: :remixes has_many :remixes, dependent: :nullify, class_name: :Project, foreign_key: :remixed_from_id, inverse_of: :parent has_many :components, -> { order(default: :desc, name: :asc) }, dependent: :destroy, inverse_of: :project + has_one :scratch_component, dependent: :destroy, inverse_of: :project, required: false has_many :project_errors, dependent: :nullify has_many_attached :images has_many_attached :videos @@ -20,6 +21,7 @@ module Types has_one :school_project, dependent: :destroy accepts_nested_attributes_for :components + accepts_nested_attributes_for :scratch_component before_validation :check_unique_not_null, on: :create before_validation :create_school_project_if_needed @@ -69,6 +71,10 @@ def components=(array) super(array.map { |o| o.is_a?(Hash) ? Component.new(o) : o }) end + def scratch_component=(value) + super(value.is_a?(Hash) ? ScratchComponent.new(value) : value) + end + def last_edited_at # datetime that the project or one of its components was last updated [updated_at, components.maximum(:updated_at)].compact.max diff --git a/lib/concepts/project/operations/create.rb b/lib/concepts/project/operations/create.rb index a2e942889..5c5ba0a2d 100644 --- a/lib/concepts/project/operations/create.rb +++ b/lib/concepts/project/operations/create.rb @@ -18,8 +18,9 @@ def call(project_hash:, current_user:) def build_project(project_hash, current_user) project_hash[:identifier] = PhraseIdentifier.generate unless current_user&.experience_cs_admin? - new_project = Project.new(project_hash.except(:components)) + new_project = Project.new(project_hash.except(:components, :scratch_component)) new_project.components.build(project_hash[:components]) + new_project.build_scratch_component(project_hash[:scratch_component]) new_project end end diff --git a/spec/factories/scratch_components.rb b/spec/factories/scratch_components.rb index dfcada218..22c211e40 100644 --- a/spec/factories/scratch_components.rb +++ b/spec/factories/scratch_components.rb @@ -1,6 +1,8 @@ +# frozen_string_literal: true + FactoryBot.define do factory :scratch_component do - content { "" } - project { nil } + content { { targets: [], monitors: [], extensions: [], meta: {} } } + project end end diff --git a/spec/features/scratch/showing_a_scratch_project_spec.rb b/spec/features/scratch/showing_a_scratch_project_spec.rb index 12e584740..40bd25c3b 100644 --- a/spec/features/scratch/showing_a_scratch_project_spec.rb +++ b/spec/features/scratch/showing_a_scratch_project_spec.rb @@ -4,7 +4,14 @@ RSpec.describe 'Showing a Scratch project', type: :request do it 'returns scratch project JSON' do - get '/api/scratch/projects/any-identifier' + project = create( + :project, + project_type: Project::Types::CODE_EDITOR_SCRATCH, + locale: 'en' + ) + create(:scratch_component, project: project) + + get "/api/scratch/projects/#{project.identifier}" expect(response).to have_http_status(:ok) From a94e2f330ad039d4f924bce2aab274850f94cdd9 Mon Sep 17 00:00:00 2001 From: Matthew Trew Date: Tue, 10 Mar 2026 09:09:05 +0000 Subject: [PATCH 03/13] Fix Rubocop violations --- app/models/scratch_component.rb | 2 ++ spec/models/scratch_component_spec.rb | 5 ----- 2 files changed, 2 insertions(+), 5 deletions(-) delete mode 100644 spec/models/scratch_component_spec.rb diff --git a/app/models/scratch_component.rb b/app/models/scratch_component.rb index cb92b6236..6bc8c8bd2 100644 --- a/app/models/scratch_component.rb +++ b/app/models/scratch_component.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class ScratchComponent < ApplicationRecord belongs_to :project end diff --git a/spec/models/scratch_component_spec.rb b/spec/models/scratch_component_spec.rb deleted file mode 100644 index 75bf6dcec..000000000 --- a/spec/models/scratch_component_spec.rb +++ /dev/null @@ -1,5 +0,0 @@ -require 'rails_helper' - -RSpec.describe ScratchComponent, type: :model do - pending "add some examples to (or delete) #{__FILE__}" -end From 5ab6a08ad56593e6fde910e862c908ba0b10640b Mon Sep 17 00:00:00 2001 From: Matthew Trew Date: Tue, 10 Mar 2026 11:51:30 +0000 Subject: [PATCH 04/13] Add Scratch project updating --- .../api/scratch/projects_controller.rb | 4 +++- .../scratch/updating_a_scratch_project_spec.rb | 15 +++++++++++++-- 2 files changed, 16 insertions(+), 3 deletions(-) diff --git a/app/controllers/api/scratch/projects_controller.rb b/app/controllers/api/scratch/projects_controller.rb index ec9d46e0a..b61cb0b65 100644 --- a/app/controllers/api/scratch/projects_controller.rb +++ b/app/controllers/api/scratch/projects_controller.rb @@ -8,10 +8,12 @@ class ProjectsController < ScratchController before_action :load_project, only: %i[show update] def show - render json: @project.scratch_component.content.to_json, formats: [:json] + render json: @project.scratch_component&.content end def update + @project.scratch_component&.content = params[:content] + @project.save render json: { status: 'ok' }, status: :ok end end diff --git a/spec/features/scratch/updating_a_scratch_project_spec.rb b/spec/features/scratch/updating_a_scratch_project_spec.rb index eaa91936b..a0396025c 100644 --- a/spec/features/scratch/updating_a_scratch_project_spec.rb +++ b/spec/features/scratch/updating_a_scratch_project_spec.rb @@ -21,20 +21,31 @@ it 'responds 404 Not Found when cat_mode is not enabled' do authenticated_in_hydra_as(teacher) - put '/api/scratch/projects/any-identifier', params: { project: { targets: [] } }, headers: cookie_headers + put '/api/scratch/projects/any-identifier', params: { content: { targets: [] } }, headers: cookie_headers expect(response).to have_http_status(:not_found) end it 'updates a project when cat_mode is enabled and a cookie is provided' do + # Arrange authenticated_in_hydra_as(teacher) Flipper.enable_actor :cat_mode, school + project = create( + :project, + project_type: Project::Types::CODE_EDITOR_SCRATCH, + locale: 'en' + ) + create(:scratch_component, project: project) - put '/api/scratch/projects/any-identifier', params: { project: { targets: [] } }, headers: cookie_headers + # Act + put "/api/scratch/projects/#{project.identifier}", params: { content: { targets: ['some update'] } }, headers: cookie_headers + # Assert expect(response).to have_http_status(:ok) data = JSON.parse(response.body, symbolize_names: true) expect(data[:status]).to eq('ok') + + expect(project.reload.scratch_component.content.to_h['targets']).to eq(['some update']) end end From 37337435c171c95b1508410565dad2342264199c Mon Sep 17 00:00:00 2001 From: Matthew Trew Date: Tue, 10 Mar 2026 15:45:11 +0000 Subject: [PATCH 05/13] Fix Scratch project update body shape and test --- app/controllers/api/scratch/projects_controller.rb | 2 +- spec/features/scratch/updating_a_scratch_project_spec.rb | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/controllers/api/scratch/projects_controller.rb b/app/controllers/api/scratch/projects_controller.rb index b61cb0b65..3cd95da72 100644 --- a/app/controllers/api/scratch/projects_controller.rb +++ b/app/controllers/api/scratch/projects_controller.rb @@ -12,7 +12,7 @@ def show end def update - @project.scratch_component&.content = params[:content] + @project.scratch_component&.content = params @project.save render json: { status: 'ok' }, status: :ok end diff --git a/spec/features/scratch/updating_a_scratch_project_spec.rb b/spec/features/scratch/updating_a_scratch_project_spec.rb index a0396025c..2ada2ce06 100644 --- a/spec/features/scratch/updating_a_scratch_project_spec.rb +++ b/spec/features/scratch/updating_a_scratch_project_spec.rb @@ -38,7 +38,7 @@ create(:scratch_component, project: project) # Act - put "/api/scratch/projects/#{project.identifier}", params: { content: { targets: ['some update'] } }, headers: cookie_headers + put "/api/scratch/projects/#{project.identifier}", params: { targets: ['some update'] }, headers: cookie_headers # Assert expect(response).to have_http_status(:ok) From 6d4d4bc801e01c53c38a4b5fae5e23853f91d6db Mon Sep 17 00:00:00 2001 From: Matthew Trew Date: Wed, 11 Mar 2026 13:23:23 +0000 Subject: [PATCH 06/13] Add asset creation for SVGs Only SVGs are working at present, as non-text formats like PNG are not recognised due to Scratch sending the incorrect content type header. Further work needed to support other file types. --- app/controllers/api/scratch/assets_controller.rb | 10 ++++++++++ app/models/scratch_asset.rb | 7 +++++++ config/routes.rb | 2 +- db/migrate/20260310161646_create_scratch_assets.rb | 9 +++++++++ db/schema.rb | 8 +++++++- 5 files changed, 34 insertions(+), 2 deletions(-) create mode 100644 app/models/scratch_asset.rb create mode 100644 db/migrate/20260310161646_create_scratch_assets.rb diff --git a/app/controllers/api/scratch/assets_controller.rb b/app/controllers/api/scratch/assets_controller.rb index 08e6cf2c5..614872d4c 100644 --- a/app/controllers/api/scratch/assets_controller.rb +++ b/app/controllers/api/scratch/assets_controller.rb @@ -11,6 +11,16 @@ def show end def create + filename_with_extension = "#{params[:id]}.#{params[:format]}" + + asset = ScratchAsset.new(filename: filename_with_extension) + asset.file.attach( + io: StringIO.new(params[:content].to_s), + filename: filename_with_extension + ) + + asset.save! + render json: { status: 'ok', 'content-name': params[:id] }, status: :created end end diff --git a/app/models/scratch_asset.rb b/app/models/scratch_asset.rb new file mode 100644 index 000000000..04a9c3b65 --- /dev/null +++ b/app/models/scratch_asset.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +class ScratchAsset < ApplicationRecord + validates :filename, presence: true + + has_one_attached :file +end diff --git a/config/routes.rb b/config/routes.rb index 6dfc482c3..059b29546 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -36,7 +36,7 @@ namespace :scratch do resources :projects, only: %i[show update] get '/assets/internalapi/asset/:id(.:format)/get/' => 'assets#show' - post '/assets/:id' => 'assets#create' + post '/assets/:id(.:format)' => 'assets#create' end resource :default_project, only: %i[show] do diff --git a/db/migrate/20260310161646_create_scratch_assets.rb b/db/migrate/20260310161646_create_scratch_assets.rb new file mode 100644 index 000000000..c5233ed4c --- /dev/null +++ b/db/migrate/20260310161646_create_scratch_assets.rb @@ -0,0 +1,9 @@ +class CreateScratchAssets < ActiveRecord::Migration[7.2] + def change + create_table :scratch_assets, id: :uuid do |t| + t.string :filename + + t.timestamps + end + end +end diff --git a/db/schema.rb b/db/schema.rb index afa3525a9..34567c449 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema[7.2].define(version: 2026_03_09_104851) do +ActiveRecord::Schema[7.2].define(version: 2026_03_10_161646) do # These are extensions that must be enabled in order to support this database enable_extension "pgcrypto" enable_extension "plpgsql" @@ -331,6 +331,12 @@ t.index ["school_roll_number"], name: "index_schools_on_school_roll_number", unique: true, where: "(rejected_at IS NULL)" end + create_table "scratch_assets", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t| + t.string "filename" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + end + create_table "scratch_components", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t| t.jsonb "content" t.uuid "project_id", null: false From 8283e49e91502bda903c1c17fa31650e6a6f2185 Mon Sep 17 00:00:00 2001 From: Matthew Trew Date: Thu, 12 Mar 2026 13:26:04 +0000 Subject: [PATCH 07/13] Revert "Add asset creation for SVGs" This reverts commit 6d4d4bc801e01c53c38a4b5fae5e23853f91d6db. This was only partially working, we will revisit it in a subsequent PR. --- app/controllers/api/scratch/assets_controller.rb | 10 ---------- app/models/scratch_asset.rb | 7 ------- config/routes.rb | 2 +- db/migrate/20260310161646_create_scratch_assets.rb | 9 --------- db/schema.rb | 8 +------- 5 files changed, 2 insertions(+), 34 deletions(-) delete mode 100644 app/models/scratch_asset.rb delete mode 100644 db/migrate/20260310161646_create_scratch_assets.rb diff --git a/app/controllers/api/scratch/assets_controller.rb b/app/controllers/api/scratch/assets_controller.rb index 614872d4c..08e6cf2c5 100644 --- a/app/controllers/api/scratch/assets_controller.rb +++ b/app/controllers/api/scratch/assets_controller.rb @@ -11,16 +11,6 @@ def show end def create - filename_with_extension = "#{params[:id]}.#{params[:format]}" - - asset = ScratchAsset.new(filename: filename_with_extension) - asset.file.attach( - io: StringIO.new(params[:content].to_s), - filename: filename_with_extension - ) - - asset.save! - render json: { status: 'ok', 'content-name': params[:id] }, status: :created end end diff --git a/app/models/scratch_asset.rb b/app/models/scratch_asset.rb deleted file mode 100644 index 04a9c3b65..000000000 --- a/app/models/scratch_asset.rb +++ /dev/null @@ -1,7 +0,0 @@ -# frozen_string_literal: true - -class ScratchAsset < ApplicationRecord - validates :filename, presence: true - - has_one_attached :file -end diff --git a/config/routes.rb b/config/routes.rb index 059b29546..6dfc482c3 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -36,7 +36,7 @@ namespace :scratch do resources :projects, only: %i[show update] get '/assets/internalapi/asset/:id(.:format)/get/' => 'assets#show' - post '/assets/:id(.:format)' => 'assets#create' + post '/assets/:id' => 'assets#create' end resource :default_project, only: %i[show] do diff --git a/db/migrate/20260310161646_create_scratch_assets.rb b/db/migrate/20260310161646_create_scratch_assets.rb deleted file mode 100644 index c5233ed4c..000000000 --- a/db/migrate/20260310161646_create_scratch_assets.rb +++ /dev/null @@ -1,9 +0,0 @@ -class CreateScratchAssets < ActiveRecord::Migration[7.2] - def change - create_table :scratch_assets, id: :uuid do |t| - t.string :filename - - t.timestamps - end - end -end diff --git a/db/schema.rb b/db/schema.rb index 34567c449..afa3525a9 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema[7.2].define(version: 2026_03_10_161646) do +ActiveRecord::Schema[7.2].define(version: 2026_03_09_104851) do # These are extensions that must be enabled in order to support this database enable_extension "pgcrypto" enable_extension "plpgsql" @@ -331,12 +331,6 @@ t.index ["school_roll_number"], name: "index_schools_on_school_roll_number", unique: true, where: "(rejected_at IS NULL)" end - create_table "scratch_assets", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t| - t.string "filename" - t.datetime "created_at", null: false - t.datetime "updated_at", null: false - end - create_table "scratch_components", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t| t.jsonb "content" t.uuid "project_id", null: false From a867341063d3d0bee6204fac4073da5ced218d9e Mon Sep 17 00:00:00 2001 From: Matthew Trew Date: Mon, 16 Mar 2026 08:20:01 +0000 Subject: [PATCH 08/13] Use save! when updating projects This avoids missing errors. --- app/controllers/api/scratch/projects_controller.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/controllers/api/scratch/projects_controller.rb b/app/controllers/api/scratch/projects_controller.rb index 3cd95da72..797c2c06b 100644 --- a/app/controllers/api/scratch/projects_controller.rb +++ b/app/controllers/api/scratch/projects_controller.rb @@ -13,7 +13,7 @@ def show def update @project.scratch_component&.content = params - @project.save + @project.save! render json: { status: 'ok' }, status: :ok end end From e8b84eec51c7a945a509f2d1b69825f10e105686 Mon Sep 17 00:00:00 2001 From: Matthew Trew Date: Mon, 16 Mar 2026 08:24:00 +0000 Subject: [PATCH 09/13] Include Scratch component in last_edited_at calculation --- app/models/project.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/models/project.rb b/app/models/project.rb index 1cfa3faab..6842e8b61 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -75,7 +75,7 @@ def scratch_component=(value) def last_edited_at # datetime that the project or one of its components was last updated - [updated_at, components.maximum(:updated_at)].compact.max + [updated_at, components.maximum(:updated_at), scratch_component&.updated_at].compact.max end def media From c7c58ef27d6f252052e7553e67168afb24471d8d Mon Sep 17 00:00:00 2001 From: Matthew Trew Date: Mon, 16 Mar 2026 08:30:13 +0000 Subject: [PATCH 10/13] Remove safe navigation operator when handling scratch_component In the update case it won't matter, and in the show case an error would be better than nil. --- app/controllers/api/scratch/projects_controller.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/controllers/api/scratch/projects_controller.rb b/app/controllers/api/scratch/projects_controller.rb index 797c2c06b..0affb2b27 100644 --- a/app/controllers/api/scratch/projects_controller.rb +++ b/app/controllers/api/scratch/projects_controller.rb @@ -8,11 +8,11 @@ class ProjectsController < ScratchController before_action :load_project, only: %i[show update] def show - render json: @project.scratch_component&.content + render json: @project.scratch_component.content end def update - @project.scratch_component&.content = params + @project.scratch_component.content = params @project.save! render json: { status: 'ok' }, status: :ok end From ce63d8bd36402470fb8cebf2efa1963b70d7080c Mon Sep 17 00:00:00 2001 From: Matthew Trew Date: Mon, 16 Mar 2026 08:50:59 +0000 Subject: [PATCH 11/13] Revert "Include Scratch component in last_edited_at calculation" This reverts commit e8b84eec51c7a945a509f2d1b69825f10e105686. This causes an "unoptimised query" error that I'd rather not dive into just now. --- app/models/project.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/models/project.rb b/app/models/project.rb index 6842e8b61..1cfa3faab 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -75,7 +75,7 @@ def scratch_component=(value) def last_edited_at # datetime that the project or one of its components was last updated - [updated_at, components.maximum(:updated_at), scratch_component&.updated_at].compact.max + [updated_at, components.maximum(:updated_at)].compact.max end def media From 2192a3ce4e062f19b9517ecd8822b48981c9e957 Mon Sep 17 00:00:00 2001 From: Matthew Trew Date: Mon, 16 Mar 2026 08:58:32 +0000 Subject: [PATCH 12/13] Only build scratch component for project if present --- lib/concepts/project/operations/create.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/concepts/project/operations/create.rb b/lib/concepts/project/operations/create.rb index 5c5ba0a2d..6c4c9e5cc 100644 --- a/lib/concepts/project/operations/create.rb +++ b/lib/concepts/project/operations/create.rb @@ -20,7 +20,7 @@ def build_project(project_hash, current_user) project_hash[:identifier] = PhraseIdentifier.generate unless current_user&.experience_cs_admin? new_project = Project.new(project_hash.except(:components, :scratch_component)) new_project.components.build(project_hash[:components]) - new_project.build_scratch_component(project_hash[:scratch_component]) + new_project.build_scratch_component(project_hash[:scratch_component]) if project_hash[:scratch_component].present? new_project end end From 3463ed591d954d1a27b4dee3050cf318a000d7b7 Mon Sep 17 00:00:00 2001 From: Matthew Trew Date: Mon, 16 Mar 2026 13:15:33 +0000 Subject: [PATCH 13/13] Filter out non-project.json params --- app/controllers/api/scratch/projects_controller.rb | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/controllers/api/scratch/projects_controller.rb b/app/controllers/api/scratch/projects_controller.rb index 0affb2b27..2e044493c 100644 --- a/app/controllers/api/scratch/projects_controller.rb +++ b/app/controllers/api/scratch/projects_controller.rb @@ -12,7 +12,8 @@ def show end def update - @project.scratch_component.content = params + scratch_content = params.permit!.slice(:meta, :targets, :monitors, :extensions) + @project.scratch_component&.content = scratch_content.to_unsafe_h @project.save! render json: { status: 'ok' }, status: :ok end