Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Update routes for code docs launch, using a dcdo flag #45145

Merged
merged 8 commits into from
Mar 31, 2022
71 changes: 48 additions & 23 deletions dashboard/app/controllers/programming_environments_controller.rb
Original file line number Diff line number Diff line change
@@ -1,12 +1,30 @@
class ProgrammingEnvironmentsController < ApplicationController
include ProxyHelper
EXPIRY_TIME = 30.minutes

before_action :require_levelbuilder_mode_or_test_env, except: [:index, :show]
before_action :set_programming_environment, except: [:index, :new, :create]
before_action :set_programming_environment, except: [:index, :docs_index, :new, :create, :docs_show]
authorize_resource

def index
@programming_environments = ProgrammingEnvironment.where(published: true).order(:name).map(&:summarize_for_index)
end

def docs_index
if DCDO.get('use-studio-code-docs', false)
@programming_environments = ProgrammingEnvironment.all.order(:name).map(&:summarize_for_index)
render :index
else
render_proxied_url(
'https://curriculum.code.org/docs/',
allowed_content_types: nil,
allowed_hostname_suffixes: %w(curriculum.code.org),
expiry_time: EXPIRY_TIME,
infer_content_type: true
)
end
end

def new
end

Expand All @@ -21,56 +39,62 @@ def create
end

def edit
@programming_environment = ProgrammingEnvironment.find_by_name(params[:name])
return render :not_found unless @programming_environment
end

def update
programming_environment = ProgrammingEnvironment.find_by_name(params[:name])
unless programming_environment
render :not_found
return
end
programming_environment.assign_attributes(programming_environment_params.except(:categories))
@programming_environment.assign_attributes(programming_environment_params.except(:categories))
begin
if programming_environment_params[:categories]
programming_environment.categories =
@programming_environment.categories =
programming_environment_params[:categories].each_with_index.map do |category, i|
if category['id'].blank?
ProgrammingEnvironmentCategory.create!(category.merge(programming_environment_id: programming_environment.id, position: i))
ProgrammingEnvironmentCategory.create!(category.merge(programming_environment_id: @programming_environment.id, position: i))
else
existing_category = programming_environment.categories.find(category['id'])
existing_category = @programming_environment.categories.find(category['id'])
existing_category.assign_attributes(category.except('id'))
existing_category.position = i
existing_category.save! if existing_category.changed?
existing_category
end
end
end
programming_environment.save! if programming_environment.changed?
programming_environment.write_serialization
render json: programming_environment.summarize_for_edit.to_json
@programming_environment.save! if @programming_environment.changed?
@programming_environment.write_serialization
render json: @programming_environment.summarize_for_edit.to_json
rescue ActiveRecord::RecordInvalid => e
render(status: :not_acceptable, plain: e.message)
end
end

def show
return render :not_found unless @programming_environment
return head :forbidden unless can?(:read, @programming_environment)
@programming_environment_categories = @programming_environment.categories.select {|c| c.programming_expressions.count > 0}.map(&:summarize_for_environment_show)
end

def destroy
return render :not_found unless @programming_environment
begin
@programming_environment.destroy!
render(status: 200, plain: "Destroyed #{@programming_environment.name}")
rescue => e
render(status: :not_acceptable, plain: e.message)
def docs_show
if DCDO.get('use-studio-code-docs', false)
@programming_environment = ProgrammingEnvironment.find_by_name(params[:programming_environment_name])
return render :not_found unless @programming_environment
@programming_environment_categories = @programming_environment.categories.select {|c| c.programming_expressions.count > 0}.map(&:summarize_for_environment_show)
render :show
else
render_proxied_url(
"https://curriculum.code.org/docs/#{params[:programming_environment_name]}/",
allowed_content_types: nil,
allowed_hostname_suffixes: %w(curriculum.code.org),
expiry_time: EXPIRY_TIME,
infer_content_type: true
)
end
end

def destroy
@programming_environment.destroy!
render(status: 200, plain: "Destroyed #{@programming_environment.name}")
rescue => e
render(status: :not_acceptable, plain: e.message)
end

private

def programming_environment_params
Expand All @@ -90,5 +114,6 @@ def programming_environment_params

def set_programming_environment
@programming_environment = ProgrammingEnvironment.find_by_name(params[:name])
raise ActiveRecord::RecordNotFound unless @programming_environment
end
end
29 changes: 24 additions & 5 deletions dashboard/app/controllers/programming_expressions_controller.rb
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
class ProgrammingExpressionsController < ApplicationController
include Rails.application.routes.url_helpers

load_and_authorize_resource
include ProxyHelper
EXPIRY_TIME = 30.minutes

before_action :require_levelbuilder_mode_or_test_env, except: [:search, :show, :show_by_keys]
before_action :set_expression_by_keys, only: [:show_by_keys, :docs_show]
load_and_authorize_resource

# GET /programming_expressions/get_filtered_expressions
# Possible filters:
Expand Down Expand Up @@ -93,10 +95,8 @@ def show
end

def show_by_keys
return render :not_found unless @programming_expression
if params[:programming_environment_name] && params[:programming_expression_key]
@programming_expression = ProgrammingEnvironment.find_by_name(params[:programming_environment_name])&.programming_expressions&.find_by_key(params[:programming_expression_key])
return render :not_found unless @programming_expression
return head :forbidden unless can?(:read, @programming_expression)
@programming_environment_categories = @programming_expression.programming_environment.categories.select {|c| c.programming_expressions.count > 0}.map(&:summarize_for_environment_show)
return render :show
end
Expand Down Expand Up @@ -125,6 +125,21 @@ def clone
end
end

def docs_show
if DCDO.get('use-studio-code-docs', false)
return render :not_found unless @programming_expression
@programming_environment_categories = @programming_expression.programming_environment.categories.select {|c| c.programming_expressions.count > 0}.map(&:summarize_for_environment_show)
return render :show
end
render_proxied_url(
"https://curriculum.code.org/docs/#{params[:programming_environment_name]}/#{params[:programming_expression_key]}/",
allowed_content_types: nil,
allowed_hostname_suffixes: %w(curriculum.code.org),
expiry_time: EXPIRY_TIME,
infer_content_type: true
)
end

private

def programming_expression_params
Expand All @@ -146,4 +161,8 @@ def programming_expression_params
)
transformed_params
end

def set_expression_by_keys
@programming_expression = ProgrammingEnvironment.find_by_name(params[:programming_environment_name])&.programming_expressions&.find_by_key(params[:programming_expression_key])
end
end
6 changes: 5 additions & 1 deletion dashboard/app/models/ability.rb
Original file line number Diff line number Diff line change
Expand Up @@ -81,10 +81,14 @@ def initialize(user)
environment.published || user.permission?(UserPermission::LEVELBUILDER)
end

can [:read, :show_by_keys], ProgrammingExpression do |expression|
can [:read, :show_by_keys, :docs_show], ProgrammingExpression do |expression|
can? :read, expression.programming_environment
end

can [:docs_index, :docs_show], ProgrammingEnvironment do |environment|
can? :read, environment
end

if user.persisted?
can :manage, user

Expand Down
10 changes: 9 additions & 1 deletion dashboard/app/models/programming_environment.rb
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,14 @@ def remove_serialization
File.delete(file_path) if File.exist?(file_path)
end

def studio_documentation_path
if DCDO.get('use-studio-code-docs', false) && ['applab', 'gamelab', 'spritelab', 'weblab'].include?(name)
"/docs/#{name}"
else
programming_environment_path(name)
end
end

def summarize_for_lesson_edit
{id: id, name: name}
end
Expand Down Expand Up @@ -119,7 +127,7 @@ def summarize_for_index
title: title,
imageUrl: image_url,
description: description,
showPath: programming_environment_path(name)
showPath: studio_documentation_path
}
end
end
6 changes: 5 additions & 1 deletion dashboard/app/models/programming_expression.rb
Original file line number Diff line number Diff line change
Expand Up @@ -148,7 +148,11 @@ def documentation_path
end

def studio_documentation_path
programming_environment_programming_expression_path(programming_environment.name, key)
if DCDO.get('use-studio-code-docs', false)
documentation_path
else
programming_environment_programming_expression_path(programming_environment.name, key)
end
end

def summarize_for_lesson_edit
Expand Down
2 changes: 2 additions & 0 deletions dashboard/app/views/programming_expressions/show.html.haml
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
//%script{src: webpack_asset_path('js/googleblockly.js')}
- content_for(:head) do
= tag 'meta', name: 'description', content: @programming_expression.syntax
%script{src: webpack_asset_path('js/blockly.js')}
%script{src: webpack_asset_path("js/#{js_locale}/blockly_locale.js")}
%script{src: webpack_asset_path('js/programming_expressions/show.js'),
Expand Down
11 changes: 10 additions & 1 deletion dashboard/config/routes.rb
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,16 @@

get 'redirected_url', to: 'redirect_proxy#get', format: false

get 'docs/', to: 'curriculum_proxy#get_doc_landing'
# We moved code docs off of curriculum builder in spring 2022.
# In that move, we wanted to preserve the previous /docs routes for these
# pages. However, there are a lot of other /docs URLs that did not move over
# so we're allow-listing the four IDEs that lived on curriculum builder to be
# served by ProgrammingEnvironmentsController and ProgrammingExpressionsController,
# with the rest falling back to the old proxying logic.
get 'docs/', to: 'programming_environments#docs_index'
Copy link
Contributor

Choose a reason for hiding this comment

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

Are there unit tests that could be added to check that all of these URLs go to the right place and that ones that should go to CB for concept maps aren't impacted?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I forgot to add one for this route but the fact that this test still passes is evidence that concept maps aren't affected.

Copy link
Contributor

Choose a reason for hiding this comment

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

Great. Thanks for pointing to existing tests that are covering this

Copy link
Member

Choose a reason for hiding this comment

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

hmm, my understanding is that controller tests do not look at our routing logic, so I think that test is verifying that the curriculum proxy controller does the right thing, but not that the right requests will get routed to the curriculum proxy controller. to test the routing, you'd need an integration test which operates on urls, maybe like one of these:

given how much complexity there is in the routing logic, I think it would be a good idea to have at least one integration test verifying that the fallback to /docs/concepts on CB is working end-to-end.

get 'docs/:programming_environment_name', to: 'programming_environments#docs_show', constraints: {programming_environment_name: /(applab|gamelab|spritelab|weblab)/}
get 'docs/:programming_environment_name/:programming_expression_key', constraints: {programming_environment_name: /(applab|gamelab|spritelab|weblab)/, programming_expression_key: /#{CurriculumHelper::KEY_CHAR_RE}+/}, to: 'programming_expressions#docs_show'
get 'docs/:programming_environment_name/:programming_expression_key/index.html', constraints: {programming_environment_name: /(applab|gamelab|spritelab|weblab)/, programming_expression_key: /#{CurriculumHelper::KEY_CHAR_RE}+/}, to: 'programming_expressions#docs_show'
get 'docs/*path', to: 'curriculum_proxy#get_doc'
Copy link
Member

Choose a reason for hiding this comment

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

am I correct in understanding that this line handles the following routes mentioned in the PR description?

we will continue to proxy /docs/concepts, /docs/csd, /docs/csd-1718, etc and I was having trouble coming up with a good allow-list for all the various routes we use (but I could if needed).

if so, this would be helpful to note in a comment on this line. also, what is the endgame for /docs/csd and /docs/csd-1718 -- will we continue to proxy these requests to curriculum builder indefinitely?

@tim-dot-org , heads up that you'll need to modify this routing when you launch concept maps. it looks like you should just be able to add any new routing rules above this line.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yep these routes only direct requests to /docs/{applab,gamelab,spritelab,weblab} to the new controllers. Everything else will still go to curriculum_proxy. Because there are all these other routes in our curriculum, I figured it would be better to explicitly list these exceptions.

I don't know what the endgame is for those routes. Older levels, such as https://levelbuilder-studio.code.org/s/csd2-2019/lessons/3/levels/7, link to these routes in the "Help and Tips" tab, but it looks like some of them were replaced by concept maps (eg the same level in 2020). It might be possible to pull support for these routes but a. I'm not sure and b. this doesn't seem like something we'll have time for before curriculum launch.

get 'curriculum/*path', to: 'curriculum_proxy#get_curriculum'

Expand Down
2 changes: 1 addition & 1 deletion dashboard/config/scripts/levels/allthethings_map.level
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
"user_id": 556,
"properties": {
"instructions_important": "false",
"reference": "/docs/applab/checkbox/index.html"
"reference": "/docs/concepts/app-lab/the-counter-pattern/index.html"
},
"published": true,
"notes": ""
Expand Down
10 changes: 0 additions & 10 deletions dashboard/test/controllers/curriculum_proxy_controller_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,6 @@
require 'test_helper'

class CurriculumProxyControllerTest < ActionController::TestCase
test "should redirect from studio.code.org/docs to curriculum.code.org/docs" do
stub_request(:get, "https://curriculum.code.org/docs/").
to_return(body: 'curriculum.code.org/docs content', headers: {})

request.host = "studio.code.org"
get :get_doc_landing
assert_response :success
assert_equal response.body, 'curriculum.code.org/docs content'
end

test "should redirect from studio.code.org/docs path to curriculum.code.org/docs path" do
stub_request(:get, "https://curriculum.code.org/docs/concepts/game-lab/drawing-shapes/").
to_return(body: 'curriculum.code.org content', headers: {})
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
require 'webmock/minitest'
WebMock.disable_net_connect!(allow_localhost: true)
require 'test_helper'

class ProgrammingEnvironmentsControllerTest < ActionController::TestCase
Expand Down Expand Up @@ -39,13 +41,77 @@ class ProgrammingEnvironmentsControllerTest < ActionController::TestCase
assert_equal 1, JSON.parse(nav_data).length
end

test 'data is passed down to docs show page if using studio code docs' do
DCDO.expects(:get).at_least_once
DCDO.expects(:get).with('use-studio-code-docs', false).returns(true).at_least_once

programming_environment = create :programming_environment, name: 'weblab'
category = create :programming_environment_category, programming_environment: programming_environment
create :programming_environment_category, programming_environment: programming_environment
create :programming_expression, programming_environment: programming_environment, programming_environment_category: category

get :docs_show, params: {programming_environment_name: programming_environment.name}
assert_response :ok

show_data = css_select('script[data-programmingenvironment]').first.attribute('data-programmingenvironment').to_s
assert_equal programming_environment.summarize_for_show.to_json, show_data

nav_data = css_select('script[data-categoriesfornavigation]').first.attribute('data-categoriesfornavigation').to_s
assert_equal 1, JSON.parse(nav_data).length
end

test 'page is proxied to docs show page if not using studio code docs' do
DCDO.expects(:get).with('use-studio-code-docs', false).returns(false).at_least_once

programming_environment = create :programming_environment, name: 'weblab'
category = create :programming_environment_category, programming_environment: programming_environment
create :programming_environment_category, programming_environment: programming_environment
create :programming_expression, programming_environment: programming_environment, programming_environment_category: category

stub_request(:get, "https://curriculum.code.org/docs/#{programming_environment.name}/").
to_return(body: 'curriculum.code.org/docs content', headers: {})

request.host = "studio.code.org"
get :docs_show, params: {programming_environment_name: programming_environment.name}
assert_response :ok
assert_equal @response.body, 'curriculum.code.org/docs content'
end

test 'page is not proxied to docs index if using studio code docs' do
DCDO.expects(:get).at_least_once
DCDO.expects(:get).with('use-studio-code-docs', false).returns(true).at_least_once

create :programming_environment
stubbed_request = stub_request(:get, "https://curriculum.code.org/docs/").
to_return(body: 'curriculum.code.org/docs content', headers: {})

get :docs_index
assert_response :ok
assert_not_requested stubbed_request
end

test 'page is proxied to docs index page if not using studio code docs' do
DCDO.expects(:get).with('use-studio-code-docs', false).returns(false).at_least_once
create :programming_environment

stubbed_request = stub_request(:get, "https://curriculum.code.org/docs/").
to_return(body: 'curriculum.code.org/docs content', headers: {})

request.host = "studio.code.org"
get :docs_index
assert_response :ok
assert_equal @response.body, 'curriculum.code.org/docs content'
assert_requested stubbed_request
end

test 'returns not_found if editing a non-existant programming environment' do
sign_in @levelbuilder

post :edit, params: {
name: 'fake_name'
}
assert_response :not_found
assert_raises(ActiveRecord::RecordNotFound) do
post :edit, params: {
name: 'fake_name'
}
end
end

test 'can update programming expression from params' do
Expand Down Expand Up @@ -97,11 +163,12 @@ class ProgrammingEnvironmentsControllerTest < ActionController::TestCase
test 'returns not_found if updating a non-existant programming environment' do
sign_in @levelbuilder

post :update, params: {
name: 'fake_name',
title: 'title'
}
assert_response :not_found
assert_raises(ActiveRecord::RecordNotFound) do
post :update, params: {
name: 'fake_name',
title: 'title'
}
end
end

test 'can create a new programming environment' do
Expand Down