diff --git a/app/coffeescripts/models/Assignment.coffee b/app/coffeescripts/models/Assignment.coffee index bd4595adffd..13f687e4cb7 100644 --- a/app/coffeescripts/models/Assignment.coffee +++ b/app/coffeescripts/models/Assignment.coffee @@ -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', diff --git a/app/coffeescripts/views/assignments/EditView.coffee b/app/coffeescripts/views/assignments/EditView.coffee index cc2fbf5b8b4..6296277daef 100644 --- a/app/coffeescripts/views/assignments/EditView.coffee +++ b/app/coffeescripts/views/assignments/EditView.coffee @@ -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 diff --git a/app/jsx/assignments/EditAssignment.js b/app/jsx/assignments/EditAssignment.js new file mode 100644 index 00000000000..444a77580ce --- /dev/null +++ b/app/jsx/assignments/EditAssignment.js @@ -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 . + */ + +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 ( + + } + > + }> + + + + ) +} diff --git a/app/jsx/shared/components/ErrorBoundary.js b/app/jsx/shared/components/ErrorBoundary.js index fd6529ac0ab..052f475792f 100644 --- a/app/jsx/shared/components/ErrorBoundary.js +++ b/app/jsx/shared/components/ErrorBoundary.js @@ -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}) } diff --git a/app/views/jst/assignments/_submission_types_form.handlebars b/app/views/jst/assignments/_submission_types_form.handlebars index a03987c4129..97e81ce4eb9 100644 --- a/app/views/jst/assignments/_submission_types_form.handlebars +++ b/app/views/jst/assignments/_submission_types_form.handlebars @@ -7,14 +7,13 @@
{{!-- Submission type accepted --}} - - {{#if submissionTypesFrozen }} - + {{#if submissionTypesFrozen}} + {{/if}} {{!-- Online submission types --}} -
@@ -53,79 +51,85 @@ {{#if kalturaEnabled}} - + + {{/if}} + {{#if annotatedDocumentSubmissionsEnabled}} + +
+
+ {{/if}} {{!-- Online submission restrict file types? --}} -
{{!-- Online submission allowed extensions --}} -
+
- +
{{#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}}
@@ -135,17 +139,15 @@
- {{#t "advanced_turnitin_settings"}}Advanced Turnitin Settings{{/t}} @@ -155,16 +157,14 @@
- {{!-- LTI launch button for getting additional data for external tool assignments (when selected via placement) --}} -
+ {{!-- LTI launch button for getting additional data for external tool assignments (when selected via placement) + --}} +
- - {{!-- this is very specific to Mastery Connect currently. We could potentially make this a dynamic partial based on the type of external data --}} + + {{!-- this is very specific to Mastery Connect currently. We could potentially make this a dynamic partial + based on the type of external data --}}

{{name}}

-
{{externalToolData.points}} {{#if externalToolData.points}} {{#t}}Points{{/t}} {{/if}}
+
{{externalToolData.points}} {{#if externalToolData.points}} + {{#t}}Points{{/t}} {{/if}}
{{externalToolData.objectives}}

{{externalToolData.trackerName}}

{{externalToolData.trackerAlignment}}
-
{{externalToolData.studentCount}} {{externalToolDataStudentLabelText}}
+
{{externalToolData.studentCount}} {{externalToolDataStudentLabelText}} +
{{!-- Default external tool configuration --}} -
+
{{!-- External tool submissions --}} @@ -217,57 +224,48 @@ {{#t}}Enter or find an External Tool URL{{/t}} + name="external_tool_tag_attributes[custom_params]" value="{{externalToolCustomParamsStringified}}" />
- + - {{#if submissionTypesFrozen }} - + {{#if submissionTypesFrozen}} + {{/if}}
- + name="external_tool_tag_attributes[content_type]" type="text" style="display: none" /> +
{{#if groupCategoryId}} -
- {{#t "external_tool_group_warning"}} - Group assignments can't use External Tools. - The group setting will be unchecked when you save - {{/t}} -
+
+ {{#t "external_tool_group_warning"}} + Group assignments can't use External Tools. + The group setting will be unchecked when you save + {{/t}} +
{{/if}}
{{#unless isQuizLTIAssignment}} - + {{/unless}}
- + \ No newline at end of file diff --git a/db/migrate/20210121171158_add_annotatable_attachment_id_to_assignments.rb b/db/migrate/20210121171158_add_annotatable_attachment_id_to_assignments.rb new file mode 100644 index 00000000000..31fda16c065 --- /dev/null +++ b/db/migrate/20210121171158_add_annotatable_attachment_id_to_assignments.rb @@ -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 . +# + +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 diff --git a/lib/api/v1/assignment.rb b/lib/api/v1/assignment.rb index 296c8d6c2d5..25e2a1453b3 100644 --- a/lib/api/v1/assignment.rb +++ b/lib/api/v1/assignment.rb @@ -574,6 +574,7 @@ module Api::V1::Assignment "media_recording", "not_graded", "wiki_page", + "annotated_document", "" ].freeze diff --git a/spec/coffeescripts/views/assignments/EditViewSpec.js b/spec/coffeescripts/views/assignments/EditViewSpec.js index 1c3cb65aeea..68d931b80e5 100644 --- a/spec/coffeescripts/views/assignments/EditViewSpec.js +++ b/spec/coffeescripts/views/assignments/EditViewSpec.js @@ -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 = '

<E>

' const view = this.editView({description: '

<E>

'}) 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')) $('').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('') $('').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('') $('').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 = '' + 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')) + }) +})