force navigation tabs cache to invalidate when changing external tools

fixes:PLAT-277

test plan:
1. Go to https://www.eduappcenter.com/apps/redirect
2. Enter criteria for a redirect link configuration URL
3. Access an account in Canvas
4. Add the Redirect External Tool with your configuration URL
5. Observe link is added to your Navigation per your settings
6. Attempt to edit the External Tool in Canvas with a new configuration URL you get from https://www.eduappcenter.com/apps/redirect after putting in new data
7. Observe that changes are reflected in the External Tools tab but the actual link in Navigation menus is changed

Change-Id: I2b233cb89e4b446cd66b6e826ec8de894a02bfdd
Reviewed-on: https://gerrit.instructure.com/33757
Tested-by: Jenkins <jenkins@instructure.com>
QA-Review: Nathan Rogowski <nathan@instructure.com>
Reviewed-by: Eric Berry <ericb@instructure.com>
Product-Review: Nathan Mills <nathanm@instructure.com>
This commit is contained in:
Nathan Mills 2014-04-22 16:27:37 -06:00
parent d7b5b7f09a
commit 079f2d60cc
5 changed files with 276 additions and 30 deletions

View File

@ -74,27 +74,27 @@ class ExternalToolsController < ApplicationController
end
@tools = ContextExternalTool.search_by_attribute(@tools, :name, params[:search_term])
respond_to do |format|
@tools = Api.paginate(@tools, self, tool_pagination_url)
format.json {render :json => external_tools_json(@tools, @context, @current_user, session)}
@tools = Api.paginate(@tools, self, tool_pagination_url)
format.json { render :json => external_tools_json(@tools, @context, @current_user, session) }
end
end
end
def homework_submissions
if authorized_action(@context, @current_user, :read)
@tools = ContextExternalTool.all_tools_for(@context, :user => @current_user).select(&:has_homework_submission)
respond_to do |format|
format.json {render :json => external_tools_json(@tools, @context, @current_user, session)}
format.json { render :json => external_tools_json(@tools, @context, @current_user, session) }
end
end
end
def finished
@headers = false
if authorized_action(@context, @current_user, :read)
end
end
def retrieve
if authorized_action(@context, @current_user, :read)
@tool = ContextExternalTool.find_external_tool(params[:url], @context)
@ -215,7 +215,7 @@ class ExternalToolsController < ApplicationController
end
uri.query = {:verifier => verifier}.to_query
render :json => { :id => @tool.id, :name => @tool.name, :url => uri.to_s }
render :json => {:id => @tool.id, :name => @tool.name, :url => uri.to_s}
end
end
@ -304,8 +304,8 @@ class ExternalToolsController < ApplicationController
selection_type = 'editor_button' if params[:editor]
selection_type = 'homework_submission' if params[:homework]
@return_url = external_content_success_url('external_tool')
@headers = false
@return_url = external_content_success_url('external_tool')
@headers = false
@tool_launch_type = 'self'
find_tool(params[:external_tool_id], selection_type)
@ -315,12 +315,14 @@ class ExternalToolsController < ApplicationController
def find_tool(id, selection_type)
begin
@tool = ContextExternalTool.find_for(id, @context, selection_type)
rescue ActiveRecord::RecordNotFound; end
rescue ActiveRecord::RecordNotFound;
end
if !@tool
flash[:error] = t "#application.errors.invalid_external_tool_id", "Couldn't find valid settings for this tool"
redirect_to named_context_url(@context, :context_url)
end
end
protected :find_tool
def render_tool(selection_type)
@ -345,6 +347,7 @@ class ExternalToolsController < ApplicationController
render :template => 'external_tools/tool_show'
end
protected :render_tool
# @API Create an external tool
@ -511,6 +514,7 @@ class ExternalToolsController < ApplicationController
set_tool_attributes(@tool, params[:external_tool] || params)
respond_to do |format|
if @tool.save
invalidate_nav_tabs_cache(@tool)
if api_request?
format.json { render :json => external_tool_json(@tool, @context, @current_user, session) }
else
@ -539,6 +543,7 @@ class ExternalToolsController < ApplicationController
respond_to do |format|
set_tool_attributes(@tool, params[:external_tool] || params)
if @tool.save
invalidate_nav_tabs_cache(@tool)
if api_request?
format.json { render :json => external_tool_json(@tool, @context, @current_user, session) }
else
@ -565,6 +570,7 @@ class ExternalToolsController < ApplicationController
respond_to do |format|
if @tool.destroy
if api_request?
invalidate_nav_tabs_cache(@tool)
format.json { render :json => external_tool_json(@tool, @context, @current_user, session) }
else
format.json { render :json => @tool.as_json(:methods => [:readable_state, :custom_fields_string], :include_root => false) }
@ -575,15 +581,22 @@ class ExternalToolsController < ApplicationController
end
end
end
private
def set_tool_attributes(tool, params)
attrs = ContextExternalTool::EXTENSION_TYPES
attrs += [:name, :description, :url, :icon_url, :domain, :privacy_level, :consumer_key, :shared_secret,
:custom_fields, :custom_fields_string, :text, :config_type, :config_url, :config_xml]
:custom_fields, :custom_fields_string, :text, :config_type, :config_url, :config_xml]
attrs.each do |prop|
tool.send("#{prop}=", params[prop]) if params.has_key?(prop)
end
end
def invalidate_nav_tabs_cache(tool)
if tool.has_user_navigation || tool.has_course_navigation || tool.has_account_navigation
Lti::NavigationCache.new(@domain_root_account).invalidate_cache_key
end
end
end

View File

@ -309,7 +309,7 @@ module ApplicationHelper
@section_tabs ||= begin
if @context
html = []
tabs = Rails.cache.fetch([@context, @current_user, @domain_root_account, "section_tabs_hash", I18n.locale].cache_key) do
tabs = Rails.cache.fetch([@context, @current_user, @domain_root_account, Lti::NavigationCache.new(@domain_root_account), "section_tabs_hash", I18n.locale].cache_key) do
if @context.respond_to?(:tabs_available) && !(tabs = @context.tabs_available(@current_user, :session => session, :root_account => @domain_root_account)).empty?
tabs.select do |tab|
if (tab[:id] == @context.class::TAB_COLLABORATIONS rescue false)

View File

@ -0,0 +1,38 @@
#
# Copyright (C) 2014 Instructure, Inc.
#
# This file is part of Canvas.
#
# Canvas is free software: you can redistribute it and/or modify it under
# the terms of the GNU Affero General Public License as published by the Free
# Software Foundation, version 3 of the License.
#
# Canvas is distributed in the hope that it will be useful, but WITHOUT ANY
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
# A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
# details.
#
# You should have received a copy of the GNU Affero General Public License along
# with this program. If not, see <http://www.gnu.org/licenses/>.
#
module Lti
class NavigationCache
CACHE_KEY = 'navigation_tabs_key'
def initialize(account)
@account = account
end
def cache_key
Rails.cache.fetch([@account, CACHE_KEY].cache_key) { SecureRandom.uuid }
end
def invalidate_cache_key
Rails.cache.delete([@account, CACHE_KEY].cache_key)
end
end
end

View File

@ -29,9 +29,9 @@ def new_valid_tool(course)
:shared_secret => "bob")
tool.url = "http://www.example.com/basic_lti"
tool.resource_selection = {
:url => "http://#{HostUrl.default_host}/selection_test",
:selection_width => 400,
:selection_height => 400 }
:url => "http://#{HostUrl.default_host}/selection_test",
:selection_width => 400,
:selection_height => 400}
tool.save!
tool
end
@ -45,7 +45,7 @@ describe ExternalToolsController do
get 'retrieve', :course_id => @course.id
assert_unauthorized
end
it "should find tools matching by exact url" do
course_with_teacher_logged_in(:active_all => true)
tool = @course.context_external_tools.new(:name => "bob", :consumer_key => "bob", :shared_secret => "bob")
@ -56,7 +56,7 @@ describe ExternalToolsController do
assigns[:tool].should == tool
assigns[:tool_settings].should_not be_nil
end
it "should find tools matching by domain" do
course_with_teacher_logged_in(:active_all => true)
tool = new_valid_tool(@course)
@ -65,7 +65,7 @@ describe ExternalToolsController do
assigns[:tool].should == tool
assigns[:tool_settings].should_not be_nil
end
it "should redirect if no matching tools are found" do
course_with_teacher_logged_in(:active_all => true)
get 'retrieve', :course_id => @course.id, :url => "http://www.example.com"
@ -73,7 +73,7 @@ describe ExternalToolsController do
flash[:error].should == "Couldn't find valid settings for this link"
end
end
describe "GET 'resource_selection'" do
it "should require authentication" do
course_with_teacher(:active_all => true)
@ -168,14 +168,14 @@ describe ExternalToolsController do
assigns[:tool_settings]['custom_canvas_enrollment_state'].should == 'inactive'
end
end
describe "POST 'create'" do
it "should require authentication" do
course_with_teacher(:active_all => true)
post 'create', :course_id => @course.id, :format => "json"
assert_status(401)
end
it "should accept basic configurations" do
course_with_teacher_logged_in(:active_all => true)
post 'create', :course_id => @course.id, :external_tool => {:name => "tool name", :url => "http://example.com", :consumer_key => "key", :shared_secret => "secret"}, :format => "json"
@ -186,7 +186,7 @@ describe ExternalToolsController do
assigns[:tool].consumer_key.should == "key"
assigns[:tool].shared_secret.should == "secret"
end
it "should fail on basic xml with no url or domain set" do
rescue_action_in_public! if CANVAS_RAILS2
course_with_teacher_logged_in(:active_all => true)
@ -213,7 +213,7 @@ describe ExternalToolsController do
post 'create', :course_id => @course.id, :external_tool => {:name => "tool name", :consumer_key => "key", :shared_secret => "secret", :config_type => "by_xml", :config_xml => xml}, :format => "json"
response.should_not be_success
end
it "should handle advanced xml configurations" do
course_with_teacher_logged_in(:active_all => true)
xml = <<-XML
@ -255,7 +255,7 @@ describe ExternalToolsController do
assigns[:tool].shared_secret.should == "secret"
assigns[:tool].has_editor_button.should be_true
end
it "should handle advanced xml configurations with no url or domain set" do
course_with_teacher_logged_in(:active_all => true)
xml = <<-XML
@ -297,7 +297,7 @@ describe ExternalToolsController do
assigns[:tool].shared_secret.should == "secret"
assigns[:tool].has_editor_button.should be_true
end
it "should fail gracefully on invalid xml configurations" do
course_with_teacher_logged_in(:active_all => true)
xml = "bob"
@ -315,7 +315,7 @@ describe ExternalToolsController do
json = json_parse(response.body)
json['errors']['config_xml'][0]['message'].should == I18n.t(:invalid_xml_syntax, 'Invalid xml syntax')
end
it "should handle advanced xml configurations by URL retrieval" do
course_with_teacher_logged_in(:active_all => true)
xml = <<-XML
@ -359,7 +359,7 @@ describe ExternalToolsController do
assigns[:tool].shared_secret.should == "secret"
assigns[:tool].has_editor_button.should be_true
end
it "should fail gracefully on invalid URL retrieval or timeouts" do
Net::HTTP.any_instance.stubs(:request).raises(Timeout::Error)
course_with_teacher_logged_in(:active_all => true)
@ -370,6 +370,141 @@ describe ExternalToolsController do
json = json_parse(response.body)
json['errors']['config_url'][0]['message'].should == I18n.t(:retrieve_timeout, 'could not retrieve configuration, the server response timed out')
end
context "navigation tabs caching" do
it "shouldn't clear the navigation tabs cache for non navigtaion tools" do
enable_cache do
course_with_teacher_logged_in(:active_all => true)
nav_cache = Lti::NavigationCache.new(@course.root_account)
cache_key = nav_cache.cache_key
xml = <<-XML
<cartridge_basiclti_link xmlns="http://www.imsglobal.org/xsd/imslticc_v1p0" xmlns:blti="http://www.imsglobal.org/xsd/imsbasiclti_v1p0" xmlns:lticm="http://www.imsglobal.org/xsd/imslticm_v1p0" xmlns:lticp="http://www.imsglobal.org/xsd/imslticp_v1p0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.imsglobal.org/xsd/imslticc_v1p0 http://www.imsglobal.org/xsd/lti/ltiv1p0/imslticc_v1p0.xsd http://www.imsglobal.org/xsd/imsbasiclti_v1p0 http://www.imsglobal.org/xsd/lti/ltiv1p0/imsbasiclti_v1p0p1.xsd http://www.imsglobal.org/xsd/imslticm_v1p0 http://www.imsglobal.org/xsd/lti/ltiv1p0/imslticm_v1p0.xsd http://www.imsglobal.org/xsd/imslticp_v1p0 http://www.imsglobal.org/xsd/lti/ltiv1p0/imslticp_v1p0.xsd">
<blti:title>Redirect Tool</blti:title>
<blti:description>
Add links to external web resources that show up as navigation items in course, user or account navigation. Whatever URL you specify is loaded within the content pane when users click the link.
</blti:description>
<blti:launch_url>https://www.edu-apps.org/redirect</blti:launch_url>
<blti:custom>
<lticm:property name="url">https://</lticm:property>
</blti:custom>
<blti:extensions platform="canvas.instructure.com">
<lticm:property name="icon_url">
https://www.edu-apps.org/assets/lti_redirect_engine/redirect_icon.png
</lticm:property>
<lticm:property name="link_text"/>
<lticm:property name="privacy_level">anonymous</lticm:property>
<lticm:property name="tool_id">redirect</lticm:property>
</blti:extensions>
</cartridge_basiclti_link>
XML
post 'create', :course_id => @course.id, :external_tool => {:name => "tool name", :url => "http://example.com", :consumer_key => "key", :shared_secret => "secret", :config_type => "by_xml", :config_xml => xml}, :format => "json"
response.should be_success
nav_cache.cache_key.should == cache_key
end
end
it 'should clear the navigation tabs cache for course nav' do
enable_cache do
course_with_teacher_logged_in(:active_all => true)
cache_key = Lti::NavigationCache.new(@course.root_account).cache_key
xml = <<-XML
<cartridge_basiclti_link xmlns="http://www.imsglobal.org/xsd/imslticc_v1p0" xmlns:blti="http://www.imsglobal.org/xsd/imsbasiclti_v1p0" xmlns:lticm="http://www.imsglobal.org/xsd/imslticm_v1p0" xmlns:lticp="http://www.imsglobal.org/xsd/imslticp_v1p0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.imsglobal.org/xsd/imslticc_v1p0 http://www.imsglobal.org/xsd/lti/ltiv1p0/imslticc_v1p0.xsd http://www.imsglobal.org/xsd/imsbasiclti_v1p0 http://www.imsglobal.org/xsd/lti/ltiv1p0/imsbasiclti_v1p0p1.xsd http://www.imsglobal.org/xsd/imslticm_v1p0 http://www.imsglobal.org/xsd/lti/ltiv1p0/imslticm_v1p0.xsd http://www.imsglobal.org/xsd/imslticp_v1p0 http://www.imsglobal.org/xsd/lti/ltiv1p0/imslticp_v1p0.xsd">
<blti:title>Redirect Tool</blti:title>
<blti:description>
Add links to external web resources that show up as navigation items in course, user or account navigation. Whatever URL you specify is loaded within the content pane when users click the link.
</blti:description>
<blti:launch_url>https://www.edu-apps.org/redirect</blti:launch_url>
<blti:custom>
<lticm:property name="url">https://</lticm:property>
</blti:custom>
<blti:extensions platform="canvas.instructure.com">
<lticm:options name="course_navigation">
<lticm:property name="enabled">true</lticm:property>
<lticm:property name="visibility">public</lticm:property>
</lticm:options>
<lticm:property name="icon_url">
https://www.edu-apps.org/assets/lti_redirect_engine/redirect_icon.png
</lticm:property>
<lticm:property name="link_text"/>
<lticm:property name="privacy_level">anonymous</lticm:property>
<lticm:property name="tool_id">redirect</lticm:property>
</blti:extensions>
</cartridge_basiclti_link>
XML
post 'create', :course_id => @course.id, :external_tool => {:name => "tool name", :url => "http://example.com", :consumer_key => "key", :shared_secret => "secret", :config_type => "by_xml", :config_xml => xml}, :format => "json"
response.should be_success
Lti::NavigationCache.new(@course.root_account).cache_key.should_not == cache_key
end
end
it 'should clear the navigation tabs cache for account nav' do
enable_cache do
course_with_teacher_logged_in(:active_all => true)
cache_key = Lti::NavigationCache.new(@course.root_account).cache_key
xml = <<-XML
<cartridge_basiclti_link xmlns="http://www.imsglobal.org/xsd/imslticc_v1p0" xmlns:blti="http://www.imsglobal.org/xsd/imsbasiclti_v1p0" xmlns:lticm="http://www.imsglobal.org/xsd/imslticm_v1p0" xmlns:lticp="http://www.imsglobal.org/xsd/imslticp_v1p0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.imsglobal.org/xsd/imslticc_v1p0 http://www.imsglobal.org/xsd/lti/ltiv1p0/imslticc_v1p0.xsd http://www.imsglobal.org/xsd/imsbasiclti_v1p0 http://www.imsglobal.org/xsd/lti/ltiv1p0/imsbasiclti_v1p0p1.xsd http://www.imsglobal.org/xsd/imslticm_v1p0 http://www.imsglobal.org/xsd/lti/ltiv1p0/imslticm_v1p0.xsd http://www.imsglobal.org/xsd/imslticp_v1p0 http://www.imsglobal.org/xsd/lti/ltiv1p0/imslticp_v1p0.xsd">
<blti:title>Redirect Tool</blti:title>
<blti:description>
Add links to external web resources that show up as navigation items in course, user or account navigation. Whatever URL you specify is loaded within the content pane when users click the link.
</blti:description>
<blti:launch_url>https://www.edu-apps.org/redirect</blti:launch_url>
<blti:custom>
<lticm:property name="url">https://</lticm:property>
</blti:custom>
<blti:extensions platform="canvas.instructure.com">
<lticm:options name="account_navigation">
<lticm:property name="enabled">true</lticm:property>
<lticm:property name="visibility">public</lticm:property>
</lticm:options>
<lticm:property name="icon_url">
https://www.edu-apps.org/assets/lti_redirect_engine/redirect_icon.png
</lticm:property>
<lticm:property name="link_text"/>
<lticm:property name="privacy_level">anonymous</lticm:property>
<lticm:property name="tool_id">redirect</lticm:property>
</blti:extensions>
</cartridge_basiclti_link>
XML
post 'create', :course_id => @course.id, :external_tool => {:name => "tool name", :url => "http://example.com", :consumer_key => "key", :shared_secret => "secret", :config_type => "by_xml", :config_xml => xml}, :format => "json"
response.should be_success
Lti::NavigationCache.new(@course.root_account).cache_key.should_not == cache_key
end
end
it 'should clear the navigation tabs cache for user nav' do
enable_cache do
course_with_teacher_logged_in(:active_all => true)
cache_key = Lti::NavigationCache.new(@course.root_account).cache_key
xml = <<-XML
<cartridge_basiclti_link xmlns="http://www.imsglobal.org/xsd/imslticc_v1p0" xmlns:blti="http://www.imsglobal.org/xsd/imsbasiclti_v1p0" xmlns:lticm="http://www.imsglobal.org/xsd/imslticm_v1p0" xmlns:lticp="http://www.imsglobal.org/xsd/imslticp_v1p0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.imsglobal.org/xsd/imslticc_v1p0 http://www.imsglobal.org/xsd/lti/ltiv1p0/imslticc_v1p0.xsd http://www.imsglobal.org/xsd/imsbasiclti_v1p0 http://www.imsglobal.org/xsd/lti/ltiv1p0/imsbasiclti_v1p0p1.xsd http://www.imsglobal.org/xsd/imslticm_v1p0 http://www.imsglobal.org/xsd/lti/ltiv1p0/imslticm_v1p0.xsd http://www.imsglobal.org/xsd/imslticp_v1p0 http://www.imsglobal.org/xsd/lti/ltiv1p0/imslticp_v1p0.xsd">
<blti:title>Redirect Tool</blti:title>
<blti:description>
Add links to external web resources that show up as navigation items in course, user or account navigation. Whatever URL you specify is loaded within the content pane when users click the link.
</blti:description>
<blti:launch_url>https://www.edu-apps.org/redirect</blti:launch_url>
<blti:custom>
<lticm:property name="url">https://</lticm:property>
</blti:custom>
<blti:extensions platform="canvas.instructure.com">
<lticm:options name="user_navigation">
<lticm:property name="enabled">true</lticm:property>
<lticm:property name="visibility">public</lticm:property>
</lticm:options>
<lticm:property name="icon_url">
https://www.edu-apps.org/assets/lti_redirect_engine/redirect_icon.png
</lticm:property>
<lticm:property name="link_text"/>
<lticm:property name="privacy_level">anonymous</lticm:property>
<lticm:property name="tool_id">redirect</lticm:property>
</blti:extensions>
</cartridge_basiclti_link>
XML
post 'create', :course_id => @course.id, :external_tool => {:name => "tool name", :url => "http://example.com", :consumer_key => "key", :shared_secret => "secret", :config_type => "by_xml", :config_xml => xml}, :format => "json"
response.should be_success
Lti::NavigationCache.new(@course.root_account).cache_key.should_not == cache_key
end
end
end
end
end

View File

@ -0,0 +1,60 @@
#
# Copyright (C) 2014 Instructure, Inc.
#
# This file is part of Canvas.
#
# Canvas is free software: you can redistribute it and/or modify it under
# the terms of the GNU Affero General Public License as published by the Free
# Software Foundation, version 3 of the License.
#
# Canvas is distributed in the hope that it will be useful, but WITHOUT ANY
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
# A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
# details.
#
# You should have received a copy of the GNU Affero General Public License along
# with this program. If not, see <http://www.gnu.org/licenses/>.
#
require File.expand_path(File.dirname(__FILE__) + '/../../spec_helper.rb')
module Lti
describe NavigationCache do
let(:account) { mock }
subject { NavigationCache.new(account) }
describe "#cache_key" do
it 'creates a new cache key' do
enable_cache do
uuid = SecureRandom.uuid
SecureRandom.expects(:uuid).once.returns(uuid)
subject.cache_key.should == uuid
end
end
it 'returns the cached result on subsequent calls' do
enable_cache do
uuid = SecureRandom.uuid
SecureRandom.expects(:uuid).once.returns(uuid)
subject.cache_key.should == uuid
subject.cache_key.should == uuid
end
end
end
describe "#invalidate_cache_key" do
it 'invalidates the cache' do
enable_cache do
uuid = SecureRandom.uuid
SecureRandom.expects(:uuid).twice.returns(uuid)
subject.cache_key
subject.invalidate_cache_key
subject.cache_key
end
end
end
end
end