Assignment configuration tool UI

Fixes PLAT-1899

Test Plan:
- Turn on the 'Plagiarism Detection Platform'
  feature flag
- Install an LTI 1.x tool with the
  assignment_configuration placement
- Install an LTI 2 tool with the
  assignment_configuration placement
- Navigate to a course assignments page and
  create a new course
- Select 'Online' as the submission type
  and check 'file uploads'
- Verify that the 'Plagiarism Review'
  select box appers.
- Verify that the 'Plagiarism Review'
  select box is hidden if the submission
  type is not 'online' with 'file uploads'
- Verify that both of the tools installed appear
  in the 'Plagiarism Review' select box
- Click each tool and verify that the tool
  launches within an iframe on the page
- Verify that the iframe src contains a param
  named 'secure_params' with a JWT as the value.
- Turn off the feature flag and verify the
  'Plagiarism Review' select box never
  appears.

Change-Id: I7a0c753eb8b6674ffe2630d83731bc666260533b
Reviewed-on: https://gerrit.instructure.com/93361
Tested-by: Jenkins
Reviewed-by: Nathan Mills <nathanm@instructure.com>
QA-Review: August Thornton <august@instructure.com>
Product-Review: Weston Dransfield <wdransfield@instructure.com>
This commit is contained in:
wdransfield 2016-10-20 07:52:25 -06:00 committed by Weston Dransfield
parent 6c70201df9
commit c9a3e3ef94
11 changed files with 378 additions and 3 deletions

View File

@ -18,13 +18,14 @@ define [
'compiled/views/editor/KeyboardShortcuts'
'jsx/shared/conditional_release/ConditionalRelease'
'compiled/util/deparam'
'jsx/assignments/AssignmentConfigurationTools'
'jqueryui/dialog'
'jquery.toJSON'
'compiled/jquery.rails_flash_notifications'
], (INST, I18n, ValidatedFormView, _, $, RichContentEditor, template,
userSettings, TurnitinSettings, VeriCiteSettings, TurnitinSettingsDialog, preventDefault, MissingDateDialog,
AssignmentGroupSelector, GroupCategorySelector, toggleAccessibly, RCEKeyboardShortcuts,
ConditionalRelease, deparam) ->
ConditionalRelease, deparam, AssignmentConfigurationsTools) ->
RichContentEditor.preloadRemoteModule()
@ -64,6 +65,7 @@ ConditionalRelease, deparam) ->
GROUP_CATEGORY_BOX = '#has_group_category'
MODERATED_GRADING_BOX = '#assignment_moderated_grading'
CONDITIONAL_RELEASE_TARGET = '#conditional_release_target'
ASSIGNMENT_CONFIGURATION_TOOLS = '#assignment_configuration_tools'
els: _.extend({}, @::els, do ->
els = {}
@ -92,6 +94,7 @@ ConditionalRelease, deparam) ->
els["#{ASSIGNMENT_POINTS_CHANGE_WARN}"] = '$pointsChangeWarning'
els["#{MODERATED_GRADING_BOX}"] = '$moderatedGradingBox'
els["#{CONDITIONAL_RELEASE_TARGET}"] = '$conditionalReleaseTarget'
els["#{ASSIGNMENT_CONFIGURATION_TOOLS}"] = '$assignmentConfigurationTools'
els["#{SECURE_PARAMS}"] = '$secureParams'
els
)
@ -101,6 +104,7 @@ ConditionalRelease, deparam) ->
events["click .cancel_button"] = 'handleCancel'
events["click .save_and_publish"] = 'saveAndPublish'
events["change #{SUBMISSION_TYPE}"] = 'handleSubmissionTypeChange'
events["change #{ONLINE_SUBMISSION_TYPES}"] = 'handleOnlineSubmissionTypeChange'
events["change #{RESTRICT_FILE_UPLOADS}"] = 'handleRestrictFileUploadsChange'
events["click #{ADVANCED_TURNITIN_SETTINGS}"] = 'showTurnitinDialog'
events["change #{TURNITIN_ENABLED}"] = 'toggleAdvancedTurnitinSettings'
@ -254,6 +258,13 @@ ConditionalRelease, deparam) ->
@$externalToolSettings.toggleAccessibly subVal == 'external_tool'
@$groupCategorySelector.toggleAccessibly subVal != 'external_tool'
@$peerReviewsFields.toggleAccessibly subVal != 'external_tool'
@$assignmentConfigurationTools.toggleAccessibly subVal == 'online' && ENV.PLAGIARISM_DETECTION_PLATFORM
if subVal == 'online'
@handleOnlineSubmissionTypeChange()
handleOnlineSubmissionTypeChange: (env) =>
showConfigTools = @$onlineSubmissionTypes.find(ALLOW_FILE_UPLOADS).attr('checked')
@$assignmentConfigurationTools.toggleAccessibly showConfigTools && ENV.PLAGIARISM_DETECTION_PLATFORM
afterRender: =>
# have to do these here because they're rendered by other things
@ -261,9 +272,18 @@ ConditionalRelease, deparam) ->
@$intraGroupPeerReviews = $("#{INTRA_GROUP_PEER_REVIEWS}")
@$groupCategoryBox = $("#{GROUP_CATEGORY_BOX}")
@assignmentConfigurationTools = AssignmentConfigurationsTools.attach(
@$assignmentConfigurationTools.get(0),
parseInt(ENV.COURSE_ID),
@$secureParams.val(),
parseInt(ENV.SELECTED_CONFIG_TOOL_ID))
@_attachEditorToDescription()
@addTinyMCEKeyboardShortcuts()
@handleModeratedGradingChange()
@handleOnlineSubmissionTypeChange()
@handleSubmissionTypeChange()
if ENV?.HAS_GRADED_SUBMISSIONS
@disableCheckbox(@$moderatedGradingBox, I18n.t("Moderated grading setting cannot be changed if graded submissions exist"))
if ENV.CONDITIONAL_RELEASE_SERVICE_ENABLED

View File

@ -437,7 +437,9 @@ class AssignmentsController < ApplicationController
@current_user
)),
:ASSIGNMENT_INDEX_URL => polymorphic_url([@context, :assignments]),
:VALID_DATE_RANGE => CourseDateRange.new(@context)
:VALID_DATE_RANGE => CourseDateRange.new(@context),
:COURSE_ID => @context.id,
:PLAGIARISM_DETECTION_PLATFORM => @context.root_account.feature_enabled?(:plagiarism_detection_platform)
}
add_crumb(@assignment.title, polymorphic_url([@context, @assignment]))
@ -448,6 +450,7 @@ class AssignmentsController < ApplicationController
hash[:CANCEL_TO] = @assignment.new_record? ? polymorphic_url([@context, :assignments]) : polymorphic_url([@context, @assignment])
hash[:CONTEXT_ID] = @context.id
hash[:CONTEXT_ACTION_SOURCE] = :assignments
hash[:SELECTED_CONFIG_TOOL_ID] = @assignment.tool_settings_tools.first.id unless @assignment.tool_settings_tools.blank?
append_sis_data(hash)
js_env(hash)
conditional_release_js_env(@assignment)

View File

@ -0,0 +1,159 @@
define([
'jquery',
'react',
'react-dom',
'i18n!moderated_grading',
'compiled/jquery.rails_flash_notifications'
], ($, React, ReactDOM, I18n) => {
const AssignmentConfigurationTools = React.createClass({
displayName: 'AssignmentConfigurationTools',
propTypes: {
courseId: React.PropTypes.number.isRequired,
secureParams: React.PropTypes.string.isRequired,
selectedTool: React.PropTypes.number
},
componentWillMount() {
this.getTools();
},
componentDidMount() {
this.setToolLaunchUrl();
},
getInitialState() {
return {
toolLaunchUrl: 'none',
tools: []
};
},
getTools() {
const self = this;
const toolsUrl = this.getDefinitionsUrl();
const data = {
'placements[]': 'assignment_configuration'
};
$.ajax({
type: 'GET',
url: toolsUrl,
data: data,
success: $.proxy(function(data) {
let prevToolLaunch = undefined;
if (this.props.selectedTool) {
for (var i = 0; i < data.length; i++) {
if (data[i].definition_id == this.props.selectedTool) {
prevToolLaunch = this.getLaunch(data[i]);
break;
}
}
}
this.setState({
tools: data,
toolLaunchUrl: prevToolLaunch || 'none'
});
}, self),
error: function(xhr) {
$.flashError(I18n.t('Error retrieving assignment configuration tools'));
console.log(xhr)
}
});
},
getDefinitionsUrl() {
return `/api/v1/courses/${this.props.courseId}/lti_apps/launch_definitions`;
},
getLaunch(tool) {
const url = tool.placements.assignment_configuration.url
let query = '';
let endpoint = '';
if(tool.definition_type === 'ContextExternalTool') {
query = `?borderless=true&url=${encodeURIComponent(url)}&secure_params=${this.props.secureParams}`;
endpoint = `/courses/${this.props.courseId}/external_tools/retrieve`;
} else {
query = `?display=borderless&secure_params=${this.props.secureParams}`;
endpoint = `/courses/${this.props.courseId}/lti/basic_lti_launch_request/${tool.definition_id}`;
}
return endpoint + query;
},
setToolLaunchUrl() {
const selector = this.refs.assignmentConfigurationTool;
const selectedOption = selector.options[selector.selectedIndex];
this.setState({
toolLaunchUrl: selectedOption.getAttribute('data-launch')
});
},
renderOptions() {
return (
<select id="assignment_configuration_tool"
name="assignmentConfigurationTool"
onChange={this.setToolLaunchUrl}
ref="assignmentConfigurationTool">
<option data-launch="none">None</option>
{
this.state.tools.map((tool) => {
return (
<option
value={tool.definition_id}
data-launch={this.getLaunch(tool)}
selected={tool.definition_id == this.props.selectedTool}
>
{tool.name}
</option>
);
})
}
</select>
);
},
renderConfigTool() {
if (this.state.toolLaunchUrl !== 'none') {
return(
<iframe src={this.state.toolLaunchUrl} className="tool_launch"></iframe>
);
}
},
render() {
return (
<div>
<div className="form-column-left">
<label htmlFor="assignment_configuration_tool">
Plagiarism Review
</label>
</div>
<div className="form-column-right">
<div className="border border-trbl border-round">
{this.renderOptions()}
{this.renderConfigTool()}
</div>
</div>
</div>
)
}
});
const attach = function(element, courseId, secureParams, selectedTool) {
const configTools = (
<AssignmentConfigurationTools courseId ={courseId} secureParams={secureParams} selectedTool={selectedTool}/>
);
return ReactDOM.render(configTools, element);
};
const ConfigurationTools = {
configTools: AssignmentConfigurationTools,
attach: attach
};
return ConfigurationTools;
});

View File

@ -110,6 +110,14 @@ div.form-column-right, div.overrides-column-right {
}
}
#assignment_configuration_tools .form-column-right {
padding-left: 4px;
iframe.tool_launch {
margin: 15px 0px 0px 0px;
width: 100%;
}
}
.assignment-edit-group-alert, .assignment-edit-external-tool-alert{
margin: 15px 20px 0;
}

View File

@ -91,6 +91,9 @@
{{>[assignments/submission_types_form]}}
<fieldset id="assignment_configuration_tools">
</fieldset>
{{#unless isLargeRoster}}
<fieldset id="group_category_selector"
class="control-group"

View File

@ -360,6 +360,10 @@ module Api::V1::Assignment
assignment.lti_context_id = secure_params[:lti_assignment_id]
end
tool = ContextExternalTool.find_external_tool_by_id(assignment_params['assignmentConfigurationTool'].to_i,
context)
assignment.tool_settings_tools = [tool] if tool
overrides = deserialize_overrides(assignment_params[:assignment_overrides])
overrides = [] if !overrides && assignment_params.has_key?(:assignment_overrides)
assignment_params.delete(:assignment_overrides)

View File

@ -1095,6 +1095,16 @@ describe AssignmentsApiController, :include_lti_spec_helpers, type: :request do
expect(a.lti_context_id).to eq(lti_assignment_id)
end
it "sets the configuration tool if one is provided" do
tool = @course.context_external_tools.create!(name: "a", url: "http://www.google.com", consumer_key: '12345', shared_secret: 'secret')
api_create_assignment_in_course(@course, { 'description' => 'description',
'assignmentConfigurationTool' => tool.id
})
a = Assignment.last
expect(a.tool_settings_tools).to include(tool)
end
it "should allow valid submission types as an array" do
raw_api_call(:post, "/api/v1/courses/#{@course.id}/assignments",
{ :controller => 'assignments_api',

View File

@ -0,0 +1,109 @@
define [
'react'
'jsx/assignments/AssignmentConfigurationTools'
], (React, AssignmentConfigurationTools) ->
wrapper = null
toolDefinitions = null
secureParams = null
createElement = (data = {}) ->
React.createElement(AssignmentConfigurationTools.configTools, data)
renderComponenet = (data = {}) ->
React.render(createElement(data), wrapper)
module 'AssignmentConfigurationsTools',
setup: ->
secureParams = "asdf234.lhadf234.adfasd23324"
wrapper = document.getElementById('fixtures')
toolDefinitions = [
{
'definition_type': 'ContextExternalTool'
'definition_id': 8
'name': 'assignment_configuration Text'
'description': 'This is a Sample Tool Provider.'
'domain': 'lti-tool-provider-example.herokuapp.com'
'placements': 'assignment_configuration':
'message_type': 'basic-lti-launch-request'
'url': 'https://lti-tool-provider-example.herokuapp.com/messages/blti'
'title': 'assignment_configuration Text'
}
{
'definition_type': 'ContextExternalTool'
'definition_id': 9
'name': 'My LTI'
'description': 'The most impressive LTI app'
'domain': 'my-lti.docker'
'placements': 'assignment_configuration':
'message_type': 'basic-lti-launch-request'
'url': 'http://my-lti.docker/course-navigation'
'title': 'My LTI'
}
{
'definition_type': 'ContextExternalTool'
'definition_id': 7
'name': 'Redirect Tool'
'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.'
'domain': null
'placements': 'assignment_configuration':
'message_type': 'basic-lti-launch-request'
'url': 'https://www.edu-apps.org/redirect'
'title': 'Redirect Tool'
}
{
'definition_type': 'Lti::MessageHandler'
'definition_id': 5
'name': 'Lti2Example'
'description': null
'domain': 'localhost'
'placements': 'assignment_configuration':
'message_type': 'basic-lti-launch-request'
'url': 'http://localhost:3000/messages/blti'
'title': 'Lti2Example'
}
]
teardown: ->
wrapper.innerHTML = ''
test 'it renders', ->
component = renderComponenet({'courseId': 1, 'secureParams': secureParams})
ok component.isMounted()
test 'it uses the correct tool definitions URL', ->
courseId = 1
correctUrl = "/api/v1/courses/#{courseId}/lti_apps/launch_definitions"
component = renderComponenet({'courseId': courseId})
equal component.getDefinitionsUrl(), correctUrl
test 'it renders a "none" option', ->
component = renderComponenet({'courseId': 1, 'secureParams': secureParams})
component.setState({tools: toolDefinitions})
option = wrapper.querySelector('option')
equal option.getAttribute('data-launch'), 'none'
test 'it renders each tool', ->
component = renderComponenet({'courseId': 1, 'secureParams': secureParams})
component.setState({tools: toolDefinitions})
equal wrapper.querySelectorAll('option').length, toolDefinitions.length + 1
test 'it builds the correct Launch URL for LTI 1 tools', ->
component = renderComponenet({'courseId': 1, 'secureParams': secureParams})
tool = toolDefinitions[0]
correctUrl = "/courses/1/external_tools/retrieve?borderless=true&url=https%3A%2F%2Flti-tool-provider-example.herokuapp.com%2Fmessages%2Fblti&secure_params=" +
secureParams
computedUrl = component.getLaunch(tool)
equal computedUrl, correctUrl
test 'it builds the correct Launch URL for LTI 2 tools', ->
component = renderComponenet({'courseId': 1, 'secureParams': secureParams})
tool = toolDefinitions[3]
correctUrl = "/courses/1/lti/basic_lti_launch_request/5?display=borderless&secure_params=" +
secureParams
computedUrl = component.getLaunch(tool)
equal computedUrl, correctUrl

View File

@ -61,6 +61,7 @@ define [
setup: ->
fakeENV.setup()
ENV.VALID_DATE_RANGE = {}
ENV.COURSE_ID = 1
teardown: ->
fakeENV.teardown()
$(".ui-dialog").remove()
@ -240,6 +241,7 @@ define [
module 'EditView: group category locked',
setup: ->
fakeENV.setup()
ENV.COURSE_ID = 1
@oldAddGroupCategory = window.addGroupCategory
window.addGroupCategory = @stub()
teardown: ->
@ -264,6 +266,7 @@ define [
module 'EditView: setDefaultsIfNew',
setup: ->
fakeENV.setup()
ENV.COURSE_ID = 1
@stub(userSettings, 'contextGet').returns {submission_types: "foo", peer_reviews: "1", assignment_group_id: 99}
teardown: ->
fakeENV.teardown()
@ -299,6 +302,7 @@ define [
module 'EditView: setDefaultsIfNew: no localStorage',
setup: ->
fakeENV.setup()
ENV.COURSE_ID = 1
@stub(userSettings, 'contextGet').returns null
teardown: ->
fakeENV.teardown()
@ -314,6 +318,7 @@ define [
module 'EditView: cacheAssignmentSettings',
setup: ->
fakeENV.setup()
ENV.COURSE_ID = 1
teardown: ->
fakeENV.teardown()
editView: ->
@ -338,6 +343,7 @@ define [
module 'EditView: Conditional Release',
setup: ->
fakeENV.setup()
ENV.COURSE_ID = 1
ENV.CONDITIONAL_RELEASE_SERVICE_ENABLED = true
ENV.CONDITIONAL_RELEASE_ENV = { assignment: { id: 1 }, jwt: 'foo' }
$(document).on 'submit', -> false
@ -403,6 +409,7 @@ define [
module 'Editview: Intra-Group Peer Review toggle',
setup: ->
fakeENV.setup()
ENV.COURSE_ID = 1
teardown: ->
fakeENV.teardown()
editView: ->
@ -429,3 +436,50 @@ define [
view = @editView()
view.$el.appendTo $('#fixtures')
ok !view.$('#intra_group_peer_reviews').is(":visible")
module 'EditView: Assignment Configuration Tools',
setup: ->
fakeENV.setup()
ENV.COURSE_ID = 1
ENV.PLAGIARISM_DETECTION_PLATFORM = true
teardown: ->
fakeENV.teardown()
editView: ->
editView.apply(this, arguments)
test 'it attaches assignment configuration component', ->
view = @editView()
equal view.$assignmentConfigurationTools.children().size(), 1
test 'it is hidden if submission type is not online with a file upload', ->
view = @editView()
view.$el.appendTo $('#fixtures')
equal view.$('#assignment_configuration_tools').css('display'), 'none'
view.$('#assignment_submission_type').val('on_paper')
view.handleSubmissionTypeChange()
equal view.$('#assignment_configuration_tools').css('display'), 'none'
view.$('#assignment_submission_type').val('external_tool')
view.handleSubmissionTypeChange()
equal view.$('#assignment_configuration_tools').css('display'), 'none'
view.$('#assignment_submission_type').val('online')
view.$('#assignment_online_upload').attr('checked', false)
view.handleSubmissionTypeChange()
equal view.$('#assignment_configuration_tools').css('display'), 'none'
view.$('#assignment_submission_type').val('online')
view.$('#assignment_online_upload').attr('checked', true)
view.handleSubmissionTypeChange()
equal view.$('#assignment_configuration_tools').css('display'), 'block'
test 'it is hidden if the plagiarism_detection_platform flag is disabled', ->
ENV.PLAGIARISM_DETECTION_PLATFORM = false
view = @editView()
view.$('#assignment_submission_type').val('online')
view.$('#assignment_online_upload').attr('checked', true)
view.handleSubmissionTypeChange()
equal view.$('#assignment_configuration_tools').css('display'), 'none'

View File

@ -377,9 +377,14 @@ describe AssignmentsController do
it "bootstraps the correct assignment info to js_env" do
user_session(@teacher)
tool = @course.context_external_tools.create!(name: "a", url: "http://www.google.com", consumer_key: '12345', shared_secret: 'secret')
@assignment.tool_settings_tools = [tool]
get 'edit', :course_id => @course.id, :id => @assignment.id
expect(assigns[:js_env][:ASSIGNMENT]['id']).to eq @assignment.id
expect(assigns[:js_env][:ASSIGNMENT_OVERRIDES]).to eq []
expect(assigns[:js_env][:COURSE_ID]).to eq @course.id
expect(assigns[:js_env][:SELECTED_CONFIG_TOOL_ID]).to eq tool.id
end
context "redirects" do