Create default tool assignment from "quick create"

Closes PLAT-4695

Test Plan:
- Create a a default tool assignment from the
  quick create modal in the assignment index page
- Edit the new assignment
- Verify you can select content from the default
  tool as the default
- Verify you can save and launch the assignment
- Verify you can create non-default external
  tool assignments as before
- Verify you can create non-default and default
  external tool assignments withouth the "quick
  create" modal

Change-Id: I94578f99c3d200e3dac57a13a7d5f7157413618b
Reviewed-on: https://gerrit.instructure.com/204847
Tested-by: Jenkins
Reviewed-by: Clint Furse <cfurse@instructure.com>
Reviewed-by: Landon Gilbert-Bland <lbland@instructure.com>
QA-Review: Tucker Mcknight <tmcknight@instructure.com>
Product-Review: Jesse Poulos <jpoulos@instructure.com>
This commit is contained in:
wdransfield 2019-08-12 13:24:09 -06:00 committed by Weston Dransfield
parent d0146696db
commit a63c1f0468
12 changed files with 247 additions and 17 deletions

View File

@ -29,6 +29,7 @@ import GradingPeriodsHelper from 'jsx/grading/helpers/GradingPeriodsHelper'
import tz from 'timezone'
import numberHelper from 'jsx/shared/helpers/numberHelper'
import PandaPubPoller from '../util/PandaPubPoller'
import { matchingToolUrls } from './LtiAssignmentHelpers'
isAdmin = () ->
_.includes(ENV.current_user_roles, 'admin')
@ -63,7 +64,9 @@ export default class Assignment extends Model
isDiscussionTopic: => @_hasOnlyType 'discussion_topic'
isPage: => @_hasOnlyType 'wiki_page'
isExternalTool: => @_hasOnlyType 'external_tool'
defaultToolName: => ENV.DEFAULT_ASSIGNMENT_TOOL_NAME
defaultToolUrl: => ENV.DEFAULT_ASSIGNMENT_TOOL_URL
isNotGraded: => @_hasOnlyType 'not_graded'
isAssignment: =>
! _.includes @_submissionTypes(), 'online_quiz', 'discussion_topic',
@ -152,11 +155,29 @@ export default class Assignment extends Model
return @_submissionTypes() unless arguments.length > 0
@set 'submission_types', submissionTypes
isDefaultTool: =>
@submissionType() == 'external_tool' && (@defaultToolSelected() || @isQuickCreateDefaultTool())
isQuickCreateDefaultTool: =>
@submissionTypes().includes('default_external_tool')
defaultToolSelected: =>
matchingToolUrls(
@defaultToolUrl(),
@externalToolUrl()
)
isNonDefaultExternalTool: =>
# The assignment is type 'external_tool' and the default tool is not selected
# or chosen from the "quick create" assignment index modal.
@submissionType() == 'external_tool' && !@isDefaultTool()
submissionType: =>
submissionTypes = @_submissionTypes()
if _.includes(submissionTypes, 'none') || submissionTypes.length == 0 then 'none'
else if _.includes submissionTypes, 'on_paper' then 'on_paper'
else if _.includes submissionTypes, 'external_tool' then 'external_tool'
else if _.includes submissionTypes, 'default_external_tool' then 'external_tool'
else 'online'
expectsSubmission: =>
@ -458,7 +479,7 @@ export default class Assignment extends Model
'secureParams', 'inClosedGradingPeriod', 'dueDateRequired',
'submissionTypesFrozen', 'anonymousInstructorAnnotations',
'anonymousGrading', 'gradersAnonymousToGraders', 'showGradersAnonymousToGradersCheckbox',
'defaultToolName'
'defaultToolName', 'isDefaultTool', 'isNonDefaultExternalTool'
]
hash =

View File

@ -0,0 +1,27 @@
/*
* Copyright (C) 2019 - present 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/>.
*/
export function matchingToolUrls(firstUrl, secondUrl) {
if (!(firstUrl && secondUrl)) {
return false
}
const firstUrlParsed = new URL(firstUrl)
const secondUrlParsed = new URL(secondUrl)
return firstUrlParsed.origin === secondUrlParsed.origin
}

View File

@ -0,0 +1,93 @@
/*
* Copyright (C) 2019 - present 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/>.
*/
import { matchingToolUrls } from '../LtiAssignmentHelpers'
describe('#matchingToolUrls', () => {
let firstUrl
let secondUrl
const subject = () => matchingToolUrls(firstUrl, secondUrl)
describe('when domains and protocol match', () => {
beforeEach(() => {
firstUrl = 'https://www.mytool.com/blti'
secondUrl = 'https://www.mytool.com/blti/resourceid'
})
it('returns true', () => {
expect(subject()).toEqual(true)
})
})
describe('when domains match but protocols do not', () => {
beforeEach(() => {
firstUrl = 'http://www.mytool.com/blti'
secondUrl = 'https://www.mytool.com/blti/resourceid'
})
it('returns false', () => {
expect(subject()).toEqual(false)
})
})
describe('when domains do not match but protocols do', () => {
beforeEach(() => {
firstUrl = 'https://www.mytool.com/blti'
secondUrl = 'https://www.other-tool.com/blti'
})
it('returns false', () => {
expect(subject()).toEqual(false)
})
})
describe('when neither domain nor protocols match', () => {
beforeEach(() => {
firstUrl = 'https://www.mytool.com/blti'
secondUrl = 'http://www.other-tool.com/blti'
})
it('returns false', () => {
expect(subject()).toEqual(false)
})
})
describe('when the first url is falsey', () => {
beforeEach(() => {
firstUrl = undefined
secondUrl = 'https://www.other-tool.com/blti'
})
it('returns false', () => {
expect(subject()).toEqual(false)
})
})
describe('when the last url is falsey', () => {
beforeEach(() => {
firstUrl = 'https://www.mytool.com/blti'
secondUrl = undefined
})
it('returns false', () => {
expect(subject()).toEqual(false)
})
})
})

View File

@ -66,6 +66,8 @@ export default class CreateAssignmentView extends DialogFormView
getFormData: =>
data = super
submission_type_select = document.querySelector('select[name="submission_types"]')
unfudged = $.unfudgeDateForProfileTimezone(data.due_at)
data.due_at = @_getDueAt(unfudged) if unfudged?
data.published = true if @shouldPublish
@ -124,7 +126,8 @@ export default class CreateAssignmentView extends DialogFormView
uniqLabel: uniqLabel
disableDueAt: @disableDueAt()
postToSISName: ENV.SIS_NAME
isInClosedPeriod: @model.inClosedGradingPeriod()
isInClosedPeriod: @model.inClosedGradingPeriod(),
defaultToolName: ENV.DEFAULT_ASSIGNMENT_TOOL_NAME
# master_course_restrictions only apply if this is a child course
# and is restricted by a master course.

View File

@ -306,11 +306,16 @@ export default class EditView extends ValidatedFormView
defaultExternalToolUrl: =>
ENV.DEFAULT_ASSIGNMENT_TOOL_URL
defaultExternalToolName: =>
ENV.DEFAULT_ASSIGNMENT_TOOL_NAME
renderDefaultExternalTool: =>
props = {
toolDialog: $("#resource_selection_dialog"),
courseId: ENV.COURSE_ID,
toolUrl: @defaultExternalToolUrl()
toolUrl: @defaultExternalToolUrl(),
toolName: @defaultExternalToolName(),
previouslySelected: @assignment.defaultToolSelected()
}
ReactDOM.render(

View File

@ -69,6 +69,9 @@ class AssignmentsController < ApplicationController
DUE_DATE_REQUIRED_FOR_ACCOUNT: due_date_required_for_account,
DIRECT_SHARE_ENABLED: @domain_root_account&.feature_enabled?(:direct_share),
}
set_default_tool_env!(@context, hash)
js_env(hash)
respond_to do |format|
@ -521,8 +524,6 @@ class AssignmentsController < ApplicationController
rce_js_env
@assignment ||= @context.assignments.active.find(params[:id])
if authorized_action(@assignment, @current_user, @assignment.new_record? ? :create : :update)
root_account_settings = @context.root_account.settings
@assignment.title = params[:title] if params[:title]
@assignment.due_at = params[:due_at] if params[:due_at]
@assignment.points_possible = params[:points_possible] if params[:points_possible]
@ -615,10 +616,7 @@ class AssignmentsController < ApplicationController
hash[:active_grading_periods] = GradingPeriod.json_for(@context, @current_user)
end
if root_account_settings[:default_assignment_tool_url] && root_account_settings[:default_assignment_tool_name]
hash[:DEFAULT_ASSIGNMENT_TOOL_URL] = root_account_settings[:default_assignment_tool_url]
hash[:DEFAULT_ASSIGNMENT_TOOL_NAME] = root_account_settings[:default_assignment_tool_name]
end
set_default_tool_env!(@context, hash)
hash[:ANONYMOUS_GRADING_ENABLED] = @context.feature_enabled?(:anonymous_marking)
hash[:MODERATED_GRADING_ENABLED] = @context.feature_enabled?(:moderated_grading)
@ -663,6 +661,14 @@ class AssignmentsController < ApplicationController
protected
def set_default_tool_env!(context, hash)
root_account_settings = context.root_account.settings
if root_account_settings[:default_assignment_tool_url] && root_account_settings[:default_assignment_tool_name]
hash[:DEFAULT_ASSIGNMENT_TOOL_URL] = root_account_settings[:default_assignment_tool_url]
hash[:DEFAULT_ASSIGNMENT_TOOL_NAME] = root_account_settings[:default_assignment_tool_name]
end
end
def show_moderate_env
can_view_grader_identities = @assignment.can_view_other_grader_identities?(@current_user)

View File

@ -46,6 +46,12 @@ const DefaultToolForm = props => {
const launchDefinitionUrl = () =>
`/api/v1/courses/${props.courseId}/lti_apps/launch_definitions?per_page=100&placements%5B%5D=assignment_selection&placements%5B%5D=resource_selection`
const contentTitle = () => {
if (toolMessageData) {
return toolMessageData.content && toolMessageData.content.title
}
return props.toolName
}
useEffect(() => {
const fetchData = async () => {
const result = await axios.get(launchDefinitionUrl())
@ -68,9 +74,9 @@ const DefaultToolForm = props => {
{I18n.t('Add a Question Set')}
</Button>
{toolMessageData ? (
{toolMessageData || props.previouslySelected ? (
<Alert variant="success" margin="small small 0 0">
<Text weight="bold">{toolMessageData.content.title}</Text><br/>
<Text weight="bold">{contentTitle()}</Text><br/>
<Text>{I18n.t('Successfully Added')}</Text>
</Alert>
) : (
@ -97,7 +103,9 @@ const DefaultToolForm = props => {
DefaultToolForm.propTypes = {
toolUrl: PropTypes.string.isRequired,
courseId: PropTypes.number.isRequired
courseId: PropTypes.number.isRequired,
toolName: PropTypes.string.isRequired,
previouslySelected: PropTypes.bool.isRequired
}
export default DefaultToolForm

View File

@ -24,7 +24,8 @@ import SelectContentDialog from '../../../../public/javascripts/select_content_d
const newProps = (overrides = {}) => ({
...{
toolUrl: 'https://www.default-tool.com/blti',
courseId: 1
courseId: 1,
toolName: 'Awesome Tool'
},
...overrides
})
@ -53,6 +54,11 @@ describe('DefaultToolForm', () => {
wrapper = mount(<DefaultToolForm {...newProps()} />)
expect(wrapper.find('Alert').html()).toContain('Click the button above to add a WileyPLUS Question Set')
})
it('renders the success message if previouslySelected is true', () => {
wrapper = mount(<DefaultToolForm {...newProps({previouslySelected: true})} />)
expect(wrapper.find('Alert').html()).toContain('Successfully Added')
})
})
describe('toolSubmissionType', () => {

View File

@ -8,6 +8,9 @@
</label>
<div class="controls">
<select type="text" id="{{uniqLabel}}_assignment_type" name="submission_types">
{{#if defaultToolName}}
<option value="default_external_tool" data-default-tool="true">{{defaultToolName}}</option>
{{/if}}
<option value="none">{{#t "assignment"}}Assignment{{/t}}</option>
<option value="discussion_topic">{{#t "discussion_type"}}Discussion{{/t}}</option>
<option value="online_quiz">{{#t "quiz_type"}}Quiz{{/t}}</option>

View File

@ -6,14 +6,13 @@
</div>
<div class="form-column-right">
<div class="border border-trbl border-round">
{{!-- Submission type accepted --}}
<select id="assignment_submission_type"
name="submission_type"
aria-controls="assignment_online_submission_types,assignment_external_tool_settings,assignment_group_selector,assignment_peer_reviews_fields"
{{disabledIfIncludes frozenAttributes "submission_types"}}>
{{#if defaultToolName}}
<option value="default_external_tool" {{selectedIf submissionType "default_external_tool"}}>
<option value="default_external_tool" {{selectedIf isDefaultTool}}>
{{defaultToolName}}
</option>
{{/if}}
@ -26,7 +25,7 @@
<option value="on_paper" {{selectedIf submissionType "on_paper"}}>
{{#t "submission_types.on_paper"}}On Paper{{/t}}
</option>
<option value="external_tool" {{selectedIf submissionType "external_tool"}}>
<option value="external_tool" {{selectedIf isNonDefaultExternalTool}}>
{{#t "submission_types.external_tool"}}External Tool{{/t}}
</option>
</select>

View File

@ -545,7 +545,7 @@ module Api::V1::Assignment
if assignment_params['submission_types'].present? &&
!assignment_params['submission_types'].all? do |s|
return false if s == 'wiki_page' && !self.context.try(:feature_enabled?, :conditional_release)
API_ALLOWED_SUBMISSION_TYPES.include?(s)
API_ALLOWED_SUBMISSION_TYPES.include?(s) || (s == 'default_external_tool' && assignment.unpublished?)
end
assignment.errors.add('assignment[submission_types]',
I18n.t('assignments_api.invalid_submission_types',

View File

@ -95,6 +95,65 @@ test('returns false if record is discussion topic', () => {
equal(assignment.isDiscussionTopic(), false)
})
QUnit.module('Assignment#isDefaultTool', {
setup() {
fakeENV.setup({
DEFAULT_ASSIGNMENT_TOOL_NAME: 'Default Tool',
DEFAULT_ASSIGNMENT_TOOL_URL: 'https://www.test.com/blti'
})
},
teardown() {
fakeENV.teardown()
}
})
test('returns true if submissionType is "external_tool" and default tool is selected', () => {
const assignment = new Assignment({
name: 'foo',
external_tool_tag_attributes: {
url: 'https://www.test.com/blti?foo'
}
})
assignment.submissionTypes(['external_tool'])
equal(assignment.isDefaultTool(), true)
})
QUnit.module('Assignment#isNonDefaultExternalTool', {
setup() {
fakeENV.setup({
DEFAULT_ASSIGNMENT_TOOL_NAME: 'Default Tool',
DEFAULT_ASSIGNMENT_TOOL_URL: 'https://www.test.com/blti'
})
},
teardown() {
fakeENV.teardown()
}
})
test('returns true if submissionType is "default_external_tool"', () => {
const assignment = new Assignment({name: 'foo'})
assignment.submissionTypes(['default_external_tool'])
equal(assignment.isDefaultTool(), true)
})
test('returns true when submissionType is "external_tool" and non default tool is selected', () => {
const assignment = new Assignment({
name: 'foo',
external_tool_tag_attributes: {
url: 'https://www.non-default.com/blti?foo'
}
})
assignment.submissionTypes(['external_tool'])
equal(assignment.isNonDefaultExternalTool(), true)
})
test('returns true when submissionType is "external_tool"', () => {
const assignment = new Assignment({name: 'foo'})
assignment.submissionTypes(['external_tool'])
equal(assignment.isNonDefaultExternalTool(), true)
})
QUnit.module('Assignment#isExternalTool')
test('returns true if record is external tool', () => {