Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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
11 changes: 11 additions & 0 deletions app/controllers/api/lessons_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ class LessonsController < ApiController

before_action :authorize_user, except: %i[index show]
before_action :verify_school_class_belongs_to_school, only: :create
before_action :verify_can_create_scratch_projects, only: %i[create create_copy]
load_and_authorize_resource :lesson

def index
Expand Down Expand Up @@ -83,6 +84,12 @@ def verify_school_class_belongs_to_school
raise ParameterError, 'school_class_id does not correspond to school_id'
end

def verify_can_create_project_type
return unless scratch_project? && !school.scratch_enabled?

render json: { error: 'Forbidden' }, status: :forbidden
end

def user_remixes(lessons)
lessons.map { |lesson| user_remix(lesson) }
end
Expand All @@ -97,6 +104,10 @@ def user_remix(lesson)
)
end

def scratch_project?
base_params.dig(:project_attributes, :project_type) == Project::Types::CODE_EDITOR_SCRATCH
end

def lesson_params
base_params.merge(user_id: current_user.id)
end
Expand Down
12 changes: 9 additions & 3 deletions app/controllers/api/schools_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ def show
end

def create
result = School::Create.call(school_params:, creator_id: current_user.id, token: current_user.token)
result = School::Create.call(school_params: school_create_params, creator_id: current_user.id, token: current_user.token)

if result.success?
@school = result[:school]
Expand All @@ -31,7 +31,7 @@ def create

def update
school = School.find(params[:id])
result = School::Update.call(school:, school_params:)
result = School::Update.call(school:, school_params: school_update_params)

if result.success?
@school = result[:school]
Expand Down Expand Up @@ -76,7 +76,7 @@ def import

private

def school_params
def school_create_params
params.expect(
school: %i[name
website
Expand All @@ -99,5 +99,11 @@ def school_params
user_origin]
)
end

def school_update_params
params.expect(
school: %i[scratch_enabled]
)
end
end
end
1 change: 0 additions & 1 deletion app/controllers/api/scratch/assets_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ module Scratch
class AssetsController < ScratchController
include ActiveStorage::SetCurrent

skip_before_action :authorize_user, only: [:show]
prepend_before_action :load_project_from_header, only: %i[show create]

def show
Expand Down
3 changes: 0 additions & 3 deletions app/controllers/api/scratch/projects_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,7 @@ module Scratch
class ProjectsController < ScratchController
include RemixSelection

skip_before_action :authorize_user, only: [:show]
skip_before_action :check_scratch_feature, only: [:show]
before_action :load_project, only: %i[show update]

before_action :ensure_create_is_a_remix, only: %i[create]

def show
Expand Down
9 changes: 3 additions & 6 deletions app/controllers/api/scratch/scratch_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,10 @@ module Api
module Scratch
class ScratchController < ApiController
before_action :authorize_user
before_action :check_scratch_feature
before_action :can_use_scratch?

def check_scratch_feature
return if current_user.nil?

school = current_user&.schools&.first
return if Flipper.enabled?(:cat_mode, school)
def can_use_scratch?
return true if current_user.schools.any?

raise ActiveRecord::RecordNotFound, 'Not Found'
end
Expand Down
3 changes: 2 additions & 1 deletion app/views/api/schools/_school.json.jbuilder
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,8 @@ json.call(
:country_code,
:verified_at,
:created_at,
:updated_at
:updated_at,
:scratch_enabled,
)

include_roles = local_assigns.fetch(:roles, false)
Expand Down
5 changes: 5 additions & 0 deletions db/migrate/20260528141937_add_scratch_enabled_to_school.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
class AddScratchEnabledToSchool < ActiveRecord::Migration[8.1]
def change
add_column :schools, :scratch_enabled, :boolean, default: false, null: false
end
end
3 changes: 2 additions & 1 deletion db/schema.rb

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

40 changes: 40 additions & 0 deletions spec/features/lesson/creating_a_lesson_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -186,4 +186,44 @@
expect(response).to have_http_status(:unprocessable_content)
end
end

describe 'working with Scratch projects' do
let(:params) do
{
lesson: {
name: 'Test Lesson',
school_id: school.id,
project_attributes: {
name: 'Hello Scratch project',
project_type: Project::Types::CODE_EDITOR_SCRATCH,
scratch_component: {
content: {
example_data: 'true'
}
}
}
}
}
end

it 'creates a lesson with a scratch component when school has Scratch enabled' do
school.update!(scratch_enabled: true)
post('/api/lessons', headers:, params:)
expect(response).to have_http_status(:created)

data = JSON.parse(response.body, symbolize_names: true)

lesson_id = data[:id]

project = Lesson.find(lesson_id).project
expect(project.project_type).to eq(Project::Types::CODE_EDITOR_SCRATCH)
expect(project.scratch_component.content).to eq({ 'example_data' => 'true' })
end

it 'returns forbidden when school does not have Scratch enabled' do
school.update!(scratch_enabled: false)
post('/api/lessons', headers:, params:)
expect(response).to have_http_status(:forbidden)
end
end
end
11 changes: 3 additions & 8 deletions spec/features/school/updating_a_school_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,14 @@
authenticated_in_hydra_as(owner)
end

let!(:school) { create(:school) }
let!(:school) { create(:school, scratch_enabled: false) }
let(:headers) { { Authorization: UserProfileMock::TOKEN } }
let(:owner) { create(:owner, school:) }

let(:params) do
{
school: {
name: 'New Name'
scratch_enabled: true
}
}
end
Expand All @@ -28,7 +28,7 @@
put("/api/schools/#{school.id}", headers:, params:)
data = JSON.parse(response.body, symbolize_names: true)

expect(data[:name]).to eq('New Name')
expect(data[:scratch_enabled]).to eq(true)
end

it 'responds 404 Not Found when no school exists' do
Expand All @@ -41,11 +41,6 @@
expect(response).to have_http_status(:bad_request)
end

it 'responds 422 Unprocessable Entity when params are invalid' do
put("/api/schools/#{school.id}", headers:, params: { school: { name: ' ' } })
expect(response).to have_http_status(:unprocessable_content)
end

it 'responds 401 Unauthorized when no token is given' do
put "/api/schools/#{school.id}"
expect(response).to have_http_status(:unauthorized)
Expand Down
20 changes: 7 additions & 13 deletions spec/features/scratch/creating_a_scratch_asset_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -10,33 +10,27 @@
create(:scratch_component, project: scratch_project)
end
end
let(:auth_headers) { { 'Authorization' => UserProfileMock::TOKEN } }
let(:project_headers) { auth_headers.merge('X-Project-ID' => project.identifier) }

before do
Flipper.enable_actor :cat_mode, school
end
let(:headers) { { 'Authorization' => UserProfileMock::TOKEN, 'X-Project-ID' => project.identifier } }

it 'responds 401 Unauthorized when no Authorization header is provided' do
post '/api/scratch/assets/example.svg', headers: { 'X-Project-ID' => project.identifier }

expect(response).to have_http_status(:unauthorized)
end

it 'responds 404 Not Found when cat_mode is not enabled' do
authenticated_in_hydra_as(teacher)
Flipper.disable :cat_mode
Flipper.disable_actor :cat_mode, school
it 'responds 404 Not Found when user is not part of a school' do
user = create(:user)
authenticated_in_hydra_as(user)

post '/api/scratch/assets/example.svg', headers: project_headers
post '/api/scratch/assets/example.svg', headers: headers

expect(response).to have_http_status(:not_found)
end

it 'creates an asset when cat_mode is enabled and the required headers are provided' do
it 'creates an asset when user is part of a school and the required headers are provided' do
authenticated_in_hydra_as(teacher)

post '/api/scratch/assets/example.svg', headers: project_headers
post '/api/scratch/assets/example.svg', headers: headers

expect(response).to have_http_status(:created)

Expand Down
11 changes: 4 additions & 7 deletions spec/features/scratch/creating_a_scratch_project_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -36,9 +36,6 @@
before do
mock_phrase_generation('new-project-id')
create(:scratch_component, project: original_project)

Flipper.disable :cat_mode
Flipper.disable_actor :cat_mode, school
end

def make_request(query: request_query, request_headers: headers, request_params: scratch_project)
Expand All @@ -56,18 +53,18 @@ def make_request(query: request_query, request_headers: headers, request_params:
expect(response).to have_http_status(:unauthorized)
end

it 'responds 404 Not Found when cat_mode is not enabled' do
authenticated_in_hydra_as(teacher)
it 'responds 404 Not Found when user is not part of a school' do
user = create(:user)
authenticated_in_hydra_as(user)

make_request

expect(response).to have_http_status(:not_found)
end

context 'when authenticated and cat_mode is enabled' do
context 'when authenticated and part of a school' do
before do
authenticated_in_hydra_as(teacher)
Flipper.enable_actor :cat_mode, school
end

it 'responds 403 Forbidden when not remixing' do
Expand Down
18 changes: 3 additions & 15 deletions spec/features/scratch/creating_and_showing_a_scratch_asset_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,6 @@
let(:auth_headers) { { 'Authorization' => UserProfileMock::TOKEN } }
let(:project_headers) { auth_headers.merge('X-Project-ID' => project.identifier) }

before do
Flipper.enable_actor :cat_mode, school
end

describe 'GET #show' do
it 'responds 400 Bad Request when X-Project-ID is not provided' do
get '/api/scratch/assets/internalapi/asset/test_image_1.png/get/', headers: auth_headers
Expand Down Expand Up @@ -384,18 +380,10 @@
end
end

context 'when user is logged in and cat_mode is disabled' do
before do
authenticated_in_hydra_as(teacher)
Flipper.disable :cat_mode
Flipper.disable_actor :cat_mode, school
end

it 'responds 404 Not Found when cat_mode is not enabled' do
post '/api/scratch/assets/example.svg', headers: request_headers
it 'responds 401 unauthorized when user is not part of a school' do
post '/api/scratch/assets/example.svg', headers: { 'X-Project-ID' => project.identifier }

expect(response).to have_http_status(:not_found)
end
expect(response).to have_http_status(:unauthorized)
end
end

Expand Down
23 changes: 19 additions & 4 deletions spec/features/scratch/showing_a_scratch_project_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,20 @@
require 'rails_helper'

RSpec.describe 'Showing a Scratch project', type: :request do
let(:school) { create(:school) }
let(:teacher) { create(:teacher, school:) }
let(:headers) { { 'Authorization' => UserProfileMock::TOKEN } }

it 'returns scratch project JSON' do
authenticated_in_hydra_as(teacher)
project = create(
:project,
project_type: Project::Types::CODE_EDITOR_SCRATCH,
locale: 'en'
)
create(:scratch_component, project: project)

get "/api/scratch/projects/#{project.identifier}"
get "/api/scratch/projects/#{project.identifier}", headers: headers

expect(response).to have_http_status(:ok)

Expand All @@ -20,6 +25,7 @@
end

it 'returns the stage target first when stored targets are out of order' do
authenticated_in_hydra_as(teacher)
project = create(
:project,
project_type: Project::Types::CODE_EDITOR_SCRATCH,
Expand All @@ -37,23 +43,32 @@
}
)

get "/api/scratch/projects/#{project.identifier}"
get "/api/scratch/projects/#{project.identifier}", headers: headers

expect(response).to have_http_status(:ok)
expect(response.parsed_body.fetch('targets').pluck('name')).to eq(%w[Stage Sprite1 Sprite2])
end

it 'returns a 404 if project does not exist' do
get '/api/scratch/projects/non_existent_project'
authenticated_in_hydra_as(teacher)
get '/api/scratch/projects/non_existent_project', headers: headers

expect(response).to have_http_status(:not_found)
end

it 'returns a 404 if project is not a scratch project' do
authenticated_in_hydra_as(teacher)
project = create(:project, project_type: Project::Types::PYTHON, locale: 'en')

get "/api/scratch/projects/#{project.identifier}"
get "/api/scratch/projects/#{project.identifier}", headers: headers

expect(response).to have_http_status(:not_found)
end

it 'returns a 401 unauthorized if not logged in' do
project = create(:project, project_type: Project::Types::PYTHON, locale: 'en')
get "/api/scratch/projects/#{project.identifier}"

expect(response).to have_http_status(:unauthorized)
end
end
Loading
Loading