Skip to content

Commit

Permalink
Add LTI Integration portal UI (#55855)
Browse files Browse the repository at this point in the history
- Add form
- Add form submit result page
- Add LTI specific CSS classes
- Change lti_v1_controller to respond with HTML, not JSON
- Add unit tests
- Add flash alerts for errors

---------

Signed-off-by: Nick Lathe <nick.lathe@code.org>
  • Loading branch information
nicklathe committed Jan 29, 2024
1 parent 0b0aedb commit e5e1e5c
Show file tree
Hide file tree
Showing 10 changed files with 241 additions and 22 deletions.
97 changes: 97 additions & 0 deletions dashboard/app/assets/stylesheets/application.scss
Original file line number Diff line number Diff line change
Expand Up @@ -4074,4 +4074,101 @@ a {
}
}

#lti-integration-portal {
$signup-info-background: #f7f7f7;
$signup-info-border-color: #e5e5e5;

display: flex;
flex-wrap: wrap;

.header {
display: flex;
flex-wrap: wrap;
}

.padded-container {
padding: 20px;
display: flex;
flex-wrap: wrap;
.row {
flex-grow: 1;
}
}

.lti-integration-form {
input[type=text], input[type=email] {
height: 36px;
}

input {
margin: 0;
}

input, select {
flex-grow: 1;
}

.row {
display: flex;
margin-bottom: 20px;

[class*=span] {
align-self: center;
display: flex;
.div {
align-self: center;
display: flex;
}
}

label {
margin: 0;
}
}
.lti-integration-field-label {
flex-wrap: wrap;

p {
margin: 0;
}
}
}

@mixin button {
background-image: none;
padding: 0 20px;
margin: 5px 0;
font-size: 18px;
height: 40px;
@include main-font-semi-bold;
}

@mixin purple-button {
@include button;
border-color: $purple;
background-color: $purple;
color: $white;
}

@mixin white-button {
@include button;
border-color: $black;
background-color: $white;
color: $black;
}

.lti-btn-purple {
@include purple-button;
}

.lti-btn-purple-right {
@include purple-button;
float: right;
}

.lti-btn-white {
@include white-button;
}
}

@import "NpsSurveyBlock";
30 changes: 22 additions & 8 deletions dashboard/app/controllers/lti_v1_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -255,19 +255,20 @@ def sync_course
# Creates a new LtiIntegration
def create_integration
begin
params.require([:client_id, :lms, :email])
params.require([:name, :client_id, :lms, :email])
rescue
render status: :bad_request, json: {error: I18n.t('lti.error.missing_params')}
return
flash.alert = I18n.t('lti.error.missing_params')
return redirect_to lti_v1_integrations_path
end

integration_name = params[:name]
client_id = params[:client_id]
platform_name = params[:lms]
admin_email = params[:email]

unless Policies::Lti::LMS_PLATFORMS.key?(platform_name.to_sym)
render status: :bad_request, json: {error: I18n.t('lti.error.unsupported_lms_type')}
return
flash.alert = I18n.t('lti.error.unsupported_lms_type')
return redirect_to lti_v1_integrations_path
end

platform_urls = Policies::Lti::LMS_PLATFORMS[platform_name.to_sym]
Expand All @@ -277,9 +278,11 @@ def create_integration
access_token_url = platform_urls[:access_token_url]

existing_integration = Queries::Lti.get_lti_integration(issuer, client_id)
@integration_status = nil

if existing_integration.nil?
Services::Lti.create_lti_integration(
name: integration_name,
client_id: client_id,
issuer: issuer,
platform_name: platform_name,
Expand All @@ -288,10 +291,21 @@ def create_integration
access_token_url: access_token_url,
admin_email: admin_email
)
render status: :ok, json: {body: I18n.t('lti.create_integration_success')}
else
render status: :conflict, json: {error: I18n.t('lti.error.integration_exists')}

@integration_status = :created
end
render 'lti/v1/integration_status'
end

# GET /lti/v1/integrations
# Displays the onboarding portal for creating a new LTI Integration
def new_integration
@form_data = {}
@form_data[:lms_platforms] = Policies::Lti::LMS_PLATFORMS.map do |key, value|
{platform: key, name: value[:name]}
end

render lti_v1_integrations_path
end

private
Expand Down
27 changes: 27 additions & 0 deletions dashboard/app/views/lti/v1/integration_status.html.haml
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
#lti-integration-portal
.row
.span10
.padded-container
- if @integration_status == :created
.row
.span10.header
%h1= t('lti.integration.success_header')
.row
.span10
%p= t('lti.integration.success_body').html_safe
%p= t('lti.integration.guide_instructions').html_safe
- else
.row
.span10.header
%h1= t('lti.integration.exists_header')
.row
.span10
%p= t('lti.integration.exists_body').html_safe
%p= t('lti.integration.guide_instructions').html_safe
.row
.span10
%br
= link_to :home do
%button.lti-btn-purple-right= t('lti.integration.navigate_home')
= link_to :lti_v1_integrations do
%button.lti-btn-white= t('lti.integration.navigate_registration')
42 changes: 42 additions & 0 deletions dashboard/app/views/lti/v1/integrations.html.haml
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
#lti-integration-portal

.row
.span10.header
%h1= t('lti.integration.form_header')
.row
.span10
%p= t('lti.integration.form_instructions').html_safe
%p= t('lti.integration.form_instructions_2').html_safe
= form_with(url: :lti_v1_integrations, method: :post, class: "lti-integration-form") do |f|
.row
.span8
.padded-container
.row
.span3.lti-integration-field-label
= f.label :name, t('lti.integration.name').html_safe
.span5
= f.text_field :name, maxlength: 100
.row
.span3.lti-integration-field-label
= f.label :client_id, t('lti.integration.client_id').html_safe
.span5
= f.text_field :client_id, maxlength: 50
.row
.span3.lti-integration-field-label
= f.label :email, t('lti.integration.email').html_safe
.span5
= f.email_field :email, maxlength: 255
.row
.span3.lti-integration-field-label
= f.label :lms, t('lti.integration.lms_selector')
.span5
= f.select :lms, @form_data[:lms_platforms].map {|p| [p[:name], p[:platform].to_s]}
.row
.span10
%p= t('lti.integration.form_instructions_3').html_safe

%button.lti-btn-purple= t('lti.integration.register')

.row
.span10
%em= t('lti.integration.privacy_policy').html_safe
22 changes: 20 additions & 2 deletions dashboard/config/locales/en.yml
Original file line number Diff line number Diff line change
Expand Up @@ -1406,9 +1406,27 @@ en:
founder: "Founder, Code.org"
your_friends: "Your Friends at Code.org"
lti:
create_integration_success: "Succesfully created your LTI integration"
create_integration_success: "Successfully created your LTI integration"
error:
integration_exists: "Lti Integration already exists for this Client ID"
missing_params: "Missing required param(s): client_id, lms, email"
missing_params: "Missing required param(s): School/District name, Client ID, LMS, Email"
unsupported_lms_type: "Unsupported LMS platform type"
wrong_resource_type: "Only LtiResourceLink is supported right now"
integration:
client_id: "LMS Client ID"
email: "Your email"
name: "School or district name"
navigate_home: "Back to Code.org"
navigate_registration: "Back to Registration"
lms_selector: "What LMS are you using?"
exists_body: "It seems like your LMS is already in our system. You should be able to continue installing and using our LTI integration."
exists_header: "Looks like you're are already registered!"
form_header: "Register Your LMS with Code.org"
form_instructions: "Please use this form to register your Learning Management System (LMS) with Code.org. This will allow you to install our LTI integration on your LMS."
form_instructions_2: "Full guides for obtaining your LMS Client ID, using LMS/LTI integrations, and our list of supported Learning Management Systems <a href='https://code.org/contact'>can be found here.</a>"
form_instructions_3: "Not seeing your LMS in our list, or running into issues with your installation? Check out our <a href=''>guides</a>, or <a href='https://code.org/contact'>contact us</a> for additional help"
guide_instructions: "Full guides for installing and our LTI integration <a href=''>can be found here.</a> Running into issues? Feel free to <a href=''>contact us</a> for additional support."
privacy_policy: "Your privacy is of utmost importance to us. Your personal information will not be used for any marketing purposes. Please see our <a href='https://code.org/contact'>Privacy Policy here</a> for more information."
success_body: "Great news! Your LMS has been registered with Code.org, and you can continue installing and using your LMS integration. You should receive a confirmation email shortly."
success_header: "Thank you for registering your LMS!"
register: "Register LMS"
1 change: 1 addition & 0 deletions dashboard/config/routes.rb
Original file line number Diff line number Diff line change
Expand Up @@ -596,6 +596,7 @@
post '/lti/v1/authenticate', to: 'lti_v1#authenticate'
match '/lti/v1/sync_course', to: 'lti_v1#sync_course', via: [:get, :post]
post '/lti/v1/integrations', to: 'lti_v1#create_integration'
get '/lti/v1/integrations', to: 'lti_v1#new_integration'

# OAuth endpoints
get '/oauth/jwks', to: 'oauth_jwks#jwks'
Expand Down
2 changes: 2 additions & 0 deletions dashboard/lib/policies/lti.rb
Original file line number Diff line number Diff line change
Expand Up @@ -38,12 +38,14 @@ module AccessTokenScopes

LMS_PLATFORMS = {
canvas_cloud: {
name: 'Canvas'.freeze,
issuer: 'https://canvas.instructure.com'.freeze,
auth_redirect_url: 'https://sso.canvaslms.com/api/lti/authorize_redirect'.freeze,
jwks_url: 'https://sso.canvaslms.com/api/lti/security/jwks'.freeze,
access_token_url: 'https://sso.canvaslms.com/login/oauth2/token'.freeze,
},
schoology: {
name: 'Schoology'.freeze,
issuer: 'https://schoology.schoology.com'.freeze,
auth_redirect_url: 'https://lti-service.svc.schoology.com/lti-service/authorize-redirect'.freeze,
jwks_url: 'https://lti-service.svc.schoology.com/lti-service/.well-known/jwks'.freeze,
Expand Down
2 changes: 2 additions & 0 deletions dashboard/lib/services/lti.rb
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ def self.create_lti_user_identity(user)
end

def self.create_lti_integration(
name:,
client_id:,
issuer:,
platform_name:,
Expand All @@ -43,6 +44,7 @@ def self.create_lti_integration(
admin_email:
)
LtiIntegration.create!(
name: name,
client_id: client_id,
issuer: issuer,
platform_name: platform_name,
Expand Down
38 changes: 26 additions & 12 deletions dashboard/test/controllers/lti_v1_controller_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -501,44 +501,58 @@ def create_valid_jwt_raise_error
end

test 'integration - given valid inputs, creates a new integration if one does not exist' do
name = "Fake School"
client_id = "1234canvas"
lms = "canvas_cloud"
email = "fake@email.com"

post '/lti/v1/integrations', params: {client_id: client_id, lms: lms, email: email}
post '/lti/v1/integrations', params: {name: name, client_id: client_id, lms: lms, email: email}
assert_response :ok

client_id = "5678schoology"
lms = "schoology"

post '/lti/v1/integrations', params: {client_id: client_id, lms: lms, email: email}
post '/lti/v1/integrations', params: {name: name, client_id: client_id, lms: lms, email: email}
assert_response :ok
end

test 'integration - given missing inputs, does not create a new integration' do
name = "Fake School"
client_id = "1234canvas"
lms = "canvas_cloud"
email = "fake@email.com"

post '/lti/v1/integrations', params: {lms: lms, email: email}
assert_response :bad_request
# missing client_id
post '/lti/v1/integrations', params: {name: name, lms: lms, email: email}
assert_equal I18n.t('lti.error.missing_params'), flash[:alert]

post '/lti/v1/integrations', params: {client_id: client_id, lms: '', email: email}
assert_response :bad_request
# missing lms
post '/lti/v1/integrations', params: {name: name, client_id: client_id, lms: '', email: email}
assert_equal I18n.t('lti.error.missing_params'), flash[:alert]

post '/lti/v1/integrations', params: {client_id: client_id}
assert_response :bad_request
# missing email
post '/lti/v1/integrations', params: {name: name, client_id: client_id}
assert_equal I18n.t('lti.error.missing_params'), flash[:alert]

# unsupported lms type
post '/lti/v1/integrations', params: {name: name, client_id: client_id, lms: 'unsupported', email: email}
assert_equal I18n.t('lti.error.unsupported_lms_type'), flash[:alert]

# missing name
post '/lti/v1/integrations', params: {client_id: client_id, lms: lms, email: email}
assert_equal I18n.t('lti.error.missing_params'), flash[:alert]
end

test 'integration - if existing integration, does not create a new one' do
name = "Fake School"
client_id = "1234canvas"
lms = "canvas_cloud"
email = "fake@email.com"

post '/lti/v1/integrations', params: {client_id: client_id, lms: lms, email: email}
assert_response :ok
post '/lti/v1/integrations', params: {client_id: client_id, lms: lms, email: email}
assert_response :conflict
post '/lti/v1/integrations', params: {name: name, client_id: client_id, lms: lms, email: email}
assert_template 'lti/v1/integration_status'
post '/lti/v1/integrations', params: {name: name, client_id: client_id, lms: lms, email: email}
assert_template 'lti/v1/integration_status'
end

test 'attempting to sync a section with no LTI course should return a 400' do
Expand Down
2 changes: 2 additions & 0 deletions dashboard/test/lib/services/lti_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -197,6 +197,7 @@ class Services::LtiTest < ActiveSupport::TestCase
end

test 'create_lti_integration should create an LtiIntegration when given valid inputs' do
name = "name"
client_id = 'client_id'
issuer = 'issuer'
platform_name = 'platform_name'
Expand All @@ -206,6 +207,7 @@ class Services::LtiTest < ActiveSupport::TestCase
admin_email = 'admin_email'

integration = Services::Lti.create_lti_integration(
name: name,
client_id: client_id,
issuer: issuer,
platform_name: platform_name,
Expand Down

0 comments on commit e5e1e5c

Please sign in to comment.