Skip to content

Commit b54f450

Browse files
Add support for Scratch projects (#513)
This introduces the concept of projects of type "scratch" which we're going to need for the [Experience CS project](https://github.com/RaspberryPiFoundation/experience-cs). However, it goes to some pains to ensure that any existing clients of the API (e.g. `editor-standalone`, `editor-ui`, the admin dashboard in `editor-api`, etc) will not see a scratch project for now unless they specifically choose to by adding a `project_type=scratch` query string parameter. * [Add default scope to hide scratch projects](bdf8ccc) * [Add scratch-only option for project show endpoint](91e8466) * [Consistently use strings for Project#project_type](f442965) * [Extract string literals -> Project::Types constants](dfade60) --------- Co-authored-by: Chris Roos <[email protected]>
1 parent b6fc0b7 commit b54f450

17 files changed

+90
-30
lines changed

.rubocop.yml

+1-1
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ GraphQL/ObjectDescription:
2020
- app/graphql/types/query_type.rb
2121

2222
RSpec/NestedGroups:
23-
Max: 4
23+
Max: 5
2424

2525
RSpec/DescribeClass:
2626
Exclude:

app/controllers/api/default_projects_controller.rb

+4-4
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ class DefaultProjectsController < ApiController
55
before_action :authorize_user, only: %i[create]
66

77
def show
8-
data = if params[:type] == 'html'
8+
data = if params[:type] == Project::Types::HTML
99
html_project
1010
else
1111
python_project
@@ -23,7 +23,7 @@ def html
2323

2424
def create
2525
identifier = PhraseIdentifier.generate
26-
@project = Project.new(identifier:, project_type: 'python')
26+
@project = Project.new(identifier:, project_type: Project::Types::PYTHON)
2727
@project.components << Component.new(python_component)
2828
@project.save
2929

@@ -38,7 +38,7 @@ def python_component
3838

3939
def python_project
4040
{
41-
type: 'python',
41+
type: Project::Types::PYTHON,
4242
components: [
4343
{ lang: 'py', name: 'main',
4444
content: "import turtle\nt = turtle.Turtle()\nt.forward(100)\nprint(\"Oh yeah!\")" }
@@ -53,7 +53,7 @@ def html_project
5353
CON
5454

5555
{
56-
type: 'html',
56+
type: Project::Types::HTML,
5757
components: [
5858
{ lang: 'html', name: 'index', content: },
5959
{ lang: 'css', name: 'style', content: "h1 {\n color: blue;\n}" },

app/controllers/api/projects_controller.rb

+1-1
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ def verify_lesson_belongs_to_school
6464
def load_project
6565
project_loader = ProjectLoader.new(params[:id], [params[:locale]])
6666
@project = if action_name == 'show'
67-
project_loader.load(include_images: true)
67+
project_loader.load(include_images: true, only_scratch: params[:project_type] == Project::Types::SCRATCH)
6868
else
6969
project_loader.load
7070
end

app/models/filesystem_project.rb

+1-1
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ def self.import_all!
1515
categorized_files = categorize_files(files, dir)
1616

1717
project_importer = ProjectImporter.new(name: proj_config['NAME'], identifier: proj_config['IDENTIFIER'],
18-
type: proj_config['TYPE'] || 'python',
18+
type: proj_config['TYPE'] || Project::Types::PYTHON,
1919
locale: proj_config['LOCALE'] || 'en', **categorized_files)
2020
project_importer.import!
2121
end

app/models/project.rb

+8
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,12 @@
11
# frozen_string_literal: true
22

33
class Project < ApplicationRecord
4+
module Types
5+
PYTHON = 'python'
6+
HTML = 'html'
7+
SCRATCH = 'scratch'
8+
end
9+
410
belongs_to :school, optional: true
511
belongs_to :lesson, optional: true
612
belongs_to :parent, optional: true, class_name: :Project, foreign_key: :remixed_from_id, inverse_of: :remixes
@@ -26,6 +32,8 @@ class Project < ApplicationRecord
2632
validate :project_with_school_id_has_school_project
2733
validate :school_project_school_matches_project_school
2834

35+
default_scope -> { where.not(project_type: Types::SCRATCH) }
36+
2937
scope :internal_projects, -> { where(user_id: nil) }
3038

3139
has_paper_trail(

lib/project_loader.rb

+3-2
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,9 @@ def initialize(identifier, locales)
88
@locales = [*locales, 'en', nil]
99
end
1010

11-
def load(include_images: false)
12-
query = Project.where(identifier:, locale: @locales)
11+
def load(include_images: false, only_scratch: false)
12+
query = only_scratch ? Project.unscoped.where(project_type: Project::Types::SCRATCH) : Project
13+
query = query.where(identifier:, locale: @locales)
1314
query = query.includes(images_attachments: :blob) if include_images
1415
query.min_by { |project| @locales.find_index(project.locale) }
1516
end

lib/tasks/seeds_helper.rb

+1-1
Original file line numberDiff line numberDiff line change
@@ -98,7 +98,7 @@ def create_project(user_id, school, lesson, code = '')
9898
project.school = school
9999
project.lesson = lesson
100100
project.locale = 'en'
101-
project.project_type = 'python'
101+
project.project_type = Project::Types::PYTHON
102102
project.components << Component.new({ extension: 'py', name: 'main',
103103
content: code })
104104
end

spec/concepts/lesson/create_spec.rb

+2-2
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
school_id: school.id,
1515
project_attributes: {
1616
name: 'Hello world project',
17-
project_type: 'python',
17+
project_type: Project::Types::PYTHON,
1818
components: [
1919
{ name: 'main.py', extension: 'py', content: 'print("Hello, world!")' }
2020
]
@@ -86,7 +86,7 @@
8686
{
8787
project_attributes: {
8888
name: 'Hello world project',
89-
project_type: 'python',
89+
project_type: Project::Types::PYTHON,
9090
components: [
9191
{ name: 'main.py', extension: 'py', content: 'print("Hello, world!")' }
9292
]

spec/concepts/project/create_spec.rb

+1-1
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@
2020

2121
let(:project_hash) do
2222
{
23-
project_type: 'python',
23+
project_type: Project::Types::PYTHON,
2424
components: [{
2525
name: 'main',
2626
extension: 'py',

spec/factories/project.rb

+1-1
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
user_id { SecureRandom.uuid }
66
name { Faker::Book.title }
77
identifier { "#{Faker::Verb.base}-#{Faker::Verb.base}-#{Faker::Verb.base}" }
8-
project_type { 'python' }
8+
project_type { Project::Types::PYTHON }
99
locale { %w[en es-LA fr-FR].sample }
1010

1111
transient do

spec/features/lesson/creating_a_lesson_spec.rb

+3-3
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919
name: 'Test Lesson',
2020
project_attributes: {
2121
name: 'Hello world project',
22-
project_type: 'python',
22+
project_type: Project::Types::PYTHON,
2323
components: [
2424
{ name: 'main.py', extension: 'py', content: 'print("Hello, world!")' }
2525
]
@@ -68,7 +68,7 @@
6868
school_id: school.id,
6969
project_attributes: {
7070
name: 'Hello world project',
71-
project_type: 'python',
71+
project_type: Project::Types::PYTHON,
7272
components: [
7373
{ name: 'main.py', extension: 'py', content: 'print("Hello, world!")' }
7474
]
@@ -130,7 +130,7 @@
130130
school_class_id: school_class.id,
131131
project_attributes: {
132132
name: 'Hello world project',
133-
project_type: 'python',
133+
project_type: Project::Types::PYTHON,
134134
components: [
135135
{ name: 'main.py', extension: 'py', content: 'print("Hello, world!")' }
136136
]

spec/graphql/mutations/create_project_mutation_spec.rb

+1-1
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
{
1111
project: {
1212
name: 'Untitled project',
13-
projectType: 'python',
13+
projectType: Project::Types::PYTHON,
1414
components: [{
1515
content: 'Insert Python Here',
1616
default: true,

spec/graphql/mutations/update_project_mutation_spec.rb

+3-3
Original file line numberDiff line numberDiff line change
@@ -18,15 +18,15 @@
1818
project: {
1919
id: project_id,
2020
name: 'Untitled project again',
21-
projectType: 'html'
21+
projectType: Project::Types::HTML
2222
}
2323
}
2424
end
2525

2626
it { expect(mutation).to be_a_valid_graphql_query }
2727

2828
context 'with an existing project' do
29-
let(:project) { create(:project, user_id: authenticated_user.id, project_type: :python) }
29+
let(:project) { create(:project, user_id: authenticated_user.id, project_type: Project::Types::PYTHON) }
3030
let(:project_id) { project.to_gid_param }
3131
let(:school) { create(:school) }
3232

@@ -62,7 +62,7 @@
6262
end
6363

6464
it 'updates the project type' do
65-
expect { result }.to change { project.reload.project_type }.from(project.project_type).to('html')
65+
expect { result }.to change { project.reload.project_type }.from(project.project_type).to(Project::Types::HTML)
6666
end
6767

6868
context 'when the project cannot be found' do

spec/jobs/upload_job_spec.rb

+1-1
Original file line numberDiff line numberDiff line change
@@ -119,7 +119,7 @@
119119
{
120120
name: "Don't Collide!",
121121
identifier: 'dont-collide-starter',
122-
type: 'python',
122+
type: Project::Types::PYTHON,
123123
locale: 'ja-JP',
124124
components: [
125125
{

spec/lib/project_importer_spec.rb

+1-1
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
described_class.new(
88
name: 'My amazing project',
99
identifier: 'my-amazing-project',
10-
type: 'python',
10+
type: Project::Types::PYTHON,
1111
locale: 'ja-JP',
1212
components: [
1313
{ name: 'main', extension: 'py', content: 'print(\'hello\')', default: true },

spec/models/project_spec.rb

+23
Original file line numberDiff line numberDiff line change
@@ -319,4 +319,27 @@
319319
expect(project_without_school.versions.length).to(eq(0))
320320
end
321321
end
322+
323+
describe 'default scope' do
324+
let!(:python_project) { create(:project, project_type: Project::Types::PYTHON) }
325+
let!(:html_project) { create(:project, project_type: Project::Types::HTML) }
326+
let!(:project_with_unknown_type) { create(:project, project_type: 'unknown') }
327+
let!(:scratch_project) { create(:project, project_type: Project::Types::SCRATCH) }
328+
329+
it 'includes python projects' do
330+
expect(described_class.all).to include(python_project)
331+
end
332+
333+
it 'includes html projects' do
334+
expect(described_class.all).to include(html_project)
335+
end
336+
337+
it 'includes projects with unknown type' do
338+
expect(described_class.all).to include(project_with_unknown_type)
339+
end
340+
341+
it 'does not include scratch projects' do
342+
expect(described_class.all).not_to include(scratch_project)
343+
end
344+
end
322345
end

spec/requests/projects/show_spec.rb

+35-7
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@
2020
let(:project_json) do
2121
{
2222
identifier: project.identifier,
23-
project_type: 'python',
23+
project_type: Project::Types::PYTHON,
2424
locale: project.locale,
2525
name: project.name,
2626
user_id: project.user_id,
@@ -62,7 +62,7 @@
6262
let(:student_project_json) do
6363
{
6464
identifier: student_project.identifier,
65-
project_type: 'python',
65+
project_type: Project::Types::PYTHON,
6666
locale: student_project.locale,
6767
name: student_project.name,
6868
user_id: student_project.user_id,
@@ -102,7 +102,7 @@
102102
let(:another_teacher_project_json) do
103103
{
104104
identifier: another_teacher_project.identifier,
105-
project_type: 'python',
105+
project_type: Project::Types::PYTHON,
106106
locale: another_teacher_project.locale,
107107
name: another_teacher_project.name,
108108
user_id: teacher.id,
@@ -130,7 +130,7 @@
130130
let(:another_project_json) do
131131
{
132132
identifier: another_project.identifier,
133-
project_type: 'python',
133+
project_type: Project::Types::PYTHON,
134134
name: another_project.name,
135135
locale: another_project.locale,
136136
user_id: another_project.user_id,
@@ -156,11 +156,12 @@
156156

157157
context 'when user is not logged in' do
158158
context 'when loading a starter project' do
159-
let!(:starter_project) { create(:project, user_id: nil, locale: 'ja-JP') }
159+
let(:project_type) { Project::Types::PYTHON }
160+
let!(:starter_project) { create(:project, user_id: nil, locale: 'ja-JP', project_type:) }
160161
let(:starter_project_json) do
161162
{
162163
identifier: starter_project.identifier,
163-
project_type: 'python',
164+
project_type:,
164165
locale: starter_project.locale,
165166
name: starter_project.name,
166167
user_id: starter_project.user_id,
@@ -192,6 +193,33 @@
192193
expect(response).to have_http_status(:not_found)
193194
end
194195

196+
context 'when project is a scratch project' do
197+
let(:project_type) { Project::Types::SCRATCH }
198+
199+
context 'when project_type is set to scratch in query params' do
200+
before do
201+
get("/api/projects/#{starter_project.identifier}?locale=#{starter_project.locale}&project_type=scratch", headers:)
202+
end
203+
204+
it 'returns success response' do
205+
expect(response).to have_http_status(:ok)
206+
end
207+
208+
it 'returns json' do
209+
expect(response.content_type).to eq('application/json; charset=utf-8')
210+
end
211+
212+
it 'returns the project json' do
213+
expect(response.body).to eq(starter_project_json)
214+
end
215+
end
216+
217+
it 'returns 404 response if scratch project' do
218+
get("/api/projects/#{starter_project.identifier}?locale=#{starter_project.locale}", headers:)
219+
expect(response).to have_http_status(:not_found)
220+
end
221+
end
222+
195223
it 'creates a new ProjectLoader with the correct parameters' do
196224
allow(ProjectLoader).to receive(:new).and_call_original
197225
get("/api/projects/#{starter_project.identifier}?locale=#{starter_project.locale}", headers:)
@@ -205,7 +233,7 @@
205233
let(:project_json) do
206234
{
207235
identifier: project.identifier,
208-
project_type: 'python',
236+
project_type: Project::Types::PYTHON,
209237
locale: project.locale,
210238
name: project.name,
211239
user_id: project.user_id,

0 commit comments

Comments
 (0)