Teacher can create annotation assignment

refs EVAL-1363

test plan:
 - enable annotated_document_submissions flag
 - create new assignment
 - check "annotated document" under
   "Online Entry Options"
 - select a document
 - assignment should successfully save
 - assignment record should have annotatable_attachment_id field

flag=annotated_document_submissions

Change-Id: I5e90ca9c8f0b0501719cfe9e9d946be66fa899c5
Reviewed-on: https://gerrit.instructure.com/c/canvas-lms/+/258184
Tested-by: Service Cloud Jenkins <svc.cloudjenkins@instructure.com>
Reviewed-by: Adrian Packel <apackel@instructure.com>
Reviewed-by: Spencer Olson <solson@instructure.com>
QA-Review: Kai Bjorkman <kbjorkman@instructure.com>
Product-Review: Jody Sailor
This commit is contained in:
Aaron Shafovaloff 2021-02-04 12:17:56 -06:00
parent 88be9d1986
commit b19db4b7eb
8 changed files with 373 additions and 217 deletions

View File

@ -238,6 +238,9 @@ export default class Assignment extends Model
acceptsOnlineUpload: =>
!! _.includes @_submissionTypes(), 'online_upload'
acceptsAnnotatedDocument: =>
!! _.includes @_submissionTypes(), 'annotated_document'
acceptsOnlineURL: =>
!! _.includes @_submissionTypes(), 'online_url'
@ -250,7 +253,7 @@ export default class Assignment extends Model
isOnlineSubmission: =>
_.some @_submissionTypes(), (thing) ->
thing in ['online', 'online_text_entry',
'media_recording', 'online_url', 'online_upload']
'media_recording', 'online_url', 'online_upload', 'annotated_document']
postToSIS: (postToSisBoolean) =>
return @get 'post_to_sis' unless arguments.length > 0
@ -557,6 +560,7 @@ export default class Assignment extends Model
toView: =>
fields = [
'acceptsAnnotatedDocument',
'acceptsMediaRecording', 'acceptsOnlineTextEntries', 'acceptsOnlineURL',
'acceptsOnlineUpload', 'allDates', 'allowedExtensions', 'anonymousGrading',
'anonymousInstructorAnnotations', 'anonymousPeerReviews', 'assignmentGroupId',

View File

@ -50,6 +50,7 @@ import 'jqueryui/dialog'
import 'jquery.toJSON'
import '../../jquery.rails_flash_notifications'
import '../../behaviors/tooltip'
import {FileBrowserWrapper} from 'jsx/assignments/EditAssignment'
###
xsslint safeString.identifier srOnly
@ -69,9 +70,11 @@ export default class EditView extends ValidatedFormView
ONLINE_SUBMISSION_TYPES = '#assignment_online_submission_types'
NAME = '[name="name"]'
ALLOW_FILE_UPLOADS = '#assignment_online_upload'
ALLOW_ANNOTATED_DOCUMENT = '#assignment_annotated_document'
ALLOW_TEXT_ENTRY = '#assignment_text_entry'
RESTRICT_FILE_UPLOADS = '#assignment_restrict_file_extensions'
RESTRICT_FILE_UPLOADS_OPTIONS = '#restrict_file_extensions_container'
ANNOTATED_DOCUMENT_OPTIONS = '#annotated_document_chooser_container'
ALLOWED_EXTENSIONS = '#allowed_extensions_container'
TURNITIN_ENABLED = '#assignment_turnitin_enabled'
VERICITE_ENABLED = '#assignment_vericite_enabled'
@ -115,8 +118,10 @@ export default class EditView extends ValidatedFormView
els["#{ONLINE_SUBMISSION_TYPES}"] = '$onlineSubmissionTypes'
els["#{NAME}"] = '$name'
els["#{ALLOW_FILE_UPLOADS}"] = '$allowFileUploads'
els["#{ALLOW_ANNOTATED_DOCUMENT}"] = '$allowAnnotatedDocument'
els["#{RESTRICT_FILE_UPLOADS}"] = '$restrictFileUploads'
els["#{RESTRICT_FILE_UPLOADS_OPTIONS}"] = '$restrictFileUploadsOptions'
els["#{ANNOTATED_DOCUMENT_OPTIONS}"] = '$annotatedDocumentOptions'
els["#{ALLOWED_EXTENSIONS}"] = '$allowedExtensions'
els["#{TURNITIN_ENABLED}"] = '$turnitinEnabled'
els["#{VERICITE_ENABLED}"] = '$vericiteEnabled'
@ -159,6 +164,7 @@ export default class EditView extends ValidatedFormView
events["change #{TURNITIN_ENABLED}"] = 'toggleAdvancedTurnitinSettings'
events["change #{VERICITE_ENABLED}"] = 'toggleAdvancedTurnitinSettings'
events["change #{ALLOW_FILE_UPLOADS}"] = 'toggleRestrictFileUploads'
events["change #{ALLOW_ANNOTATED_DOCUMENT}"] = 'toggleAnnotatedDocument'
events["click #{EXTERNAL_TOOLS_URL}_find"] = 'showExternalToolsDialog'
events["change #assignment_points_possible"] = 'handlePointsChange'
events["change #{PEER_REVIEWS_BOX}"] = 'togglePeerReviewsAndGroupCategoryEnabled'
@ -322,6 +328,29 @@ export default class EditView extends ValidatedFormView
toggleRestrictFileUploads: =>
@$restrictFileUploadsOptions.toggleAccessibly @$allowFileUploads.prop('checked')
toggleAnnotatedDocument: =>
@$annotatedDocumentOptions.toggleAccessibly @$allowAnnotatedDocument.prop('checked')
documentChooserContainer = document.querySelector('#annotated_document_chooser_container')
if @$allowAnnotatedDocument.prop('checked')
fileBrowserProps = {
useContextAssets: true,
allowUpload: true,
selectFile: (fileInfo) =>
document.getElementById('annotated_document_id').value = fileInfo.id
$.screenReaderFlashMessageExclusive(
I18n.t('selected %{filename}', {filename: fileInfo.name})
)
}
ReactDOM.render(
React.createElement(FileBrowserWrapper, fileBrowserProps),
documentChooserContainer
)
else
ReactDOM.unmountComponentAtNode(documentChooserContainer)
toggleAdvancedTurnitinSettings: (ev) =>
ev.preventDefault()
@$advancedTurnitinSettings.toggleAccessibly (@$turnitinEnabled.prop('checked') || @$vericiteEnabled.prop('checked'))
@ -520,6 +549,7 @@ export default class EditView extends ValidatedFormView
lockedItems: @lockedItems
anonymousGradingEnabled: ENV?.ANONYMOUS_GRADING_ENABLED or false
anonymousInstructorAnnotationsEnabled: ENV?.ANONYMOUS_INSTRUCTOR_ANNOTATIONS_ENABLED or false
annotatedDocumentSubmissionsEnabled: ENV?.ANNOTATED_DOCUMENT_SUBMISSIONS or false
_attachEditorToDescription: =>
return if @lockedItems.content

View File

@ -0,0 +1,42 @@
/*
* Copyright (C) 2021 - 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 React, {Suspense} from 'react'
import ErrorBoundary from 'jsx/shared/components/ErrorBoundary'
import errorShipUrl from 'jsx/shared/svg/ErrorShip.svg'
import GenericErrorPage from 'jsx/shared/components/GenericErrorPage/index'
import LoadingIndicator from 'jsx/shared/LoadingIndicator'
const FileBrowser = React.lazy(() => import('jsx/shared/rce/FileBrowser'))
export function FileBrowserWrapper(props) {
return (
<ErrorBoundary
errorComponent={
<GenericErrorPage
imageUrl={errorShipUrl}
errorCategory="FileBrowser on Create Assignment page"
/>
}
>
<Suspense fallback={<LoadingIndicator />}>
<FileBrowser {...props} />
</Suspense>
</ErrorBoundary>
)
}

View File

@ -26,15 +26,17 @@ export default class ErrorBoundary extends React.Component {
}
static getDerivedStateFromError(error) {
// Update state so the next render will show the fallback UI.
return {hasError: true, error}
}
componentDidCatch(error, errorInfo) {
console.error(error, errorInfo)
}
state = {hasError: false, error: null}
render() {
if (this.state.hasError) {
// You can render any custom fallback UI
return React.cloneElement(this.props.errorComponent, {errorSubject: this.state.error.message})
}

View File

@ -7,14 +7,13 @@
<div class="form-column-right">
<div class="border border-trbl border-round">
{{!-- Submission type accepted --}}
<select id="assignment_submission_type"
name="submission_type"
<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 isDefaultTool}}>
{{defaultToolName}}
</option>
<option value="default_external_tool" {{selectedIf isDefaultTool}}>
{{defaultToolName}}
</option>
{{/if}}
<option value="none" {{selectedIf defaultToNone}}>
{{#t "submission_types.no_submission"}}No Submission{{/t}}
@ -29,20 +28,19 @@
{{#t "submission_types.external_tool"}}External Tool{{/t}}
</option>
{{#if submissionTypeSelectionTools}}
{{#each submissionTypeSelectionTools}}
<option value="external_tool_placement_{{this.id}}" {{selectedIf this.id ../selectedSubmissionTypeToolId}}>
{{this.base_title}}
</option>
{{/each}}
{{#each submissionTypeSelectionTools}}
<option value="external_tool_placement_{{this.id}}" {{selectedIf this.id ../selectedSubmissionTypeToolId}}>
{{this.base_title}}
</option>
{{/each}}
{{/if}}
</select>
{{#if submissionTypesFrozen }}
<input type="hidden" name="submission_type" value="{{submissionType}}" />
{{#if submissionTypesFrozen}}
<input type="hidden" name="submission_type" value="{{submissionType}}" />
{{/if}}
{{!-- Online submission types --}}
<div id="assignment_online_submission_types"
aria-enabled="{{isOnlineSubmission}}"
<div id="assignment_online_submission_types" aria-enabled="{{isOnlineSubmission}}"
style="{{hiddenUnless isOnlineSubmission}}">
<div class="subtitle">
@ -53,79 +51,85 @@
<label class="checkbox" for="assignment_text_entry">
{{checkbox "acceptsOnlineTextEntries"
id="assignment_text_entry"
name="online_submission_types[online_text_entry]"
aria-label=(t "Online Submission Type - Text Entry")
disabled=submissionTypesFrozen}}
id="assignment_text_entry"
name="online_submission_types[online_text_entry]"
aria-label=(t "Online Submission Type - Text Entry")
disabled=submissionTypesFrozen}}
{{#t "labels.allow_text_entry"}}Text Entry{{/t}}
</label>
<label class="checkbox" for="assignment_online_url">
{{checkbox "acceptsOnlineURL"
id="assignment_online_url"
name="online_submission_types[online_url]"
aria-label=(t "Online Submission Type - Website URL")
disabled=submissionTypesFrozen}}
id="assignment_online_url"
name="online_submission_types[online_url]"
aria-label=(t "Online Submission Type - Website URL")
disabled=submissionTypesFrozen}}
{{#t "labels.allow_url"}}Website URL{{/t}}
</label>
{{#if kalturaEnabled}}
<label class="checkbox" for="assignment_media_recording">
{{checkbox "acceptsMediaRecording"
id="assignment_media_recording"
name="online_submission_types[media_recording]"
aria-label=(t "Online Submission Type - Media Recordings")
disabled=submissionTypesFrozen}}
{{#t "labels.allow_media_recordings"}}Media Recordings{{/t}}
</label>
<label class="checkbox" for="assignment_media_recording">
{{checkbox "acceptsMediaRecording"
id="assignment_media_recording"
name="online_submission_types[media_recording]"
aria-label=(t "Online Submission Type - Media Recordings")
disabled=submissionTypesFrozen}}
{{#t "labels.allow_media_recordings"}}Media Recordings{{/t}}
</label>
{{/if}}
{{#if annotatedDocumentSubmissionsEnabled}}
<label class="checkbox" for="assignment_annotated_document">
{{checkbox "acceptsAnnotatedDocument"
id="assignment_annotated_document"
name="online_submission_types[annotated_document]"
aria-controls="restrict_file_extensions_container"
aria-label=(t "Online Submission Type - Annotated Document")
disabled=submissionTypesFrozen}}
{{#t "labels.allows_annotated_document"}}Annotated Document{{/t}}
</label>
<div id="annotated_document_chooser_container" class="nested" aria-expanded="{{acceptsAnnotatedDocument}}"
style="{{hiddenUnless acceptsAnnotatedDocument}}; padding: 6px 0 6px 42px;">
</div>
<input type="hidden" name="annotated_document_id" id="annotated_document_id" />
{{/if}}
<label class="checkbox" for="assignment_online_upload">
{{checkbox "acceptsOnlineUpload"
id="assignment_online_upload"
name="online_submission_types[online_upload]"
aria-controls="restrict_file_extensions_container"
aria-label=(t "Online Submission Type - File Uploads")
disabled=submissionTypesFrozen}}
id="assignment_online_upload"
name="online_submission_types[online_upload]"
aria-controls="restrict_file_extensions_container"
aria-label=(t "Online Submission Type - File Uploads")
disabled=submissionTypesFrozen}}
{{#t "labels.allow_file_uploads"}}File Uploads{{/t}}
</label>
{{!-- Online submission restrict file types? --}}
<div id="restrict_file_extensions_container"
class="nested"
aria-expanded="{{acceptsOnlineUpload}}"
<div id="restrict_file_extensions_container" class="nested" aria-expanded="{{acceptsOnlineUpload}}"
style="{{hiddenUnless acceptsOnlineUpload}}">
<label class="checkbox" for="assignment_restrict_file_extensions">
{{checkbox "restrictFileExtensions"
id="assignment_restrict_file_extensions"
name="restrict_file_extensions"
aria-controls="allowed_extensions_container"
aria-label=(t "Online Submission Type - File Uploads - Restrict Upload File Types")
disabled=submissionTypesFrozen}}
id="assignment_restrict_file_extensions"
name="restrict_file_extensions"
aria-controls="allowed_extensions_container"
aria-label=(t "Online Submission Type - File Uploads - Restrict Upload File Types")
disabled=submissionTypesFrozen}}
{{#t "labels.restrict_file_extensions"}}
Restrict Upload File Types
Restrict Upload File Types
{{/t}}
</label>
{{!-- Online submission allowed extensions --}}
<div id="allowed_extensions_container"
aria-expanded="{{restrictFileExtensions}}"
style="{{hiddenUnless restrictFileExtensions}}"
class="nested">
<div id="allowed_extensions_container" aria-expanded="{{restrictFileExtensions}}"
style="{{hiddenUnless restrictFileExtensions}}" class="nested">
<label for="assignment_allowed_extensions" class="hidden-readable">
{{#t "labels.allowed_extensions"}}Allowed File Extensions{{/t}}
</label>
<input id="assignment_allowed_extensions"
name="allowed_extensions"
type="text"
maxlength="254"
placeholder="{{#t "labels.allowed_extensions"}}Allowed File Extensions{{/t}}"
aria-labelledby="explanation_nest"
value="{{join allowedExtensions ","}}"
{{disabledIfIncludes frozenAttributes "submission_types"}}/>
<input id="assignment_allowed_extensions" name="allowed_extensions" type="text" maxlength="254" placeholder="{{#t "
labels.allowed_extensions"}}Allowed File Extensions{{/t}}" aria-labelledby="explanation_nest"
value="{{join allowedExtensions " ,"}}" {{disabledIfIncludes frozenAttributes "submission_types"}} />
<div id="explanation_nest" class="explanation nest">
{{#t "descriptions.allowed_extensions"}}
Enter a list of accepted extensions, for example: doc,xls,txt
Enter a list of accepted extensions, for example: doc,xls,txt
{{/t}}
</div>
</div>
@ -135,17 +139,15 @@
<div style="{{hiddenUnless turnitinAvailable}}">
<label for="assignment_turnitin_enabled" class="checkbox">
{{checkbox "turnitinEnabled"
id="assignment_turnitin_enabled"
name="turnitin_enabled"
aria-controls="advanced_turnitin_settings_link"
disabled=submissionTypesFrozen}}
id="assignment_turnitin_enabled"
name="turnitin_enabled"
aria-controls="advanced_turnitin_settings_link"
disabled=submissionTypesFrozen}}
{{#t "label.turnitin_enabled"}}Enable Turnitin Submissions{{/t}}
</label>
<div class="nested">
<a href="#"
id="advanced_turnitin_settings_link"
aria-expanded="{{turnitinEnabled}}"
<a href="#" id="advanced_turnitin_settings_link" aria-expanded="{{turnitinEnabled}}"
style="{{hiddenUnless turnitinEnabled}}">
{{#t "advanced_turnitin_settings"}}Advanced Turnitin Settings{{/t}}
</a>
@ -155,16 +157,14 @@
<div style="{{hiddenUnless vericiteAvailable}}">
<label for="assignment_vericite_enabled" class="checkbox">
{{checkbox "vericiteEnabled"
id="assignment_vericite_enabled"
name="vericite_enabled"
aria-controls="advanced_turnitin_settings_link"
disabled=submissionTypesFrozen}}
id="assignment_vericite_enabled"
name="vericite_enabled"
aria-controls="advanced_turnitin_settings_link"
disabled=submissionTypesFrozen}}
{{#t "label.vericite_enabled"}}Enable VeriCite Submissions{{/t}}
</label>
<div class="nested">
<a href="#"
id="advanced_turnitin_settings_link"
aria-expanded="{{vericiteEnabled}}"
<a href="#" id="advanced_turnitin_settings_link" aria-expanded="{{vericiteEnabled}}"
style="{{hiddenUnless vericiteEnabled}}">
{{#t "advanced_vericite_settings"}}Advanced VeriCite Settings{{/t}}
</a>
@ -172,36 +172,43 @@
</div>
</div>
{{!-- LTI launch button for getting additional data for external tool assignments (when selected via placement) --}}
<div id="assignment_submission_type_selection_tool_launch_container" style="{{hiddenUnless selectedSubmissionTypeToolId}}">
{{!-- LTI launch button for getting additional data for external tool assignments (when selected via placement)
--}}
<div id="assignment_submission_type_selection_tool_launch_container"
style="{{hiddenUnless selectedSubmissionTypeToolId}}">
<div class="pad-box">
<div class="ic-Form-control">
<button class="Button btn-primary" type="button" id="assignment_submission_type_selection_launch_button">
<i class="icon-link sr-hide" />
<span id="assignment_submission_type_selection_launch_button_text"></span>
<i class="icon-link sr-hide" />
<span id="assignment_submission_type_selection_launch_button_text"></span>
</button>
</div>
<input type="hidden" id="assignment_submission_type_external_data" name="external_tool_tag_attributes[external_data]" value="{{externalToolDataStringified}}"/>
{{!-- this is very specific to Mastery Connect currently. We could potentially make this a dynamic partial based on the type of external data --}}
<input type="hidden" id="assignment_submission_type_external_data" name="external_tool_tag_attributes[external_data]"
value="{{externalToolDataStringified}}" />
{{!-- this is very specific to Mastery Connect currently. We could potentially make this a dynamic partial
based on the type of external data --}}
<div>
<h2>
<span id="mc_external_data_assessment">{{name}}</span>
</h2>
<div id="mc_external_data_points">{{externalToolData.points}} {{#if externalToolData.points}} {{#t}}Points{{/t}} {{/if}}</div>
<div id="mc_external_data_points">{{externalToolData.points}} {{#if externalToolData.points}}
{{#t}}Points{{/t}} {{/if}}</div>
<div id="mc_external_data_objectives">{{externalToolData.objectives}}</div>
<div> </div>
<h3>
<span id="mc_external_data_tracker">{{externalToolData.trackerName}}</span>
</h3>
<div id="mc_external_data_tracker_alignment">{{externalToolData.trackerAlignment}}</div>
<div id="mc_external_data_students">{{externalToolData.studentCount}} {{externalToolDataStudentLabelText}}</div>
<div id="mc_external_data_students">{{externalToolData.studentCount}} {{externalToolDataStudentLabelText}}
</div>
</div>
</div>
</div>
<div id="assignment_submission_type_selection_tool_dialog"></div>
{{!-- Default external tool configuration --}}
<div id="default_external_tool_container" data-component="DefaultToolForm" style="{{hiddenUnless isNonPlacementExternalTool}}">
<div id="default_external_tool_container" data-component="DefaultToolForm"
style="{{hiddenUnless isNonPlacementExternalTool}}">
</div>
{{!-- External tool submissions --}}
@ -217,57 +224,48 @@
{{#t}}Enter or find an External Tool URL{{/t}}
</label>
<input type="hidden" id="assignment_external_tool_tag_attributes_custom_params"
name="external_tool_tag_attributes[custom_params]"
value="{{externalToolCustomParamsStringified}}" />
name="external_tool_tag_attributes[custom_params]" value="{{externalToolCustomParamsStringified}}" />
<div class="ic-Input-group">
<div class="ic-Input-group__add-on" aria-hidden="true">
<i class="icon-link"></i>
</div>
<input id="assignment_external_tool_tag_attributes_url"
class="ic-Input"
name="external_tool_tag_attributes[url]"
type="url"
value="{{externalToolUrl}}"
placeholder="http://www.example.com/launch"
{{disabledIfIncludes frozenAttributes "submission_types"}}/>
<input id="assignment_external_tool_tag_attributes_url" class="ic-Input" name="external_tool_tag_attributes[url]"
type="url" value="{{externalToolUrl}}" placeholder="http://www.example.com/launch" {{disabledIfIncludes
frozenAttributes "submission_types"}} />
<button class="Button" id="assignment_external_tool_tag_attributes_url_find" type="button">
{{#t}}Find{{/t}}
</button>
{{#if submissionTypesFrozen }}
<input type="hidden" name="external_tool_tag_attributes[url]" value="{{externalToolUrl}}" />
{{#if submissionTypesFrozen}}
<input type="hidden" name="external_tool_tag_attributes[url]" value="{{externalToolUrl}}" />
{{/if}}
</div>
</div>
<input id="assignment_external_tool_tag_attributes_content_type"
name="external_tool_tag_attributes[content_type]"
type="text"
style="display: none"/>
<input id="assignment_external_tool_tag_attributes_content_id"
name="external_tool_tag_attributes[content_id]"
type="text"
style="display: none"/>
name="external_tool_tag_attributes[content_type]" type="text" style="display: none" />
<input id="assignment_external_tool_tag_attributes_content_id" name="external_tool_tag_attributes[content_id]"
type="text" style="display: none" />
</div>
{{#if groupCategoryId}}
<div class="alert assignment-edit-external-tool-alert">
{{#t "external_tool_group_warning"}}
Group assignments can't use External Tools.
The group setting will be unchecked when you save
{{/t}}
</div>
<div class="alert assignment-edit-external-tool-alert">
{{#t "external_tool_group_warning"}}
Group assignments can't use External Tools.
The group setting will be unchecked when you save
{{/t}}
</div>
{{/if}}
</div>
{{#unless isQuizLTIAssignment}}
<div id="external_tool_new_tab_container" style="display: none;">
<label for="assignment_external_tool_tag_attributes_new_tab" class="checkbox">
{{checkbox "externalToolNewTab"
id="assignment_external_tool_tag_attributes_new_tab"
name="external_tool_tag_attributes[new_tab]"
disabled=submissionTypesFrozen}}
{{#t "label.external_tool_new_tab"}}Load This Tool In A New Tab{{/t}}
</label>
</div>
<div id="external_tool_new_tab_container" style="display: none;">
<label for="assignment_external_tool_tag_attributes_new_tab" class="checkbox">
{{checkbox "externalToolNewTab"
id="assignment_external_tool_tag_attributes_new_tab"
name="external_tool_tag_attributes[new_tab]"
disabled=submissionTypesFrozen}}
{{#t "label.external_tool_new_tab"}}Load This Tool In A New Tab{{/t}}
</label>
</div>
{{/unless}}
</div>
</div>
</fieldset>
</fieldset>

View File

@ -0,0 +1,39 @@
# frozen_string_literal: true
#
# Copyright (C) 2021 - 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/>.
#
class AddAnnotatableAttachmentIdToAssignments < ActiveRecord::Migration[5.2]
tag :predeploy # this migration will run before new app code is deployed
disable_ddl_transaction! # don't lock db, don't use transaction
def change
add_reference :assignments,
:annotatable_attachment,
type: :bigint,
foreign_key: {
to_table: :attachments
},
index: false
add_index :assignments,
:annotatable_attachment_id,
where: 'annotatable_attachment_id IS NOT NULL',
algorithm: :concurrently,
if_not_exists: true
end
end

View File

@ -574,6 +574,7 @@ module Api::V1::Assignment
"media_recording",
"not_graded",
"wiki_page",
"annotated_document",
""
].freeze

View File

@ -18,7 +18,6 @@
import $ from 'jquery'
import React from 'react'
import _ from 'underscore'
import RCELoader from 'jsx/shared/rce/serviceRCELoader'
import SectionCollection from 'compiled/collections/SectionCollection'
import Assignment from 'compiled/models/Assignment'
@ -39,7 +38,7 @@ const s_params = 'some super secure params'
const fixtures = document.getElementById('fixtures')
const currentOrigin = window.location.origin
const nameLengthHelper = function(
const nameLengthHelper = function (
view,
length,
maxNameLengthRequiredForAccount,
@ -52,7 +51,7 @@ const nameLengthHelper = function(
ENV.MAX_NAME_LENGTH = maxNameLength
return view.validateBeforeSave({name, post_to_sis: postToSis, grading_type: gradingType}, [])
}
const editView = function(assignmentOpts = {}) {
const editView = function (assignmentOpts = {}) {
const defaultAssignmentOpts = {
name: 'Test Assignment',
secure_params: s_params,
@ -144,32 +143,32 @@ QUnit.module('EditView', {
}
})
test('should be accessible', function(assert) {
test('should be accessible', function (assert) {
const view = this.editView()
const done = assert.async()
assertions.isAccessible(view, done, {a11yReport: true})
})
test('renders', function() {
test('renders', function () {
const view = this.editView()
equal(view.$('#assignment_name').val(), 'Test Assignment')
})
test('rejects missing group set for group assignment', function() {
test('rejects missing group set for group assignment', function () {
const view = this.editView()
const data = {group_category_id: 'blank'}
const errors = view.validateBeforeSave(data, [])
equal(errors.newGroupCategory[0].message, 'Please create a group set')
})
test('rejects a letter for points_possible', function() {
test('rejects a letter for points_possible', function () {
const view = this.editView()
const data = {points_possible: 'a'}
const errors = view.validateBeforeSave(data, [])
equal(errors.points_possible[0].message, 'Points possible must be a number')
})
test('validates presence of a final grader', function() {
test('validates presence of a final grader', function () {
const view = this.editView()
sinon.spy(view, 'validateFinalGrader')
view.validateBeforeSave({}, [])
@ -177,7 +176,7 @@ test('validates presence of a final grader', function() {
view.validateFinalGrader.restore()
})
test('validates grader count', function() {
test('validates grader count', function () {
const view = this.editView()
sinon.spy(view, 'validateGraderCount')
view.validateBeforeSave({}, [])
@ -185,7 +184,7 @@ test('validates grader count', function() {
view.validateGraderCount.restore()
})
test('does not allow group assignment for large rosters', function() {
test('does not allow group assignment for large rosters', function () {
ENV.IS_LARGE_ROSTER = true
const view = this.editView()
equal(view.$('#group_category_selector').length, 0)
@ -201,20 +200,20 @@ test('does not allow group assignment for anonymously graded assignments', () =>
strictEqual(hasGroupCategoryCheckbox.prop('disabled'), true)
})
test('does not allow peer review for large rosters', function() {
test('does not allow peer review for large rosters', function () {
ENV.IS_LARGE_ROSTER = true
const view = this.editView()
equal(view.$('#assignment_peer_reviews_fields').length, 0)
})
test('adds and removes student group', function() {
test('adds and removes student group', function () {
ENV.GROUP_CATEGORIES = [{id: 1, name: 'fun group'}]
ENV.ASSIGNMENT_GROUPS = [{id: 1, name: 'assignment group 1'}]
const view = this.editView()
equal(view.assignment.toView().groupCategoryId, null)
})
test('does not allow point value of -1 or less if grading type is letter', function() {
test('does not allow point value of -1 or less if grading type is letter', function () {
const view = this.editView()
const data = {points_possible: '-1', grading_type: 'letter_grade'}
const errors = view._validatePointsRequired(data, [])
@ -224,7 +223,7 @@ test('does not allow point value of -1 or less if grading type is letter', funct
)
})
test('requires name to save assignment', function() {
test('requires name to save assignment', function () {
const view = this.editView()
const data = {name: ''}
const errors = view.validateBeforeSave(data, [])
@ -233,7 +232,7 @@ test('requires name to save assignment', function() {
equal(errors.name[0].message, 'Name is required!')
})
test('has an error when a name has 257 chars', function() {
test('has an error when a name has 257 chars', function () {
const view = this.editView()
const errors = nameLengthHelper(view, 257, false, 30, '1', 'points')
ok(errors.name)
@ -241,19 +240,19 @@ test('has an error when a name has 257 chars', function() {
equal(errors.name[0].message, 'Name is too long, must be under 257 characters')
})
test('allows assignment to save when a name has 256 chars, MAX_NAME_LENGTH is not required and post_to_sis is true', function() {
test('allows assignment to save when a name has 256 chars, MAX_NAME_LENGTH is not required and post_to_sis is true', function () {
const view = this.editView()
const errors = nameLengthHelper(view, 256, false, 30, '1', 'points')
equal(errors.length, 0)
})
test('allows assignment to save when a name has 15 chars, MAX_NAME_LENGTH is 10 and is required, post_to_sis is true and grading_type is not_graded', function() {
test('allows assignment to save when a name has 15 chars, MAX_NAME_LENGTH is 10 and is required, post_to_sis is true and grading_type is not_graded', function () {
const view = this.editView()
const errors = nameLengthHelper(view, 15, true, 10, '1', 'not_graded')
equal(errors.length, 0)
})
test('has an error when a name has 11 chars, MAX_NAME_LENGTH is 10 and required and post_to_sis is true', function() {
test('has an error when a name has 11 chars, MAX_NAME_LENGTH is 10 and required and post_to_sis is true', function () {
const view = this.editView()
const errors = nameLengthHelper(view, 11, true, 10, '1', 'points')
ok(errors.name)
@ -261,19 +260,19 @@ test('has an error when a name has 11 chars, MAX_NAME_LENGTH is 10 and required
equal(errors.name[0].message, 'Name is too long, must be under 11 characters')
})
test('allows assignment to save when name has 11 chars, MAX_NAME_LENGTH is 10 and required, but post_to_sis is false', function() {
test('allows assignment to save when name has 11 chars, MAX_NAME_LENGTH is 10 and required, but post_to_sis is false', function () {
const view = this.editView()
const errors = nameLengthHelper(view, 11, true, 10, '0', 'points')
equal(errors.length, 0)
})
test('allows assignment to save when name has 10 chars, MAX_NAME_LENGTH is 10 and required, and post_to_sis is true', function() {
test('allows assignment to save when name has 10 chars, MAX_NAME_LENGTH is 10 and required, and post_to_sis is true', function () {
const view = this.editView()
const errors = nameLengthHelper(view, 10, true, 10, '1', 'points')
equal(errors.length, 0)
})
test("don't validate name if it is frozen", function() {
test("don't validate name if it is frozen", function () {
const view = this.editView()
view.model.set('frozen_attributes', ['title'])
@ -281,7 +280,7 @@ test("don't validate name if it is frozen", function() {
notOk(errors.name)
})
test('renders a hidden secure_params field', function() {
test('renders a hidden secure_params field', function () {
const view = this.editView()
const secure_params = view.$('#secure_params')
@ -289,7 +288,7 @@ test('renders a hidden secure_params field', function() {
equal(secure_params.val(), s_params)
})
test('does show error message on assignment point change with submissions', function() {
test('does show error message on assignment point change with submissions', function () {
const view = this.editView({has_submitted_submissions: true})
view.$el.appendTo($('#fixtures'))
notOk(view.$el.find('#point_change_warning:visible').attr('aria-expanded'))
@ -301,7 +300,7 @@ test('does show error message on assignment point change with submissions', func
notOk(view.$el.find('#point_change_warning:visible').attr('aria-expanded'))
})
test('does show error message on assignment point change without submissions', function() {
test('does show error message on assignment point change without submissions', function () {
const view = this.editView({has_submitted_submissions: false})
view.$el.appendTo($('#fixtures'))
notOk(view.$el.find('#point_change_warning:visible').attr('aria-expanded'))
@ -310,7 +309,7 @@ test('does show error message on assignment point change without submissions', f
notOk(view.$el.find('#point_change_warning:visible').attr('aria-expanded'))
})
test('does not allow point value of "" if grading type is letter', function() {
test('does not allow point value of "" if grading type is letter', function () {
const view = this.editView()
const data = {points_possible: '', grading_type: 'letter_grade'}
const errors = view._validatePointsRequired(data, [])
@ -324,7 +323,7 @@ test('does not allow point value of "" if grading type is letter', function() {
equal(view.getFormData().groupCategoryId, null)
})
test('does not allow blank external tool url', function() {
test('does not allow blank external tool url', function () {
const view = this.editView()
const data = {submission_type: 'external_tool'}
const errors = view._validateExternalTool(data, [])
@ -334,21 +333,21 @@ test('does not allow blank external tool url', function() {
)
})
test('does not allow blank default external tool url', function() {
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 validate allowed extensions if file uploads is not a submission type', function() {
test('does not validate allowed extensions if file uploads is not a submission type', function () {
const view = this.editView()
const data = {submission_types: ['online_url'], allowed_extensions: []}
const errors = view._validateAllowedExtensions(data, [])
equal(errors.allowed_extensions, null)
})
test('removes group_category_id if an external tool is selected', function() {
test('removes group_category_id if an external tool is selected', function () {
const view = this.editView()
let data = {
submission_type: 'external_tool',
@ -358,54 +357,54 @@ test('removes group_category_id if an external tool is selected', function() {
equal(data.group_category_id, null)
})
test('renders escaped angle brackets properly', function() {
test('renders escaped angle brackets properly', function () {
const desc = '<p>&lt;E&gt;</p>'
const view = this.editView({description: '<p>&lt;E&gt;</p>'})
equal(view.$description.val().match(desc), desc)
})
test('routes to discussion details normally', function() {
test('routes to discussion details normally', function () {
const view = this.editView({html_url: 'http://foo'})
equal(view.locationAfterSave({}), 'http://foo')
})
test('routes to return_to', function() {
test('routes to return_to', function () {
const view = this.editView({html_url: currentOrigin + '/foo'})
equal(view.locationAfterSave({return_to: currentOrigin + '/bar'}), currentOrigin + '/bar')
})
test('does not route to return_to with javascript protocol', function() {
test('does not route to return_to with javascript protocol', function () {
const view = this.editView({html_url: currentOrigin + '/foo'})
// eslint-disable-next-line no-script-url
equal(view.locationAfterSave({return_to: 'javascript:alert(1)'}), currentOrigin + '/foo')
})
test('cancels to env normally', function() {
test('cancels to env normally', function () {
ENV.CANCEL_TO = currentOrigin + '/foo'
const view = this.editView()
equal(view.locationAfterCancel({}), currentOrigin + '/foo')
})
test('cancels to return_to', function() {
test('cancels to return_to', function () {
ENV.CANCEL_TO = currentOrigin + '/foo'
const view = this.editView()
equal(view.locationAfterCancel({return_to: currentOrigin + '/bar'}), currentOrigin + '/bar')
})
test('does not cancel to return_to with javascript protocol', function() {
test('does not cancel to return_to with javascript protocol', function () {
ENV.CANCEL_TO = currentOrigin + '/foo'
const view = this.editView()
// eslint-disable-next-line no-script-url
equal(view.locationAfterCancel({return_to: 'javascript:alert(1)'}), currentOrigin + '/foo')
})
test('does not follow a cross-origin return_to', function() {
test('does not follow a cross-origin return_to', function () {
ENV.CANCEL_TO = currentOrigin + '/foo'
const view = this.editView()
equal(view.locationAfterCancel({return_to: 'http://evil.com'}), currentOrigin + '/foo')
})
test('disables fields when inClosedGradingPeriod', function() {
test('disables fields when inClosedGradingPeriod', function () {
const view = this.editView({in_closed_grading_period: true})
view.$el.appendTo($('#fixtures'))
@ -419,7 +418,7 @@ test('disables fields when inClosedGradingPeriod', function() {
equal(view.$el.find('#has_group_category').attr('aria-readonly'), 'true')
})
test('disables grading type field when frozen', function() {
test('disables grading type field when frozen', function () {
const view = this.editView({frozen_attributes: ['grading_type']})
view.$el.appendTo($('#fixtures'))
@ -427,14 +426,14 @@ test('disables grading type field when frozen', function() {
equal(view.$el.find('input[name="grading_type"]').attr('type'), 'hidden')
})
test('does not disable post to sis when inClosedGradingPeriod', function() {
test('does not disable post to sis when inClosedGradingPeriod', function () {
ENV.POST_TO_SIS = true
const view = this.editView({in_closed_grading_period: true})
view.$el.appendTo($('#fixtures'))
notOk(view.$el.find('#assignment_post_to_sis').attr('disabled'))
})
test('disableCheckbox is called for a disabled checkbox', function() {
test('disableCheckbox is called for a disabled checkbox', function () {
const view = this.editView({in_closed_grading_period: true})
view.$el.appendTo($('#fixtures'))
$('<input type="checkbox" id="checkbox_fixture"/>').appendTo($(view.$el))
@ -445,7 +444,7 @@ test('disableCheckbox is called for a disabled checkbox', function() {
equal(disableCheckboxStub.called, true)
})
test('ignoreClickHandler is called for a disabled radio', function() {
test('ignoreClickHandler is called for a disabled radio', function () {
const view = this.editView({in_closed_grading_period: true})
view.$el.appendTo($('#fixtures'))
@ -459,7 +458,7 @@ test('ignoreClickHandler is called for a disabled radio', function() {
equal(ignoreClickHandlerStub.calledOnce, true)
})
test('lockSelectValueHandler is called for a disabled select', function() {
test('lockSelectValueHandler is called for a disabled select', function () {
const view = this.editView({in_closed_grading_period: true})
view.$el.html('')
$('<select id="select_fixture"><option selected>1</option></option>2</option></select>').appendTo(
@ -473,7 +472,7 @@ test('lockSelectValueHandler is called for a disabled select', function() {
equal(lockSelectValueHandlerStub.calledOnce, true)
})
test('lockSelectValueHandler freezes selected value', function() {
test('lockSelectValueHandler freezes selected value', function () {
const view = this.editView({in_closed_grading_period: true})
view.$el.html('')
$('<select id="select_fixture"><option selected>1</option></option>2</option></select>').appendTo(
@ -482,14 +481,11 @@ test('lockSelectValueHandler freezes selected value', function() {
view.$el.appendTo($('#fixtures'))
const selectedValue = view.$el.find('#fixture_select').val()
view.$el
.find('#fixture_select')
.val(2)
.trigger('change')
view.$el.find('#fixture_select').val(2).trigger('change')
equal(view.$el.find('#fixture_select').val(), selectedValue)
})
test('fields are enabled when not inClosedGradingPeriod', function() {
test('fields are enabled when not inClosedGradingPeriod', function () {
const view = this.editView()
view.$el.appendTo($('#fixtures'))
@ -503,14 +499,14 @@ test('fields are enabled when not inClosedGradingPeriod', function() {
notOk(view.$el.find('#has_group_category').attr('aria-readonly'))
})
test('rounds points_possible', function() {
test('rounds points_possible', function () {
const view = this.editView()
view.$assignmentPointsPossible.val('1.234')
const data = view.getFormData()
equal(data.points_possible, 1.23)
})
test('sets seconds of due_at to 59 if the new minute value is 59', function() {
test('sets seconds of due_at to 59 if the new minute value is 59', function () {
const view = this.editView({
due_at: $.unfudgeDateForProfileTimezone(new Date('2000-08-28T11:58:23'))
})
@ -519,7 +515,7 @@ test('sets seconds of due_at to 59 if the new minute value is 59', function() {
strictEqual(view.getFormData().due_at, '2000-08-28T11:59:59.000Z')
})
test('sets seconds of due_at to 00 if the new minute value is not 59', function() {
test('sets seconds of due_at to 00 if the new minute value is not 59', function () {
const view = this.editView({
due_at: $.unfudgeDateForProfileTimezone(new Date('2000-08-28T11:59:23'))
})
@ -531,7 +527,7 @@ test('sets seconds of due_at to 00 if the new minute value is not 59', function(
// The UI doesn't allow editing the seconds value and always returns 00. If
// the seconds value was set to something different prior to the update, keep
// that value.
test('keeps original due_at seconds if only the seconds value has changed', function() {
test('keeps original due_at seconds if only the seconds value has changed', function () {
const view = this.editView({
due_at: $.unfudgeDateForProfileTimezone(new Date('2000-08-29T11:59:23'))
})
@ -540,7 +536,7 @@ test('keeps original due_at seconds if only the seconds value has changed', func
strictEqual(view.getFormData().due_at, '2000-08-29T11:59:23.000Z')
})
test('keeps original due_at seconds if the date has not changed', function() {
test('keeps original due_at seconds if the date has not changed', function () {
const view = this.editView({
due_at: $.unfudgeDateForProfileTimezone(new Date('2000-08-28T11:59:23'))
})
@ -549,7 +545,7 @@ test('keeps original due_at seconds if the date has not changed', function() {
strictEqual(view.getFormData().due_at, '2000-08-28T11:59:23.000Z')
})
test('sets seconds of unlock_at to 59 if the new minute value is 59', function() {
test('sets seconds of unlock_at to 59 if the new minute value is 59', function () {
const view = this.editView({
unlock_at: $.unfudgeDateForProfileTimezone(new Date('2000-08-28T11:58:23'))
})
@ -558,7 +554,7 @@ test('sets seconds of unlock_at to 59 if the new minute value is 59', function()
strictEqual(view.getFormData().unlock_at, '2000-08-28T11:59:59.000Z')
})
test('sets seconds of unlock_at to 00 if the new minute value is not 59', function() {
test('sets seconds of unlock_at to 00 if the new minute value is not 59', function () {
const view = this.editView({
unlock_at: $.unfudgeDateForProfileTimezone(new Date('2000-08-28T11:59:23'))
})
@ -570,7 +566,7 @@ test('sets seconds of unlock_at to 00 if the new minute value is not 59', functi
// The UI doesn't allow editing the seconds value and always returns 00. If
// the seconds value was set to something different prior to the update, keep
// that value.
test('keeps original unlock_at seconds if only the seconds value has changed', function() {
test('keeps original unlock_at seconds if only the seconds value has changed', function () {
const view = this.editView({
unlock_at: $.unfudgeDateForProfileTimezone(new Date('2000-08-29T11:59:23'))
})
@ -579,7 +575,7 @@ test('keeps original unlock_at seconds if only the seconds value has changed', f
strictEqual(view.getFormData().unlock_at, '2000-08-29T11:59:23.000Z')
})
test('keeps original unlock_at seconds if the date has not changed', function() {
test('keeps original unlock_at seconds if the date has not changed', function () {
const view = this.editView({
unlock_at: $.unfudgeDateForProfileTimezone(new Date('2000-08-28T11:59:23'))
})
@ -588,7 +584,7 @@ test('keeps original unlock_at seconds if the date has not changed', function()
strictEqual(view.getFormData().unlock_at, '2000-08-28T11:59:23.000Z')
})
test('sets seconds of lock_at to 59 if the new minute value is 59', function() {
test('sets seconds of lock_at to 59 if the new minute value is 59', function () {
const view = this.editView({
lock_at: $.unfudgeDateForProfileTimezone(new Date('2000-08-28T11:58:23'))
})
@ -597,7 +593,7 @@ test('sets seconds of lock_at to 59 if the new minute value is 59', function() {
strictEqual(view.getFormData().lock_at, '2000-08-28T11:59:59.000Z')
})
test('sets seconds of lock_at to 00 if the new minute value is not 59', function() {
test('sets seconds of lock_at to 00 if the new minute value is not 59', function () {
const view = this.editView({
lock_at: $.unfudgeDateForProfileTimezone(new Date('2000-08-28T11:59:23'))
})
@ -609,7 +605,7 @@ test('sets seconds of lock_at to 00 if the new minute value is not 59', function
// The UI doesn't allow editing the seconds value and always returns 00. If
// the seconds value was set to something different prior to the update, keep
// that value.
test('keeps original lock_at seconds if only the seconds value has changed', function() {
test('keeps original lock_at seconds if only the seconds value has changed', function () {
const view = this.editView({
lock_at: $.unfudgeDateForProfileTimezone(new Date('2000-08-29T11:59:23'))
})
@ -618,7 +614,7 @@ test('keeps original lock_at seconds if only the seconds value has changed', fun
strictEqual(view.getFormData().lock_at, '2000-08-29T11:59:23.000Z')
})
test('keeps original lock_at seconds if the date has not changed', function() {
test('keeps original lock_at seconds if the date has not changed', function () {
const view = this.editView({
lock_at: $.unfudgeDateForProfileTimezone(new Date('2000-08-28T11:59:23'))
})
@ -658,7 +654,7 @@ QUnit.module('EditView: handleGroupCategoryChange', {
}
})
test('unchecks the group category checkbox if the anonymous grading checkbox is checked', function() {
test('unchecks the group category checkbox if the anonymous grading checkbox is checked', function () {
const view = this.editView()
checkCheckbox('assignment_anonymous_grading')
checkCheckbox('has_group_category')
@ -667,7 +663,7 @@ test('unchecks the group category checkbox if the anonymous grading checkbox is
strictEqual(groupCategoryCheckbox.checked, false)
})
test('disables the anonymous grading checkbox if the group category checkbox is checked', function() {
test('disables the anonymous grading checkbox if the group category checkbox is checked', function () {
const view = this.editView()
checkCheckbox('has_group_category')
view.handleGroupCategoryChange()
@ -675,7 +671,7 @@ test('disables the anonymous grading checkbox if the group category checkbox is
strictEqual(anonymousGradingCheckbox.disabled, true)
})
test('enables the anonymous grading checkbox if the group category checkbox is unchecked', function() {
test('enables the anonymous grading checkbox if the group category checkbox is unchecked', function () {
const view = this.editView()
disableCheckbox('assignment_anonymous_grading')
view.handleGroupCategoryChange()
@ -683,7 +679,7 @@ test('enables the anonymous grading checkbox if the group category checkbox is u
strictEqual(anonymousGradingCheckbox.disabled, false)
})
test('calls togglePeerReviewsAndGroupCategoryEnabled', function() {
test('calls togglePeerReviewsAndGroupCategoryEnabled', function () {
const view = this.editView()
sinon.spy(view, 'togglePeerReviewsAndGroupCategoryEnabled')
view.handleGroupCategoryChange()
@ -863,7 +859,7 @@ QUnit.module('EditView: group category inClosedGradingPeriod', {
}
})
test('lock down group category after students submit', function() {
test('lock down group category after students submit', function () {
let view = this.editView({has_submitted_submissions: true})
ok(view.$('.group_category_locked_explanation').length)
ok(view.$('#has_group_category').prop('disabled'))
@ -905,7 +901,7 @@ QUnit.module('EditView: enableCheckbox', {
}
})
test('enables checkbox', function() {
test('enables checkbox', function () {
const view = this.editView()
sandbox
.stub(view.$('#assignment_peer_reviews'), 'parent')
@ -916,7 +912,7 @@ test('enables checkbox', function() {
notOk(view.$('#assignment_peer_reviews').prop('disabled'))
})
test('does nothing if assignment is in closed grading period', function() {
test('does nothing if assignment is in closed grading period', function () {
const view = this.editView()
sandbox.stub(view.assignment, 'inClosedGradingPeriod').returns(true)
@ -951,21 +947,21 @@ QUnit.module('EditView: setDefaultsIfNew', {
}
})
test('returns values from localstorage', function() {
test('returns values from localstorage', function () {
sandbox.stub(userSettings, 'contextGet').returns({submission_types: ['foo']})
const view = this.editView()
view.setDefaultsIfNew()
deepEqual(view.assignment.get('submission_types'), ['foo'])
})
test('returns string booleans as integers', function() {
test('returns string booleans as integers', function () {
sandbox.stub(userSettings, 'contextGet').returns({peer_reviews: '1'})
const view = this.editView()
view.setDefaultsIfNew()
equal(view.assignment.get('peer_reviews'), 1)
})
test('doesnt overwrite existing assignment settings', function() {
test('doesnt overwrite existing assignment settings', function () {
sandbox.stub(userSettings, 'contextGet').returns({assignment_group_id: 99})
const view = this.editView()
view.assignment.set('assignment_group_id', 22)
@ -973,20 +969,20 @@ test('doesnt overwrite existing assignment settings', function() {
equal(view.assignment.get('assignment_group_id'), 22)
})
test('sets assignment submission type to online if not already set', function() {
test('sets assignment submission type to online if not already set', function () {
const view = this.editView()
view.setDefaultsIfNew()
deepEqual(view.assignment.get('submission_types'), ['online'])
})
test('doesnt overwrite assignment submission type', function() {
test('doesnt overwrite assignment submission type', function () {
const view = this.editView()
view.assignment.set('submission_types', ['external_tool'])
view.setDefaultsIfNew()
deepEqual(view.assignment.get('submission_types'), ['external_tool'])
})
test('will overwrite empty arrays', function() {
test('will overwrite empty arrays', function () {
sandbox.stub(userSettings, 'contextGet').returns({submission_types: ['foo']})
const view = this.editView()
view.assignment.set('submission_types', [])
@ -1021,7 +1017,7 @@ QUnit.module('EditView: setDefaultsIfNew: no localStorage', {
}
})
test('submission_type is online if no cache', function() {
test('submission_type is online if no cache', function () {
const view = this.editView()
view.setDefaultsIfNew()
deepEqual(view.assignment.get('submission_types'), ['online'])
@ -1053,7 +1049,7 @@ QUnit.module('EditView: cacheAssignmentSettings', {
}
})
test('saves valid attributes to localstorage', function() {
test('saves valid attributes to localstorage', function () {
const view = this.editView()
sandbox.stub(view, 'getFormData').returns({points_possible: 34})
userSettings.contextSet('new_assignment_settings', {})
@ -1061,7 +1057,7 @@ test('saves valid attributes to localstorage', function() {
equal(34, userSettings.contextGet('new_assignment_settings').points_possible)
})
test('rejects invalid attributes when caching', function() {
test('rejects invalid attributes when caching', function () {
const view = this.editView()
sandbox.stub(view, 'getFormData').returns({invalid_attribute_example: 30})
userSettings.contextSet('new_assignment_settings', {})
@ -1101,19 +1097,19 @@ QUnit.module('EditView: Conditional Release', {
}
})
test('attaches conditional release editor', function() {
test('attaches conditional release editor', function () {
const view = this.editView()
equal(1, view.$conditionalReleaseTarget.children().size())
})
test('calls update on first switch', function() {
test('calls update on first switch', function () {
const view = this.editView()
const stub = sandbox.stub(view.conditionalReleaseEditor, 'updateAssignment')
view.updateConditionalRelease()
ok(stub.calledOnce)
})
test('calls update when modified once', function() {
test('calls update when modified once', function () {
const view = this.editView()
const stub = sandbox.stub(view.conditionalReleaseEditor, 'updateAssignment')
view.onChange()
@ -1121,7 +1117,7 @@ test('calls update when modified once', function() {
ok(stub.calledOnce)
})
test('does not call update when not modified', function() {
test('does not call update when not modified', function () {
const view = this.editView()
const stub = sandbox.stub(view.conditionalReleaseEditor, 'updateAssignment')
view.updateConditionalRelease()
@ -1130,7 +1126,7 @@ test('does not call update when not modified', function() {
notOk(stub.called)
})
test('validates conditional release', function() {
test('validates conditional release', function () {
const view = this.editView()
ENV.ASSIGNMENT = view.assignment
const stub = sandbox.stub(view.conditionalReleaseEditor, 'validateBeforeSave').returns('foo')
@ -1138,15 +1134,11 @@ test('validates conditional release', function() {
ok(errors.conditional_release === 'foo')
})
test('calls save in conditional release', function(assert) {
test('calls save in conditional release', function (assert) {
const resolved = assert.async()
const view = this.editView()
const superPromise = $.Deferred()
.resolve()
.promise()
const crPromise = $.Deferred()
.resolve()
.promise()
const superPromise = $.Deferred().resolve().promise()
const crPromise = $.Deferred().resolve().promise()
const mockSuper = sinon.mock(EditView.__super__)
mockSuper.expects('saveFormData').returns(superPromise)
const stub = sandbox.stub(view.conditionalReleaseEditor, 'save').returns(crPromise)
@ -1158,7 +1150,7 @@ test('calls save in conditional release', function(assert) {
})
})
test('focuses in conditional release editor if conditional save validation fails', function() {
test('focuses in conditional release editor if conditional save validation fails', function () {
const view = this.editView()
const focusOnError = sandbox.stub(view.conditionalReleaseEditor, 'focusOnError')
view.showErrors({conditional_release: {type: 'foo'}})
@ -1191,7 +1183,7 @@ QUnit.module('Editview: Intra-Group Peer Review toggle', {
}
})
test('only appears for group assignments', function() {
test('only appears for group assignments', function () {
sandbox.stub(userSettings, 'contextGet').returns({
peer_reviews: '1',
group_category_id: 1,
@ -1202,7 +1194,7 @@ test('only appears for group assignments', function() {
ok(view.$('#intra_group_peer_reviews').is(':visible'))
})
test('does not appear when reviews are being assigned manually', function() {
test('does not appear when reviews are being assigned manually', function () {
sandbox.stub(userSettings, 'contextGet').returns({
peer_reviews: '1',
group_category_id: 1
@ -1212,7 +1204,7 @@ test('does not appear when reviews are being assigned manually', function() {
notOk(view.$('#intra_group_peer_reviews').is(':visible'))
})
test('toggle does not appear when there is no group', function() {
test('toggle does not appear when there is no group', function () {
sandbox.stub(userSettings, 'contextGet').returns({peer_reviews: '1'})
const view = this.editView()
view.$el.appendTo($('#fixtures'))
@ -1248,12 +1240,12 @@ QUnit.module('EditView: Assignment Configuration Tools', {
}
})
test('it attaches assignment configuration component', function() {
test('it attaches assignment configuration component', function () {
const view = this.editView()
equal(view.$similarityDetectionTools.children().size(), 1)
})
test('it is hidden if submission type is not online with a file upload', function() {
test('it is hidden if submission type is not online with a file upload', function () {
const view = this.editView()
view.$el.appendTo($('#fixtures'))
equal(view.$('#similarity_detection_tools').css('display'), 'none')
@ -1288,7 +1280,7 @@ test('it is hidden if submission type is not online with a file upload', functio
equal(view.$('#similarity_detection_tools').css('display'), 'block')
})
test('it is hidden if the plagiarism_detection_platform flag is disabled', function() {
test('it is hidden if the plagiarism_detection_platform flag is disabled', function () {
ENV.PLAGIARISM_DETECTION_PLATFORM = false
const view = this.editView()
view.$('#assignment_submission_type').val('online')
@ -1316,7 +1308,7 @@ QUnit.module('EditView: Assignment External Tools', {
}
})
test('it attaches assignment external tools component', function() {
test('it attaches assignment external tools component', function () {
const view = this.editView()
equal(view.$assignmentExternalTools.children().size(), 1)
})
@ -1347,7 +1339,7 @@ QUnit.module('EditView: Quizzes 2', {
}
})
test('does not show the description textarea', function() {
test('does not show the description textarea', function () {
equal(this.view.$description.length, 0)
})
@ -1355,7 +1347,7 @@ test('does not show the moderated grading checkbox', () => {
equal(document.getElementById('assignment_moderated_grading'), null)
})
test('does not show the load in new tab checkbox', function() {
test('does not show the load in new tab checkbox', function () {
equal(this.view.$externalToolsNewTab.length, 0)
})
@ -1917,3 +1909,51 @@ QUnit.module('EditView#uncheckAndHideGraderAnonymousToGraders', hooks => {
strictEqual(isHidden, true)
})
})
QUnit.module('EditView annotatable document submission', hooks => {
let server
let view
hooks.beforeEach(() => {
fixtures.innerHTML = '<span data-component="ModeratedGradingFormFieldGroup"></span>'
fakeENV.setup({
AVAILABLE_MODERATORS: [],
current_user_roles: ['teacher'],
HAS_GRADED_SUBMISSIONS: false,
LOCALE: 'en',
MODERATED_GRADING_ENABLED: true,
MODERATED_GRADING_MAX_GRADER_COUNT: 2,
VALID_DATE_RANGE: {},
use_rce_enhancements: true,
COURSE_ID: 1,
ANNOTATED_DOCUMENT_SUBMISSIONS: true
})
server = sinon.fakeServer.create()
sandbox.fetch.mock('path:/api/v1/courses/1/lti_apps/launch_definitions', 200)
RCELoader.RCE = null
return RCELoader.loadRCE()
})
hooks.afterEach(() => {
server.restore()
fakeENV.teardown()
tinymce.remove() // Make sure we clean stuff up
$('.ui-dialog').remove()
$('ul[id^=ui-id-]').remove()
$('.form-dialog').remove()
document.getElementById('fixtures').innerHTML = ''
})
test('does not render annotatable document option (flag missing)', function () {
ENV.ANNOTATED_DOCUMENT_SUBMISSIONS = false
view = editView()
equal(view.$('#assignment_annotated_document').length, 0)
})
test('renders annotatable document option (flag turned on)', function () {
ENV.ANNOTATED_DOCUMENT_SUBMISSIONS = true
view = editView()
const label = view.$('#assignment_annotated_document').parent()
ok(label.text().includes('Annotated Document'))
})
})