add file_menu external tools to new files cog menu

test plan:
* add an external tool (see the example xml file
referenced in the ticket) configured for the file_menu
* with new files enabled, the external tool should add
an item in the cog menu for files
* if the file's content-type is unrecognized by the tool
 (i.e. not a standard document/image/video file), the
 item should be disabled

* the external tool should also add an item to the
cog menu for file module items on the modules page

closes #CNVS-17005

Change-Id: I8a5497be2f784d5fc64969baf30e33ae53b5dc1a
Reviewed-on: https://gerrit.instructure.com/45203
Reviewed-by: Jeremy Stanley <jeremy@instructure.com>
Tested-by: Jenkins <jenkins@instructure.com>
QA-Review: Jahnavi Yetukuri <jyetukuri@instructure.com>
Product-Review: James Williams  <jamesw@instructure.com>
This commit is contained in:
James Williams 2014-12-03 11:34:52 -07:00
parent 96da5de2d8
commit 96765144c9
19 changed files with 155 additions and 20 deletions

View File

@ -68,3 +68,8 @@ define [
present: ->
_.clone(@attributes)
externalToolEnabled: (tool) =>
if tool.accept_media_types && tool.accept_media_types.length > 0
_.indexOf(tool.accept_media_types.split(","), @get('content-type')) != -1
else
true

View File

@ -74,6 +74,7 @@ define [
userCanManageFilesForContext = filesEnv.userHasPermission({contextType: contextType, contextId: contextId}, 'manage_files')
usageRightsRequiredForContext = filesEnv.contextsDictionary["#{contextType}_#{contextId}"]?.usage_rights_required
externalToolsForContext = filesEnv.contextFor({contextType: contextType, contextId: contextId})?.file_menu_tools || []
div null,
# For whatever reason, VO in Safari didn't like just the h1 tag.
@ -129,6 +130,7 @@ define [
areAllItemsSelected: @areAllItemsSelected
userCanManageFilesForContext: userCanManageFilesForContext
usageRightsRequiredForContext: usageRightsRequiredForContext
externalToolsForContext: externalToolsForContext
previewItem: @previewItem
dndOptions:
onItemDragStart: @onItemDragStart

View File

@ -173,4 +173,5 @@ define [
startEditingName: @startEditingName
userCanManageFilesForContext: @props.userCanManageFilesForContext
usageRightsRequiredForContext: @props.usageRightsRequiredForContext
externalToolsForContext: @props.externalToolsForContext
})

View File

@ -5,6 +5,7 @@ define [
'compiled/fn/preventDefault'
'../modules/customPropTypes'
'../modules/filesEnv'
'compiled/models/File'
'compiled/models/Folder'
'./RestrictedDialogForm'
'../utils/openMoveDialog'
@ -13,7 +14,7 @@ define [
'../utils/deleteStuff'
'jquery'
'jqueryui/dialog'
], (I18n, React, withReactDOM, preventDefault, customPropTypes, filesEnv, Folder, RestrictedDialogForm, openMoveDialog, openUsageRightsDialog, downloadStuffAsAZip, deleteStuff, $) ->
], (I18n, React, withReactDOM, preventDefault, customPropTypes, filesEnv, File, Folder, RestrictedDialogForm, openMoveDialog, openUsageRightsDialog, downloadStuffAsAZip, deleteStuff, $) ->
ItemCog = React.createClass
displayName: 'ItemCog'
@ -22,6 +23,24 @@ define [
model: customPropTypes.filesystemObject
render: withReactDOM ->
if @props.model instanceof File
externalToolMenuItems = @props.externalToolsForContext.map (tool) =>
if @props.model.externalToolEnabled(tool)
li {},
a {
href: "#{tool.base_url}&files[]=#{@props.model.id}",
},
tool.title
else
li {},
a {
className: "disabled",
href: "#"
},
tool.title
else
externalToolMenuItems = []
wrap = (fn) =>
preventDefault (event) =>
singularContextType = @props.model.collection?.parentFolder?.get('context_type').toLowerCase()
@ -91,4 +110,4 @@ define [
ref: 'deleteLink'
},
I18n.t('delete', 'Delete')
]
].concat(externalToolMenuItems)

View File

@ -103,6 +103,7 @@ define [
isSelected: child in @props.selectedItems
toggleSelected: @props.toggleItemSelected.bind(null, child)
userCanManageFilesForContext: @props.userCanManageFilesForContext
externalToolsForContext: @props.externalToolsForContext
previewItem: @props.previewItem.bind(null, child)
dndOptions: @props.dndOptions
LoadingIndicator isLoading: !@state.collection.loadedAll

View File

@ -113,6 +113,7 @@ define [
toggleSelected: @props.toggleItemSelected.bind(null, child)
userCanManageFilesForContext: @props.userCanManageFilesForContext
usageRightsRequiredForContext: @props.usageRightsRequiredForContext
externalToolsForContext: @props.externalToolsForContext
previewItem: @props.previewItem.bind(null, child)
dndOptions: @props.dndOptions

View File

@ -31,16 +31,20 @@ define [
folder.fetch()
folder
filesEnv.userHasPermission = (folderOrFile, action) ->
return false unless folderOrFile
filesEnv.contextFor = (folderOrFile) ->
if folderOrFile.collection?.parentFolder
folderOrFile = folderOrFile.collection.parentFolder
if folderOrFile instanceof Folder
folder = folderOrFile
assetString = (folder?.get('context_type') + 's_' + folder?.get('context_id')).toLowerCase()
else if folderOrFile.contextType and folderOrFile.contextId
assetString = "#{folderOrFile.contextType}_#{folderOrFile.contextId}".toLowerCase()
filesEnv.contextsDictionary?[assetString]
filesEnv.contextsDictionary?[assetString]?.permissions?[action]
filesEnv.userHasPermission = (folderOrFile, action) ->
return false unless folderOrFile
filesEnv.contextFor(folderOrFile)?.permissions?[action]
filesEnv.baseUrl = if filesEnv.showingAllContexts
'/files'

View File

@ -30,6 +30,7 @@ define [
{extension_type: 'discussion_topic_menu', text: I18n.t 'discussion_topic_menu_configured', 'Discussion Topic menu configured'}
{extension_type: 'module_menu', text: I18n.t 'module_menu_configured', 'Module menu configured'}
{extension_type: 'quiz_menu', text: I18n.t 'quiz_menu_configured', 'Quiz menu configured'}
{extension_type: 'file_menu', text: I18n.t 'file_menu_configured', 'File menu configured'}
{extension_type: 'wiki_page_menu', text: I18n.t 'wiki_page_menu_configured', 'Wiki page menu configured'}
]

View File

@ -131,15 +131,21 @@ class ApplicationController < ActionController::Base
end
helper_method :js_env
def external_tools_display_hashes(type)
tools = ContextExternalTool.all_tools_for(@context, :type => type,
def external_tools_display_hashes(type, context=@context, custom_settings=[])
context = context.account if context.is_a?(User)
tools = ContextExternalTool.all_tools_for(context, :type => type,
:root_account => @domain_root_account, :current_user => @current_user)
extension_settings = [:icon_url] + custom_settings
tools.map do |tool|
{
hash = {
:title => tool.label_for(type),
:icon_url => tool.extension_setting(type, :icon_url),
:base_url => course_external_tool_path(@context, tool, :launch_type => type)
:base_url => named_context_url(context, :context_external_tool_path, tool, :launch_type => type)
}
extension_settings.each do |setting|
hash[setting] = tool.extension_setting(type, setting)
end
hash
end
end

View File

@ -27,7 +27,7 @@ class ContextModulesController < ApplicationController
@collapsed_modules = ContextModuleProgression.for_user(@current_user).for_modules(@modules).select([:context_module_id, :collapsed]).select{|p| p.collapsed? }.map(&:context_module_id)
@menu_tools = {}
[:assignment_menu, :module_menu, :quiz_menu, :wiki_page_menu].each do |type|
[:assignment_menu, :discussion_topic_menu, :file_menu, :module_menu, :quiz_menu, :wiki_page_menu].each do |type|
@menu_tools[type] = ContextExternalTool.all_tools_for(@context, :type => type,
:root_account => @domain_root_account, :current_user => @current_user)
end

View File

@ -461,12 +461,16 @@ class ExternalToolsController < ApplicationController
protected :content_item_selection_response
def content_item_response
#contstruct query params for the export endpoint
export_type = params["export_type"] || "common_cartridge"
content_items = []
if export_type == "common_cartridge"
content_items << content_item_for_common_cartridge
if params[:files].present?
content_items << content_item_for_file
else
#construct query params for the export endpoint
export_type = params["export_type"] || "common_cartridge"
if export_type == "common_cartridge"
content_items << content_item_for_common_cartridge
end
end
{
@ -476,6 +480,30 @@ class ExternalToolsController < ApplicationController
end
protected :content_item_response
def content_item_for_file
#find the content title
file = Attachment.where(:id => params[:files].first).first
if @context.is_a?(Account)
raise ActiveRecord::RecordNotFound unless file.context == @current_user
elsif file.context.is_a?(Course)
raise ActiveRecord::RecordNotFound unless file.context == @context
elsif file.context.is_a?(Group)
raise ActiveRecord::RecordNotFound unless file.context.context == @context
end
render_unauthorized_action if file.locked_for?(@current_user, check_policies: true)
{
"@type" => "ContentItemPlacement",
"placementOf" => {
"@type" => "FileItem",
"@id" => file_download_url(file, { :verifier => file.uuid, :download => '1', :download_frd => '1' }),
"mediaType" => file.content_type,
"title" => file.display_name
}
}
end
protected :content_item_for_file
def content_item_for_common_cartridge
query_params = {"export_type" => "common_cartridge"}

View File

@ -326,13 +326,23 @@ class FilesController < ApplicationController
@contexts = [@context]
get_all_pertinent_contexts(include_groups: true) if @context == @current_user
files_contexts = @contexts.map do |context|
tool_context = if context.is_a?(Course)
context
elsif context.is_a?(User)
@domain_root_account
elsif context.is_a?(Group)
context.context
end
file_menu_tools = (tool_context ? external_tools_display_hashes(:file_menu, tool_context, [:accept_media_types]) : [])
{
asset_string: context.asset_string,
name: context == @current_user ? t('my_files', 'My Files') : context.name,
usage_rights_required: context.feature_enabled?(:usage_rights_required),
permissions: {
manage_files: context.grants_right?(@current_user, session, :manage_files),
}
},
file_menu_tools: file_menu_tools
}
end
@ -340,7 +350,9 @@ class FilesController < ApplicationController
@body_classes << 'full-width padless-content'
js_bundle :react_files
jammit_css :react_files
js_env :FILES_CONTEXTS => files_contexts
js_env({
:FILES_CONTEXTS => files_contexts
})
render :text => "".html_safe, :layout => true
end

View File

@ -46,9 +46,11 @@ class ContextExternalTool < ActiveRecord::Base
:user_navigation, :course_navigation, :account_navigation, :resource_selection,
:editor_button, :homework_submission, :migration_selection, :course_home_sub_navigation,
:course_settings_sub_navigation, :global_navigation,
:assignment_menu, :discussion_topic_menu, :module_menu, :quiz_menu, :wiki_page_menu
:assignment_menu, :file_menu, :discussion_topic_menu, :module_menu, :quiz_menu, :wiki_page_menu
]
CUSTOM_EXTENSION_KEYS = {:file_menu => [:accept_media_types]}
EXTENSION_TYPES.each do |type|
class_eval <<-RUBY, __FILE__, __LINE__ + 1
def #{type}(setting=nil)
@ -79,6 +81,9 @@ class ContextExternalTool < ActiveRecord::Base
extension_keys = [:custom_fields, :default, :display_type, :enabled, :icon_url,
:selection_height, :selection_width, :text, :url, :message_type]
if custom_keys = CUSTOM_EXTENSION_KEYS[type]
extension_keys += custom_keys
end
extension_keys += {
:visibility => lambda{|v| %w{members admins}.include?(v)}
}.to_a
@ -452,7 +457,8 @@ class ContextExternalTool < ActiveRecord::Base
private_class_method :contexts_to_search
LOR_TYPES = [:course_home_sub_navigation, :course_settings_sub_navigation, :global_navigation,
:assignment_menu, :discussion_topic_menu, :module_menu, :quiz_menu, :wiki_page_menu]
:assignment_menu, :file_menu, :discussion_topic_menu, :module_menu, :quiz_menu,
:wiki_page_menu]
def self.all_tools_for(context, options={})
#options[:type] is deprecated, use options[:placements] instead
placements =* options[:placements] || options[:type]

View File

@ -447,6 +447,14 @@ $context_module_bg_color: #f2f3f4
li.assignment_menu
display: none
.context_module_item:not(.discussion_topic)
li.discussion_topic_menu
display: none
.context_module_item:not(.attachment)
li.file_menu
display: none
.context_module_item:not(.quiz)
li.quiz_menu
display: none

View File

@ -149,6 +149,8 @@
<li><a href="<%= context_url(@context, :context_url) %>/modules/items/<%= tag ? tag.id : "{{ id }}" %>" class="delete_item_link delete_link" title="<%= t('links.remove_item_from_module', %{Remove this item from the module}) %>"><%= @module_item_image_tags['delete'] %> <%= t('links.remove_item', %{Remove}) %></a></li>
<% menu_type_to_class = {
:assignment_menu => Assignment,
:discussion_topic_menu => DiscussionTopic,
:file_menu => Attachment,
:quiz_menu => Quizzes::Quiz,
:wiki_page_menu => WikiPage
}

View File

@ -419,6 +419,7 @@ describe ExternalToolsController, type: :request do
et.global_navigation = {:url=>"http://www.example.com/ims/lti/resource", :text => "global navigation", display_type: 'full_width', visibility: 'admins'}
et.assignment_menu = {:url=>"http://www.example.com/ims/lti/resource", :text => "assignment menu", display_type: 'full_width', visibility: 'admins'}
et.discussion_topic_menu = {:url=>"http://www.example.com/ims/lti/resource", :text => "discussion topic menu", display_type: 'full_width', visibility: 'admins'}
et.file_menu = {:url=>"http://www.example.com/ims/lti/resource", :text => "module menu", display_type: 'full_width', visibility: 'admins'}
et.module_menu = {:url=>"http://www.example.com/ims/lti/resource", :text => "module menu", display_type: 'full_width', visibility: 'admins'}
et.quiz_menu = {:url=>"http://www.example.com/ims/lti/resource", :text => "quiz menu", display_type: 'full_width', visibility: 'admins'}
et.wiki_page_menu = {:url=>"http://www.example.com/ims/lti/resource", :text => "wiki page menu", display_type: 'full_width', visibility: 'admins'}
@ -572,6 +573,14 @@ describe ExternalToolsController, type: :request do
"display_type"=>'full_width',
"selection_height"=>400,
"selection_width"=>800},
"file_menu"=>
{"text"=>"module menu",
"label"=>"module menu",
"url"=>"http://www.example.com/ims/lti/resource",
"visibility"=>'admins',
"display_type"=>'full_width',
"selection_height"=>400,
"selection_width"=>800},
"module_menu"=>
{"text"=>"module menu",
"label"=>"module menu",

View File

@ -130,6 +130,18 @@ describe ExternalToolsController do
expect(placement['placementOf']['title']).to eq 'blah'
end
it "sends content item json for a file" do
user_session(@teacher)
attachment_model
get :show, :course_id => @course.id, id: @tool.id, :files => [@attachment.id]
placement = JSON.parse(assigns[:lti_launch].params['content_items'])['@graph'].first
download_url = placement['placementOf']['@id']
expect(download_url).to include(@attachment.uuid)
expect(placement['placementOf']['mediaType']).to eq @attachment.content_type
expect(placement['placementOf']['title']).to eq @attachment.display_name
end
it "sends content item json for a quiz" do
user_session(@teacher)
quiz = @course.quizzes.create!(title: 'a quiz')

View File

@ -118,6 +118,19 @@ describe ContextExternalTool do
expect(@tool.has_placement?(:course_navigation)).to eq true
end
it "should allow accept_media_types setting exclusively for file_menu extension" do
@tool = @course.context_external_tools.create!(:name => "a", :url => "http://google.com", :consumer_key => '12345', :shared_secret => 'secret')
@tool.course_navigation = {
:accept_media_types => "types"
}
@tool.file_menu = {
:accept_media_types => "types"
}
@tool.save!
expect(@tool.extension_setting(:course_navigation, :accept_media_types)).to be_blank
expect(@tool.extension_setting(:file_menu, :accept_media_types)).to eq "types"
end
it "should clear disabled extensions" do
@tool = @course.context_external_tools.create!(:name => "a", :url => "http://google.com", :consumer_key => '12345', :shared_secret => 'secret')
@tool.course_navigation = {

View File

@ -184,6 +184,11 @@ shared_examples_for "external tools tests" do
<lticm:property name="text">Build/Link to Wiki Page</lticm:property>
<lticm:property name="display_type">full_width</lticm:property>
</lticm:options>
<lticm:options name="file_menu">
<lticm:property name="url">https://example.com/wiki</lticm:property>
<lticm:property name="text">Build/Link to Wiki Page</lticm:property>
<lticm:property name="display_type">full_width</lticm:property>
</lticm:options>
<lticm:options name="module_menu">
<lticm:property name="url">https://example.com/wiki</lticm:property>
<lticm:property name="text">Build/Link to Wiki Page</lticm:property>