allow submission type LTIs to require a resource
Adds an option `require_resource_selection` for the `submission_type_selection` placement to force a user to select a resource (by launching the tool) before saving the assignment. closes INTEROP-8626 flag=none Note that the validation error message is intentionally brief to avoid overflowing the allotted size for the error message popup/tooltip. Test plan: - have a tool with the submission_type_selection placement. In accordance with ContextExternalTool#placement_allowed?, you may need to add the domain or dev key to the allowed keys or allowed domains setting. - edit the tool config (dev key config if the tool is 1.3) to have "require_resource_selection": true in the submission_type_selection placement settings. - start creating a new assignment. Test out lots of different scenarios, but attempting to save the tool (for just testing whether the new validation triggers or not without actually saving the tool, you can always set the assignment title to be empty so the form won't actually submit), such as: - choosing submission type = "My Tool" and not doing anything else. there should be a validation error "Please click below to launch the tool and select a resource." - choosing submission type = "My Tool" and then launching and selecting a resource (no validation error) - selecting a resource and then hitting the 'x' to remove it (validation error) - selecting a resource, hitting the 'x' to remove it, choosing 'No Submission' and choosing 'My Tool' again. there shouldn't be any resource shown, and there should be a validation error. - selecting a resource, hitting the 'x' to remove it, clicking the button to launch the tool but closing the window before choosing another ressource. IT should show no resource and the validation error should trigger. Before this commit, it was showing the old (removed) resource. - selecting a resource, then choosing "External Tool". the URL should field should be emptied out and saving should fail with the (already existing) 'no URL' validation error. - selecting a resource, then choosing "External URL", and entering in some URL. this should succeed (make sure neither the "no URL" error or the new validation error are triggered) - choosing "External Tool" in the dropdown, then launching the tool in the normal assignment_selection placement and selecting a resource there. saving should succeed (this commit should only affect the submission_type_selection placement) - selecting a resource through assignment_selection as in the last step, and then choosing the tool in the dropdown (submission_type_selection placement). It should not show a resource and the validation should fail. - when you are done testing, choose the tool (as submission type selection) with content and save the assignment. ideally, send some custom variables in the deep linking response so you can test that those are maintained when editing the assignment. - edit the assignment and test a variety of scenarios, including: - changing only title or description (don't type submission type at all). Save the tool. Check the Lti::ResourceLink in the database to make sure that the custom URL and/or custom parameters there have been preserved - switching to "No Submission" and back to the tool. This should show the resource preserved. Save and make sure the Lti::ResourceLink has been preserved. - switching to "External Tool". this should empty out the URL field and make the validation fail. - switching to "External Tool" as in the last check, but then set the dropdown back to the tool. There should not be a resource shown and saving should fire the validation. - edit again (reload the page if necessary) and just clear out the resource by clicking the 'x' under in the resource card. validation should fail. - remove and add back the resource. that should succeed (database Lti::ResourceLink should be updated with new data) - choose "External Tool" and choose the tool in assignment_selection. choose a resource and save the assignment. That should succeed and the Lti::ResourceLink in the DB should be updated. - make an assignment with some other (ideally LTI 1.3) tool that doesn't have the submission_type_selection placement (or temporarily remove/disable submission_type_placement on your tool). edit the assignment without changing anything regarding submission type to make sure that still doesn't modify the resource link (if LTI 1.3) - remove the require_resource_selection option on the tool and make sure you can create a new assignment using that tool's submission_type_selection placement without selecting a resource. - if possible, to make sure we didn't severely make anything with MasteryConnect, test with a tool that acts like mastery connect: - change this line in the EditView.jsx: const mc_ext = item[LTI_EXT_MASTERY_CONNECT] to: const mc_ext = {points: 1, title: "foo", objectives: [], trackerName: 'foo', trackerAlignment: 'foo', studentCount: 0} - add an LTI 1.1 (cannot be 1.3) tool (such as Xander's vercel app) with the submission_type_selection placement and use Setting.set("submission_type_selection_allowed_launch_domains", ...) to allow it to use the placement (or stub out allowed_placement?) - play around with editing and editing a tool in the submission type selection placement. You will probably notice some difference (URL isn't cleared when you choose 'External Tool', values still exist when you choose the tool in the dropdown) but nothing should be too broken. At the very least, make sure you create an assignment with the tool, and then can edit the assignment and edit the description/title without changing the LTI stuff. Change-Id: I86966abe815f6370db9e5723802398491c6fb66d Reviewed-on: https://gerrit.instructure.com/c/canvas-lms/+/348871 Tested-by: Service Cloud Jenkins <svc.cloudjenkins@instructure.com> Reviewed-by: Bence Árpási <bence.arpasi@instructure.com> QA-Review: Bence Árpási <bence.arpasi@instructure.com> Product-Review: Alexis Nast <alexis.nast@instructure.com>
This commit is contained in:
parent
c5b024ad71
commit
fed8e96adc
|
@ -548,27 +548,32 @@ class ApplicationController < ActionController::Base
|
|||
helper_method :external_tools_display_hashes
|
||||
|
||||
def external_tool_display_hash(tool, type, url_params = {}, context = @context, custom_settings = [])
|
||||
url_params = {
|
||||
id: tool.id,
|
||||
launch_type: type
|
||||
}.merge(url_params)
|
||||
|
||||
hash = {
|
||||
id: tool.id,
|
||||
title: tool.label_for(type, I18n.locale),
|
||||
base_url: polymorphic_url([context, :external_tool], url_params),
|
||||
}
|
||||
hash[:tool_id] = tool.tool_id if tool.tool_id.present?
|
||||
base_url: polymorphic_url(
|
||||
[context, :external_tool],
|
||||
{ id: tool.id, launch_type: type }.merge(url_params)
|
||||
),
|
||||
tool_id: tool.tool_id.presence
|
||||
}.compact
|
||||
|
||||
extension_settings = [:icon_url, :canvas_icon_class] | custom_settings
|
||||
extension_settings.each do |setting|
|
||||
hash[setting] = tool.extension_setting(type, setting)
|
||||
end
|
||||
|
||||
hash[:base_title] = tool.default_label(I18n.locale) if custom_settings.include?(:base_title)
|
||||
hash[:external_url] = tool.url if custom_settings.include?(:external_url)
|
||||
if type == :submission_type_selection && tool.submission_type_selection[:description].present?
|
||||
hash[:description] = tool.submission_type_selection[:description]
|
||||
|
||||
if type == :submission_type_selection
|
||||
hash.merge!({
|
||||
description: tool.submission_type_selection[:description].presence,
|
||||
require_resource_selection:
|
||||
tool.submission_type_selection[:require_resource_selection]
|
||||
}.compact)
|
||||
end
|
||||
|
||||
if type == :top_navigation
|
||||
hash[:pinned] = tool.placement_pinned?(type)
|
||||
end
|
||||
|
|
|
@ -105,7 +105,8 @@ class ContextExternalTool < ActiveRecord::Base
|
|||
|
||||
CUSTOM_EXTENSION_KEYS = {
|
||||
file_menu: [:accept_media_types].freeze,
|
||||
editor_button: [:use_tray].freeze
|
||||
editor_button: [:use_tray].freeze,
|
||||
submission_type_selection: [:description, :require_resource_selection].freeze,
|
||||
}.freeze
|
||||
|
||||
DISABLED_STATE = "disabled"
|
||||
|
|
|
@ -139,6 +139,9 @@ module Schemas::Lti
|
|||
"maxLength" => 255,
|
||||
"errorMessage" => "description must be a string with a maximum length of 255 characters"
|
||||
}.freeze,
|
||||
"require_resource_selection" => {
|
||||
"type" => "boolean"
|
||||
}.freeze,
|
||||
**LAUNCH_INFO_SCHEMA,
|
||||
}.freeze
|
||||
}.freeze
|
||||
|
|
|
@ -2273,22 +2273,28 @@ describe AssignmentsController do
|
|||
let(:domain) { "justanexamplenotarealwebsite.com" }
|
||||
|
||||
let(:tool) do
|
||||
factory_with_protected_attributes(@course.context_external_tools,
|
||||
domain:,
|
||||
url: "http://www.justanexamplenotarealwebsite.com/tool1",
|
||||
shared_secret: "test123",
|
||||
consumer_key: "test123",
|
||||
name: tool_settings[:base_title],
|
||||
settings: {
|
||||
submission_type_selection: tool_settings
|
||||
})
|
||||
factory_with_protected_attributes(
|
||||
@course.context_external_tools,
|
||||
domain:,
|
||||
url: "http://www.justanexamplenotarealwebsite.com/tool1",
|
||||
shared_secret: "test123",
|
||||
consumer_key: "test123",
|
||||
name: tool_settings[:base_title],
|
||||
settings: {
|
||||
submission_type_selection: tool_settings
|
||||
}
|
||||
)
|
||||
end
|
||||
|
||||
let(:tool_in_js_env) do
|
||||
Setting.set("submission_type_selection_allowed_launch_domains", domain)
|
||||
tool
|
||||
subject
|
||||
assigns[:js_env][:SUBMISSION_TYPE_SELECTION_TOOLS][0]
|
||||
end
|
||||
|
||||
it "is correctly set" do
|
||||
tool
|
||||
Setting.set("submission_type_selection_allowed_launch_domains", domain)
|
||||
subject
|
||||
expect(assigns[:js_env][:SUBMISSION_TYPE_SELECTION_TOOLS][0]).to include(
|
||||
expect(tool_in_js_env).to include(
|
||||
base_title: tool_settings[:base_title],
|
||||
title: tool_settings[:base_title],
|
||||
selection_width: tool_settings[:selection_width],
|
||||
|
@ -2296,20 +2302,36 @@ describe AssignmentsController do
|
|||
)
|
||||
end
|
||||
|
||||
context "the tool includes a description propery" do
|
||||
let(:description) { "This is a description" }
|
||||
let(:tool_settings) do
|
||||
res = super()
|
||||
res[:description] = description
|
||||
res
|
||||
describe "require_resourse_selection property" do
|
||||
context "when not given in the settings" do
|
||||
it "is not set in the js_env tool" do
|
||||
expect(tool_in_js_env).to_not include(:require_resource_selection)
|
||||
end
|
||||
end
|
||||
|
||||
context "when set if set to false in the settings" do
|
||||
let(:tool_settings) { super().merge(require_resource_selection: false) }
|
||||
|
||||
it "is set in the js_env tool" do
|
||||
expect(tool_in_js_env).to include(require_resource_selection: false)
|
||||
end
|
||||
end
|
||||
|
||||
context "when set if set to true in the settings" do
|
||||
let(:tool_settings) { super().merge(require_resource_selection: true) }
|
||||
|
||||
it "is set in the js_env tool" do
|
||||
expect(tool_in_js_env).to include(require_resource_selection: true)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context "the tool includes a description propery" do
|
||||
let(:description) { "This is a description" }
|
||||
let(:tool_settings) { super().merge(description:) }
|
||||
|
||||
it "includes the launch points" do
|
||||
tool
|
||||
Setting.set("submission_type_selection_allowed_launch_domains", domain)
|
||||
subject
|
||||
expect(assigns[:js_env][:SUBMISSION_TYPE_SELECTION_TOOLS][0])
|
||||
.to include(description:)
|
||||
expect(tool_in_js_env).to include(description:)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1215,6 +1215,19 @@ describe ContextExternalTool do
|
|||
expect(@tool.extension_setting(:file_menu, :accept_media_types)).to eq "types"
|
||||
end
|
||||
|
||||
it "allows description and require_resource_selection exclusively for submission_type_selection extension" do
|
||||
@tool = external_tool_model
|
||||
description = "my description"
|
||||
require_resource_selection = true
|
||||
@tool.submission_type_selection = { description:, require_resource_selection: }
|
||||
@tool.file_menu = { description:, require_resource_selection: }
|
||||
@tool.save!
|
||||
expect(@tool.extension_setting(:submission_type_selection, :description)).to eq description
|
||||
expect(@tool.extension_setting(:submission_type_selection, :require_resource_selection)).to eq require_resource_selection
|
||||
expect(@tool.extension_setting(:file_menu, :description)).to be_blank
|
||||
expect(@tool.extension_setting(:file_menu, :require_resource_selection)).to be_blank
|
||||
end
|
||||
|
||||
it "clears disabled extensions" do
|
||||
@tool = @course.context_external_tools.create!(name: "a", url: "http://google.com", consumer_key: "12345", shared_secret: "secret")
|
||||
@tool.course_navigation = {
|
||||
|
|
|
@ -37,6 +37,17 @@ module Lti
|
|||
let(:tool_configuration) { described_class.new(settings:) }
|
||||
let(:developer_key) { DeveloperKey.create }
|
||||
|
||||
def make_placement(type, message_type, extra = {})
|
||||
{
|
||||
"target_link_uri" => "http://example.com/launch?placement=#{type}",
|
||||
"text" => "Test Title",
|
||||
"message_type" => message_type,
|
||||
"icon_url" => "https://static.thenounproject.com/png/131630-211.png",
|
||||
"placement" => type.to_s,
|
||||
**extra
|
||||
}
|
||||
end
|
||||
|
||||
describe "validations" do
|
||||
subject { tool_configuration.save }
|
||||
|
||||
|
@ -50,20 +61,27 @@ module Lti
|
|||
|
||||
context "with a description property at the submission_type_selection placement" do
|
||||
let(:settings) do
|
||||
res = super()
|
||||
super().tap do |res|
|
||||
res["extensions"].first["settings"]["placements"] << make_placement(
|
||||
:submission_type_selection,
|
||||
"LtiDeepLinkingRequest",
|
||||
"description" => "Test Description"
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
res["extensions"].first["settings"]["placements"].push(
|
||||
{
|
||||
"target_link_uri" => "http://example.com/launch?placement=submission_type_selection",
|
||||
"text" => "Test Title",
|
||||
"message_type" => "LtiResourceLinkRequest",
|
||||
"icon_url" => "https://static.thenounproject.com/png/131630-211.png",
|
||||
"description" => "Test Description",
|
||||
"placement" => "submission_type_selection",
|
||||
}
|
||||
)
|
||||
it { is_expected.to be true }
|
||||
end
|
||||
|
||||
res
|
||||
context "with a require_resource_selection property at the submission_type_selection placement" do
|
||||
let(:settings) do
|
||||
super().tap do |res|
|
||||
res["extensions"].first["settings"]["placements"] << make_placement(
|
||||
:submission_type_selection,
|
||||
"LtiDeepLinkingRequest",
|
||||
"require_resource_selection" => true
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
it { is_expected.to be true }
|
||||
|
@ -92,20 +110,27 @@ module Lti
|
|||
|
||||
context "when the submission_type_selection description is longer than 255 characters" do
|
||||
let(:settings) do
|
||||
s = super()
|
||||
super().tap do |s|
|
||||
s["extensions"].first["settings"]["placements"] << make_placement(
|
||||
:submission_type_selection,
|
||||
"LtiDeepLinkingRequest",
|
||||
"description" => "a" * 256
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
s["extensions"].first["settings"]["placements"].push(
|
||||
{
|
||||
"target_link_uri" => "http://example.com/launch?placement=submission_type_selection",
|
||||
"text" => "Test Title",
|
||||
"message_type" => "LtiResourceLinkRequest",
|
||||
"icon_url" => "https://static.thenounproject.com/png/131630-211.png",
|
||||
"description" => "a" * 256,
|
||||
"placement" => "submission_type_selection",
|
||||
}
|
||||
)
|
||||
it { is_expected.to be false }
|
||||
end
|
||||
|
||||
s
|
||||
context "when the submission_type_selection require_resource_selection is of the wrong type" do
|
||||
let(:settings) do
|
||||
super().tap do |s|
|
||||
s["extensions"].first["settings"]["placements"] << make_placement(
|
||||
:submission_type_selection,
|
||||
"LtiDeepLinkingRequest",
|
||||
"require_resource_selection" => "true"
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
it { is_expected.to be false }
|
||||
|
@ -702,15 +727,10 @@ module Lti
|
|||
|
||||
context "when the configuration has a #{placement} placement" do
|
||||
let(:tool_configuration) do
|
||||
tc = super()
|
||||
|
||||
tc.settings["extensions"].first["settings"]["placements"] << {
|
||||
"placement" => placement,
|
||||
"message_type" => "LtiResourceLinkRequest",
|
||||
"target_link_uri" => "http://example.com/launch?placement=#{placement}"
|
||||
}
|
||||
|
||||
tc
|
||||
super().tap do |tc|
|
||||
tc.settings["extensions"].first["settings"]["placements"] <<
|
||||
make_placement(placement, "LtiResourceLinkRequest")
|
||||
end
|
||||
end
|
||||
|
||||
it { is_expected.to include("Warning").and include(placement) }
|
||||
|
|
|
@ -679,20 +679,22 @@ EditView.prototype.handleContentItem = function (item) {
|
|||
}
|
||||
}
|
||||
|
||||
this.renderAssignmentSubmissionTypeContainer({
|
||||
tool: this.selectedTool || {title: item.title},
|
||||
resource: item,
|
||||
})
|
||||
this.renderAssignmentSubmissionTypeContainer()
|
||||
|
||||
// TODO: add date prefill here
|
||||
}
|
||||
|
||||
EditView.prototype.handleRemoveResource = function () {
|
||||
// Restore things to how they were before the user pushed the button to
|
||||
// launch the submission_type_selection tool
|
||||
this.$externalToolsContentType.val('context_external_tool')
|
||||
this.$externalToolsUrl.val(this.selectedTool.external_url)
|
||||
this.$externalToolsTitle.val('')
|
||||
this.$externalToolsCustomParams.val('')
|
||||
this.$externalToolsIframeWidth.val('')
|
||||
this.$externalToolsIframeHeight.val('')
|
||||
|
||||
this.renderAssignmentSubmissionTypeContainer()
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -954,14 +956,30 @@ EditView.prototype.handleGradingTypeChange = function (gradingType) {
|
|||
return this.handleSubmissionTypeChange(null)
|
||||
}
|
||||
|
||||
EditView.prototype.hasMasteryConnectData = function () {
|
||||
// Some places check for this data before clearing/overwriting...
|
||||
// It's not clear the reasoning behind this, but I'm for leaving as-is for now.
|
||||
// If some of the resulting odd behavior (switching to MasteryCoinnect from another submission type placement tool) surfaces, revisit with Mastery Connect team (git ref ef3249f62f)
|
||||
return !!this.$externalToolExternalData.val()
|
||||
}
|
||||
|
||||
EditView.prototype.handleSubmissionTypeChange = function (_ev) {
|
||||
const subVal = this.$submissionType.val()
|
||||
this.$onlineSubmissionTypes.toggleAccessibly(subVal === 'online')
|
||||
this.$externalToolSettings.toggleAccessibly(subVal === 'external_tool')
|
||||
const isPlacementTool = subVal.includes('external_tool_placement')
|
||||
this.$externalToolPlacementLaunchContainer.toggleAccessibly(isPlacementTool)
|
||||
|
||||
if (isPlacementTool) {
|
||||
this.handlePlacementExternalToolSelect(subVal)
|
||||
} else if (this.selectedTool && subVal === 'external_tool' && !this.hasMasteryConnectData()) {
|
||||
// Moving from a submission_type_placement tool to generic
|
||||
// "External Tool" type: empty out the fields. this prevents people
|
||||
// from working around the "require_resource_selection" field just
|
||||
// by choosing the tool and then choosing "External Tool"
|
||||
this.handleRemoveResource()
|
||||
this.$externalToolsUrl.val('')
|
||||
this.selectedTool = undefined
|
||||
}
|
||||
this.$groupCategorySelector.toggleAccessibly(subVal !== 'external_tool' && !isPlacementTool)
|
||||
this.$peerReviewsFields.toggleAccessibly(subVal !== 'external_tool' && !isPlacementTool)
|
||||
|
@ -1012,34 +1030,34 @@ EditView.prototype.handlePlacementExternalToolSelect = function (selection) {
|
|||
return toolId === tool.id
|
||||
})
|
||||
|
||||
const hasNoExtToolData = !this.$externalToolExternalData.val()
|
||||
const toolIdsMatch = toolId === this.assignment.selectedSubmissionTypeToolId()
|
||||
const extToolUrlsMatch = this.$externalToolsUrl.val() === this.assignment.externalToolUrl()
|
||||
|
||||
if (hasNoExtToolData && !(toolIdsMatch && extToolUrlsMatch)) {
|
||||
// Set the URL of the tool, but only if we haven't just come back from a
|
||||
// deep linking response (so we'll have a URL from the deep linking
|
||||
// response); also don't set if we are just editing the assignment (in this
|
||||
// case the URL [this.$externalToolsUrl] will be the assignment URL)
|
||||
if (!this.hasMasteryConnectData() && !(toolIdsMatch && extToolUrlsMatch)) {
|
||||
// When switching to a submission_type_selection tool in the dropdown, set
|
||||
// the URL of the tool. Don't set if we are just editing the assignment
|
||||
// (toolIdsMatch and extToolUrlsMatch).
|
||||
|
||||
this.$externalToolsUrl.val(this.selectedTool.external_url)
|
||||
// Ensure that custom params & other stuff left over from another previous
|
||||
// deep link response get cleared out when the user chooses a tool:
|
||||
this.$externalToolsCustomParams.val('')
|
||||
this.$externalToolsIframeWidth.val('')
|
||||
this.$externalToolsIframeHeight.val('')
|
||||
this.$externalToolsTitle.val('')
|
||||
} else if (this.assignment.resourceLink() && this.assignment.resourceLink().title) {
|
||||
// NOTE: not sure this is necessary, but not risking changing now.
|
||||
this.$externalToolsTitle.val(this.assignment.resourceLink().title)
|
||||
}
|
||||
|
||||
this.renderAssignmentSubmissionTypeContainer({
|
||||
tool: this.selectedTool,
|
||||
resource: this.assignment.resourceLink(),
|
||||
})
|
||||
this.renderAssignmentSubmissionTypeContainer()
|
||||
}
|
||||
|
||||
EditView.prototype.renderAssignmentSubmissionTypeContainer = function ({tool, resource}) {
|
||||
EditView.prototype.renderAssignmentSubmissionTypeContainer = function () {
|
||||
const resource = {title: this.$externalToolsTitle.val()}
|
||||
|
||||
const props = {
|
||||
tool,
|
||||
tool: this.selectedTool,
|
||||
resource,
|
||||
onRemoveResource: this.handleRemoveResource,
|
||||
onLaunchButtonClick: this.handleSubmissionTypeSelectionLaunch,
|
||||
|
@ -1289,6 +1307,7 @@ EditView.prototype._adjustDateValue = function (newDate, originalDate) {
|
|||
EditView.prototype.getFormData = function () {
|
||||
let data
|
||||
data = EditView.__super__.getFormData.apply(this, arguments)
|
||||
data.submission_type = this.toolSubmissionType(data.submission_type)
|
||||
data = this._inferSubmissionTypes(data)
|
||||
data = this._filterAllowedExtensions(data)
|
||||
data = this._unsetGroupsIfExternalTool(data)
|
||||
|
@ -1364,7 +1383,6 @@ EditView.prototype.submit = function (event) {
|
|||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
this.cacheAssignmentSettings()
|
||||
$(SUBMISSION_TYPE).val(this.toolSubmissionType($(SUBMISSION_TYPE).val()))
|
||||
if (this.dueDateOverrideView.containsSectionsWithoutOverrides()) {
|
||||
sections = this.dueDateOverrideView.sectionsWithoutOverrides()
|
||||
missingDateDialog = new MissingDateDialog({
|
||||
|
@ -1704,9 +1722,12 @@ EditView.prototype._validatePointsRequired = function (data, errors) {
|
|||
}
|
||||
|
||||
EditView.prototype._validateExternalTool = function (data, errors) {
|
||||
let message, ref, ref1
|
||||
if (data.submission_type !== 'external_tool') {
|
||||
return errors
|
||||
}
|
||||
|
||||
let ref, ref1
|
||||
if (
|
||||
data.submission_type === 'external_tool' &&
|
||||
data.grading_type !== 'not_graded' &&
|
||||
$.trim(
|
||||
(ref = data.external_tool_tag_attributes) != null
|
||||
|
@ -1716,18 +1737,27 @@ EditView.prototype._validateExternalTool = function (data, errors) {
|
|||
: void 0
|
||||
).length === 0
|
||||
) {
|
||||
message = I18n.t('External Tool URL cannot be left blank')
|
||||
errors['external_tool_tag_attributes[url]'] = [
|
||||
{
|
||||
message,
|
||||
},
|
||||
]
|
||||
errors['default-tool-launch-button'] = [
|
||||
{
|
||||
message,
|
||||
},
|
||||
]
|
||||
const message = I18n.t('External Tool URL cannot be left blank')
|
||||
errors['external_tool_tag_attributes[url]'] = [{message}]
|
||||
errors['default-tool-launch-button'] = [{message}]
|
||||
}
|
||||
|
||||
// This can happen when:
|
||||
// * user chooses a tool in the submission type dropdown (Submission Type
|
||||
// Selection placement) but doesn't launch the tool and finish deep linking
|
||||
// flow
|
||||
// * user edits assignment and removes resource by clicking the 'x'
|
||||
// (we reset content_type back to 'context_external_tool' then)
|
||||
if (
|
||||
typeof data.external_tool_tag_attributes === 'object' &&
|
||||
!data.external_tool_tag_attributes.title &&
|
||||
this.selectedTool &&
|
||||
this.selectedTool.require_resource_selection
|
||||
) {
|
||||
const message = I18n.t('Please click below to launch the tool and select a resource.')
|
||||
errors.assignment_submission_container = [{message}]
|
||||
}
|
||||
|
||||
return errors
|
||||
}
|
||||
|
||||
|
|
|
@ -37,6 +37,7 @@ import userSettings from '@canvas/user-settings'
|
|||
import assertions from '@canvas/test-utils/assertionsSpec'
|
||||
import '@canvas/jquery/jquery.simulate'
|
||||
import ExternalToolModalLauncher from '@canvas/external-tools/react/components/ExternalToolModalLauncher'
|
||||
import {AssignmentSubmissionTypeContainer} from '../../../react/AssignmentSubmissionTypeContainer'
|
||||
|
||||
const s_params = 'some super secure params'
|
||||
const fixtures = document.getElementById('fixtures')
|
||||
|
@ -154,6 +155,7 @@ QUnit.module('EditView', {
|
|||
$('ul[id^=ui-id-]').remove()
|
||||
$('.form-dialog').remove()
|
||||
document.getElementById('fixtures').innerHTML = ''
|
||||
sinon.restore()
|
||||
},
|
||||
editView() {
|
||||
return editView.apply(this, arguments)
|
||||
|
@ -365,6 +367,13 @@ test('does not allow point value of "" if grading type is letter', function () {
|
|||
equal(view.getFormData().groupCategoryId, null)
|
||||
})
|
||||
|
||||
test('does not allow blank default external tool url', function () {
|
||||
const view = this.editView()
|
||||
const data = {submission_type: 'external_tool'}
|
||||
const errors = view._validateExternalTool(data, [])
|
||||
equal(errors['default-tool-launch-button'][0].message, 'External Tool URL cannot be left blank')
|
||||
})
|
||||
|
||||
test('does not allow blank external tool url', function () {
|
||||
const view = this.editView()
|
||||
const data = {submission_type: 'external_tool'}
|
||||
|
@ -375,11 +384,165 @@ test('does not allow blank external tool url', function () {
|
|||
)
|
||||
})
|
||||
|
||||
test('does not allow blank default external tool url', function () {
|
||||
// #region "tests regarding Submission Type Selection"
|
||||
|
||||
test('does not allow submission_type_selection tools (selectedTool set) with require_resource_selection=true with no resource title', function () {
|
||||
const view = this.editView()
|
||||
const data = {submission_type: 'external_tool'}
|
||||
view.selectedTool = {require_resource_selection: true}
|
||||
const data = {
|
||||
submission_type: 'external_tool',
|
||||
external_tool_tag_attributes: {
|
||||
content_type: 'context_external_tool',
|
||||
},
|
||||
}
|
||||
const errors = view._validateExternalTool(data, [])
|
||||
equal(errors['default-tool-launch-button'][0].message, 'External Tool URL cannot be left blank')
|
||||
deepEqual(
|
||||
errors.assignment_submission_container[0].message,
|
||||
'Please click below to launch the tool and select a resource.'
|
||||
)
|
||||
})
|
||||
|
||||
test('allows submission_type_selection tools (selectedTool set) with require_resource_selection not set with no resource title', function () {
|
||||
const view = this.editView()
|
||||
view.selectedTool = {}
|
||||
const data = {
|
||||
submission_type: 'external_tool',
|
||||
external_tool_tag_attributes: {content_type: 'context_external_tool'},
|
||||
}
|
||||
const errors = view._validateExternalTool(data, [])
|
||||
notOk(errors.assignment_submission_container)
|
||||
})
|
||||
|
||||
test('allows submission_type_selection tools with require_resource_selection=true with a resource title', function () {
|
||||
const view = this.editView()
|
||||
view.selectedTool = {require_resource_selection: true}
|
||||
const data = {
|
||||
submission_type: 'external_tool',
|
||||
external_tool_tag_attributes: {
|
||||
content_type: 'context_external_tool',
|
||||
title: 'some title',
|
||||
},
|
||||
}
|
||||
const errors = view._validateExternalTool(data, [])
|
||||
notOk(errors.assignment_submission_container)
|
||||
})
|
||||
|
||||
// sets up the editView and chooses a submission_type_selection tool
|
||||
// calls block with the edit view
|
||||
// resets mocks at the end
|
||||
function editViewWithSubmissionTypeSelection() {
|
||||
const tool = {
|
||||
id: '123',
|
||||
title: 'foo',
|
||||
description: 'bar',
|
||||
external_url: 'http://example.com',
|
||||
require_resource_selection: true,
|
||||
}
|
||||
ENV.SUBMISSION_TYPE_SELECTION_TOOLS = [tool]
|
||||
const view = editView()
|
||||
const types = [...view.$submissionType.find('option')].map(el => el.value)
|
||||
ok(types.includes('external_tool_placement_123'))
|
||||
|
||||
// ignore render in renderSubmissionTypeSelectionDialog:
|
||||
sinon.mock(ReactDOM).expects('render').atLeast(0)
|
||||
|
||||
// tests will check what is rendered to the SubmissionTypeSelection card
|
||||
sinon.spy(React, 'createElement')
|
||||
|
||||
return view
|
||||
}
|
||||
|
||||
test('when submission_type_selection tool chosen: sets selectedTool and leaves title empty', function () {
|
||||
const view = editViewWithSubmissionTypeSelection()
|
||||
view.$submissionType.val('external_tool_placement_123')
|
||||
view.$submissionType.trigger('change')
|
||||
const formData = view.getFormData()
|
||||
|
||||
// Needed for proper creation of the assignment on the backend, and
|
||||
// validation in _validateExternalTool()
|
||||
equal(formData.submission_type, 'external_tool')
|
||||
deepEqual(formData.submission_types, ['external_tool'])
|
||||
|
||||
ok(view.selectedTool.require_resource_selection)
|
||||
notOk(formData.external_tool_tag_attributes.title)
|
||||
})
|
||||
|
||||
const lastSubmissionTypeContainerProps = function () {
|
||||
const calls = React.createElement
|
||||
.getCalls()
|
||||
.filter(call => call.args[0] === AssignmentSubmissionTypeContainer)
|
||||
return calls[calls.length - 1].args[1]
|
||||
}
|
||||
|
||||
test('when a submission_type_selection tool chosen and a resource selected: sets selectedTool and title', function () {
|
||||
const view = editViewWithSubmissionTypeSelection()
|
||||
view.$submissionType.val('external_tool_placement_123')
|
||||
view.$submissionType.trigger('change')
|
||||
view.handleContentItem({
|
||||
type: 'ltiResourceLink',
|
||||
custom: {},
|
||||
url: 'http://example.com',
|
||||
title: 'someResource',
|
||||
lineItem: {},
|
||||
})
|
||||
const formData = view.getFormData()
|
||||
|
||||
ok(view.selectedTool.require_resource_selection)
|
||||
equal(formData.external_tool_tag_attributes.title, 'someResource')
|
||||
|
||||
// Card is shown (props include title):
|
||||
equal(lastSubmissionTypeContainerProps().resource.title, 'someResource')
|
||||
})
|
||||
|
||||
test('when a submission_type_selection tool chosen, a resource selected, and the resource removed: keeps selectedTool but clears out title', function () {
|
||||
const view = editViewWithSubmissionTypeSelection()
|
||||
view.$submissionType.val('external_tool_placement_123')
|
||||
view.$submissionType.trigger('change')
|
||||
|
||||
view.handleContentItem({
|
||||
type: 'ltiResourceLink',
|
||||
custom: {},
|
||||
url: 'http://example.com',
|
||||
title: 'someResourceLinkTitle',
|
||||
lineItem: {},
|
||||
})
|
||||
view.handleRemoveResource()
|
||||
|
||||
const formData = view.getFormData()
|
||||
ok(view.selectedTool.require_resource_selection)
|
||||
notOk(formData.external_tool_tag_attributes.title)
|
||||
|
||||
// Card is NOT shown:
|
||||
notOk(lastSubmissionTypeContainerProps().resource.title)
|
||||
})
|
||||
|
||||
test('when a submission_type_select tool chosen but changed back to generic "External Tool": URL reset', function () {
|
||||
// Moving from a submission_type_placement tool to generic
|
||||
// clear out URL & selectedTool -- shouldn't carry over selectedTool
|
||||
// and prefill external_tool URL
|
||||
|
||||
const view = editViewWithSubmissionTypeSelection()
|
||||
view.$submissionType.val('external_tool_placement_123')
|
||||
view.$submissionType.trigger('change')
|
||||
view.handleContentItem({
|
||||
type: 'ltiResourceLink',
|
||||
custom: {},
|
||||
url: 'http://example.com',
|
||||
title: 'someResourceLinkTitle',
|
||||
lineItem: {},
|
||||
})
|
||||
|
||||
const formData = view.getFormData()
|
||||
ok(view.selectedTool)
|
||||
ok(formData.external_tool_tag_attributes.url)
|
||||
|
||||
view.$submissionType.val('external_tool')
|
||||
view.$submissionType.trigger('change')
|
||||
|
||||
const newFormData = view.getFormData()
|
||||
|
||||
notOk(view.selectedTool)
|
||||
notOk(newFormData.external_tool_tag_attributes.url)
|
||||
})
|
||||
|
||||
test('does not validate allowed extensions if file uploads is not a submission type', function () {
|
||||
|
@ -389,6 +552,8 @@ test('does not validate allowed extensions if file uploads is not a submission t
|
|||
equal(errors.allowed_extensions, null)
|
||||
})
|
||||
|
||||
// #endregion "tests regarding Submission Type Selection"
|
||||
|
||||
test('removes group_category_id if an external tool is selected', function () {
|
||||
const view = this.editView()
|
||||
let data = {
|
||||
|
|
|
@ -16,7 +16,7 @@
|
|||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import React, {useState} from 'react'
|
||||
import React from 'react'
|
||||
|
||||
import type {GlobalEnv} from '@canvas/global/env/GlobalEnv.d'
|
||||
|
||||
|
@ -24,7 +24,7 @@ import {AssignmentSubmissionTypeSelectionResourceLinkCard} from './AssignmentSub
|
|||
import {AssignmentSubmissionTypeSelectionLaunchButton} from './AssignmentSubmissionTypeSelectionLaunchButton'
|
||||
|
||||
export type AssignmentSubmissionTypeContainerProps = {
|
||||
tool: {
|
||||
tool?: {
|
||||
developer_key?: {
|
||||
global_id: string
|
||||
}
|
||||
|
@ -44,43 +44,25 @@ declare const ENV: GlobalEnv & {
|
|||
ASSIGNMENT_SUBMISSION_TYPE_CARD_ENABLED: boolean
|
||||
}
|
||||
|
||||
export function AssignmentSubmissionTypeContainer(props: AssignmentSubmissionTypeContainerProps) {
|
||||
const {resource, tool, onLaunchButtonClick, onRemoveResource} = props
|
||||
|
||||
const [removedResource, setRemovedResource] = useState(false)
|
||||
|
||||
return (
|
||||
<>
|
||||
{ENV.ASSIGNMENT_SUBMISSION_TYPE_CARD_ENABLED ? (
|
||||
<>
|
||||
{resource?.title && !removedResource ? (
|
||||
<AssignmentSubmissionTypeSelectionResourceLinkCard
|
||||
tool={tool}
|
||||
resourceTitle={resource.title}
|
||||
onCloseButton={() => {
|
||||
setRemovedResource(true)
|
||||
onRemoveResource()
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<AssignmentSubmissionTypeSelectionLaunchButton
|
||||
tool={tool}
|
||||
onClick={() => {
|
||||
setRemovedResource(false)
|
||||
onLaunchButtonClick()
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<AssignmentSubmissionTypeSelectionLaunchButton
|
||||
tool={tool}
|
||||
onClick={() => {
|
||||
setRemovedResource(false)
|
||||
onLaunchButtonClick()
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
export function AssignmentSubmissionTypeContainer(
|
||||
props: AssignmentSubmissionTypeContainerProps
|
||||
): React.ReactElement | null {
|
||||
if (!props.tool) {
|
||||
return null
|
||||
} else if (ENV.ASSIGNMENT_SUBMISSION_TYPE_CARD_ENABLED && props.resource?.title) {
|
||||
return (
|
||||
<AssignmentSubmissionTypeSelectionResourceLinkCard
|
||||
tool={props.tool}
|
||||
resourceTitle={props.resource.title}
|
||||
onCloseButton={props.onRemoveResource}
|
||||
/>
|
||||
)
|
||||
} else {
|
||||
return (
|
||||
<AssignmentSubmissionTypeSelectionLaunchButton
|
||||
tool={props.tool}
|
||||
onClick={props.onLaunchButtonClick}
|
||||
/>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -65,7 +65,7 @@ describe('AssignmentSubmissionTypeContainer', () => {
|
|||
expect(screen.queryByTestId('assignment_submission_type_selection_launch_button')).toBeFalsy()
|
||||
})
|
||||
|
||||
it('renders the launch button again after user clicks the close button on the resource link card', () => {
|
||||
it('calls onRemoveResource when the user clicks the resource close button', () => {
|
||||
const resource = {title: 'Resource Title'}
|
||||
renderComponent(resource)
|
||||
expect(
|
||||
|
@ -75,9 +75,6 @@ describe('AssignmentSubmissionTypeContainer', () => {
|
|||
|
||||
fireEvent.click(screen.getByRole('button'))
|
||||
|
||||
expect(screen.getByTestId('assignment_submission_type_selection_launch_button')).toBeTruthy()
|
||||
expect(
|
||||
screen.queryByTestId('assignment-submission-type-selection-resource-link-card')
|
||||
).toBeFalsy()
|
||||
expect(onRemoveResourceFn).toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
|
Loading…
Reference in New Issue