Skip to content

Commit 39f4ebb

Browse files
committed
Handle scratch project saving and updating
This shows a static project for now, later we will likely pull project data from the database or object store. The updating always succeeds and returns an ok but doesn't perform any updates.
1 parent 3f9e975 commit 39f4ebb

8 files changed

Lines changed: 377 additions & 0 deletions

File tree

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
# frozen_string_literal: true
2+
3+
module Api
4+
module Scratch
5+
class ProjectsController < ScratchController
6+
skip_before_action :authorize_user, only: [:show]
7+
skip_before_action :check_scratch_feature, only: [:show]
8+
9+
def show
10+
render :show, formats: [:json]
11+
end
12+
13+
def update
14+
render json: { status: 'ok' }, status: :ok
15+
end
16+
end
17+
end
18+
end
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
# frozen_string_literal: true
2+
3+
module Api
4+
module Scratch
5+
class ScratchController < ApiController
6+
include IdentifiableByCookie
7+
8+
before_action :authorize_user
9+
before_action :check_scratch_feature
10+
11+
def check_scratch_feature
12+
return if current_user.nil?
13+
14+
school = current_user&.schools&.first
15+
return if Flipper.enabled?(:cat_mode, school)
16+
17+
raise ActiveRecord::RecordNotFound, 'Not Found'
18+
end
19+
end
20+
end
21+
end
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
# frozen_string_literal: true
2+
3+
module IdentifiableByCookie
4+
extend ActiveSupport::Concern
5+
include ActionController::Cookies
6+
7+
def identify_user
8+
token = cookies[:scratch_auth]
9+
User.from_token(token:) if token
10+
end
11+
12+
def current_user
13+
@current_user ||= identify_user
14+
end
15+
end
Lines changed: 264 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,264 @@
1+
{
2+
"targets": [
3+
{
4+
"isStage": true,
5+
"name": "Stage",
6+
"variables": {
7+
"`jEk@4|i[#Fk?(8x)AV.-my variable": [
8+
"my variable",
9+
"1"
10+
]
11+
},
12+
"lists": {},
13+
"broadcasts": {},
14+
"blocks": {
15+
"Q*IGJj0mdm@]wJ,v%1M5": {
16+
"opcode": "event_whenflagclicked",
17+
"next": "]ML!StPuGDgh@B`^[v0d",
18+
"parent": null,
19+
"inputs": {},
20+
"fields": {},
21+
"shadow": false,
22+
"topLevel": true,
23+
"x": 188,
24+
"y": 132
25+
},
26+
"]ML!StPuGDgh@B`^[v0d": {
27+
"opcode": "data_setvariableto",
28+
"next": null,
29+
"parent": "Q*IGJj0mdm@]wJ,v%1M5",
30+
"inputs": {
31+
"VALUE": [
32+
1,
33+
[
34+
10,
35+
"1"
36+
]
37+
]
38+
},
39+
"fields": {
40+
"VARIABLE": [
41+
"my variable",
42+
"`jEk@4|i[#Fk?(8x)AV.-my variable"
43+
]
44+
},
45+
"shadow": false,
46+
"topLevel": false
47+
}
48+
},
49+
"comments": {},
50+
"currentCostume": 0,
51+
"costumes": [
52+
{
53+
"name": "backdrop1",
54+
"dataFormat": "svg",
55+
"assetId": "cd21514d0531fdffb22204e0ec5ed84a",
56+
"md5ext": "cd21514d0531fdffb22204e0ec5ed84a.svg",
57+
"rotationCenterX": 240,
58+
"rotationCenterY": 180
59+
}
60+
],
61+
"sounds": [],
62+
"volume": 100,
63+
"layerOrder": 0,
64+
"tempo": 60,
65+
"videoTransparency": 50,
66+
"videoState": "on",
67+
"textToSpeechLanguage": null
68+
},
69+
{
70+
"isStage": false,
71+
"name": "teapot",
72+
"variables": {},
73+
"lists": {},
74+
"broadcasts": {},
75+
"blocks": {
76+
"+7D@27U/UaREbT1:$D2p": {
77+
"opcode": "motion_goto_menu",
78+
"next": null,
79+
"parent": "WQem=5FI$H@eF8Oy8=%m",
80+
"inputs": {},
81+
"fields": {
82+
"TO": [
83+
"_random_"
84+
]
85+
},
86+
"shadow": true,
87+
"topLevel": false
88+
},
89+
":Cul13Fz-$kW#uI)b3Ih": {
90+
"opcode": "motion_turnright",
91+
"next": "`HzTVWK}[NHS}n(wXj(~",
92+
"parent": "o6dY$%i#z$_:%J%y.1Uv",
93+
"inputs": {
94+
"DEGREES": [
95+
1,
96+
[
97+
4,
98+
"15"
99+
]
100+
]
101+
},
102+
"fields": {},
103+
"shadow": false,
104+
"topLevel": false
105+
},
106+
"El0`*m+.st%JF!*L54E,": {
107+
"opcode": "control_repeat",
108+
"next": null,
109+
"parent": "JP;xPsfOSUqBfPdVA)C9",
110+
"inputs": {
111+
"TIMES": [
112+
1,
113+
[
114+
6,
115+
"5"
116+
]
117+
],
118+
"SUBSTACK": [
119+
2,
120+
"WQem=5FI$H@eF8Oy8=%m"
121+
]
122+
},
123+
"fields": {},
124+
"shadow": false,
125+
"topLevel": false
126+
},
127+
"JP;xPsfOSUqBfPdVA)C9": {
128+
"opcode": "event_whenflagclicked",
129+
"next": "El0`*m+.st%JF!*L54E,",
130+
"parent": null,
131+
"inputs": {},
132+
"fields": {},
133+
"shadow": false,
134+
"topLevel": true,
135+
"x": 5,
136+
"y": -1196
137+
},
138+
"Lf3w#r641AVS(Z;,+4y3": {
139+
"opcode": "event_whenthisspriteclicked",
140+
"next": "o6dY$%i#z$_:%J%y.1Uv",
141+
"parent": null,
142+
"inputs": {},
143+
"fields": {},
144+
"shadow": false,
145+
"topLevel": true,
146+
"x": 277,
147+
"y": -1199
148+
},
149+
"TFT|@Y(or^}uOFGDC|FB": {
150+
"opcode": "looks_setsizeto",
151+
"next": null,
152+
"parent": "`HzTVWK}[NHS}n(wXj(~",
153+
"inputs": {
154+
"SIZE": [
155+
1,
156+
[
157+
4,
158+
"100"
159+
]
160+
]
161+
},
162+
"fields": {},
163+
"shadow": false,
164+
"topLevel": false
165+
},
166+
"WQem=5FI$H@eF8Oy8=%m": {
167+
"opcode": "motion_goto",
168+
"next": "k`v/#:wkfU{|QkjM)6G|",
169+
"parent": "El0`*m+.st%JF!*L54E,",
170+
"inputs": {
171+
"TO": [
172+
1,
173+
"+7D@27U/UaREbT1:$D2p"
174+
]
175+
},
176+
"fields": {},
177+
"shadow": false,
178+
"topLevel": false
179+
},
180+
"`HzTVWK}[NHS}n(wXj(~": {
181+
"opcode": "control_wait",
182+
"next": "TFT|@Y(or^}uOFGDC|FB",
183+
"parent": ":Cul13Fz-$kW#uI)b3Ih",
184+
"inputs": {
185+
"DURATION": [
186+
1,
187+
[
188+
5,
189+
"1"
190+
]
191+
]
192+
},
193+
"fields": {},
194+
"shadow": false,
195+
"topLevel": false
196+
},
197+
"k`v/#:wkfU{|QkjM)6G|": {
198+
"opcode": "control_wait",
199+
"next": null,
200+
"parent": "WQem=5FI$H@eF8Oy8=%m",
201+
"inputs": {
202+
"DURATION": [
203+
1,
204+
[
205+
5,
206+
"0.1"
207+
]
208+
]
209+
},
210+
"fields": {},
211+
"shadow": false,
212+
"topLevel": false
213+
},
214+
"o6dY$%i#z$_:%J%y.1Uv": {
215+
"opcode": "looks_setsizeto",
216+
"next": ":Cul13Fz-$kW#uI)b3Ih",
217+
"parent": "Lf3w#r641AVS(Z;,+4y3",
218+
"inputs": {
219+
"SIZE": [
220+
1,
221+
[
222+
4,
223+
"200"
224+
]
225+
]
226+
},
227+
"fields": {},
228+
"shadow": false,
229+
"topLevel": false
230+
}
231+
},
232+
"comments": {},
233+
"currentCostume": 0,
234+
"costumes": [
235+
{
236+
"name": "teapot",
237+
"bitmapResolution": 1,
238+
"dataFormat": "svg",
239+
"assetId": "8a9dadf4eea61892ec6908b1c99e4961",
240+
"md5ext": "8a9dadf4eea61892ec6908b1c99e4961.svg",
241+
"rotationCenterX": 23.171110153198242,
242+
"rotationCenterY": 22.78113555908203
243+
}
244+
],
245+
"sounds": [],
246+
"volume": 100,
247+
"layerOrder": 1,
248+
"visible": true,
249+
"x": 0,
250+
"y": 6,
251+
"size": 100,
252+
"direction": 105,
253+
"draggable": false,
254+
"rotationStyle": "all around"
255+
}
256+
],
257+
"monitors": [],
258+
"extensions": [],
259+
"meta": {
260+
"semver": "3.0.0",
261+
"vm": "12.2.2",
262+
"agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/145.0.0.0 Safari/537.36"
263+
}
264+
}

config/initializers/cors.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
end
2424

2525
def standard_cors_options
26+
resource '/api/scratch/*', headers: :any, methods: %i[get post put], credentials: true, expose: ['Link']
2627
resource '/api/projects/*', headers: :any, methods: %i[get post patch put delete], credentials: true, expose: ['Link']
2728
resource '*', headers: :any, methods: %i[get post patch put delete], expose: ['Link']
2829
end

config/routes.rb

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,10 @@
3333
mount GraphiQL::Rails::Engine, at: '/graphql', graphql_path: '/graphql#execute' unless Rails.env.production?
3434

3535
namespace :api do
36+
namespace :scratch do
37+
resources :projects, only: %i[show update]
38+
end
39+
3640
resource :default_project, only: %i[show] do
3741
get '/html', to: 'default_projects#html'
3842
get '/python', to: 'default_projects#python'
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
# frozen_string_literal: true
2+
3+
require 'rails_helper'
4+
5+
RSpec.describe 'Showing a Scratch project', type: :request do
6+
it 'returns scratch project JSON' do
7+
get '/api/scratch/projects/any-identifier'
8+
9+
expect(response).to have_http_status(:ok)
10+
11+
data = JSON.parse(response.body, symbolize_names: true)
12+
expect(data).to have_key(:targets)
13+
end
14+
end
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
# frozen_string_literal: true
2+
3+
require 'rails_helper'
4+
5+
RSpec.describe 'Updating a Scratch project', type: :request do
6+
let(:school) { create(:school) }
7+
let(:teacher) { create(:teacher, school:) }
8+
let(:cookie_headers) { { 'Cookie' => "scratch_auth=#{UserProfileMock::TOKEN}" } }
9+
10+
before do
11+
Flipper.disable :cat_mode
12+
Flipper.disable_actor :cat_mode, school
13+
end
14+
15+
it 'responds 401 Unauthorized when no cookie is provided' do
16+
put '/api/scratch/projects/any-identifier', params: { project: { targets: [] } }
17+
18+
expect(response).to have_http_status(:unauthorized)
19+
end
20+
21+
it 'responds 404 Not Found when cat_mode is not enabled' do
22+
authenticated_in_hydra_as(teacher)
23+
24+
put '/api/scratch/projects/any-identifier', params: { project: { targets: [] } }, headers: cookie_headers
25+
26+
expect(response).to have_http_status(:not_found)
27+
end
28+
29+
it 'updates a project when cat_mode is enabled and a cookie is provided' do
30+
authenticated_in_hydra_as(teacher)
31+
Flipper.enable_actor :cat_mode, school
32+
33+
put '/api/scratch/projects/any-identifier', params: { project: { targets: [] } }, headers: cookie_headers
34+
35+
expect(response).to have_http_status(:ok)
36+
37+
data = JSON.parse(response.body, symbolize_names: true)
38+
expect(data[:status]).to eq('ok')
39+
end
40+
end

0 commit comments

Comments
 (0)