diff --git a/app/coffeescripts/ember/screenreader_gradebook/templates/screenreader_gradebook.hbs b/app/coffeescripts/ember/screenreader_gradebook/templates/screenreader_gradebook.hbs
index 5b4ba7e95ba74..5a81f85bd41eb 100644
--- a/app/coffeescripts/ember/screenreader_gradebook/templates/screenreader_gradebook.hbs
+++ b/app/coffeescripts/ember/screenreader_gradebook/templates/screenreader_gradebook.hbs
@@ -16,18 +16,10 @@ You should have received a copy of the GNU Affero General Public License along
with this program. If not, see .
}}
-{{#if gradezilla}}
- {{partial "gradezillaHeader"}}
-{{else}}
- {{partial "header"}}
-{{/if}}
+{{partial "gradebookHeader"}}
{{#ic-tabs}}
- {{#if gradezilla}}
- {{else}}
-
- {{/if}}
{{#ic-tab-list}}
{{#ic-tab}}{{#t 'assignments'}}Assignments{{/t}}{{/ic-tab}}
{{#ic-tab}}{{#t 'learning_mastery'}}Learning Mastery{{/t}}{{/ic-tab}}
diff --git a/app/coffeescripts/ember/screenreader_gradebook/tests/controllers/screenreader_gradebook.spec.js b/app/coffeescripts/ember/screenreader_gradebook/tests/controllers/screenreader_gradebook.spec.js
index e05716ae60e56..b05e0c77478ee 100644
--- a/app/coffeescripts/ember/screenreader_gradebook/tests/controllers/screenreader_gradebook.spec.js
+++ b/app/coffeescripts/ember/screenreader_gradebook/tests/controllers/screenreader_gradebook.spec.js
@@ -25,7 +25,7 @@ import {createCourseGradesWithGradingPeriods} from 'spec/jsx/gradebook/GradeCalc
import SRGBController from '../../controllers/screenreader_gradebook_controller'
import userSettings from '../../../../userSettings'
import CourseGradeCalculator from 'jsx/gradebook/CourseGradeCalculator'
-import * as FinalGradeOverrideApi from '../../../../../jsx/gradezilla/default_gradebook/FinalGradeOverrides/FinalGradeOverrideApi'
+import * as FinalGradeOverrideApi from '../../../../../jsx/gradebook/default_gradebook/FinalGradeOverrides/FinalGradeOverrideApi'
import 'vendor/jquery.ba-tinypubsub'
import AsyncHelper from '../AsyncHelper'
diff --git a/app/coffeescripts/ember/screenreader_gradebook/tests/routes/screenreader_gradebook_route.spec.js b/app/coffeescripts/ember/screenreader_gradebook/tests/routes/screenreader_gradebook_route.spec.js
index 3b557221512d2..3bb078803dd95 100644
--- a/app/coffeescripts/ember/screenreader_gradebook/tests/routes/screenreader_gradebook_route.spec.js
+++ b/app/coffeescripts/ember/screenreader_gradebook/tests/routes/screenreader_gradebook_route.spec.js
@@ -16,7 +16,7 @@
// with this program. If not, see .
//
-import * as FinalGradeOverrideApi from 'jsx/gradezilla/default_gradebook/FinalGradeOverrides/FinalGradeOverrideApi'
+import * as FinalGradeOverrideApi from 'jsx/gradebook/default_gradebook/FinalGradeOverrides/FinalGradeOverrideApi'
import ScreenreaderGradebookRoute from '../../routes/screenreader_gradebook_route'
QUnit.module('ScreenreaderGradebookRoute', suiteHooks => {
diff --git a/app/coffeescripts/ember/screenreader_gradebook/tests/shared_ajax_fixtures.js b/app/coffeescripts/ember/screenreader_gradebook/tests/shared_ajax_fixtures.js
index ac2a2972ec76e..e14ff1171e999 100644
--- a/app/coffeescripts/ember/screenreader_gradebook/tests/shared_ajax_fixtures.js
+++ b/app/coffeescripts/ember/screenreader_gradebook/tests/shared_ajax_fixtures.js
@@ -427,6 +427,7 @@ const assignmentGroups = [
submission_types: ['none'],
due_at: '2013-09-01T10:00:00Z',
position: 9,
+ published: true,
assignment_group_id: '2'
}
]
@@ -607,7 +608,10 @@ const submissions = [
}
]
-const sections = [{id: '1', name: 'Vampires and Demons'}, {id: '2', name: 'Slayers and Scoobies'}]
+const sections = [
+ {id: '1', name: 'Vampires and Demons'},
+ {id: '2', name: 'Slayers and Scoobies'}
+]
const customColumns = [
{
@@ -633,7 +637,10 @@ const outcomeRollupsRaw = {
rollups: [
{
links: {user: '1'},
- scores: [{links: {outcome: '1'}, score: 5}, {links: {outcome: '2'}, score: 4}]
+ scores: [
+ {links: {outcome: '1'}, score: 5},
+ {links: {outcome: '2'}, score: 4}
+ ]
},
{
links: {user: '2'},
@@ -658,7 +665,7 @@ export default {
sections,
outcomes,
outcome_rollups: outcomeRollups,
- create(overrides) {
+ create() {
window.ENV = {
current_user_id: 1,
context_asset_string: 'course_1',
diff --git a/app/coffeescripts/gradebook/AssignmentGroupWeightsDialog.coffee b/app/coffeescripts/gradebook/AssignmentGroupWeightsDialog.coffee
deleted file mode 100644
index 1d90192bb942d..0000000000000
--- a/app/coffeescripts/gradebook/AssignmentGroupWeightsDialog.coffee
+++ /dev/null
@@ -1,111 +0,0 @@
-#
-# Copyright (C) 2011 - 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 round from '../util/round'
-import I18n from 'i18nObj'
-import $ from 'jquery'
-import assignmentGroupWeightsDialogTemplate from 'jst/AssignmentGroupWeightsDialog'
-import numberHelper from 'jsx/shared/helpers/numberHelper'
-import 'jquery.ajaxJSON'
-import 'jquery.disableWhileLoading'
-import 'jqueryui/dialog'
-import 'jquery.instructure_misc_helpers'
-import 'vendor/jquery.ba-tinypubsub'
-
-export default class AssignmentGroupWeightsDialog
-
- constructor: (options) ->
- @$dialog = $ assignmentGroupWeightsDialogTemplate()
- @$dialog.dialog
- autoOpen: false
- resizable: false
- width: 350
- buttons: [{
- text: @$dialog.find('button[type=submit]').hide().text()
- click: @save
- }]
-
- @$dialog.delegate 'input', 'change keyup keydown input', @calcTotal
- @$dialog.find('#group_weighting_scheme').change (event) =>
- disable = !event.currentTarget.checked
- @$dialog.find('table').css('opacity', if disable then 0.5 else 1 )
- @$dialog.find('.assignment_group_row input').attr('disabled', disable)
-
- @$group_template = @$dialog.find('.assignment_group_row.blank').removeClass('blank').detach().show()
- @$groups_holder = @$dialog.find('.groups_holder')
- # ember objects dont work with $.extend, so for srgb we pass in options.mergeFunction
- @mergeFunction = options.mergeFunction || $.extend
- @update(options)
-
- render: =>
- @$groups_holder.empty()
- for group in @options.assignmentGroups
- uniqueId = "assignment_group_#{group.id}_weight"
- @$group_template
- .clone()
- .data('assignment_group', group)
- .find('label').attr('for', uniqueId ).text(group.name).end()
- .find('input').attr('id', uniqueId).val(I18n.n(group.group_weight)).end()
- .appendTo(@$groups_holder)
- @$dialog.find('#group_weighting_scheme').prop('checked', @options.context.group_weighting_scheme == 'percent').change()
- @calcTotal()
- @addGroupWeightListener()
-
- update: (newOptions) =>
- @options = newOptions
- @render()
-
- addGroupWeightListener: =>
- $(".group_weight").on 'change', (e) ->
- value = $(e.target).val()
- rounded_value = round(numberHelper.parse(value), 2)
- unless isNaN(rounded_value)
- $(e.target).val(rounded_value)
-
- calcTotal: =>
- total = 0
- @$dialog.find('.assignment_group_row input').each ->
- total += numberHelper.parse($(this).val())
- total = round(total,2)
- @$dialog.find('.total_weight').text(I18n.n(total))
-
- save: =>
- courseUrl = "/courses/#{@options.context.context_id}"
- requests = []
-
- newGroupWeightingScheme = if @$dialog.find('#group_weighting_scheme').is(':checked') then 'percent' else 'equal'
- if newGroupWeightingScheme != @options.context.group_weighting_scheme
- requests.push $.ajaxJSON courseUrl, 'PUT', {'course[group_weighting_scheme]' : newGroupWeightingScheme}, (data) =>
- @options.context.group_weighting_scheme = data.course.group_weighting_scheme
- if @options.context.group_weighting_scheme == "percent"
- @options.context.show_total_grade_as_points = false
-
- @$dialog.find('.assignment_group_row').each (i, row) =>
- group = $(row).data('assignment_group')
- newWeight = numberHelper.parse($(row).find('input').val())
- if newWeight != group.group_weight
- requests.push $.ajaxJSON "/api/v1#{courseUrl}/assignment_groups/#{group.id}", 'PUT', {'group_weight' : newWeight}, (data) =>
- @mergeFunction(group, data)
-
- # when all the requests come back, call @afterSave
- promise = $.when.apply($, requests).done(@afterSave)
- @$dialog.disableWhileLoading(promise, buttons: ['.ui-button-text'])
-
- afterSave: =>
- @$dialog.dialog('close')
- @render()
- $.publish('assignment_group_weights_changed', @options)
diff --git a/app/coffeescripts/gradebook/Gradebook.coffee b/app/coffeescripts/gradebook/Gradebook.coffee
index e9f2f2e1f90a2..c3ae6e535c1f1 100644
--- a/app/coffeescripts/gradebook/Gradebook.coffee
+++ b/app/coffeescripts/gradebook/Gradebook.coffee
@@ -18,50 +18,69 @@
import $ from 'jquery'
import _ from 'underscore'
-import Backbone from 'Backbone'
import tz from 'timezone'
-import DataLoader from 'jsx/gradebook/DataLoader'
import React from 'react'
import ReactDOM from 'react-dom'
+
import LongTextEditor from 'slickgrid.long_text_editor'
import KeyboardNavDialog from '../views/KeyboardNavDialog'
import KeyboardNavTemplate from 'jst/KeyboardNavDialog'
-import Slick from 'vendor/slickgrid'
-import TotalColumnHeaderView from './TotalColumnHeaderView'
-import round from '../util/round'
+import GradingPeriodSetsApi from '../api/gradingPeriodSetsApi'
import InputFilterView from '../views/InputFilterView'
-import i18nObj from 'i18nObj'
-import I18n from 'i18n!gradebookold'
-import GRADEBOOK_TRANSLATIONS from './GradebookTranslations'
+import I18n from 'i18n!gradebook'
import CourseGradeCalculator from 'jsx/gradebook/CourseGradeCalculator'
import * as EffectiveDueDates from 'jsx/gradebook/EffectiveDueDates'
-import {scoreToGrade} from 'jsx/gradebook/GradingSchemeHelper'
import GradeFormatHelper from 'jsx/gradebook/shared/helpers/GradeFormatHelper'
import UserSettings from '../userSettings'
import Spinner from 'spin.js'
-import SubmissionDetailsDialog from '../SubmissionDetailsDialog'
-import AssignmentGroupWeightsDialog from './AssignmentGroupWeightsDialog'
import GradeDisplayWarningDialog from '../shared/GradeDisplayWarningDialog'
import PostGradesFrameDialog from './PostGradesFrameDialog'
-import SubmissionCell from './SubmissionCell'
-import GradebookHeaderMenu from './GradebookHeaderMenu'
import NumberCompare from '../util/NumberCompare'
import natcompare from '../util/natcompare'
+import * as ConvertCase from 'convert_case'
import htmlEscape from 'str/htmlEscape'
+import * as EnterGradesAsSetting from 'jsx/gradebook/shared/EnterGradesAsSetting'
+import SetDefaultGradeDialogManager from 'jsx/gradebook/shared/SetDefaultGradeDialogManager'
+import AsyncComponents from 'jsx/gradebook/default_gradebook/AsyncComponents'
+import CurveGradesDialogManager from 'jsx/gradebook/default_gradebook/CurveGradesDialogManager'
+import GradebookApi from 'jsx/gradebook/default_gradebook/apis/GradebookApi'
+import SubmissionCommentApi from 'jsx/gradebook/default_gradebook/apis/SubmissionCommentApi'
+import CourseSettings from 'jsx/gradebook/default_gradebook/CourseSettings'
+import DataLoader from 'jsx/gradebook/default_gradebook/DataLoader'
+import FinalGradeOverrides from 'jsx/gradebook/default_gradebook/FinalGradeOverrides'
+import GradebookGrid from 'jsx/gradebook/default_gradebook/GradebookGrid'
+import studentRowHeaderConstants from 'jsx/gradebook/default_gradebook/constants/studentRowHeaderConstants'
+import AssignmentRowCellPropFactory from 'jsx/gradebook/default_gradebook/GradebookGrid/editors/AssignmentCellEditor/AssignmentRowCellPropFactory'
+import TotalGradeOverrideCellPropFactory from 'jsx/gradebook/default_gradebook/GradebookGrid/editors/TotalGradeOverrideCellEditor/TotalGradeOverrideCellPropFactory'
+import PostPolicies from 'jsx/gradebook/default_gradebook/PostPolicies'
+import GradebookMenu from 'jsx/gradebook/default_gradebook/components/GradebookMenu'
+import ViewOptionsMenu from 'jsx/gradebook/default_gradebook/components/ViewOptionsMenu'
+import ActionMenu from 'jsx/gradebook/default_gradebook/components/ActionMenu'
+import AssignmentGroupFilter from 'jsx/gradebook/default_gradebook/components/content-filters/AssignmentGroupFilter'
+import GradingPeriodFilter from 'jsx/gradebook/default_gradebook/components/content-filters/GradingPeriodFilter'
+import ModuleFilter from 'jsx/gradebook/default_gradebook/components/content-filters/ModuleFilter'
+import SectionFilter from 'jsx/gradebook/default_gradebook/components/content-filters/SectionFilter'
+import StudentGroupFilter from 'jsx/gradebook/default_gradebook/components/content-filters/StudentGroupFilter'
+import GridColor from 'jsx/gradebook/default_gradebook/components/GridColor'
+import StatusesModal from 'jsx/gradebook/default_gradebook/components/StatusesModal'
+import AnonymousSpeedGraderAlert from 'jsx/gradebook/default_gradebook/components/AnonymousSpeedGraderAlert'
+import { statusColors } from 'jsx/gradebook/default_gradebook/constants/colors'
+import StudentDatastore from 'jsx/gradebook/default_gradebook/stores/StudentDatastore'
import PostGradesStore from 'jsx/gradebook/SISGradePassback/PostGradesStore'
-import PostGradesApp from 'jsx/gradebook/SISGradePassback/PostGradesApp'
import SubmissionStateMap from 'jsx/gradebook/SubmissionStateMap'
-import ColumnHeaderTemplate from 'jst/gradebook/column_header'
-import GroupTotalCellTemplate from 'jst/gradebook/group_total_cell'
-import RowStudentNameTemplate from 'jst/gradebook/row_student_name'
-import SectionMenuView from '../views/gradebook/SectionMenuView'
-import GradingPeriodMenuView from '../views/gradebook/GradingPeriodMenuView'
+import DownloadSubmissionsDialogManager from 'jsx/gradebook/shared/DownloadSubmissionsDialogManager'
+import ReuploadSubmissionsDialogManager from 'jsx/gradebook/shared/ReuploadSubmissionsDialogManager'
import GradebookKeyboardNav from './GradebookKeyboardNav'
import assignmentHelper from 'jsx/gradebook/shared/helpers/assignmentHelper'
-import GradingPeriodsApi from '../api/gradingPeriodsApi'
-import GradingPeriodSetsApi from '../api/gradingPeriodSetsApi'
-import {scoreToPercentage} from '../../jsx/gradebook/shared/helpers/GradeCalculationHelper'
-import 'jst/_avatar' #needed by row_student_name
+import TextMeasure from 'jsx/gradebook/shared/helpers/TextMeasure'
+import * as GradeInputHelper from 'jsx/grading/helpers/GradeInputHelper'
+import OutlierScoreHelper from 'jsx/grading/helpers/OutlierScoreHelper'
+import {isPostable} from 'jsx/grading/helpers/SubmissionHelper'
+import LatePolicyApplicator from 'jsx/grading/LatePolicyApplicator'
+import {Button} from '@instructure/ui-buttons'
+import {IconSettingsSolid} from '@instructure/ui-icons'
+import {ScreenReaderContent} from '@instructure/ui-a11y'
+import * as FlashAlert from 'jsx/shared/FlashAlert'
import 'jquery.ajaxJSON'
import 'jquery.instructure_date_and_time'
import 'jqueryui/dialog'
@@ -72,13 +91,167 @@ import 'jquery.instructure_misc_helpers'
import 'jquery.instructure_misc_plugins'
import 'vendor/jquery.ba-tinypubsub'
import 'jqueryui/position'
-import 'jqueryui/sortable'
-import '../jquery.kylemenu'
import '../jquery/fixDialogButtons'
-import 'jsx/context_cards/StudentContextCardTrigger'
-# This class both creates the slickgrid instance, and acts as the data source for that instance.
-export default class Gradebook
+export default do ->
+
+ ensureAssignmentVisibility = (assignment, submission) =>
+ if assignment?.only_visible_to_overrides && !assignment.assignment_visibility.includes(submission.user_id)
+ assignment.assignment_visibility.push(submission.user_id)
+
+ isAdmin = =>
+ _.includes(ENV.current_user_roles, 'admin')
+
+ HEADER_START_AND_END_WIDTHS_IN_PIXELS = 36
+ IS_ADMIN = isAdmin()
+
+ htmlDecode = (input) ->
+ input && new DOMParser().parseFromString(input, "text/html").documentElement.textContent
+
+ testWidth = (text, minWidth, maxWidth) ->
+ padding = HEADER_START_AND_END_WIDTHS_IN_PIXELS * 2
+ width = Math.max(TextMeasure.getWidth(text) + padding, minWidth)
+ Math.min width, maxWidth
+
+ renderComponent = (reactClass, mountPoint, props = {}, children = null) ->
+ component = React.createElement(reactClass, props, children)
+ ReactDOM.render(component, mountPoint)
+
+ getAssignmentGroupPointsPossible = (assignmentGroup) ->
+ assignmentGroup.assignments.reduce(
+ (sum, assignment) -> sum + (assignment.points_possible || 0),
+ 0
+ )
+
+ ASSIGNMENT_KEY_REGEX = /^assignment_(?!group)/
+ forEachSubmission = (students, fn) ->
+ Object.keys(students).forEach (studentIdx) =>
+ student = students[studentIdx]
+ Object.keys(student).forEach (key) =>
+ if key.match ASSIGNMENT_KEY_REGEX
+ fn(student[key])
+
+ getCourseFromOptions = (options) ->
+ {
+ id: options.context_id
+ }
+
+ getCourseFeaturesFromOptions = (options) ->
+ {
+ additionalSortOptionsEnabled: options.additional_sort_options_enabled,
+ finalGradeOverrideEnabled: options.final_grade_override_enabled
+ }
+
+ ## Gradebook Display Settings
+ getInitialGridDisplaySettings = (settings, colors) ->
+ selectedPrimaryInfo = if studentRowHeaderConstants.primaryInfoKeys.includes(settings.student_column_display_as)
+ settings.student_column_display_as
+ else
+ studentRowHeaderConstants.defaultPrimaryInfo
+
+ # in case of no user preference, determine the default value after @hasSections has resolved
+ selectedSecondaryInfo = settings.student_column_secondary_info
+
+ sortRowsByColumnId = settings.sort_rows_by_column_id || 'student'
+ sortRowsBySettingKey = settings.sort_rows_by_setting_key || 'sortable_name'
+ sortRowsByDirection = settings.sort_rows_by_direction || 'ascending'
+
+ filterColumnsBy =
+ assignmentGroupId: null
+ contextModuleId: null
+ gradingPeriodId: null
+
+ if settings.filter_columns_by?
+ Object.assign(filterColumnsBy, ConvertCase.camelize(settings.filter_columns_by))
+
+ filterRowsBy =
+ sectionId: null
+ studentGroupId: null
+
+ if settings.filter_rows_by?
+ Object.assign(filterRowsBy, ConvertCase.camelize(settings.filter_rows_by))
+
+ {
+ colors
+ enterGradesAs: settings.enter_grades_as || {}
+ filterColumnsBy
+ filterRowsBy
+ selectedPrimaryInfo
+ selectedSecondaryInfo
+ selectedViewOptionsFilters: settings.selected_view_options_filters || []
+ showEnrollments:
+ concluded: false
+ inactive: false
+ sortRowsBy:
+ columnId: sortRowsByColumnId # the column controlling the sort
+ settingKey: sortRowsBySettingKey # the key describing the sort criteria
+ direction: sortRowsByDirection # the direction of the sort
+ submissionTray:
+ open: false
+ studentId: null
+ assignmentId: null
+ comments: []
+ commentsLoaded: false
+ commentsUpdating: false
+ editedCommentId: null
+ }
+
+ ## Gradebook Application State
+ getInitialContentLoadStates = ->
+ {
+ assignmentsLoaded: false
+ contextModulesLoaded: false
+ overridesColumnUpdating: false
+ studentsLoaded: false
+ submissionsLoaded: false
+ teacherNotesColumnUpdating: false
+ }
+
+ getInitialCourseContent = (options) ->
+ courseGradingScheme = null
+ defaultGradingScheme = null
+
+ if options.grading_standard
+ courseGradingScheme = {
+ data: options.grading_standard
+ }
+
+ if options.default_grading_standard
+ defaultGradingScheme = {
+ data: options.default_grading_standard
+ }
+
+ {
+ contextModules: []
+ courseGradingScheme
+ defaultGradingScheme
+ gradingSchemes: options.grading_schemes.map(ConvertCase.camelize)
+ gradingPeriodAssignments: {}
+ assignmentStudentVisibility: {}
+ latePolicy: ConvertCase.camelize(options.late_policy) if options.late_policy
+ }
+
+ getInitialGradebookContent = (options) ->
+ {
+ customColumns: if options.teacher_notes then [options.teacher_notes] else []
+ }
+
+ getInitialActionStates = () ->
+ {
+ pendingGradeInfo: []
+ }
+
+ anonymousSpeedGraderAlertMountPoint = () ->
+ document.querySelector("[data-component='AnonymousSpeedGraderAlert']")
+
+ formatStudentGroupsForFilter = (groupCategoryList) ->
+ groupCategoryList.map((category) => {
+ children: category.groups.sort((a, b) => a.id - b.id),
+ id: category.id,
+ name: category.name
+ })
+
+ class Gradebook
columnWidths =
assignment:
min: 10
@@ -90,136 +263,187 @@ export default class Gradebook
max: 400
total:
min: 95
- max: 110
+ max: 400
+ total_grade_override:
+ min: 95
+ max: 400
hasSections: $.Deferred()
constructor: (@options) ->
+ @course = getCourseFromOptions(@options)
+ @courseFeatures = getCourseFeaturesFromOptions(@options)
+ @courseSettings = new CourseSettings(@, {
+ allowFinalGradeOverride: @options.course_settings.allow_final_grade_override
+ })
+
+ @dataLoader = new DataLoader(@)
+
+ @gridData = {
+ columns: {
+ definitions: {}
+ frozen: []
+ scrollable: []
+ }
+ rows: []
+ }
+
+ @gradebookGrid = new GradebookGrid({
+ $container: document.getElementById('gradebook_grid')
+ activeBorderColor: '#1790DF' # $active-border-color
+ data: @gridData
+ editable: @options.gradebook_is_editable
+ gradebook: @
+ })
+
+ if @courseFeatures.finalGradeOverrideEnabled
+ @finalGradeOverrides = new FinalGradeOverrides(@)
+ else
+ @finalGradeOverrides = null
+
+ if @options.post_policies_enabled
+ @postPolicies = new PostPolicies(@)
+ else
+ @postPolicies = null
+
+ $.subscribe 'assignment_muting_toggled', @handleSubmissionPostedChange
+ $.subscribe 'submissions_updated', @updateSubmissionsFromExternal
+
+ # emitted by SectionMenuView; also subscribed in OutcomeGradebookView
+ $.subscribe 'currentSection/change', @updateCurrentSection
+
+ # emitted by GradingPeriodMenuView
+ $.subscribe 'currentGradingPeriod/change', @updateCurrentGradingPeriod
+
@gridReady = $.Deferred()
- @courseContent =
- gradingPeriodAssignments: {}
+ @setInitialState()
+ @loadSettings()
+ @bindGridEvents()
- @assignments = {}
- @assignmentGroups = {}
+ # End of constructor
+
+ setInitialState: =>
+ @courseContent = getInitialCourseContent(@options)
+ @gradebookContent = getInitialGradebookContent(@options)
+ @gridDisplaySettings = getInitialGridDisplaySettings(@options.settings, @options.colors)
+ @contentLoadStates = getInitialContentLoadStates()
+ @actionStates = getInitialActionStates()
+
+ @headerComponentRefs = {}
+ @filteredContentInfo =
+ invalidAssignmentGroups: []
+ mutedAssignments: []
+ totalPointsPossible: 0
+
+ @setAssignments({})
+ @setAssignmentGroups({})
@effectiveDueDates = {}
@students = {}
@studentViewStudents = {}
- @rows = []
- @assignmentsToHide = UserSettings.contextGet('hidden_columns') || []
- @sectionToShow = UserSettings.contextGet 'grading_show_only_section'
- @sectionToShow = @sectionToShow && String(@sectionToShow)
- @show_attendance = !!UserSettings.contextGet 'show_attendance'
- @include_ungraded_assignments = UserSettings.contextGet 'include_ungraded_assignments'
- @userFilterRemovedRows = []
- # preferences serialization causes these to always come
- # from the database as strings
- @showConcludedEnrollments = @options.course_is_concluded ||
- @options.settings['show_concluded_enrollments'] == "true"
- @showInactiveEnrollments =
- @options.settings['show_inactive_enrollments'] == "true"
- @totalColumnInFront = UserSettings.contextGet 'total_column_in_front'
- @gradingPeriods = GradingPeriodsApi.deserializePeriods(@options.active_grading_periods)
+ @courseContent.students = new StudentDatastore(@students, @studentViewStudents)
+
+ @initPostGradesStore()
+ @initPostGradesLtis()
+ @checkForUploadComplete()
+
+ loadSettings: ->
if @options.grading_period_set
@gradingPeriodSet = GradingPeriodSetsApi.deserializeSet(@options.grading_period_set)
else
@gradingPeriodSet = null
- @gradingPeriodToShow = @getGradingPeriodToShow()
- @submissionStateMap = new SubmissionStateMap
- hasGradingPeriods: @gradingPeriodSet?
- selectedGradingPeriodID: @gradingPeriodToShow
- isAdmin: _.includes(ENV.current_user_roles, "admin")
+ @show_attendance = !!UserSettings.contextGet 'show_attendance'
+ # preferences serialization causes these to always come
+ # from the database as strings
+ if @options.course_is_concluded || @options.settings.show_concluded_enrollments == 'true'
+ @toggleEnrollmentFilter('concluded', true)
+ if @options.settings.show_inactive_enrollments == 'true'
+ @toggleEnrollmentFilter('inactive', true)
+ @initShowUnpublishedAssignments(@options.settings.show_unpublished_assignments)
+ @initSubmissionStateMap()
@gradebookColumnSizeSettings = @options.gradebook_column_size_settings
- @gradebookColumnOrderSettings = @options.gradebook_column_order_settings
- @teacherNotesNotYetLoaded = !@options.teacher_notes? || @options.teacher_notes.hidden
-
- $.subscribe 'assignment_group_weights_changed', @handleAssignmentGroupWeightChange
- $.subscribe 'assignment_muting_toggled', @handleAssignmentMutingChange
- $.subscribe 'submissions_updated', @updateSubmissionsFromExternal
- $.subscribe 'currentSection/change', @updateCurrentSection
- $.subscribe 'currentGradingPeriod/change', @updateCurrentGradingPeriod
+ @setColumnOrder(Object.assign(
+ {},
+ @options.gradebook_column_order_settings,
+ freezeTotalGrade: @options.gradebook_column_order_settings?.freezeTotalGrade == 'true'
+ ))
+ @teacherNotesNotYetLoaded = !@getTeacherNotesColumn()? || @getTeacherNotesColumn().hidden
- assignmentGroupsParams = { exclude_response_fields: @fieldsToExcludeFromAssignments }
- if @gradingPeriodSet? && @gradingPeriodToShow && @gradingPeriodToShow != '0' && @gradingPeriodToShow != ''
- $.extend(assignmentGroupsParams, {grading_period_id: @gradingPeriodToShow})
+ @gotSections(@options.sections)
+ @hasSections.then () =>
+ if !@getSelectedSecondaryInfo()
+ if @sections_enabled
+ @gridDisplaySettings.selectedSecondaryInfo = 'section'
+ else
+ @gridDisplaySettings.selectedSecondaryInfo = 'none'
- $('li.external-tools-dialog > a[data-url], button.external-tools-dialog').on 'click keyclick', (event) ->
- postGradesDialog = new PostGradesFrameDialog({
- returnFocusTo: $('#post_grades, #post_grades_action, #download_csv').first(),
- baseUrl: $(event.target).attr('data-url'),
- launchHeight: $(event.target).attr('data-height'),
- launchWidth: $(event.target).attr('data-width'),
- })
- postGradesDialog.open()
+ @setStudentGroups(@options.student_groups)
- submissionParams =
- response_fields: [
- 'id', 'user_id', 'url', 'score', 'grade', 'submission_type', 'submitted_at', 'assignment_id',
- 'grade_matches_current_submission', 'attachments', 'late', 'workflow_state', 'excused', 'cached_due_date'
- ]
- exclude_response_fields: ['preview_url']
- submissionParams['grading_period_id'] = @gradingPeriodToShow if @gradingPeriodSet? && @gradingPeriodToShow && @gradingPeriodToShow != '0' && @gradingPeriodToShow != ''
- dataLoader = DataLoader.loadGradebookData(
- assignmentGroupsURL: @options.assignment_groups_url
- assignmentGroupsParams: assignmentGroupsParams
- courseId: @options.context_id
+ bindGridEvents: =>
+ @gradebookGrid.events.onColumnsReordered.subscribe (_event, columns) =>
+ # determine if assignment columns or custom columns were reordered
+ # (this works because frozen columns and non-frozen columns are can't be
+ # swapped)
- customColumnsURL: @options.custom_columns_url
- getGradingPeriodAssignments: @gradingPeriodSet?
+ currentFrozenIds = @gridData.columns.frozen
+ updatedFrozenIds = columns.frozen.map((column) => column.id)
- sectionsURL: @options.sections_url
+ @gridData.columns.frozen = updatedFrozenIds
+ @gridData.columns.scrollable = columns.scrollable.map((column) -> column.id)
- studentsURL: @options[@studentsUrl()]
- studentsPageCb: @gotChunkOfStudents
+ if !_.isEqual(currentFrozenIds, updatedFrozenIds)
+ currentFrozenColumns = currentFrozenIds.map((columnId) => @gridData.columns.definitions[columnId])
+ currentCustomColumnIds = (column.customColumnId for column in currentFrozenColumns when column.type == 'custom_column')
+ updatedCustomColumnIds = (column.customColumnId for column in columns.frozen when column.type == 'custom_column')
- submissionsURL: @options.submissions_url
- submissionsParams: submissionParams
- submissionsChunkCb: @gotSubmissionsChunk
- submissionsChunkSize: @options.chunk_size
- customColumnDataURL: @options.custom_column_data_url
- customColumnDataPageCb: @gotCustomColumnDataChunk
- )
+ if !_.isEqual(currentCustomColumnIds, updatedCustomColumnIds)
+ @reorderCustomColumns(updatedCustomColumnIds)
+ .then =>
+ colsById = _(@gradebookContent.customColumns).indexBy (c) -> c.id
+ @gradebookContent.customColumns = _(updatedCustomColumnIds).map (id) -> colsById[id]
+ else
+ @saveCustomColumnOrder()
- dataLoader.gotGradingPeriodAssignments?.then (response) =>
- @courseContent.gradingPeriodAssignments = response.grading_period_assignments
+ @renderViewOptionsMenu()
+ @updateColumnHeaders()
- dataLoader.gotAssignmentGroups.then @gotAllAssignmentGroups
- dataLoader.gotCustomColumns.then @gotCustomColumns
- dataLoader.gotStudents.then @gotAllStudents
+ @gradebookGrid.events.onColumnsResized.subscribe (_event, columns) =>
+ columns.forEach (column) =>
+ @saveColumnWidthPreference(column.id, column.width)
- $.when(
- dataLoader.gotCustomColumns,
- dataLoader.gotAssignmentGroups,
- dataLoader.gotGradingPeriodAssignments
- ).then(@doSlickgridStuff)
+ initialize: ->
+ @dataLoader.loadInitialData()
- @studentsLoaded = dataLoader.gotStudents
- @allSubmissionsLoaded = dataLoader.gotSubmissions
+ @gridReady.then () =>
+ # Preload the Grade Detail Tray
+ AsyncComponents.loadGradeDetailTray()
+ @renderViewOptionsMenu()
+ @renderGradebookSettingsModal()
- @showCustomColumnDropdownOption()
- @initPostGradesStore()
- @showPostGradesButton()
- @checkForUploadComplete()
+ # called from app/jsx/bundles/gradebook.js
+ onShow: ->
+ $(".post-grades-button-placeholder").show()
+ return if @startedInitializing
+ @startedInitializing = true
- @gotSections(@options.sections)
+ if @gridReady.state() != 'resolved'
+ @spinner = new Spinner() unless @spinner
+ $(@spinner.spin().el).css(
+ opacity: 0.5
+ top: '55px'
+ left: '50%'
+ ).addClass('use-css-transitions-for-show-hide').appendTo('#main')
+ $('#gradebook-grid-wrapper').hide()
+ else
+ $('#gradebook_grid').trigger('resize.fillWindowWithMe')
loadOverridesForSIS: ->
- return unless $('.post-grades-placeholder').length > 0
-
- assignmentGroupsURL = @options.assignment_groups_url.replace('&include%5B%5D=assignment_visibility', '')
- overrideDataLoader = DataLoader.loadGradebookData(
- assignmentGroupsURL: assignmentGroupsURL
- assignmentGroupsParams:
- exclude_response_fields: @fieldsToExcludeFromAssignments
- include: ['grades_published', 'overrides']
- onlyLoadAssignmentGroups: true
- )
- $.when(overrideDataLoader.gotAssignmentGroups).then(@addOverridesToPostGradesStore)
+ if @options.post_grades_feature
+ @dataLoader.loadOverridesForSIS()
addOverridesToPostGradesStore: (assignmentGroups) =>
for group in assignmentGroups
- group.assignments = _.select group.assignments, (a) -> a.published
for assignment in group.assignments
@assignments[assignment.id].overrides = assignment.overrides if @assignments[assignment.id]
@postGradesStore.setGradeBookAssignments @assignments
@@ -238,9 +462,6 @@ export default class Gradebook
for studentId in _.uniq(studentsWithHiddenAssignments)
student = @student(studentId)
@calculateStudentGrade(student)
- @grid.invalidateRow(student.row)
-
- @grid.render()
hiddenStudentIdsForAssignment: (studentIds, assignment) ->
# TODO: _.difference is ridic expensive. may need to do something else
@@ -252,99 +473,80 @@ export default class Gradebook
filteredVisibility = assignment.assignment_visibility.filter (id) -> id != hiddenSub.user_id
assignment.assignment_visibility = filteredVisibility
- gradingPeriodIsClosed: (gradingPeriod) ->
- new Date(gradingPeriod.close_date) < new Date()
-
- gradingPeriodIsActive: (gradingPeriodId) ->
- activePeriodIds = _.pluck(@gradingPeriods, 'id')
- _.includes(activePeriodIds, gradingPeriodId)
-
- getGradingPeriodToShow: () =>
- return null unless @gradingPeriodSet?
- currentPeriodId = UserSettings.contextGet('gradebook_current_grading_period')
- if currentPeriodId && (@isAllGradingPeriods(currentPeriodId) || @gradingPeriodIsActive(currentPeriodId))
- currentPeriodId
- else
- @options.current_grading_period_id
-
- onShow: ->
- $(".post-grades-button-placeholder").show()
- return if @startedInitializing
- @startedInitializing = true
- if @gridReady.state() != 'resolved'
- @spinner = new Spinner() unless @spinner
- $(@spinner.spin().el).css(
- opacity: 0.5
- top: '55px'
- left: '50%'
- ).addClass('use-css-transitions-for-show-hide').appendTo('#main')
- $('#gradebook-grid-wrapper').hide()
- else
- $('#gradebook_grid').trigger('resize.fillWindowWithMe')
-
gotCustomColumns: (columns) =>
- @customColumns = columns
+ @gradebookContent.customColumns = columns
+ columns.forEach (column) =>
+ customColumn = @buildCustomColumn(column)
+ @gridData.columns.definitions[customColumn.id] = customColumn
+
+ gotCustomColumnDataChunk: (customColumnId, columnData) =>
+ studentIds = []
- gotCustomColumnDataChunk: (column, columnData) =>
for datum in columnData
student = @student(datum.user_id)
if student? #ignore filtered students
- student["custom_col_#{column.id}"] = datum.content
- @grid?.invalidateRow(student.row)
- @grid?.render()
+ student["custom_col_#{customColumnId}"] = datum.content
+ studentIds.push(student.id)
- doSlickgridStuff: =>
- @initGrid()
- @initHeader()
- @gridReady.resolve()
- @loadOverridesForSIS()
+ @invalidateRowsForStudentIds(_.uniq(studentIds))
gotAllAssignmentGroups: (assignmentGroups) =>
+ @setAssignmentGroupsLoaded(true)
# purposely passing the @options and assignmentGroups by reference so it can update
# an assigmentGroup's .group_weight and @options.group_weighting_scheme
- new AssignmentGroupWeightsDialog context: @options, assignmentGroups: assignmentGroups
for group in assignmentGroups
@assignmentGroups[group.id] = group
- group.assignments = _.select group.assignments, (a) -> a.published
for assignment in group.assignments
assignment.assignment_group = group
assignment.due_at = tz.parse(assignment.due_at)
- assignment.moderation_in_progress = assignment.moderated_grading and !assignment.grades_published
@updateAssignmentEffectiveDueDates(assignment)
@assignments[assignment.id] = assignment
- gotSections: (sections) =>
- @sections = {}
- for section in sections
- htmlEscape(section)
- @sections[section.id] = section
+ gotGradingPeriodAssignments: ({ grading_period_assignments: gradingPeriodAssignments }) =>
+ @courseContent.gradingPeriodAssignments = gradingPeriodAssignments
- @sections_enabled = sections.length > 1
+ gotSections: (sections) =>
+ @setSections(sections.map(htmlEscape))
@hasSections.resolve()
@postGradesStore.setSections @sections
gotChunkOfStudents: (students) =>
+ @courseContent.assignmentStudentVisibility = {}
for student in students
- student.enrollments = _.filter student.enrollments, @isStudentEnrollment
+ student.enrollments = _.filter student.enrollments, (e) ->
+ e.type == "StudentEnrollment" || e.type == "StudentViewEnrollment"
isStudentView = student.enrollments[0].type == "StudentViewEnrollment"
student.sections = student.enrollments.map (e) -> e.course_section_id
if isStudentView
- @studentViewStudents[student.id] ||= htmlEscape(student)
+ @studentViewStudents[student.id] = htmlEscape(student)
else
- @students[student.id] ||= htmlEscape(student)
- @addRow(student) # not adding student view students until all students have loaded
+ @students[student.id] = htmlEscape(student)
+
+ @updateStudentAttributes(student)
+ @updateStudentRow(student)
@gridReady.then =>
@setupGrading(students)
- @grid?.render()
+ if @isFilteringRowsBySearchTerm()
+ # When filtering, students cannot be matched until loaded. The grid must
+ # be re-rendered more aggressively to ensure new rows are inserted.
+ @buildRows()
+ else
+ @gradebookGrid.render()
+
+ ## Post-Data Load Initialization
- isStudentEnrollment: (e) =>
- e.type == "StudentEnrollment" || e.type == "StudentViewEnrollment"
+ finishRenderingUI: =>
+ @initGrid()
+ @initHeader()
+ @gridReady.resolve()
+ @loadOverridesForSIS()
setupGrading: (students) =>
+ # set up a submission for each student even if we didn't receive one
@submissionStateMap.setup(students, @assignments)
for student in students
for assignment_id, assignment of @assignments
@@ -356,11 +558,19 @@ export default class Gradebook
student.initialized = true
@calculateStudentGrade(student)
- @grid?.invalidateRow(student.row)
- @setAssignmentVisibility(_.pluck(students, 'id'))
+ studentIds = _.pluck(students, 'id')
+ @setAssignmentVisibility(studentIds)
+
+ @invalidateRowsForStudentIds(studentIds)
+
+ resetGrading: =>
+ @initSubmissionStateMap()
+ @setupGrading(@courseContent.students.listStudents())
- @grid.render()
+ getSubmission: (studentId, assignmentId) =>
+ student = @student(studentId)
+ student?["assignment_#{assignmentId}"]
updateEffectiveDueDatesFromSubmissions: (submissions) =>
EffectiveDueDates.updateWithSubmissions(@effectiveDueDates, submissions, @gradingPeriodSet?.gradingPeriods)
@@ -369,132 +579,118 @@ export default class Gradebook
assignment.effectiveDueDates = @effectiveDueDates[assignment.id] || {}
assignment.inClosedGradingPeriod = _.some(assignment.effectiveDueDates, (date) => date.in_closed_grading_period)
- rowIndex: 0
- addRow: (student) =>
+ updateStudentAttributes: (student) =>
student.computed_current_score ||= 0
student.computed_final_score ||= 0
- student.secondary_identifier = student.sis_login_id || student.login_id
student.isConcluded = _.every student.enrollments, (e) ->
e.enrollment_state == 'completed'
student.isInactive = _.every student.enrollments, (e) ->
e.enrollment_state == 'inactive'
- if @sections_enabled
- mySections = (@sections[sectionId].name for sectionId in student.sections when @sections[sectionId])
- sectionNames = $.toSentence(mySections.sort())
+ student.cssClass = "student_#{student.id}"
- displayName = if @options.list_students_by_sortable_name_enabled
- student.sortable_name
- else
- student.name
-
- enrollmentStatus = if student.isConcluded
- I18n.t 'concluded'
- else if student.isInactive
- I18n.t 'inactive'
-
- student.display_name = RowStudentNameTemplate
- student_id: student.id
- course_id: @options.context_id
- avatar_url: student.avatar_url
- display_name: displayName
- enrollment_status: enrollmentStatus
- url: student.enrollments[0].grades.html_url+'#tab-assignments'
- sectionNames: sectionNames
- alreadyEscaped: true
-
- if @rowFilter(student)
- student.row = @rowIndex
- @rowIndex++
- @rows.push(student)
-
- @grid?.updateRowCount(@rows.length)
+ updateStudentRow: (student) =>
+ index = @gridData.rows.findIndex (row) => row.id == student.id
+ if index != -1
+ @gridData.rows[index] = @buildRow(student)
+ @gradebookGrid.invalidateRow(index)
gotAllStudents: =>
- # add test students
- _.each _.values(@studentViewStudents), (testStudent) =>
- @addRow(testStudent)
+ @setStudentsLoaded(true)
+ @renderedGrid.then =>
+ @gradebookGrid.gridSupport.columns.updateColumnHeaders(['student'])
+
+ studentsThatCanSeeAssignment: (assignmentId) ->
+ @courseContent.assignmentStudentVisibility[assignmentId] ||= (
+ assignment = @getAssignment(assignmentId)
+ allStudents = Object.assign({}, @students, @studentViewStudents)
+ if assignment.only_visible_to_overrides
+ _.pick allStudents, assignment.assignment_visibility...
+ else
+ allStudents
+ )
- defaultSortType: 'assignment_group'
+ isInvalidSort: =>
+ sortSettings = @gradebookColumnOrderSettings
- studentsThatCanSeeAssignment: (potential_students, assignment) ->
- if assignment.only_visible_to_overrides
- _.pick potential_students, assignment.assignment_visibility...
- else
- potential_students
+ # This course was sorted by a custom column sort at some point but no longer has any stored
+ # column order to sort by
+ # let's mark it invalid so it reverts to default sort
+ return true if sortSettings?.sortType == 'custom' && !sortSettings?.customOrder
- isInvalidCustomSort: =>
- sortSettings = @gradebookColumnOrderSettings
- sortSettings && sortSettings.sortType == 'custom' && !sortSettings.customOrder
+ # This course was sorted by module_position at some point but no longer contains modules
+ # let's mark it invalid so it reverts to default sort
+ return true if sortSettings?.sortType == 'module_position' && @listContextModules().length == 0
+
+ false
- columnOrderHasNotBeenSaved: =>
- !@gradebookColumnOrderSettings
+ isDefaultSortOrder: (sortOrder) =>
+ not (['due_date', 'name', 'points', 'module_position', 'custom'].includes(sortOrder))
- getStoredSortOrder: =>
- if @isInvalidCustomSort() || @columnOrderHasNotBeenSaved()
- {sortType: @defaultSortType}
+ setColumnOrder: (order) ->
+ @gradebookColumnOrderSettings ?= {
+ direction: 'ascending'
+ freezeTotalGrade: false
+ sortType: @defaultSortType
+ }
+
+ return unless order
+
+ if order.freezeTotalGrade?
+ @gradebookColumnOrderSettings.freezeTotalGrade = order.freezeTotalGrade
+
+ if order.sortType == 'custom' and order.customOrder?
+ @gradebookColumnOrderSettings.sortType = 'custom'
+ @gradebookColumnOrderSettings.customOrder = order.customOrder
+ else if order.sortType? and order.direction?
+ @gradebookColumnOrderSettings.sortType = order.sortType
+ @gradebookColumnOrderSettings.direction = order.direction
+
+ getColumnOrder: =>
+ if @isInvalidSort() || !@gradebookColumnOrderSettings
+ direction: 'ascending'
+ freezeTotalGrade: false
+ sortType: @defaultSortType
else
@gradebookColumnOrderSettings
- setStoredSortOrder: (newSortOrder) ->
- @gradebookColumnOrderSettings = newSortOrder
- unless @isInvalidCustomSort()
+ saveColumnOrder: ->
+ unless @isInvalidSort()
url = @options.gradebook_column_order_settings_url
- $.ajaxJSON(url, 'POST', {column_order: newSortOrder})
-
- onColumnsReordered: =>
- # determine if assignment columns or custom columns were reordered
- # (this works because frozen columns and non-frozen columns are can't be
- # swapped)
- columns = @grid.getColumns()
- currentIds = _(@customColumns).map (c) -> c.id
- reorderedIds = (m[1] for c in columns when m = c.id.match /^custom_col_(\d+)/)
-
- if !_.isEqual(reorderedIds, currentIds)
- @reorderCustomColumns(reorderedIds)
- .then =>
- colsById = _(@customColumns).indexBy (c) -> c.id
- @customColumns = _(reorderedIds).map (id) -> colsById[id]
- else
- @storeCustomColumnOrder()
-
- @fixColumnReordering()
+ $.ajaxJSON(url, 'POST', { column_order: @getColumnOrder() })
reorderCustomColumns: (ids) ->
$.ajaxJSON(@options.reorder_custom_columns_url, "POST", order: ids)
- storeCustomColumnOrder: =>
- newSortOrder =
+ saveCustomColumnOrder: =>
+ @setColumnOrder(
+ customOrder: @gridData.columns.scrollable
sortType: 'custom'
- customOrder: []
- columns = @grid.getColumns()
- assignment_columns = _.filter(columns, (c) -> c.type is 'assignment')
- newSortOrder.customOrder = _.map(assignment_columns, (a) -> a.object.id)
- @setStoredSortOrder(newSortOrder)
-
- setArrangementTogglersVisibility: (newSortOrder) =>
- @$columnArrangementTogglers.each ->
- $(this).closest('li').showIf $(this).data('arrangeColumnsBy') isnt newSortOrder.sortType
+ )
+ @saveColumnOrder()
arrangeColumnsBy: (newSortOrder, isFirstArrangement) =>
- @setArrangementTogglersVisibility(newSortOrder)
- @setStoredSortOrder(newSortOrder) unless isFirstArrangement
+ unless isFirstArrangement
+ @setColumnOrder(newSortOrder)
+ @saveColumnOrder()
- columns = @grid.getColumns()
- frozen = columns.splice(0, @getFrozenColumnCount())
+ columns = @gridData.columns.scrollable.map((columnId) => @gridData.columns.definitions[columnId])
columns.sort @makeColumnSortFn(newSortOrder)
- columns.splice(0, 0, frozen...)
- @grid.setColumns(columns)
+ @gridData.columns.scrollable = columns.map((column) -> column.id)
- @fixColumnReordering()
+ @updateGrid()
+ @renderViewOptionsMenu()
+ @updateColumnHeaders()
makeColumnSortFn: (sortOrder) =>
- fn = switch sortOrder.sortType
- when 'due_date' then @compareAssignmentDueDates
+ switch sortOrder.sortType
+ when 'due_date' then @wrapColumnSortFn(@compareAssignmentDueDates, sortOrder.direction)
+ when 'module_position' then @wrapColumnSortFn(@compareAssignmentModulePositions, sortOrder.direction)
+ when 'name' then @wrapColumnSortFn(@compareAssignmentNames, sortOrder.direction)
+ when 'points' then @wrapColumnSortFn(@compareAssignmentPointsPossible, sortOrder.direction)
when 'custom' then @makeCompareAssignmentCustomOrderFn(sortOrder)
- else @compareAssignmentPositions
- @wrapColumnSortFn(fn)
+ else @wrapColumnSortFn(@compareAssignmentPositions, sortOrder.direction)
compareAssignmentPositions: (a, b) ->
diffOfAssignmentGroupPosition = a.object.assignment_group.position - b.object.assignment_group.position
@@ -509,6 +705,33 @@ export default class Gradebook
secondAssignment = b.object
assignmentHelper.compareByDueDate(firstAssignment, secondAssignment)
+ compareAssignmentModulePositions: (a, b) =>
+ firstAssignmentModulePosition = @getContextModule(a.object.module_ids[0])?.position
+ secondAssignmentModulePosition = @getContextModule(b.object.module_ids[0])?.position
+
+ if firstAssignmentModulePosition? && secondAssignmentModulePosition?
+ if firstAssignmentModulePosition == secondAssignmentModulePosition
+ # let's determine their order in the module because both records are in the same module
+ firstPositionInModule = a.object.module_positions[0]
+ secondPositionInModule = b.object.module_positions[0]
+
+ firstPositionInModule - secondPositionInModule
+ else
+ # let's determine the order of their modules because both records are in different modules
+ firstAssignmentModulePosition - secondAssignmentModulePosition
+ else if !firstAssignmentModulePosition? && secondAssignmentModulePosition?
+ 1
+ else if firstAssignmentModulePosition? && !secondAssignmentModulePosition?
+ -1
+ else
+ @compareAssignmentPositions(a, b)
+
+ compareAssignmentNames: (a, b) =>
+ @localeSort(a.object.name, b.object.name)
+
+ compareAssignmentPointsPossible: (a, b) ->
+ a.object.points_possible - b.object.points_possible
+
makeCompareAssignmentCustomOrderFn: (sortOrder) =>
sortMap = {}
indexCounter = 0
@@ -516,155 +739,165 @@ export default class Gradebook
sortMap[String(assignmentId)] = indexCounter
indexCounter += 1
return (a, b) =>
- aIndex = sortMap[String(a.object.id)]
- bIndex = sortMap[String(b.object.id)]
+ # The second lookup for each index is to maintain backwards
+ # compatibility with old gradebook sorting on load which only
+ # considered assignment ids.
+ aIndex = sortMap[a.id]
+ aIndex ?= sortMap[String(a.object.id)] if a.object?
+ bIndex = sortMap[b.id]
+ bIndex ?= sortMap[String(b.object.id)] if b.object?
if aIndex? and bIndex?
return aIndex - bIndex
- # if there's a new assignment and its order has not been stored, it should come at the end
+ # if there's a new assignment or assignment group and its
+ # order has not been stored, it should come at the end
else if aIndex? and not bIndex?
return -1
else if bIndex?
return 1
else
- return @compareAssignmentPositions(a, b)
+ return @wrapColumnSortFn(@compareAssignmentPositions)(a, b)
- wrapColumnSortFn: (wrappedFn) ->
+ wrapColumnSortFn: (wrappedFn, direction = 'ascending') ->
(a, b) ->
+ return -1 if b.type is 'total_grade_override'
+ return 1 if a.type is 'total_grade_override'
return -1 if b.type is 'total_grade'
return 1 if a.type is 'total_grade'
return -1 if b.type is 'assignment_group' and a.type isnt 'assignment_group'
return 1 if a.type is 'assignment_group' and b.type isnt 'assignment_group'
if a.type is 'assignment_group' and b.type is 'assignment_group'
return a.object.position - b.object.position
- return wrappedFn(a, b)
+
+ [a, b] = [b, a] if direction == 'descending'
+ wrappedFn(a, b)
+
+ ## Filtering
rowFilter: (student) =>
- matchingSection = !@sectionToShow || (@sectionToShow in student.sections)
- matchingFilter = if @userFilterTerm == ""
- true
- else
- propertiesToMatch = ['name', 'login_id', 'short_name', 'sortable_name']
- pattern = new RegExp @userFilterTerm, 'i'
- matched = _.some propertiesToMatch, (prop) ->
- student[prop]?.match pattern
-
- matchingSection and matchingFilter
-
- handleAssignmentMutingChange: (assignment) =>
- gradebookAssignment = @assignments[assignment.id]
- gradebookAssignment.anonymize_students = assignment.anonymize_students
- gradebookAssignment.muted = assignment.muted
- idx = @grid.getColumnIndex("assignment_#{assignment.id}")
- colDef = @grid.getColumns()[idx]
- colDef.name = @assignmentHeaderHtml(assignment)
- @grid.setColumns(@grid.getColumns())
- @fixColumnReordering()
- @buildRows()
- allStudents = Object.values(@students).concat(Object.values(@studentViewStudents))
- @setupGrading(allStudents)
-
- handleAssignmentGroupWeightChange: (assignment_group_options) =>
- columns = @grid.getColumns()
- for assignment_group in assignment_group_options.assignmentGroups
- column = _.findWhere columns, id: "assignment_group_#{assignment_group.id}"
- column.name = @assignmentGroupHtml(column.object.name, column.object.group_weight)
- @setAssignmentWarnings()
- @grid.setColumns(columns)
- @renderTotalHeader()
- # TODO: don't buildRows?
- @buildRows()
+ return true unless @isFilteringRowsBySearchTerm()
+
+ propertiesToMatch = ['name', 'login_id', 'short_name', 'sortable_name', 'sis_user_id']
+ pattern = new RegExp(@userFilterTerm, 'i')
+ _.some propertiesToMatch, (prop) ->
+ student[prop]?.match pattern
+
+ filterAssignments: (assignments) =>
+ assignmentFilters = [
+ @filterAssignmentBySubmissionTypes,
+ @filterAssignmentByPublishedStatus,
+ @filterAssignmentByAssignmentGroup,
+ @filterAssignmentByGradingPeriod,
+ @filterAssignmentByModule
+ ]
- renderTotalHeader: () =>
- @totalHeader = new TotalColumnHeaderView
- showingPoints: @options.show_total_grade_as_points
- toggleShowingPoints: @togglePointsOrPercentTotals.bind(this)
- weightedGrades: @weightedGrades
- totalColumnInFront: @totalColumnInFront
- moveTotalColumn: @moveTotalColumn.bind(this)
- @totalHeader.render()
-
- moveTotalColumn: =>
- @totalColumnInFront = not @totalColumnInFront
- UserSettings.contextSet 'total_column_in_front', @totalColumnInFront
- window.location.reload()
-
- assignmentGroupHtml: (group_name, group_weight) =>
- if @weightedGroups()
- percentage = I18n.toPercentage(group_weight, precision: 2)
- """
- #{htmlEscape(group_name)}
- #{htmlEscape I18n.t 'percent_of_grade', "%{percentage} of grade", percentage: percentage}
-
- """
- else
- htmlEscape(group_name)
+ matchesAllFilters = (assignment) =>
+ assignmentFilters.every ((filter) => filter(assignment))
+
+ assignments.filter(matchesAllFilters)
+
+ filterAssignmentBySubmissionTypes: (assignment) =>
+ submissionType = '' + assignment.submission_types
+ submissionType isnt 'not_graded' and
+ (submissionType isnt 'attendance' or @show_attendance)
+
+ filterAssignmentByPublishedStatus: (assignment) =>
+ assignment.published or @gridDisplaySettings.showUnpublishedAssignments
+
+ filterAssignmentByAssignmentGroup: (assignment) =>
+ return true unless @isFilteringColumnsByAssignmentGroup()
+ @getAssignmentGroupToShow() == assignment.assignment_group_id
+
+ filterAssignmentByGradingPeriod: (assignment) =>
+ return true unless @isFilteringColumnsByGradingPeriod()
+ assignment.id in (@courseContent.gradingPeriodAssignments[@getGradingPeriodToShow()] or [])
+
+ filterAssignmentByModule: (assignment) =>
+ contextModuleFilterSetting = @getFilterColumnsBySetting('contextModuleId')
+ return true unless contextModuleFilterSetting
+ # Firefox returns a value of "null" (String) for this when nothing is set. The comparison
+ # to 'null' below is a result of that
+ return true if contextModuleFilterSetting == '0' || contextModuleFilterSetting == 'null'
+
+ @getFilterColumnsBySetting('contextModuleId') in (assignment.module_ids || [])
+
+ ## Course Content Event Handlers
+
+ handleSubmissionPostedChange: (assignment) =>
+ if assignment.anonymize_students
+ anonymousColumnIds = [
+ @getAssignmentColumnId(assignment.id),
+ @getAssignmentGroupColumnId(assignment.assignment_group_id),
+ 'total_grade',
+ 'total_grade_override'
+ ]
+
+ if @getSortRowsBySetting().columnId in anonymousColumnIds
+ @setSortRowsBySetting('student', 'sortable_name', 'ascending')
+
+ @gradebookGrid.gridSupport.columns.updateColumnHeaders([@getAssignmentColumnId(assignment.id)])
+ @updateFilteredContentInfo()
+ @resetGrading()
+
+ handleSubmissionsDownloading: (assignmentId) =>
+ @getAssignment(assignmentId).hasDownloadedSubmissions = true
+ @gradebookGrid.gridSupport.columns.updateColumnHeaders([@getAssignmentColumnId(assignmentId)])
# filter, sort, and build the dataset for slickgrid to read from, then
# force a full redraw
buildRows: =>
- @rows.length = 0
+ @gridData.rows.length = 0 # empty the list of rows
- for id, column of @grid.getColumns() when ''+column.object?.submission_types is "attendance"
- column.unselectable = !@show_attendance
- column.cssClass = if @show_attendance then '' else 'completely-hidden'
- @$grid.find("##{@uid}#{column.id}").showIf(@show_attendance)
+ for student in @courseContent.students.listStudents()
+ if @rowFilter(student)
+ @gridData.rows.push(@buildRow(student))
+ @calculateStudentGrade(student) # TODO: this may not be necessary
- @withAllStudents (students) =>
- @rowIndex = 0
- for id, student of @students
- student.row = -1
- if @rowFilter(student)
- student.row = @rowIndex
- @rowIndex += 1
- @rows.push(student)
- @calculateStudentGrade(student) # TODO: this may not be necessary
+ @gradebookGrid.invalidate()
- @grid.updateRowCount(@rows.length)
-
- @sortRowsBy (a, b) => @localeSort(a.sortable_name, b.sortable_name)
+ buildRow: (student) =>
+ # because student is current mutable, we need to retain the reference
+ student
gotSubmissionsChunk: (student_submissions) =>
- @gridReady.then =>
- changedStudentIds = []
- submissions = []
+ changedStudentIds = []
+ submissions = []
- for data in student_submissions
- changedStudentIds.push(data.user_id)
- student = @student(data.user_id)
- for submission in data.submissions
- submissions.push(submission)
- @updateSubmission(submission)
+ for studentSubmissionGroup in student_submissions
+ changedStudentIds.push(studentSubmissionGroup.user_id)
+ student = @student(studentSubmissionGroup.user_id)
+ for submission in studentSubmissionGroup.submissions
+ ensureAssignmentVisibility(@getAssignment(submission.assignment_id), submission)
+ submissions.push(submission)
+ @updateSubmission(submission)
- student.loaded = true
+ student.loaded = true
- @updateEffectiveDueDatesFromSubmissions(submissions)
- _.each @assignments, (assignment) =>
- @updateAssignmentEffectiveDueDates(assignment)
+ @updateEffectiveDueDatesFromSubmissions(submissions)
+ _.each @assignments, (assignment) =>
+ @updateAssignmentEffectiveDueDates(assignment)
- changedStudentIds = _.uniq(changedStudentIds)
- students = changedStudentIds.map(@student)
- @setupGrading(students)
+ changedStudentIds = _.uniq(changedStudentIds)
+ students = changedStudentIds.map(@student)
+ @setupGrading(students)
student: (id) =>
@students[id] || @studentViewStudents[id]
- # @students contains all *real* students (e.g., not the student view student)
- # when you do need to operate on *all* students (like for rendering the grid), use
- # function
- withAllStudents: (f) =>
- for id, s of @studentViewStudents
- @students[id] = s
-
- f(@students)
-
- for id, s of @studentViewStudents
- delete @students[id]
-
updateSubmission: (submission) =>
student = @student(submission.user_id)
submission.submitted_at = tz.parse(submission.submitted_at)
+ submission.excused = !!submission.excused
+ submission.hidden = !!submission.hidden
submission.rawGrade = submission.grade # save the unformatted version of the grade too
- submission.grade = GradeFormatHelper.formatGrade(submission.grade, { gradingType: submission.gradingType, delocalize: false })
+
+ if assignment = @assignments[submission.assignment_id]
+ submission.gradingType = assignment.grading_type
+
+ if submission.gradingType != "pass_fail"
+ submission.grade = GradeFormatHelper.formatGrade(submission.grade, {
+ gradingType: submission.gradingType, delocalize: false
+ })
+
cell = student["assignment_#{submission.assignment_id}"] ||= {}
_.extend(cell, submission)
@@ -674,19 +907,20 @@ export default class Gradebook
# where each student has an array of submissions. This one just expects an array of submissions,
# they are not grouped by student.
updateSubmissionsFromExternal: (submissions) =>
- activeCell = @grid.getActiveCell()
- editing = $(@grid.getActiveCellNode()).hasClass('editable')
- columns = @grid.getColumns()
+ columns = @gradebookGrid.grid.getColumns()
+ changedColumnHeaders = {}
+ changedStudentIds = []
+
for submission in submissions
student = @student(submission.user_id)
continue unless student # if the student isn't loaded, we don't need to update it
- idToMatch = "assignment_#{submission.assignment_id}"
+ idToMatch = @getAssignmentColumnId(submission.assignment_id)
cell = index for column, index in columns when column.id is idToMatch
- thisCellIsActive = activeCell? and
- editing and
- activeCell.row is student.row and
- activeCell.cell is cell
+
+ unless changedColumnHeaders[submission.assignment_id]
+ changedColumnHeaders[submission.assignment_id] = cell
+
#check for DA visible
@updateAssignmentVisibilities(submission) unless submission.assignment_visible
@updateSubmission(submission)
@@ -694,93 +928,21 @@ export default class Gradebook
submissionState = @submissionStateMap.getSubmissionState(submission)
student["assignment_#{submission.assignment_id}"].gradeLocked = submissionState.locked
@calculateStudentGrade(student)
- @grid.updateCell student.row, cell unless thisCellIsActive
- @updateRowTotals student.row
+ changedStudentIds.push(student.id)
- updateRowTotals: (rowIndex) ->
- columns = @grid.getColumns()
- for column, columnIndex in columns
- @grid.updateCell rowIndex, columnIndex if column.type isnt 'assignment'
+ changedColumnIds = Object.keys(changedColumnHeaders).map(@getAssignmentColumnId)
+ @gradebookGrid.gridSupport.columns.updateColumnHeaders(changedColumnIds)
- cellFormatter: (row, col, submission) =>
- if !@rows[row].loaded or !@rows[row].initialized
- @staticCellFormatter(row, col, '')
- else
- cellAttributes = @submissionStateMap.getSubmissionState(submission)
- if cellAttributes.hideGrade
- @lockedAndHiddenGradeCellFormatter(row, col, cellAttributes.tooltip)
- else
- assignment = @assignments[submission.assignment_id]
- student = @students[submission.user_id]
- formatterOpts =
- isLocked: cellAttributes.locked
- tooltip: cellAttributes.tooltip
-
- if !assignment?
- @staticCellFormatter(row, col, '')
- else if assignment.anonymize_students
- @lockedAndHiddenGradeCellFormatter(row, col, 'anonymous')
- else if submission.workflow_state == 'pending_review'
- (SubmissionCell[assignment.grading_type] || SubmissionCell).formatter(row, col, submission, assignment, student, formatterOpts)
- else if assignment.grading_type == 'points' && assignment.points_possible
- SubmissionCell.out_of.formatter(row, col, submission, assignment, student, formatterOpts)
- else
- (SubmissionCell[assignment.grading_type] || SubmissionCell).formatter(row, col, submission, assignment, student, formatterOpts)
-
- staticCellFormatter: (row, col, val) ->
- "