move final grades override setting to modal

closes: GRADE-1867

test plan:
 - checking and unchecking the final grade override checkbox still works
 - interacting with both the late policies and the advanced tab works as
   expected (two separate requests that both fire on save)
 - interacting with just the late policies tab does not trigger a
   request to save the gradebook settings
 - interacting with just the advanced tab settings does not trigger a
   request to save late policy settings

Change-Id: I5c5bfdc7c62cf0adec64d06b8e2af88c4f39ff7a
Reviewed-on: https://gerrit.instructure.com/175559
Reviewed-by: Gary Mei <gmei@instructure.com>
Reviewed-by: Keith Garner <kgarner@instructure.com>
Tested-by: Jenkins
QA-Review: Adrian Packel <apackel@instructure.com>
Product-Review: Jonathan Fenton <jfenton@instructure.com>
This commit is contained in:
Derek Bender 2018-12-10 13:44:52 -06:00
parent 44e5d114dc
commit 21e45fa076
24 changed files with 2310 additions and 1364 deletions

View File

@ -2,6 +2,10 @@ function appAndSpecDirsFor(dir) {
return `{app/jsx,app/coffeescripts,spec/javascripts/jsx,spec/coffeescripts}/${dir}/**/*.js`
}
function appAndSpecFilesFor(path) {
return `{app/jsx,app/coffeescripts,spec/javascripts/jsx,spec/coffeescripts}/${path}{,Spec}.js`
}
// If you are starting a new project or section of greenfield code,
// or if there is a folder of code that your team controls that you want
// to start ensuring conforms to prettier, add it to this array to opt-in
@ -12,6 +16,8 @@ const PRETTIER_WHITELIST = module.exports = [
'frontend_build/**/*.js',
'script/**/*.js',
'app/jsx/account_settings/**/*.js',
appAndSpecFilesFor('gradezilla/default_gradebook/components/AdvancedTabPanel'),
appAndSpecFilesFor('gradezilla/default_gradebook/components/GradebookSettingsModal'),
appAndSpecDirsFor('account_course_user_search'),
appAndSpecDirsFor('announcements'),
appAndSpecDirsFor('assignments_2'),

View File

@ -104,8 +104,8 @@ define [
EnterGradesAsSetting, SetDefaultGradeDialogManager, CurveGradesDialogManager, GradebookApi, SubmissionCommentApi,
FinalGradeOverrides, GradebookGrid, studentRowHeaderConstants, AssignmentRowCellPropFactory, GradebookMenu, ViewOptionsMenu, ActionMenu,
AssignmentGroupFilter, GradingPeriodFilter, ModuleFilter, SectionFilter, GridColor, StatusesModal, SubmissionTray,
GradebookSettingsModal, AnonymousSpeedGraderAlert, { statusColors }, StudentDatastore, PostGradesStore, PostGradesApp, SubmissionStateMap,
DownloadSubmissionsDialogManager, ReuploadSubmissionsDialogManager, GradebookKeyboardNav,
GradebookSettingsModal, AnonymousSpeedGraderAlert, { statusColors }, StudentDatastore, PostGradesStore, PostGradesApp,
SubmissionStateMap, DownloadSubmissionsDialogManager, ReuploadSubmissionsDialogManager, GradebookKeyboardNav,
AssignmentMuterDialogManager, assignmentHelper, TextMeasure, GradeInputHelper, { default: OutlierScoreHelper },
LatePolicyApplicator, { default: Button }, { default: IconSettingsSolid }, FlashAlert) ->
@ -178,8 +178,6 @@ define [
showEnrollments:
concluded: false
inactive: false
showUnpublishedAssignments: true
showFinalGradeOverrides: false
sortRowsBy:
columnId: sortRowsByColumnId # the column controlling the sort
settingKey: sortRowsBySettingKey # the key describing the sort criteria
@ -454,6 +452,7 @@ define [
@gridReady.then () =>
@renderViewOptionsMenu()
@renderGradebookSettingsModal()
# called from app/jsx/bundles/gradezilla.js
onShow: ->
@ -1297,11 +1296,12 @@ define [
onSelect: onSelect
selected: showingNotes
getOverridesViewOptionsMenuProps: ->
disabled: @contentLoadStates.overridesColumnUpdating || @gridReady.state() != 'resolved'
label: if @options.grading_period_set then I18n.t('Grading Period Overrides') else I18n.t('Overrides')
onSelect: @toggleOverrides
selected: @gridDisplaySettings.showFinalGradeOverrides
getFinalGradeOverridesSettingsModalProps: ->
disabled: !@options.final_grade_override_enabled ||
@contentLoadStates.overridesColumnUpdating ||
@gridReady.state() != 'resolved'
onChange: @toggleOverrides
defaultChecked: @gridDisplaySettings.showFinalGradeOverrides
getColumnSortSettingsViewOptionsMenuProps: ->
storedSortOrder = @getColumnOrder()
@ -1346,8 +1346,6 @@ define [
getViewOptionsMenuProps: ->
teacherNotes: @getTeacherNotesViewOptionsMenuProps()
overrides: @getOverridesViewOptionsMenuProps()
finalGradeOverrideEnabled: @options.final_grade_override_enabled
columnSortSettings: @getColumnSortSettingsViewOptionsMenuProps()
filterSettings: @getFilterSettingsViewOptionsMenuProps()
showUnpublishedAssignments: @gridDisplaySettings.showUnpublishedAssignments
@ -1422,6 +1420,7 @@ define [
onClose: => @gradebookSettingsModalButton.focus()
onLatePolicyUpdate: @onLatePolicyUpdate
gradedLateSubmissionsExist: @options.graded_late_submissions_exist
overrides: @getFinalGradeOverridesSettingsModalProps()
@gradebookSettingsModal = renderComponent(
GradebookSettingsModal,
gradebookSettingsModalMountPoint,
@ -1652,6 +1651,7 @@ define [
id: 'total_grade_override'
maxWidth: columnWidths.total_grade_override.max
minWidth: columnWidths.total_grade_override.min
toolTip: label
type: 'total_grade_override'
width: totalWidth
}
@ -2103,6 +2103,10 @@ define [
@updateColumns()
@renderViewOptionsMenu()
updateColumnsAndRenderGradebookSettingsModal: =>
@updateColumns()
@renderGradebookSettingsModal()
## React Header Component Ref Methods
setHeaderComponentRef: (columnId, ref) =>
@ -2408,15 +2412,15 @@ define [
@gridDisplaySettings.showUnpublishedAssignments = showUnpublishedAssignments == 'true'
toggleUnpublishedAssignments: =>
@gridDisplaySettings.showUnpublishedAssignments = !@gridDisplaySettings.showUnpublishedAssignments
@updateColumnsAndRenderViewOptionsMenu()
toggleableAction = =>
@gridDisplaySettings.showUnpublishedAssignments = !@gridDisplaySettings.showUnpublishedAssignments
@updateColumnsAndRenderViewOptionsMenu()
toggleableAction()
@saveSettings(
{ showUnpublishedAssignments: @gridDisplaySettings.showUnpublishedAssignments },
() =>, # on success, do nothing since the render happened earlier
() => # on failure, undo
@gridDisplaySettings.showUnpublishedAssignments = !@gridDisplaySettings.showUnpublishedAssignments
@updateColumnsAndRenderViewOptionsMenu()
toggleableAction
)
initShowOverrides: (showFinalGradeOverrides = 'false') =>
@ -2425,17 +2429,18 @@ define [
setShowFinalGradeOverrides: (show) =>
@gridDisplaySettings.showFinalGradeOverrides = show
toggleOverrides: =>
@setShowFinalGradeOverrides(!@gridDisplaySettings.showFinalGradeOverrides)
@updateColumnsAndRenderViewOptionsMenu()
toggleOverrides: () =>
toggleableAction = =>
@setShowFinalGradeOverrides(!@gridDisplaySettings.showFinalGradeOverrides)
@updateColumnsAndRenderGradebookSettingsModal()
@saveSettings(
{ showFinalGradeOverrides: @gridDisplaySettings.showFinalGradeOverrides },
() =>, # on success, do nothing since the render happened earlier
() => # on failure, undo
@gridDisplaySettings.showFinalGradeOverrides = !@gridDisplaySettings.showFinalGradeOverrides
@updateColumnsAndRenderViewOptionsMenu()
)
toggleableAction()
new Promise (resolve, reject) =>
@saveSettings(
{ showFinalGradeOverrides: @gridDisplaySettings.showFinalGradeOverrides },
() =>, # on success, do nothing since the render happened earlier
toggleableAction
).done(resolve).fail(reject)
setAssignmentsLoaded: (loaded) =>
@contentLoadStates.assignmentsLoaded = loaded

View File

@ -77,7 +77,7 @@ export function createGradebook(options = {}) {
}
export function setFixtureHtml($fixture) {
$fixture.innerHTML = `
return $fixture.innerHTML = `
<div id="application">
<div id="wrapper">
<div data-component="GridColor"></div>
@ -112,5 +112,5 @@ export function stubDataLoader() {
gotSubmissions: $.Deferred()
}
window.sandbox.stub(DataLoader, 'loadGradebookData').returns(dataLoaderPromises)
return window.sandbox.stub(DataLoader, 'loadGradebookData').returns(dataLoaderPromises)
}

View File

@ -65,10 +65,3 @@ export function updateLatePolicy (courseId, latePolicyData) {
const data = { late_policy: underscore(latePolicyData) };
return axios.patch(url, data);
}
export default {
DEFAULT_LATE_POLICY_DATA,
fetchLatePolicy,
createLatePolicy,
updateLatePolicy
};

View File

@ -0,0 +1,43 @@
/*
* Copyright (C) 2018 - 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 from 'react'
import {bool, func, shape} from 'prop-types'
import Checkbox from '@instructure/ui-forms/lib/components/Checkbox'
import View from '@instructure/ui-layout/lib/components/View'
import I18n from 'i18n!gradebook'
export default function AdvancedTabPanel({overrides}) {
return (
<div id="AdvancedTabPanel__Container">
<View as="div" margin="small">
<Checkbox {...overrides} label={I18n.t('Allow final grade override')} />
</View>
</div>
)
}
AdvancedTabPanel.propTypes = {
overrides: shape({
defaultChecked: bool.isRequired,
disabled: bool.isRequired,
onChange: func.isRequired
}).isRequired
}

View File

@ -16,17 +16,49 @@
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import React from 'react';
import { bool, func, string } from 'prop-types';
import _ from 'underscore';
import Button from '@instructure/ui-buttons/lib/components/Button';
import LatePoliciesTabPanel from '../../../gradezilla/default_gradebook/components/LatePoliciesTabPanel';
import GradebookSettingsModalApi from '../../../gradezilla/default_gradebook/apis/GradebookSettingsModalApi';
import Modal, { ModalBody, ModalFooter, ModalHeader } from '@instructure/ui-overlays/lib/components/Modal';
import Heading from '@instructure/ui-elements/lib/components/Heading';
import TabList, { TabPanel } from '@instructure/ui-tabs/lib/components/TabList';
import I18n from 'i18n!gradebook';
import { showFlashAlert } from '../../../shared/FlashAlert';
import React from 'react'
import {bool, func, shape, string} from 'prop-types'
import _ from 'underscore'
import I18n from 'i18n!gradebook'
import Button from '@instructure/ui-buttons/lib/components/Button'
import Modal, {ModalBody, ModalFooter} from '@instructure/ui-overlays/lib/components/Modal'
import TabList, {TabPanel} from '@instructure/ui-tabs/lib/components/TabList'
import AdvancedTabPanel from './AdvancedTabPanel'
import {
fetchLatePolicy,
createLatePolicy,
updateLatePolicy
} from '../apis/GradebookSettingsModalApi'
import LatePoliciesTabPanel from './LatePoliciesTabPanel'
import {showFlashAlert} from '../../../shared/FlashAlert'
function isLatePolicySaveable({latePolicy: {changes, validationErrors}}) {
return !_.isEmpty(changes) && _.isEmpty(validationErrors)
}
function isOverridesChanged({
props: {
overrides: {defaultChecked}
},
state: {overrides}
}) {
return defaultChecked !== overrides
}
function onSaveSettingsFailure() {
const message = I18n.t('An error occurred while saving your settings')
showFlashAlert({message, type: 'error'})
return Promise.reject(new Error(message))
}
function onUpdateSuccess({close}) {
const message = I18n.t('Gradebook Settings updated')
showFlashAlert({message, type: 'success'})
close()
return Promise.resolve()
}
class GradebookSettingsModal extends React.Component {
static propTypes = {
@ -34,96 +66,117 @@ class GradebookSettingsModal extends React.Component {
locale: string.isRequired,
onClose: func.isRequired,
gradedLateSubmissionsExist: bool.isRequired,
onLatePolicyUpdate: func.isRequired
onLatePolicyUpdate: func.isRequired,
overrides: shape({
disabled: bool.isRequired,
onChange: func.isRequired,
defaultChecked: bool.isRequired
})
}
constructor (props) {
super(props);
this.state = {
isOpen: false,
latePolicy: { changes: {}, validationErrors: {} }
};
state = {
isOpen: false,
latePolicy: {changes: {}, validationErrors: {}},
overrides: this.props.overrides.defaultChecked,
processingRequests: false
}
onFetchLatePolicySuccess = ({ data }) => {
this.changeLatePolicy({ ...this.state.latePolicy, data: data.latePolicy });
onFetchLatePolicySuccess = ({data}) => {
this.changeLatePolicy({...this.state.latePolicy, data: data.latePolicy})
}
onFetchLatePolicyFailure = () => {
const message = I18n.t('An error occurred while loading late policies');
showFlashAlert({ message, type: 'error' });
const message = I18n.t('An error occurred while loading late policies')
showFlashAlert({message, type: 'error'})
}
onUpdateLatePolicySuccess = () => {
const message = I18n.t('Late policies updated');
showFlashAlert({ message, type: 'success' });
this.props.onLatePolicyUpdate({...this.state.latePolicy.data, ...this.state.latePolicy.changes});
this.close();
}
onUpdateLatePolicyFailure = () => {
const message = I18n.t('An error occurred while updating late policies');
showFlashAlert({ message, type: 'error' });
}
handleUpdateButtonClicked = () => {
if (this.state.latePolicy.data.newRecord) {
this.createLatePolicy();
} else {
this.updateLatePolicy();
}
onSaveLatePolicyFailure = () => {
const message = I18n.t('An error occurred while updating late policies')
showFlashAlert({message, type: 'error'})
return Promise.reject(new Error(message))
}
fetchLatePolicy = () => {
GradebookSettingsModalApi
.fetchLatePolicy(this.props.courseId)
fetchLatePolicy(this.props.courseId)
.then(this.onFetchLatePolicySuccess)
.catch(this.onFetchLatePolicyFailure);
.catch(this.onFetchLatePolicyFailure)
}
createLatePolicy = () => {
GradebookSettingsModalApi
.createLatePolicy(this.props.courseId, this.state.latePolicy.changes)
.then(this.onUpdateLatePolicySuccess)
.catch(this.onUpdateLatePolicyFailure);
saveLatePolicy = () => {
const createOrUpdate = this.state.latePolicy.data.newRecord
? createLatePolicy
: updateLatePolicy
return createOrUpdate(this.props.courseId, this.state.latePolicy.changes)
.then(() =>
this.props.onLatePolicyUpdate({
...this.state.latePolicy.data,
...this.state.latePolicy.changes
})
)
.catch(this.onSaveLatePolicyFailure)
}
updateLatePolicy = () => {
GradebookSettingsModalApi
.updateLatePolicy(this.props.courseId, this.state.latePolicy.changes)
.then(this.onUpdateLatePolicySuccess)
.catch(this.onUpdateLatePolicyFailure);
saveSettings = () => this.props.overrides.onChange().catch(onSaveSettingsFailure)
handleUpdateButtonClicked = () => {
const promises = []
this.setState({processingRequests: true}, () => {
if (isLatePolicySaveable(this.state)) {
promises.push(this.saveLatePolicy())
}
if (isOverridesChanged(this)) {
promises.push(this.saveSettings())
}
// can't use finally() to remove the duplication because we need to
// skip onUpdateSuccess if an earlier promise rejected and removing the
// last catch will mean these rejected promises are uncaught, which
// causes `Uncaught (in promise) Error` to be logged in the console
Promise.all(promises)
.then(() => onUpdateSuccess(this))
.then(() => this.setState({processingRequests: false}))
.catch(() => this.setState({processingRequests: false}))
})
}
changeLatePolicy = (latePolicy) => {
this.setState({ latePolicy });
changeLatePolicy = latePolicy => {
this.setState({latePolicy})
}
isUpdateButtonDisabled = () => {
const { latePolicy: { changes, validationErrors } } = this.state;
return _.isEmpty(changes) || !_.isEmpty(validationErrors);
changeOverrides = ({target: {checked}}) => {
this.setState({overrides: checked})
}
isUpdateButtonEnabled = () => {
if (this.state.processingRequests) return false
return isOverridesChanged(this) || isLatePolicySaveable(this.state)
}
open = () => {
this.setState({ isOpen: true });
this.setState({isOpen: true})
}
close = () => {
this.setState({ isOpen: false }, () => {
const latePolicy = { changes: {}, data: undefined, validationErrors: {} };
this.setState({isOpen: false}, () => {
const latePolicy = {changes: {}, data: undefined, validationErrors: {}}
// need to reset the latePolicy state _after_ the modal is closed, otherwise
// the spinner will be visible for a brief moment before the modal closes.
this.setState({ latePolicy });
});
this.setState({latePolicy})
})
}
render () {
const { isOpen, latePolicy } = this.state;
render() {
const overrides = {
disabled: this.props.overrides.disabled,
onChange: this.changeOverrides,
defaultChecked: this.state.overrides
}
return (
<Modal
size="large"
open={isOpen}
open={this.state.isOpen}
label={I18n.t('Gradebook Settings')}
onOpen={this.fetchLatePolicy}
onDismiss={this.close}
@ -131,38 +184,37 @@ class GradebookSettingsModal extends React.Component {
>
<ModalBody>
<TabList defaultSelectedIndex={0}>
<TabPanel id="late-policies-tab" title={I18n.t('Late Policies')}>
<TabPanel title={I18n.t('Late Policies')}>
<LatePoliciesTabPanel
latePolicy={latePolicy}
latePolicy={this.state.latePolicy}
changeLatePolicy={this.changeLatePolicy}
locale={this.props.locale}
showAlert={this.props.gradedLateSubmissionsExist}
/>
</TabPanel>
<TabPanel title={I18n.t('Advanced')}>
<AdvancedTabPanel overrides={overrides} />
</TabPanel>
</TabList>
</ModalBody>
<ModalFooter>
<Button
id="gradebook-settings-cancel-button"
onClick={this.close}
margin="0 small"
>
<Button id="gradebook-settings-cancel-button" onClick={this.close} margin="0 small">
{I18n.t('Cancel')}
</Button>
<Button
id="gradebook-settings-update-button"
onClick={this.handleUpdateButtonClicked}
disabled={this.isUpdateButtonDisabled()}
disabled={!this.isUpdateButtonEnabled()}
variant="primary"
>
{I18n.t('Update')}
</Button>
</ModalFooter>
</Modal>
);
)
}
}
export default GradebookSettingsModal;
export default GradebookSettingsModal

View File

@ -67,13 +67,6 @@ class ViewOptionsMenu extends React.Component {
onSelect: func.isRequired,
selected: bool.isRequired
}).isRequired,
overrides: shape({
disabled: bool.isRequired,
label: string.isRequired,
onSelect: func.isRequired,
selected: bool.isRequired
}),
finalGradeOverrideEnabled: bool.isRequired,
onSelectShowStatusesModal: func.isRequired,
showUnpublishedAssignments: bool.isRequired,
onSelectShowUnpublishedAssignments: func.isRequired
@ -240,16 +233,6 @@ class ViewOptionsMenu extends React.Component {
>
{I18n.t('Unpublished Assignments')}
</MenuItem>
{this.props.finalGradeOverrideEnabled &&
<MenuItem
disabled={this.props.overrides.disabled}
onSelect={this.props.overrides.onSelect}
selected={this.props.overrides.selected}
>
<span data-menu-item-id="show-overrides-column">{this.props.overrides.label}</span>
</MenuItem>
}
</MenuItemGroup>
</Menu>
);

View File

@ -231,7 +231,9 @@ export function showFlashAlert ({ message, err, type = err ? 'error' : 'info', s
export function destroyContainer () {
const container = document.getElementById(messageHolderId)
const liveRegion = document.getElementById(screenreaderMessageHolderId)
if (container) container.remove()
if (liveRegion) liveRegion.remove()
}
export function showFlashError (message = I18n.t('An error occurred making a network request')) {

View File

@ -19,10 +19,7 @@
require 'spec_helper'
RSpec.describe GradebookSettingsController, type: :controller do
let!(:teacher) do
course_with_teacher
@teacher
end
let(:teacher) { course_with_teacher.user }
before do
user_session(teacher)
@ -30,10 +27,8 @@ RSpec.describe GradebookSettingsController, type: :controller do
end
describe "PUT update" do
let(:json_response) { JSON.parse(response.body) }
context "given valid params" do
let(:show_settings) do
let(:gradebook_settings) do
{
"enter_grades_as" => {
"2301" => "points"
@ -65,122 +60,143 @@ RSpec.describe GradebookSettingsController, type: :controller do
}
end
let(:show_settings_massaged) do
show_settings.merge('filter_rows_by' => { 'section_id' => nil })
let(:gradebook_settings_massaged) do
gradebook_settings.merge('filter_rows_by' => { 'section_id' => nil })
end
let(:valid_params) do
{
"course_id" => @course.id,
"gradebook_settings" => show_settings
"gradebook_settings" => gradebook_settings
}
end
it "saves new gradebook_settings in preferences" do
put :update, params: valid_params
expect(response).to be_ok
let(:expected_settings) do
{
@course.id => gradebook_settings_massaged.except("colors"),
colors: gradebook_settings_massaged.fetch("colors")
}.as_json
end
expected_settings = {
@course.id => show_settings_massaged.except("colors"),
colors: show_settings_massaged.fetch("colors")
}
expect(teacher.preferences[:gradebook_settings]).to eq expected_settings
expect(json_response["gradebook_settings"]).to eql expected_settings.as_json
context 'given a valid PUT request' do
subject { json_parse.fetch('gradebook_settings').fetch(@course.id.to_s) }
before { put :update, params: valid_params }
it { expect(response).to be_ok }
it { is_expected.to include 'enter_grades_as' => {'2301' => 'points'} }
it { is_expected.to include 'filter_columns_by' => {'grading_period_id' => '1401', 'assignment_group_id' => '888'} }
it { is_expected.to include 'filter_rows_by' => {'section_id' => nil} }
it { is_expected.to include 'selected_view_options_filters' => ['assignmentGroups'] }
it { is_expected.to include 'show_inactive_enrollments' => 'true' }
it { is_expected.to include 'show_concluded_enrollments' => 'false' }
it { is_expected.to include 'show_unpublished_assignments' => 'true' }
it { is_expected.to include 'show_final_grade_overrides' => 'false' }
it { is_expected.to include 'student_column_display_as' => 'last_first' }
it { is_expected.to include 'student_column_secondary_info' => 'login_id' }
it { is_expected.to include 'sort_rows_by_column_id' => 'student' }
it { is_expected.to include 'sort_rows_by_setting_key' => 'sortable_name' }
it { is_expected.to include 'sort_rows_by_direction' => 'descending' }
it { is_expected.not_to include 'colors' }
it { is_expected.to have(13).items } # ensure we add specs for new additions
context 'colors' do
subject { json_parse.fetch('gradebook_settings').fetch('colors') }
it { is_expected.to have(5).items } # ensure we add specs for new additions
it do
is_expected.to include({
'late' => '#000000',
'missing' => '#000001',
'resubmitted' => '#000002',
'dropped' => '#000003',
'excused' => '#000004'
})
end
end
end
it "transforms 'null' string values to nil" do
put :update, params: valid_params
expect(teacher.preferences[:gradebook_settings][@course.id]['filter_rows_by']['section_id']).to be_nil
section_id = teacher.preferences.
fetch(:gradebook_settings).
fetch(@course.id).
fetch('filter_rows_by').
fetch('section_id')
expect(section_id).to be_nil
end
it "allows saving gradebook settings for multiple courses" do
previous_course = Course.create!(name: 'Previous Course')
teacher.preferences[:gradebook_settings] = {
previous_course.id => show_settings_massaged.except("colors"),
colors: show_settings_massaged.fetch("colors")
}
teacher.save!
teacher.update!(preferences: {
gradebook_settings: {
previous_course.id => gradebook_settings_massaged.except("colors"),
colors: gradebook_settings_massaged.fetch("colors")
}
})
put :update, params: valid_params
expected_user_settings = {
@course.id => show_settings_massaged.except("colors"),
previous_course.id => show_settings_massaged.except("colors"),
colors: show_settings_massaged.fetch("colors")
}
expected_response = {
@course.id => show_settings_massaged.except("colors"),
colors: show_settings_massaged.fetch("colors")
}
expect(teacher.reload.preferences[:gradebook_settings]).to eq(expected_user_settings)
expect(json_response["gradebook_settings"]).to eql(expected_response.as_json)
expect(json_parse.fetch('gradebook_settings')).to eql expected_settings
end
it "is allowed for courses in concluded enrollment terms" do
term = teacher.account.enrollment_terms.create!(start_at: 2.months.ago, end_at: 1.month.ago)
@course.enrollment_term = term # `update_attribute` with a term has unwanted side effects
@course.save!
@course.update!(enrollment_term: teacher.account.enrollment_terms.create!(start_at: 2.months.ago, end_at: 1.month.ago))
put :update, params: valid_params
expect(response).to be_ok
expected_settings = {
@course.id => show_settings_massaged.except("colors"),
colors: show_settings_massaged.fetch("colors")
}
expect(teacher.preferences[:gradebook_settings]).to eq expected_settings
expect(json_response["gradebook_settings"]).to eql expected_settings.as_json
expect(json_parse.fetch('gradebook_settings')).to eql expected_settings
end
it "is allowed for courses with concluded workflow state" do
@course.workflow_state = "concluded"
@course.save!
@course.update!(workflow_state: "concluded")
put :update, params: valid_params
expect(response).to be_ok
expected_settings = {
@course.id => show_settings_massaged.except("colors"),
colors: show_settings_massaged.fetch("colors")
}
expect(teacher.preferences[:gradebook_settings]).to eq expected_settings
expect(json_response["gradebook_settings"]).to eql expected_settings.as_json
expect(json_parse.fetch('gradebook_settings')).to eql expected_settings
end
context "given invalid status colors (but otherwise valid params)" do
subject { response }
let(:malevolent_color) { "; background: url(https://httpbin.org/basic-auth/user/passwd)" }
let(:invalid_params) do
{
"course_id" => @course.id,
"gradebook_settings" => {
"colors" => {
"dropped" => "#FEF0E5",
"excused" => "#FEF7E5",
"late" => "#cccccc",
"missing" => malevolent_color,
"resubmitted" => "#E5F7E5"
}
}
}
end
before { put :update, params: invalid_params }
it { is_expected.to be_ok }
it "does not store invalid status colors" do
colors = json_parse.fetch("gradebook_settings").fetch("colors")
expect(colors).not_to have_key "missing"
end
end
end
context "given invalid params" do
it "give an error response" do
subject { response }
before do
invalid_params = { "course_id" => @course.id }
put :update, params: invalid_params
expect(response).not_to be_ok
expect(json_response).to include(
"errors" => [{
"message" => "gradebook_settings is missing"
}]
)
end
it "does not store invalid status colors" do
malevolent_color = "; background: url(https://httpbin.org/basic-auth/user/passwd)"
invalid_params = {
"course_id" => @course.id,
"gradebook_settings" => {
"colors" => {
"dropped" => "#FEF0E5",
"excused" => "#FEF7E5",
"late" => "#cccccc",
"missing" => malevolent_color,
"resubmitted" => "#E5F7E5"
}
}
}
put :update, params: invalid_params
it { is_expected.to have_http_status :bad_request }
expect(response).to be_ok
expect(json_response["gradebook_settings"]["colors"]).not_to have_key("missing")
it "gives an error message" do
expect(json_parse).to include "errors" => [{"message" => "gradebook_settings is missing"}]
end
end
end

View File

@ -22,6 +22,8 @@ import {mount} from 'enzyme'
import GradeSelect from 'jsx/assignments/GradeSummary/components/GradesGrid/GradeSelect'
import {FAILURE, STARTED, SUCCESS} from 'jsx/assignments/GradeSummary/grades/GradeActions'
import {waitFor} from '../../../../support/Waiters'
function Container(props) {
/*
* This class exists because Enzyme does not update props of children, which
@ -241,26 +243,6 @@ QUnit.module('GradeSummary GradeSelect', suiteHooks => {
}
}
async function waitFor(conditionFn, timeout = 200) {
return new Promise((resolve, reject) => {
let timeoutId
const intervalId = setInterval(() => {
const result = conditionFn()
if (result) {
clearInterval(intervalId)
clearTimeout(timeoutId)
resolve(result)
}
}, 10)
timeoutId = setTimeout(() => {
clearInterval(intervalId)
reject(new Error('Timeout waiting for condition'))
}, timeout)
})
}
test('renders a text input', async () => {
await mountComponent()
const input = wrapper.find('input[type="text"]')

View File

@ -39,6 +39,7 @@ import SubmissionStateMap from 'jsx/gradezilla/SubmissionStateMap';
import studentRowHeaderConstants from 'jsx/gradezilla/default_gradebook/constants/studentRowHeaderConstants';
import { darken, statusColors, defaultColors } from 'jsx/gradezilla/default_gradebook/constants/colors';
import ViewOptionsMenu from 'jsx/gradezilla/default_gradebook/components/ViewOptionsMenu';
import GradebookSettingsModal from 'jsx/gradezilla/default_gradebook/components/GradebookSettingsModal';
import { createGradebook, stubDataLoader } from 'jsx/gradezilla/default_gradebook/__tests__/GradebookSpecHelper';
import { createCourseGradesWithGradingPeriods as createGrades } from '../gradebook/GradeCalculatorSpecHelper';
@ -254,40 +255,51 @@ test('updates partial .filterColumnsBy settings with the default values', functi
strictEqual(gradebook.getFilterColumnsBySetting('gradingPeriodId'), null);
});
QUnit.module('Gradebook#initialize', {
setup () {
stubDataLoader()
$fixtures.innerHTML = `
<div id="search-filter-container">
<input type="text" />
</div>
`;
},
QUnit.module('Gradebook#initialize', () => {
QUnit.module('with dataloader stubs', (moduleHooks) => {
moduleHooks.beforeEach(() => {
stubDataLoader()
$fixtures.innerHTML = `
<div id="search-filter-container">
<input type="text" />
</div>
`
})
createInitializedGradebook (options) {
const gradebook = createGradebook(options);
gradebook.initialize();
return gradebook;
},
moduleHooks.afterEach(() => {
$fixtures.innerHTML = ''
})
teardown () {
$fixtures.innerHTML = '';
}
});
function createInitializedGradebook (options) {
const gradebook = createGradebook(options)
gradebook.initialize()
return gradebook
}
test('stores the late policy with camelized keys, if one exists', function () {
const gradebook = this.createInitializedGradebook({ late_policy: { late_submission_interval: 'hour' } });
deepEqual(gradebook.courseContent.latePolicy, { lateSubmissionInterval: 'hour' });
});
test('stores the late policy with camelized keys, if one exists', () => {
const gradebook = createInitializedGradebook({ late_policy: { late_submission_interval: 'hour' } })
deepEqual(gradebook.courseContent.latePolicy, { lateSubmissionInterval: 'hour' })
})
test('stores the late policy as undefined if the late_policy option is null', function () {
const gradebook = this.createInitializedGradebook({ late_policy: null });
strictEqual(gradebook.courseContent.latePolicy, undefined);
});
test('stores the late policy as undefined if the late_policy option is null', () => {
const gradebook = createInitializedGradebook({ late_policy: null })
strictEqual(gradebook.courseContent.latePolicy, undefined)
})
test('sets assignmentGroupsLoaded to false', function () {
const gradebook = this.createInitializedGradebook()
strictEqual(gradebook.contentLoadStates.assignmentGroupsLoaded, false)
test('sets assignmentGroupsLoaded to false', function () {
const gradebook = createInitializedGradebook()
strictEqual(gradebook.contentLoadStates.assignmentGroupsLoaded, false)
})
})
test('calls DataLoader.loadGradebookData with getFinalGradeOverrides', () => {
const gradebook = createGradebook()
const loadGradebookDataStub = stubDataLoader()
gradebook.initialize()
const {firstCall: {args: [{getFinalGradeOverrides}]}} = loadGradebookDataStub
strictEqual(getFinalGradeOverrides, false)
loadGradebookDataStub.restore()
})
})
QUnit.module('Gradebook#gotChunkOfStudents', {
@ -2363,16 +2375,6 @@ QUnit.module('Gradebook#getViewOptionsMenuProps', () => {
consoleSpy.restore()
})
test('finalGradeOverrideEnabled is false', () => {
const {finalGradeOverrideEnabled} = createGradebook().getViewOptionsMenuProps()
strictEqual(finalGradeOverrideEnabled, false)
})
test('finalGradeOverrideEnabled is set via final_grade_override_enabled', () => {
const {finalGradeOverrideEnabled} = createGradebook({final_grade_override_enabled: true}).getViewOptionsMenuProps()
strictEqual(finalGradeOverrideEnabled, true)
})
test('showUnpublishedAssignments is true', () => {
const {showUnpublishedAssignments} = createGradebook().getViewOptionsMenuProps()
strictEqual(showUnpublishedAssignments, true)
@ -5242,6 +5244,30 @@ QUnit.module('Gradebook#updateColumnsAndRenderViewOptionsMenu', function (hooks)
});
});
QUnit.module('Gradebook#updateColumnsAndRenderGradebookSettingsModal', (moduleHooks) => {
let gradebook
moduleHooks.beforeEach(() => {
gradebook = createGradebook()
sinon.stub(gradebook, 'updateColumns')
sinon.stub(gradebook, 'renderGradebookSettingsModal')
})
moduleHooks.afterEach(() => {
gradebook.destroy()
})
test('calls updateColumns', () => {
gradebook.updateColumnsAndRenderGradebookSettingsModal()
strictEqual(gradebook.updateColumns.callCount, 1)
})
test('calls renderGradebookSettingsModal', () => {
gradebook.updateColumnsAndRenderGradebookSettingsModal()
strictEqual(gradebook.renderGradebookSettingsModal.callCount, 1)
})
})
QUnit.module('Gradebook React Header Component References', {
setup () {
this.gradebook = createGradebook();
@ -5468,8 +5494,8 @@ QUnit.module('Gradebook#toggleUnpublishedAssignments', () => {
QUnit.module('Gradebook#toggleOverrides', () => {
test('toggles showFinalGradeOverrides to true when currently false', function () {
const gradebook = createGradebook();
gradebook.gridDisplaySettings.showFinalGradeOverrides = false;
sandbox.stub(gradebook, 'updateColumnsAndRenderViewOptionsMenu');
gradebook.setShowFinalGradeOverrides(false);
sandbox.stub(gradebook, 'updateColumnsAndRenderGradebookSettingsModal');
sandbox.stub(gradebook, 'saveSettings');
gradebook.toggleOverrides();
@ -5478,8 +5504,8 @@ QUnit.module('Gradebook#toggleOverrides', () => {
test('toggles showFinalGradeOverrides to false when currently true', function () {
const gradebook = createGradebook();
gradebook.gridDisplaySettings.showFinalGradeOverrides = true;
sandbox.stub(gradebook, 'updateColumnsAndRenderViewOptionsMenu');
gradebook.setShowFinalGradeOverrides(true);
sandbox.stub(gradebook, 'updateColumnsAndRenderGradebookSettingsModal');
sandbox.stub(gradebook, 'saveSettings');
gradebook.toggleOverrides();
@ -5488,8 +5514,8 @@ QUnit.module('Gradebook#toggleOverrides', () => {
test('calls showFinalGradeOverrides after toggling', function () {
const gradebook = createGradebook();
gradebook.gridDisplaySettings.showFinalGradeOverrides = true;
const stubFn = sandbox.stub(gradebook, 'updateColumnsAndRenderViewOptionsMenu').callsFake(function () {
gradebook.setShowFinalGradeOverrides(true);
const stubFn = sandbox.stub(gradebook, 'updateColumnsAndRenderGradebookSettingsModal').callsFake(function () {
strictEqual(gradebook.gridDisplaySettings.showFinalGradeOverrides, false);
});
sandbox.stub(gradebook, 'saveSettings');
@ -5501,7 +5527,7 @@ QUnit.module('Gradebook#toggleOverrides', () => {
test('calls saveSettings with showFinalGradeOverrides', function () {
const gradebookProps = {settings: {show_final_grade_overrides: 'true'}, final_grade_override_enabled: true}
const gradebook = createGradebook(gradebookProps);
sandbox.stub(gradebook, 'updateColumnsAndRenderViewOptionsMenu');
sandbox.stub(gradebook, 'updateColumnsAndRenderGradebookSettingsModal');
const saveSettingsStub = sandbox.stub(gradebook, 'saveSettings');
gradebook.toggleOverrides();
@ -5517,8 +5543,8 @@ QUnit.module('Gradebook#toggleOverrides', () => {
]);
const gradebook = createGradebook({ options });
gradebook.gridDisplaySettings.showFinalGradeOverrides = true;
sandbox.stub(gradebook, 'updateColumnsAndRenderViewOptionsMenu');
gradebook.setShowFinalGradeOverrides(true);
sandbox.stub(gradebook, 'updateColumnsAndRenderGradebookSettingsModal');
const saveSettingsStub = sinon.spy(gradebook, 'saveSettings');
gradebook.toggleOverrides();
@ -5534,8 +5560,9 @@ QUnit.module('Gradebook#toggleOverrides', () => {
]);
const gradebook = createGradebook({ options });
gradebook.gridDisplaySettings.showFinalGradeOverrides = true;
const stubFn = sandbox.stub(gradebook, 'updateColumnsAndRenderViewOptionsMenu');
gradebook.setShowFinalGradeOverrides(true);
sandbox.stub(gradebook, 'renderGradebookSettingsModal')
const stubFn = sandbox.stub(gradebook, 'updateColumns');
stubFn.onFirstCall().callsFake(function () {
strictEqual(gradebook.gridDisplaySettings.showFinalGradeOverrides, false);
});
@ -6122,59 +6149,110 @@ QUnit.module('Gradebook', () => {
})
})
QUnit.module('Gradebook#getOverridesViewOptionsMenuProps', () => {
test('includes exactly what ViewOptionsMenu overrides props require', () => {
const props = createGradebook().getOverridesViewOptionsMenuProps()
const {propTypes: {overrides}} = ViewOptionsMenu
QUnit.module('Gradebook#getFinalGradeOverridesSettingsModalProps', () => {
test('includes the exact properties that GradebookSettingsModal overrides props require', () => {
const props = createGradebook().getFinalGradeOverridesSettingsModalProps()
const {propTypes: {overrides}} = GradebookSettingsModal
const consoleSpy = sinon.spy(console, 'error')
PropTypes.checkPropTypes({overrides}, props, 'prop', 'ViewOptionsMenu')
PropTypes.checkPropTypes({overrides}, props, 'prop', 'GradebookSettingsModal')
strictEqual(consoleSpy.called, false)
consoleSpy.restore()
})
test('disabled defaults to true', function () {
const gradebook = createGradebook()
const props = gradebook.getOverridesViewOptionsMenuProps()
strictEqual(props.disabled, true)
})
test('disabled is false when the grid is ready', function () {
test('`disabled` defaults to true', () => {
const gradebook = createGradebook()
sinon.stub(gradebook.gridReady, 'state').returns('resolved')
const props = gradebook.getOverridesViewOptionsMenuProps()
strictEqual(props.disabled, false)
const {disabled} = gradebook.getFinalGradeOverridesSettingsModalProps()
strictEqual(disabled, true)
})
test('disabled is true if the overrides column is updating', function () {
const gradebook = createGradebook()
test('`disabled` is false when final grades override is enabled and checked', () => {
const final_grade_override_enabled = true
const gradebook = createGradebook({final_grade_override_enabled})
sinon.stub(gradebook.gridReady, 'state').returns('resolved')
gradebook.setOverridesColumnUpdating(true)
const props = gradebook.getOverridesViewOptionsMenuProps()
strictEqual(props.disabled, true)
const {disabled} = gradebook.getFinalGradeOverridesSettingsModalProps()
strictEqual(disabled, !final_grade_override_enabled)
})
test('disabled is false if the overrides column is not updating', function () {
const gradebook = createGradebook()
test('`disabled` is true when overrides column is updating', () => {
const gradebook = createGradebook({final_grade_override_enabled: true})
sinon.stub(gradebook.gridReady, 'state').returns('resolved')
gradebook.setOverridesColumnUpdating(false)
const props = gradebook.getOverridesViewOptionsMenuProps()
strictEqual(props.disabled, false)
const overridesColumnUpdating = true
gradebook.setOverridesColumnUpdating(overridesColumnUpdating)
const {disabled} = gradebook.getFinalGradeOverridesSettingsModalProps()
strictEqual(disabled, overridesColumnUpdating)
})
test('onSelect calls toggleOverrides', function () {
const gradebook = createGradebook({ showFinalGradeOverrides: true })
test('`onChange` calls `toggleOverrides`', () => {
const gradebook = createGradebook({final_grade_override_enabled: true})
sinon.stub(gradebook.gridReady, 'state').returns('resolved')
sinon.stub(gradebook, 'toggleOverrides')
const props = gradebook.getOverridesViewOptionsMenuProps()
props.onSelect()
const props = gradebook.getFinalGradeOverridesSettingsModalProps()
props.onChange()
strictEqual(gradebook.toggleOverrides.callCount, 1)
gradebook.toggleOverrides.restore()
})
test('selected reports showFinalGradeOverrides', function () {
const show_final_grade_overrides = false
test('`defaultChecked` defaults to false', () => {
const gradebook = createGradebook()
sinon.stub(gradebook.gridReady, 'state').returns('resolved')
const {defaultChecked} = gradebook.getFinalGradeOverridesSettingsModalProps()
strictEqual(defaultChecked, false)
})
test('`defaultChecked` is false when final grades override is enabled', () => {
const final_grade_override_enabled = true
const gradebook = createGradebook({final_grade_override_enabled})
sinon.stub(gradebook.gridReady, 'state').returns('resolved')
const {defaultChecked} = gradebook.getFinalGradeOverridesSettingsModalProps()
strictEqual(defaultChecked, !final_grade_override_enabled)
})
test('`defaultChecked` is false when show final override setting is enabled', () => {
const show_final_grade_overrides = 'true'
const gradebook = createGradebook({settings: {show_final_grade_overrides}})
const props = gradebook.getOverridesViewOptionsMenuProps()
equal(props.selected, show_final_grade_overrides)
sinon.stub(gradebook.gridReady, 'state').returns('resolved')
const {defaultChecked} = gradebook.getFinalGradeOverridesSettingsModalProps()
strictEqual(defaultChecked, !show_final_grade_overrides)
})
test('`defaultChecked` is true when show final grades is enabled and the setting to show final grades is enabled', () => {
const gradebook = createGradebook({
final_grade_override_enabled: true,
settings: {show_final_grade_overrides: 'true'}
})
sinon.stub(gradebook.gridReady, 'state').returns('resolved')
const {defaultChecked} = gradebook.getFinalGradeOverridesSettingsModalProps()
strictEqual(defaultChecked, true)
})
test('`defaultChecked` is false when final grades is disabled and the setting to show final grades is enabled', () => {
const gradebook = createGradebook({
final_grade_override_enabled: false,
settings: {show_final_grade_overrides: 'true'}
})
sinon.stub(gradebook.gridReady, 'state').returns('resolved')
const {defaultChecked} = gradebook.getFinalGradeOverridesSettingsModalProps()
strictEqual(defaultChecked, false)
})
test('`defaultChecked` is false when final grades is enabled and the setting to show final grades is disabled', () => {
const gradebook = createGradebook({
final_grade_override_enabled: true,
settings: {show_final_grade_overrides: 'false'}
})
sinon.stub(gradebook.gridReady, 'state').returns('resolved')
const {defaultChecked} = gradebook.getFinalGradeOverridesSettingsModalProps()
strictEqual(defaultChecked, false)
})
test('`defaultChecked` is false when final grades is disabled and the setting to show final grades is disabled', () => {
const gradebook = createGradebook({
final_grade_override_enabled: false,
settings: {show_final_grade_overrides: 'false'}
})
sinon.stub(gradebook.gridReady, 'state').returns('resolved')
const {defaultChecked} = gradebook.getFinalGradeOverridesSettingsModalProps()
strictEqual(defaultChecked, false)
})
})
})
@ -8816,7 +8894,17 @@ QUnit.module('#renderGradebookSettingsModal', (hooks) => {
gradebook.renderGradebookSettingsModal();
strictEqual(gradebookSettingsModalProps().locale, 'de');
});
});
test('passes override props', () => {
gradebook = createGradebook().renderGradebookSettingsModal()
const expectedProps = {
defaultChecked: false,
disabled: true,
onChange: {}
}
propEqual(gradebookSettingsModalProps().overrides, expectedProps)
})
})
QUnit.module('Gradebook#renderAnonymousSpeedGraderAlert', (hooks) => {
let gradebook;

View File

@ -0,0 +1,95 @@
/*
* Copyright (C) 2018 - 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 from 'react'
import ReactDOM from 'react-dom'
import AdvancedTabPanel from 'jsx/gradezilla/default_gradebook/components/AdvancedTabPanel'
const fixtures = document.getElementById('fixtures')
const overridesOnChangeStub = sinon.stub()
function renderComponent({overrides, props} = {}) {
const componentProps = {
overrides: {
defaultChecked: true,
disabled: false,
onChange: overridesOnChangeStub,
...overrides
},
...props
}
ReactDOM.render(<AdvancedTabPanel {...componentProps} />, fixtures)
return fixtures.children[0]
}
function findCheckBox(label, scope = fixtures) {
const labels = []
scope.querySelectorAll('label').forEach(node => labels.push(node))
const labelFor = labels.find(node => node.innerText.trim() === label).getAttribute('for')
return scope.querySelector(`#${labelFor}`)
}
function overridesCheckbox() {
return findCheckBox('Allow final grade override')
}
QUnit.module('AdvancedTabPanel', moduleHooks => {
moduleHooks.beforeEach(() => {})
moduleHooks.afterEach(() => {
ReactDOM.unmountComponentAtNode(fixtures)
})
test('it renders', () => {
renderComponent()
const container = fixtures.querySelectorAll('#AdvancedTabPanel__Container')
equal(container.length, 1)
})
QUnit.module('Overrides', () => {
test('checkbox is checked', () => {
renderComponent()
const {checked} = overridesCheckbox()
strictEqual(checked, true)
})
test('checkbox is not checked when `overrides.defaultChecked` is false', () => {
renderComponent({overrides: {defaultChecked: false}})
const {checked} = overridesCheckbox()
strictEqual(checked, false)
})
test('checkbox is not disabled', () => {
renderComponent()
const {disabled} = overridesCheckbox()
strictEqual(disabled, false)
})
test('checkbox is disabled when `overrides.disabled` is true', () => {
renderComponent({overrides: {disabled: true}})
const {disabled} = overridesCheckbox()
strictEqual(disabled, true)
})
test('onChange is called when checkbox is clicked', () => {
renderComponent()
overridesCheckbox().click()
strictEqual(overridesOnChangeStub.callCount, 1)
})
})
})

View File

@ -170,56 +170,6 @@ QUnit.module('ViewOptionsMenu - Overrides', (moduleHooks) => {
const menuItem = getMenuItem(wrapper.instance().menuContent, 'Overrides')
strictEqual(menuItem, undefined)
})
QUnit.module('when "Final Grade Override" is enabled', hooks => {
hooks.beforeEach(() => {
props = {
...defaultProps(),
finalGradeOverrideEnabled: true
}
})
test('is not disabled', () => {
wrapper = mountAndOpenOptions(props)
const menuItem = getMenuItem(wrapper.instance().menuContent, 'Overrides')
strictEqual(menuItem.getAttribute('aria-disabled'), null)
})
test('can be optionally disabled', () => {
props.overrides.disabled = true
wrapper = mountAndOpenOptions(props)
const menuItem = getMenuItem(wrapper.instance().menuContent, 'Overrides')
strictEqual(menuItem.getAttribute('aria-disabled'), props.overrides.disabled.toString())
})
test('triggers the onSelect when the "Overrides" option is clicked', () => {
sandbox.stub(props.overrides, 'onSelect')
wrapper = mountAndOpenOptions(props)
getMenuItem(wrapper.instance().menuContent, 'Overrides').click()
equal(props.overrides.onSelect.callCount, 1)
})
test('is optionally not selected', () => {
wrapper = mountAndOpenOptions(props)
const menuItem = getMenuItem(wrapper.instance().menuContent, 'Overrides')
strictEqual(menuItem.getAttribute('aria-checked'), props.overrides.selected.toString())
})
test('is optionally selected', () => {
props.overrides.selected = true
wrapper = mountAndOpenOptions(props)
const menuItem = getMenuItem(wrapper.instance().menuContent, 'Overrides')
strictEqual(menuItem.getAttribute('aria-checked'), props.overrides.selected.toString())
})
test('can be given a different label', () => {
const someLabel = 'Grading Periods Label'
props.overrides.label = someLabel
wrapper = mountAndOpenOptions(props)
const menuItem = getMenuItem(wrapper.instance().menuContent, someLabel)
strictEqual(menuItem.textContent, someLabel)
})
})
})
QUnit.module('ViewOptionsMenu - Filters', {

View File

@ -0,0 +1,49 @@
/*
* Copyright (C) 2019 - present Instructure, Inc.
*
* This file is part of Canvas.
*
* Canvas is free software: you can redistribute it and/or modify it under
* the terms of the GNU Affero General Public License as published by the Free
* Software Foundation, version 3 of the License.
*
* Canvas is distributed in the hope that it will be useful, but WITHOUT ANY
* WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
* A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
* details.
*
* You should have received a copy of the GNU Affero General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
export async function waitFor(conditionFn, timeout = 200) {
return new Promise((resolve, reject) => {
let timeoutId
const interval = 10 // ms
const intervalFn = () => {
const result = conditionFn()
if (result || result == null) {
clearInterval(intervalId)
clearTimeout(timeoutId)
}
if (result == null) {
reject(
new Error(
"waitFor's criteria function returned null or undefined, did you mean for this function to return a boolean?"
)
)
}
if (result) {
resolve(result)
}
}
const intervalId = setInterval(intervalFn, interval)
const timeoutFn = () => {
clearInterval(intervalId)
reject(new Error('Timeout waiting for condition'))
}
timeoutId = setTimeout(timeoutFn, timeout)
})
}

View File

@ -17,8 +17,7 @@
require_relative '../../common'
require_relative '../pages/gradezilla_page'
require_relative '../pages/gradezilla_advanced_options_page'
require_relative '../pages/gradezilla_main_settings'
require_relative '../pages/gradezilla/settings/advanced'
require_relative '../pages/gradezilla_cells_page'
require_relative '../pages/srgb_page'
require_relative '../pages/student_grades_page'
@ -27,7 +26,6 @@ describe 'Final Grade Override' do
include_context 'in-process server selenium tests'
before(:once) do
skip('Unskip in GRADE-1867')
course_with_teacher(course_name: "Grade Override", active_course: true,active_enrollment: true,name: "Teacher Boss1",active_user: true)
@students = create_users_in_course(@course, 5, return_type: :record, name_prefix: "Purple")
Account.default.enable_feature!(:final_grades_override)
@ -50,14 +48,12 @@ describe 'Final Grade Override' do
user_session(@teacher)
Gradezilla.visit(@course)
Gradezilla.settings_cog_select
#select option for override
MainSettings::Advanced.grade_override_checkbox.click
MainSettings::Controls.click_update_button
Gradezilla::Settings.click_advanced_tab
Gradezilla::Settings::Advanced.select_grade_override_checkbox
Gradezilla::Settings.click_update_button
end
it 'display override column in new gradebook', priority: '1', test_id: 3682130 do
skip('Unskip in GRADE-1867')
# TODO: verify new column on NG
expect(f(".slick-header-column[title='Override']")).to be_displayed
end

View File

@ -17,13 +17,11 @@
require_relative '../pages/gradezilla_page'
require_relative '../pages/gradezilla_cells_page'
require_relative '../pages/gradezilla_late_policies_page'
require_relative '../pages/gradezilla_main_settings'
require_relative '../pages/gradezilla/settings/late_policies'
describe 'Late Policies:' do
include_context "in-process server selenium tests"
context 'when applied' do
before(:once) do
now = Time.zone.now
@ -171,8 +169,8 @@ describe 'Late Policies:' do
it 'saves late policy', test_id: 3196970, priority: '1' do
percentage = 10
increment = 'Day'
MainSettings::LatePolicies.create_late_policy(percentage, increment)
MainSettings::Controls.click_update_button
Gradezilla::Settings::LatePolicies.create_late_policy(percentage, increment)
Gradezilla::Settings.click_update_button
expect(@course.late_policy.late_submission_deduction_enabled).to be true
expect(@course.late_policy.late_submission_deduction.to_i).to be percentage
@ -181,8 +179,8 @@ describe 'Late Policies:' do
it 'saves missing policy', test_id: 3196968, priority: '1' do
percentage = 50
MainSettings::LatePolicies.create_missing_policy(percentage)
MainSettings::Controls.click_update_button
Gradezilla::Settings::LatePolicies.create_missing_policy(percentage)
Gradezilla::Settings.click_update_button
expect(@course.late_policy.missing_submission_deduction_enabled).to be true
expect(@course.late_policy.missing_submission_deduction.to_i).to be percentage
@ -193,8 +191,8 @@ describe 'Late Policies:' do
percentage = 10
increment = 'Day'
lowest_percentage = 50
MainSettings::LatePolicies.create_late_policy(percentage, increment, lowest_percentage)
MainSettings::Controls.click_update_button
Gradezilla::Settings::LatePolicies.create_late_policy(percentage, increment, lowest_percentage)
Gradezilla::Settings.click_update_button
expect(@course.late_policy.late_submission_minimum_percent_enabled).to be true
expect(@course.late_policy.late_submission_minimum_percent.to_i).to be lowest_percentage

View File

@ -0,0 +1,56 @@
#
# Copyright (C) 2017 - 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/>.
require_relative '../../../common'
module Gradezilla
module Settings
extend SeleniumDependencies
def self.tab(label:)
# only works if not currently active
ff('[data-ui-testable="TabList"] > [role="presentation"]').find do |el|
el.text == label
end
end
def self.click_advanced_tab
tab(label: 'Advanced').click
end
def self.click_late_policy_tab
tab(label: 'Late Policies').click
end
def self.cancel_button
f('#gradebook-settings-cancel-button')
end
def self.update_button
f('#gradebook-settings-update-button')
end
def self.click_cancel_button
cancel_button.click
end
def self.click_update_button
update_button.click
wait_for_animations
end
end
end

View File

@ -15,17 +15,17 @@
# 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/>.
require_relative '../../common'
require_relative '../../../../common'
require_relative '../settings'
module MainSettings
class Advanced
class << self
include SeleniumDependencies
module Gradezilla
module Settings
module Advanced
extend SeleniumDependencies
def grade_override_checkbox
# TODO: add locator for checkbox
def self.select_grade_override_checkbox
fj('label:contains("Allow final grade override")').click
end
end
end
end

View File

@ -15,64 +15,62 @@
# 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/>.
require_relative '../../common'
module MainSettings
class LatePolicies
class << self
include SeleniumDependencies
require_relative '../../../../common'
require_relative '../settings'
def late_policy_tab
# f('#late_policy_tab')
end
module Gradezilla
module Settings
module LatePolicies
extend SeleniumDependencies
def missing_policy_checkbox
def self.missing_policy_checkbox
fj('label:contains("Automatically apply grade for missing submissions")')
end
def missing_policy_percent_input
def self.missing_policy_percent_input
f('#missing-submission-grade')
end
def late_policy_checkbox
def self.late_policy_checkbox
fj('label:contains("Automatically apply deduction to late submissions")')
end
def late_policy_deduction_input
def self.late_policy_deduction_input
f('#late-submission-deduction')
end
def late_policy_increment_combobox(increment)
def self.late_policy_increment_combobox(increment)
click_option(f('#late-submission-interval'), increment)
end
def lowest_grade_percent_input
def self.lowest_grade_percent_input
f('#late-submission-minimum-percent')
end
def select_late_policy_tab
def self.select_late_policy_tab
late_policy_tab.click
end
def create_missing_policy(percent_per_assignment)
def self.create_missing_policy(percent_per_assignment)
unless missing_policy_checkbox.attribute('checked')
missing_policy_checkbox.click
end
set_value(missing_policy_percent_input, percent_per_assignment)
end
def disable_missing_policy
def self.disable_missing_policy
if missing_policy_checkbox.attribute('checked')
missing_policy_checkbox.click
end
end
def disable_late_policy
def self.disable_late_policy
if late_policy_checkbox.attribute('checked')
late_policy_checkbox.click
end
end
def create_late_policy(percentage, time_increment, lowest_percentage = nil)
def self.create_late_policy(percentage, time_increment, lowest_percentage = nil)
late_policy_checkbox.click
set_value(late_policy_deduction_input, percentage)
late_policy_increment_combobox(time_increment)

View File

@ -17,7 +17,7 @@
require_relative '../../common'
class Gradezilla
module Gradezilla
class Cells
class << self
include SeleniumDependencies

View File

@ -16,7 +16,8 @@
# with this program. If not, see <http://www.gnu.org/licenses/>.
require_relative '../../common'
class Gradezilla
module Gradezilla
class GradeDetailTray
class << self
include SeleniumDependencies

View File

@ -1,42 +0,0 @@
#
# Copyright (C) 2017 - 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/>.
require_relative '../../common'
module MainSettings
class Controls
class << self
include SeleniumDependencies
def cancel_button
f('#gradebook-settings-cancel-button')
end
def update_button
f('#gradebook-settings-update-button')
end
def click_cancel_button
cancel_button.click
end
def click_update_button
update_button.click
wait_for_animations
end
end
end
end

File diff suppressed because it is too large Load Diff