revert several grade calculation changes

Revert "make grade_summary page use GradeCalculator"

This reverts commit 4776cb2880.

Revert "fix gradebook bug in ie8"

This reverts commit c927237458.

Revert "gb2: don't lie about which assignments are dropped"

This reverts commit da37933079.

Revert "fix ruby calculation of grades (grade-dropping)"

This reverts commit 63f75597a2.

Revert "fix grade-dropping in gradebooks"

This reverts commit 4a4a2f53c1.

Conflicts:

	spec/selenium/grading_standards_spec.rb

Change-Id: I597d777fee23c4dc57bb915c6cbe5185dc18d0af
Reviewed-on: https://gerrit.instructure.com/13716
Tested-by: Jenkins <jenkins@instructure.com>
Reviewed-by: Ryan Florence <ryanf@instructure.com>
Reviewed-by: Cameron Matheson <cameron@instructure.com>
This commit is contained in:
Bryan Madsen 2012-09-13 18:53:07 -06:00
parent 6d2d60b3f1
commit c835ccd39a
16 changed files with 351 additions and 581 deletions

View File

@ -1 +1 @@
require ['grade_summary']
require ['grade_summary', 'compiled/grade_calculator']

View File

@ -1,6 +1,7 @@
define [
'underscore'
], (_) ->
'INST'
'jquery'
], (INST, $) ->
class GradeCalculator
# each submission needs fields: score, points_possible, assignment_id, assignment_group_id
@ -13,7 +14,9 @@ define [
# if weighting_scheme is "percent", group weights are used, otherwise no weighting is applied
@calculate: (submissions, groups, weighting_scheme) ->
result = {}
result.group_sums = _(groups).map (group) =>
# NOTE: purposely using $.map because it can handle array or object, old gradebook sends array
# new gradebook sends object, needs jquery >1.6's version of $.map, since it can handle both
result.group_sums = $.map groups, (group) =>
group: group
current: @create_group_sum(group, submissions, true)
'final': @create_group_sum(group, submissions, false)
@ -21,133 +24,59 @@ define [
result['final'] = @calculate_total(result.group_sums, false, weighting_scheme)
result
@create_group_sum: (group, submissions, ignoreUngraded) ->
arrayToObj = (arr, property) ->
obj = {}
for e in arr
obj[e[property]] = e
obj
@create_group_sum: (group, submissions, ignore_ungraded) ->
sum = { submissions: [], score: 0, possible: 0, submission_count: 0 }
# Never include 'not_graded' assignments in the totals. The submission_types is an array (always?), adding a ''
# to it converts it to a string and will correctly evaluate if it is an array of multiple entries.
for assignment in group.assignments when (''+ assignment.submission_types != 'not_graded')
data = { score: 0, possible: 0, percent: 0, drop: false, submitted: false }
gradeableAssignments = _(group.assignments).filter (a) ->
not _.isEqual(a.submission_types, ['not_graded'])
assignments = arrayToObj gradeableAssignments, "id"
submission = null
for s in submissions when s.assignment_id == assignment.id
submission = s
break
# filter out submissions from other assignment groups
submissions = _(submissions).filter (s) -> assignments[s.assignment_id]?
submission ?= { score: null }
submission.assignment_group_id = group.id
submission.points_possible ?= assignment.points_possible
data.submission = submission
sum.submissions.push data
unless ignore_ungraded and (!submission.score? || submission.score == '')
data.score = @parse submission.score
data.possible = @parse assignment.points_possible
data.percent = @parse(data.score / data.possible)
data.submitted = (submission.score? and submission.score != '')
sum.submission_count += 1 if data.submitted
# fill in any missing submissions
unless ignoreUngraded
submissionAssignmentIds = _(submissions).map (s) ->
s.assignment_id.toString()
missingSubmissions = _.difference(_.keys(assignments),
submissionAssignmentIds)
dummySubmissions = _(missingSubmissions).map (assignmentId) ->
s = assignment_id: assignmentId, score: null
submissions.push dummySubmissions...
# sort the submissions by assigned score
sum.submissions.sort (a,b) -> a.percent - b.percent
rules = $.extend({ drop_lowest: 0, drop_highest: 0, never_drop: [] }, group.rules)
submissionsByAssignment = arrayToObj submissions, "assignment_id"
dropped = 0
submissionData = _(submissions).map (s) =>
sub =
total: @parse assignments[s.assignment_id].points_possible
score: @parse s.score
submitted: s.score? and s.score != ''
submission: s
relevantSubmissionData = if ignoreUngraded
_(submissionData).filter (s) -> s.submitted
else
submissionData
# drop the lowest and highest assignments
for lowOrHigh in ['low', 'high']
for data in sum.submissions
if !data.drop and rules["drop_#{lowOrHigh}est"] > 0 and $.inArray(data.assignment_id, rules.never_drop) == -1 and data.possible > 0 and data.submitted
data.drop = true
# TODO: do I want to do this, it actually modifies the passed in submission object but it
# it seems like the best way to tell it it should be dropped.
data.submission?.drop = true
rules["drop_#{lowOrHigh}est"] -= 1
dropped += 1
kept = @dropAssignments relevantSubmissionData, group.rules
# if everything was dropped, un-drop the highest single submission
if dropped > 0 and dropped == sum.submission_count
sum.submissions[sum.submissions.length - 1].drop = false
# see TODO above
sum.submissions[sum.submissions.length - 1].submission?.drop = false
dropped -= 1
[score, possible] = _.reduce kept
, ([scoreSum, totalSum], {score,total}) =>
[scoreSum + @parse(score), totalSum + total]
, [0, 0]
sum.submission_count -= dropped
ret =
possible: possible
score: score
# TODO: figure out what submission_count is actually counting
submission_count: (_(submissionData).filter (s) -> s.submitted).length
submissions: _(submissionData).map (s) =>
submissionRet =
drop: s.drop
percent: @parse(s.score / s.total)
possible: s.total
score: @parse(s.score)
submission: _.extend(s.submission, drop: s.drop)
submitted: s.submitted
# I'm not going to pretend that this code is understandable.
#
# The naive approach to dropping the lowest grades (calculate the
# grades for each combination of assignments and choose the set which
# results in the best overall score) is obviously too slow.
#
# This approach is based on the algorithm described in "Dropping Lowest
# Grades" by Daniel Kane and Jonathan Kane. Please see that paper for
# a full explanation of the math.
# (http://web.mit.edu/dankane/www/droplowest.pdf)
@dropAssignments: (submissions, rules) ->
rules or= {}
dropLowest = rules.drop_lowest || 0
dropHighest = rules.drop_highest || 0
neverDropIds = rules.never_drop || []
return submissions unless dropLowest or dropHighest
if neverDropIds.length > 0
cantDrop = _(submissions).filter (s) ->
_.indexOf(neverDropIds, parseInt s.submission.assignment_id) >= 0
submissions = _.difference submissions, cantDrop
else
cantDrop = []
return cantDrop if submissions.length == 0
dropLowest = submissions.length - 1 if dropLowest >= submissions.length
dropHighest = 0 if dropLowest + dropHighest >= submissions.length
totals = (s.total for s in submissions)
maxTotal = Math.max(totals...)
grades = (s.score / s.total for s in submissions).sort (a,b) -> a - b
qLow = grades[0]
qHigh = grades[grades.length - 1]
qMid = (qLow + qHigh) / 2
keepHighest = submissions.length - dropLowest
bigF = (q, submissions) ->
ratedScores = _(submissions).map (s) ->
ratedScore = s.score - q * s.total
[ratedScore, s]
rankedScores = ratedScores.sort (a, b) -> b[0] - a[0]
keptScores = rankedScores[0...keepHighest]
qKept = _.reduce keptScores
, (sum, [ratedScore, s]) ->
sum + ratedScore
, 0
keptSubmissions = (s for [ratedScore, s] in keptScores)
[qKept, keptSubmissions]
[x, kept] = bigF(qMid, submissions)
threshold = 1 /(2 * keepHighest * Math.pow(maxTotal, 2))
until qHigh - qLow < threshold
if x < 0
qHigh = qMid
else
qLow = qMid
qMid = (qLow + qHigh) / 2
[x, kept] = bigF(qMid, submissions)
if dropHighest
kept.splice 0, dropHighest
kept.push cantDrop...
dropped = _.difference(submissions, kept)
# SIDE EFFECT: The gradebooks require this behavior
s.drop = true for s in dropped
kept
sum.score += s.score for s in sum.submissions when !s.drop
sum.possible += s.possible for s in sum.submissions when !s.drop
sum
@calculate_total: (group_sums, ignore_ungraded, weighting_scheme) ->
data_idx = if ignore_ungraded then 'current' else 'final'
@ -186,7 +115,10 @@ define [
@letter_grade: (grading_scheme, score) ->
score = 0 if score < 0
letters = _(grading_scheme).filter (row, i) ->
letters = $.grep grading_scheme, (row, i) ->
score >= row[1] * 100 || i == (grading_scheme.length - 1)
letter = letters[0]
letter[0]
window.INST.GradeCalculator = GradeCalculator

View File

@ -304,25 +304,11 @@ define [
if student.loaded
finalOrCurrent = if @include_ungraded_assignments then 'final' else 'current'
submissionsAsArray = (value for key, value of student when key.match /^assignment_(?!group)/)
result = GradeCalculator.calculate(submissionsAsArray, @assignmentGroups, @options.group_weighting_scheme)
result = INST.GradeCalculator.calculate(submissionsAsArray, @assignmentGroups, @options.group_weighting_scheme)
for group in result.group_sums
student["assignment_group_#{group.group.id}"] = group[finalOrCurrent]
for submissionData in group[finalOrCurrent].submissions
submissionData.submission.drop = submissionData.drop
student["total_grade"] = result[finalOrCurrent]
@addDroppedClass(student)
addDroppedClass: (student) ->
droppedAssignments = (name for name, assignment of student when name.match(/assignment_\d+/) and assignment.drop?)
drops = {}
drops[student.row] = {}
for a in droppedAssignments
drops[student.row][a] = 'dropped'
styleKey = "dropsForRow#{student.row}"
@gradeGrid.removeCellCssStyles(styleKey)
@gradeGrid.addCellCssStyles(styleKey, drops)
highlightColumn: (columnIndexOrEvent) =>
if isNaN(columnIndexOrEvent)
@ -588,7 +574,6 @@ define [
for student, index in @multiGrid.data
student.beforeFilteredRow = student.row
student.row = index
@addDroppedClass student
@multiGrid.invalidate()
@ -737,9 +722,7 @@ define [
$('body').on('click', @onGridBlur)
sortRowsBy = (sortFn) =>
@rows.sort(sortFn)
for student, i in @rows
student.row = i
@addDroppedClass(student)
student.row = i for student, i in @rows
@multiGrid.invalidate()
@gradeGrid.onSort.subscribe (event, data) =>
sortRowsBy (a, b) ->

View File

@ -85,6 +85,7 @@ define [
if assignment.due_at && submission.submitted_at
classes.push('late') if submission.submission_type isnt 'online_quiz' && (submission.submitted_at.timestamp > assignment.due_at.timestamp)
classes.push('late') if submission.submission_type is 'online_quiz' && ((submission.submitted_at.timestamp - assignment.due_at.timestamp) > 60)
classes.push('dropped') if submission.drop
classes.push('ungraded') if ''+assignment.submission_types is "not_graded"
classes.push('muted') if assignment.muted
classes.push(submission.submission_type) if submission.submission_type

View File

@ -22,7 +22,7 @@
class AssignmentGroupsController < ApplicationController
before_filter :require_context
include Api::V1::AssignmentGroup
include Api::V1::Assignment
# @API List assignment groups
# Returns the list of assignment groups for the current context. The returned
@ -65,18 +65,27 @@ class AssignmentGroupsController < ApplicationController
def index
@groups = @context.assignment_groups.active
params[:include] = Array(params[:include])
if params[:include].include? 'assignments'
include_assignments = Array(params[:include]).include?('assignments')
if include_assignments
@groups = @groups.scoped(:include => { :assignments => :rubric })
end
if authorized_action(@context.assignment_groups.new, @current_user, :read)
respond_to do |format|
format.json {
json = @groups.map { |g|
assignment_group_json(g, @current_user, session, params[:include])
}
render :json => json
hashes = @groups.map do |group|
hash = group.as_json(:include_root => false,
:only => %w(id name position group_weight))
# note that 'rules_hash' gets to_jsoned as just 'rules' because that is what GradeCalculator expects.
hash['rules'] = group.rules_hash
if include_assignments
hash['assignments'] = group.assignments.active.map { |a| assignment_json(a, @current_user, session) }
end
hash
end
hashes.each { |group| group['group_weight'] = nil } unless @context.apply_group_weights?
render :json => hashes.to_json
}
end
end

View File

@ -18,8 +18,6 @@
class GradebooksController < ApplicationController
include ActionView::Helpers::NumberHelper
include Api::V1::AssignmentGroup
include Api::V1::Submission
before_filter :require_context, :except => :public_feed
@ -64,10 +62,7 @@ class GradebooksController < ApplicationController
groups_as_assignments(@groups, :out_of_final => true, :exclude_total => @context.settings[:hide_final_grade])
@no_calculations = groups_assignments.empty?
@assignments.concat(groups_assignments)
@submissions = @context.submissions.all(
:conditions => {:user_id => @student.id},
:include => %w(submission_comments rubric_assessments assignment)
)
@submissions = @context.submissions.find(:all, :conditions => ['user_id = ?', @student.id], :include => [ :submission_comments, :rubric_assessments ])
# pre-cache the assignment group for each assignment object
@assignments.each { |a| a.assignment_group = @groups.find { |g| g.id == a.assignment_group_id } }
# Yes, fetch *all* submissions for this course; otherwise the view will end up doing a query for each
@ -76,16 +71,6 @@ class GradebooksController < ApplicationController
if @student == @current_user
@courses_with_grades = @student.available_courses.select{|c| c.grants_right?(@student, nil, :participate_as_student)}
end
submissions_json = @submissions.map { |s|
submission_json(s, s.assignment, @current_user, session)
}
groups_json = @context.assignment_groups.active.map { |g|
assignment_group_json(g, @current_json, session, ['assignments'])
}
js_env :submissions => submissions_json,
:assignment_groups => groups_json,
:group_weighting_scheme => @context.group_weighting_scheme
format.html { render :action => 'grade_summary' }
else
format.html { render :action => 'grade_summary_list' }

View File

@ -216,13 +216,11 @@ $cell_height: 33px
&.late
background-image: url("/images/gradebook-late-indicator.png")
&.dropped
background-image: url("/images/gradebook-dropped-indicator.png")
&.resubmitted
background-image: url("/images/gradebook-resubmitted-indicator.png")
.slick-cell.dropped
background-image: url("/images/gradebook-dropped-indicator.png")
background-repeat: repeat
.gradebook-cell-turnitin
position: absolute
top: 2px

View File

@ -1,39 +0,0 @@
#
# Copyright (C) 2012 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/>.
#
module Api::V1::AssignmentGroup
include Api::V1::Json
include Api::V1::Assignment
def assignment_group_json(group, user, session, includes = [])
includes ||= []
hash = api_json(group, user, session,
:only => %w(id name position group_weight))
hash['group_weight'] = nil unless group.context.apply_group_weights?
hash['rules'] = group.rules_hash
if includes.include? 'assignments'
hash['assignments'] = group.assignments.active.map { |a|
assignment_json(a, user, session)
}
end
hash
end
end

View File

@ -51,7 +51,7 @@ class GradeCalculator
end
end
private
:private
# The score ignoring unsubmitted assignments
def calculate_current_score(user_id, submissions)
@ -67,126 +67,92 @@ class GradeCalculator
@final_updates << "WHEN user_id=#{user_id} THEN #{score || "NULL"}"
end
# returns information about assignments groups in the form:
# {1=>
# {:tally=>0.818181818181818,
# :submission_count=>2,
# :total_points=>110,
# :user_points=>90,
# :name=>"Assignments",
# :weighted_tally=>0,
# :group_weight=>0},
# 5=> {...}}
# each group
# Creates a hash for each assignment group with stats and the end score for each group
def create_group_sums(submissions, ignore_ungraded=true)
assignments_by_group_id = @assignments.group_by(&:assignment_group_id)
submissions_by_assignment_id = Hash[
submissions.map { |s| [s.assignment_id, s] }
]
group_sums = {}
@groups.each do |group|
assignments = assignments_by_group_id[group.id] || []
group_assignments = @assignments.select { |a| a.assignment_group_id == group.id }
assignment_submissions = []
sums = {:name => group.name,
:total_points => 0,
:user_points => 0,
:group_weight => group.group_weight || 0,
:submission_count => 0}
group_submissions = assignments.map do |a|
s = submissions_by_assignment_id[a.id]
# ignore submissions for muted assignments
s = nil if a.muted?
{
:assignment => a,
:submission => s,
:score => s && s.score,
:total => a.points_possible
}
# collect submissions for this user for all the assignments
# if an assignment is muted it will be treated as if there is no submission
group_assignments.each do |assignment|
submission = submissions.detect { |s| s.assignment_id == assignment.id }
submission = nil if assignment.muted
submission ||= OpenStruct.new(:assignment_id=>assignment.id, :score=>0) unless ignore_ungraded
assignment_submissions << {:assignment => assignment, :submission => submission}
end
group_submissions.reject! { |s| s[:score].nil? } if ignore_ungraded
group_submissions.reject! { |s| s[:total].to_i.zero? }
group_submissions.each { |s| s[:score] ||= 0 }
kept = drop_assignments(group_submissions, group.rules_hash)
score, possible = kept.inject([0, 0]) { |(s_sum,p_sum),s|
[s_sum + s[:score], p_sum + s[:total]]
}
grade = score.to_f / possible
weight = group.group_weight.to_f
group_sums[group.id] = {
:name => group.name,
:tally => grade,
:submission_count => kept.size,
:total_points => possible,
:user_points => score,
:group_weight => weight,
:weighted_tally => grade * weight,
}
# Sort the submissions that have a grade by score (to easily drop lowest/highest grades)
if ignore_ungraded
sorted_assignment_submissions = assignment_submissions.select { |hash| hash[:submission] && hash[:submission].score }
else
sorted_assignment_submissions = assignment_submissions.select { |hash| hash[:submission] }
end
sorted_assignment_submissions = sorted_assignment_submissions.sort_by do |hash|
val = (((hash[:submission].score || 0)/ hash[:assignment].points_possible) rescue 999999)
val.to_f.finite? ? val : 999999
end
# Mark the assignments that aren't allowed to be dropped
if group.rules_hash[:never_drop]
sorted_assignment_submissions.each do |hash|
never_drop = group.rules_hash[:never_drop].include?(hash[:assignment].id)
hash[:never_drop] = true if never_drop
end
end
# Flag the the lowest assignments to be dropped
low_drop_count = 0
high_drop_count = 0
total_scored = sorted_assignment_submissions.length
if group.rules_hash[:drop_lowest]
drop_total = group.rules_hash[:drop_lowest] || 0
sorted_assignment_submissions.each do |hash|
if !hash[:drop] && !hash[:never_drop] && low_drop_count < drop_total && (low_drop_count + high_drop_count + 1) < total_scored
low_drop_count += 1
hash[:drop] = true
end
end
end
# Flag the highest assignments to be dropped
if group.rules_hash[:drop_highest]
drop_total = group.rules_hash[:drop_highest] || 0
sorted_assignment_submissions.reverse.each do |hash|
if !hash[:drop] && !hash[:never_drop] && high_drop_count < drop_total && (low_drop_count + high_drop_count + 1) < total_scored
high_drop_count += 1
hash[:drop] = true
end
end
end
# If all submissions are marked to be dropped: don't drop the highest because we need at least one
if !sorted_assignment_submissions.empty? && sorted_assignment_submissions.all? { |hash| hash[:drop] }
sorted_assignment_submissions[-1][:drop] = false
end
# Count the points from all the non-dropped submissions
sorted_assignment_submissions.select { |hash| !hash[:drop] }.each do |hash|
sums[:submission_count] += 1
sums[:total_points] += hash[:assignment].points_possible || 0
sums[:user_points] += (hash[:submission] && hash[:submission].score) || 0
end
# Calculate the tally for this group
sums[:group_weight] = group.group_weight || 0
sums[:tally] = sums[:user_points].to_f / sums[:total_points].to_f
sums[:tally] = 0.0 unless sums[:tally].finite?
sums[:weighted_tally] = sums[:tally] * sums[:group_weight].to_f
group_sums[group.id] = sums
end
group_sums
end
# see comments for dropAssignments in grade_calculator.coffee
def drop_assignments(submissions, rules)
drop_lowest = rules[:drop_lowest] || 0
drop_highest = rules[:drop_highest] || 0
never_drop_ids = rules[:never_drop] || []
return submissions if drop_lowest.zero? && drop_highest.zero?
if never_drop_ids.empty?
cant_drop = []
else
cant_drop, submissions = submissions.partition { |s|
never_drop_ids.include? s[:assignment].id
}
end
# fudge the drop rules if there aren't enough submissions
return cant_drop if submissions.empty?
drop_lowest = submissions.size - 1 if drop_lowest >= submissions.size
drop_highest = 0 if drop_lowest + drop_highest >= submissions.size
totals = submissions.map { |s| s[:total] }
max_total = totals.max
grades = submissions.map { |s| s[:score].to_f / s[:total] }.sort
q_low = grades.first
q_high = grades.last
q_mid = (q_low + q_high) / 2
keep_highest = submissions.size - drop_lowest
x, kept = big_f(q_mid, submissions, keep_highest)
threshold = 1 / (2 * keep_highest * max_total**2)
until q_high - q_low < threshold
x < 0 ?
q_high = q_mid :
q_low = q_mid
q_mid = (q_low + q_high) / 2
x, kept = big_f(q_mid, submissions, keep_highest)
end
kept.shift(drop_highest)
kept += cant_drop
kept
end
# helper function for drop_assignments
#
# returns a new q value (which should eventually approach 0) and any
# submissions kept according to the given value of q
def big_f(q, submissions, keep)
kept = submissions.map { |s|
rated_score = s[:score] - q * s[:total]
[rated_score, s]
}.sort { |(a_rated_score,az), (b_rated_score,bz)|
b_rated_score <=> a_rated_score
}.first(keep)
q_kept = kept.reduce(0) { |sum,(rated_score,z)| sum + rated_score }
[q_kept, kept.map(&:last)]
end
# Calculates the final score from the sums of all the assignment groups
def calculate_total_from_group_scores(group_sums, ignore_ungraded=true)

View File

@ -20,77 +20,205 @@ define([
'INST' /* INST */,
'i18n!gradebook',
'jquery' /* $ */,
'underscore',
'compiled/grade_calculator',
'jquery.ajaxJSON' /* ajaxJSON */,
'jquery.instructure_forms' /* getFormData */,
'jquery.instructure_misc_helpers' /* replaceTags, scrollSidebar */,
'jquery.instructure_misc_plugins' /* showIf */,
'jquery.templateData' /* fillTemplateData, getTemplateData */,
'media_comments' /* mediaComment, mediaCommentThumbnail */
], function(INST, I18n, $, _, GradeCalculator) {
], function(INST, I18n, $) {
function setGroupData(groups, $group) {
if($group.length === 0) { return; }
var data = $group.getTemplateData({textValues: ['assignment_group_id', 'rules', 'group_weight']});
data = $.extend(data, $group.getFormData());
var groupData = groups[data.assignment_group_id] || {};
if(!groupData.group_weight) {
groupData.group_weight = parseFloat(data.group_weight) / 100.0;
}
groupData.scores = groupData.scores || [];
groupData.full_points = groupData.full_points || [];
groupData.count = groupData.count || 0;
groupData.submissions = groupData.submissions || [];
groupData.sorted_submissions = groupData.sorted_submissions || [];
if (isNaN(groupData.score_total)) { groupData.score_total = null; }
if (isNaN(groupData.full_total)) { groupData.full_total = null; }
if(groupData.score_total !== null || groupData.full_total !== null) {
groupData.calculated_score = (groupData.score_total / groupData.full_total);
if(isNaN(groupData.calculated_score) || !isFinite(groupData.calculated_score)) {
groupData.calculated_score = 0.0;
}
} else {
groupData.score_total = 0;
groupData.full_total = 0;
}
if(!groupData.rules) {
data.rules = data.rules || "";
var rules = {drop_highest: 0, drop_lowest: 0, never_drop: []};
var rulesList = data.rules.split("\n");
for(var idx in rulesList) {
var rule = rulesList[idx].split(":");
var drop = null;
if(rule.length > 1) {
drop = parseInt(rule[1], 10);
}
if(drop && !isNaN(drop) && isFinite(drop)) {
if(rule[0] == 'drop_lowest') {
rules['drop_lowest'] = drop;
} else if(rule[0] == 'drop_highest') {
rules['drop_highest'] = drop;
} else if(rule[0] == 'never_drop') {
rules['never_drop'].push(drop);
}
}
}
groupData.rules = rules;
}
groups[data.assignment_group_id] = groupData;
return groupData;
}
function updateStudentGrades() {
var ignoreUngradedSubmissions = $("#only_consider_graded_assignments").attr('checked'),
currentOrFinal = ignoreUngradedSubmissions ? 'current' : 'final',
dropLabel = I18n.t('titles.dropped_assignment_no_total', 'This assignment will not be considered in the total calculation');
// reset the drop information
for (var i = 0; i < ENV.submissions.length; i++) {
ENV.submissions[i].drop = false;
}
var calculatedGrades = GradeCalculator.calculate(
ENV.submissions,
ENV.assignment_groups,
ENV.group_weighting_scheme
);
// mark dropped assignments
// submissions are mutated in GradeCalculator.calculate
$('.student_assignment')
.removeClass('dropped')
.find('.points_possible').attr('aria-label', '');
for (var i = 0; i < ENV.submissions.length; i++) {
var submission = ENV.submissions[i];
if (submission.drop) {
$('#submission_' + submission.assignment_id)
.addClass('dropped')
.find('.points_possible').attr('aria-label', dropLabel);
var ignoreUngradedSubmissions = $("#only_consider_graded_assignments").attr('checked');
var $submissions = $("#grades_summary .student_assignment");
var groups = {};
var $groups = $(".group_total");
$groups.each(function() {
setGroupData(groups, $(this));
});
$submissions.removeClass('dropped');
$submissions.each(function() {
var $submission = $(this),
submission;
if($submission.hasClass('hard_coded')) { return; }
var data = $submission.getTemplateData({textValues: ['assignment_group_id', 'score', 'points_possible', 'assignment_id']});
if((!data.score || isNaN(parseFloat(data.score))) && ignoreUngradedSubmissions) {
$(this).addClass('dropped');
return;
}
}
var calculateGrade = function(score, possible) {
if (possible === 0 || isNaN(score)) {
grade = "N/A"
} else {
grade = Math.round((score / possible) * 1000) / 10;
var groupData = groups[data.assignment_group_id];
if(!groupData) {
groupData = setGroupData(groups, $("#submission_group-" + data.assignment_group_id));
}
return grade;
if(!groupData) {
return;
}
var score = parseFloat(data.score);
if(!score || isNaN(score) || !isFinite(score)) {
score = 0;
}
var possible = parseFloat(data.points_possible);
if(!possible || isNaN(possible)) {
possible = 0;
}
var percent = score / possible;
if(isNaN(percent) || !isFinite(percent)) {
percent = 0;
}
data.calculated_score = score;
data.calculated_possible = possible;
data.calculated_percent = percent;
groupData.submissions.push(data);
groups[data.assignment_group_id] = groupData;
});
for(var idx in groups) {
var groupData = groups[idx];
groupData.sorted_submissions = groupData.submissions.sort(function(a, b) {
var aa = [a.calculated_percent];
var bb = [b.calculated_percent];
if(aa > bb) { return 1; }
if(aa == bb) { return 0; }
return -1;
});
var lowDrops = 0, highDrops = 0;
for(var jdx = 0; jdx < groupData.sorted_submissions.length; jdx++) {
groupData.sorted_submissions[jdx].calculated_drop = false;
}
for(jdx = 0; jdx < groupData.sorted_submissions.length; jdx++) {
submission = groupData.sorted_submissions[jdx];
if(!submission.calculated_drop && lowDrops < groupData.rules.drop_lowest && submission.calculated_possible > 0 && $.inArray(submission.assignment_id, groupData.rules.never_drop) == -1) {
lowDrops++;
submission.calculated_drop = true;
}
groupData.sorted_submissions[jdx] = submission;
}
for(jdx = groupData.sorted_submissions.length - 1; jdx >= 0; jdx--) {
submission = groupData.sorted_submissions[jdx];
if(!submission.calculated_drop && highDrops < groupData.rules.drop_highest && submission.calculated_possible > 0 && $.inArray(submission.assignment_id, groupData.rules.never_drop) == -1) {
highDrops++;
submission.calculated_drop = true;
}
groupData.sorted_submissions[jdx] = submission;
}
for(jdx = 0; jdx < groupData.sorted_submissions.length; jdx++) {
submission = groupData.sorted_submissions[jdx];
if(submission.calculated_drop) {
$("#submission_" + submission.assignment_id).addClass('dropped');
lowDrops++;
} else {
$("#submission_" + submission.assignment_id).removeClass('dropped');
groupData.scores.push(submission.calculated_score);
groupData.full_points.push(submission.calculated_possible);
groupData.count++;
groupData.score_total += submission.calculated_score;
groupData.full_total += submission.calculated_possible;
}
}
groups[idx] = groupData;
}
for (var i = 0; i < calculatedGrades.group_sums.length; i++) {
var groupSum = calculatedGrades.group_sums[i];
var $groupRow = $('#submission_group-' + groupSum.group.id);
var groupGradeInfo = groupSum[currentOrFinal];
$groupRow.find('.grade').text(
calculateGrade(groupGradeInfo.score, groupGradeInfo.possible)
);
var finalWeightedGrade = 0.0,
finalGrade = 0.0,
totalPointsPossible = 0.0,
possibleWeightFromSubmissions = 0.0,
totalUserPoints = 0.0;
$.each(groups, function(i, group) {
var groupData = setGroupData(groups, $("#submission_group-" + i));
var score = Math.round(group.calculated_score * 1000.0) / 10.0;
$("#submission_group-" + i).find(".grade").text(score).end()
.find(".score_teaser").text(group.score_total + " / " + group.full_total);
score = group.calculated_score * group.group_weight;
if(isNaN(score) || !isFinite(score)) {
score = 0;
}
if(ignoreUngradedSubmissions && group.count > 0) {
possibleWeightFromSubmissions += group.group_weight;
}
finalWeightedGrade += score;
totalUserPoints += group.score_total;
totalPointsPossible += group.full_total;
});
var total = parseFloat($("#total_groups_weight").text());
if(isNaN(total) || !isFinite(total) || total === 0) { // if not weighting by group percents
finalGrade = Math.round(1000.0 * totalUserPoints / totalPointsPossible) / 10.0;
$(".student_assignment.final_grade .score_teaser").text(totalUserPoints + ' / ' + totalPointsPossible);
} else {
var totalPossibleWeight = parseFloat($("#total_groups_weight").text()) / 100;
if(isNaN(totalPossibleWeight) || !isFinite(totalPossibleWeight) || totalPossibleWeight === 0) {
totalPossibleWeight = 1.0;
}
if(ignoreUngradedSubmissions && possibleWeightFromSubmissions < 1.0) {
var possible = totalPossibleWeight < 1.0 ? totalPossibleWeight : 1.0 ;
finalWeightedGrade = possible * finalWeightedGrade / possibleWeightFromSubmissions;
}
finalGrade = finalWeightedGrade;
finalGrade = Math.round(finalGrade * 1000.0) / 10.0;
$(".student_assignment.final_grade .score_teaser").text(I18n.t('percent', 'Percent'));
}
var finalScore = calculatedGrades[currentOrFinal].score;
var finalPossible = calculatedGrades[currentOrFinal].possible;
var finalGrade = calculateGrade(finalScore, finalPossible);
$(".student_assignment.final_grade .grade").text(finalGrade);
if(isNaN(finalGrade) || !isFinite(finalGrade)) {
finalGrade = 0;
}
$(".student_assignment.final_grade").find(".grade").text(finalGrade);
if(window.grading_scheme) {
$(".final_letter_grade .grade").text(GradeCalculator.letter_grade(grading_scheme, finalGrade));
$(".final_letter_grade .grade").text(INST.GradeCalculator.letter_grade(grading_scheme, finalGrade));
}
$(".revert_all_scores").showIf($("#grades_summary .revert_score_link").length > 0);
var eTime = (new Date()).getTime();
}
$(document).ready(function() {
updateStudentGrades();
$(".revert_all_scores_link").click(function(event) {
@ -188,9 +316,6 @@ define([
if (val === 0) { val = '0.0'; }
if (val === originalVal) { val = originalScore; }
$assignment.find('.grade').html(val || $assignment.find('.grade').data('originalValue'));
if (update) {
updateScoreForAssignment(assignment_id, val);
}
updateStudentGrades();
});
$("#grade_entry").blur(function() {
@ -213,9 +338,6 @@ define([
.find(".grade").removeClass('changed');
$assignment.find(".revert_score_link").remove();
$assignment.find(".score_value").text(val);
var assignmentId = $assignment.getTemplateValue('assignment_id');
updateScoreForAssignment(assignmentId, val);
if(!skipEval) {
updateStudentGrades();
}
@ -270,11 +392,5 @@ define([
});
});
function updateScoreForAssignment(assignmentId, score) {
var submission = _.find(ENV.submissions, function(s) {
return s.assignment_id == assignmentId;
});
submission.score = score;
}
});

View File

@ -39,7 +39,7 @@ define([
wikiSidebar.init();
wikiSidebar.attachToEditor($("#wiki_page_body"));
};
function initShowViewSecondary(){
$("#wiki_show_view_secondary a.edit_link").click(function(event){
event.preventDefault();
@ -54,7 +54,7 @@ define([
}).end()
.find("form").hide();
}
function initForm(){
// we need to temporarily show the form so that the layout will be computed correctly.
// this all happens within 1 event loop, so it doesn't cause a flash of content.
@ -78,14 +78,14 @@ define([
toggleView();
});
$(function(){
// trigger its hanlder on page load so that it toggles to the edit view.
// its kinda hoaky because tinymce has to be initialized before the whole dom is loaded and so we have to
// trigger its hanlder on page load so that it toggles to the edit view.
// its kinda hoaky because tinymce has to be initialized before the whole dom is loaded and so we have to
// trigger it in a callback to the domloaded event.
$("a#page_doesnt_exist_so_start_editing_it_now:not(.dont_click)").triggerHandler("click");
});
}
}
function toggleView(){
$("#wiki_edit_view_main, #wiki_show_view_main, #wiki_show_view_secondary, #wiki_edit_view_secondary").toggle();
$("#wiki_edit_view_page_tools").showIf($("#wiki_edit_view_page_tools li").length > 0);
@ -98,13 +98,13 @@ define([
init: function(){
// init up the form now
initForm();
// init the rest on domReady
// init the rest on domReady
$(function(){
initEditViewSecondary();
initShowViewSecondary();
});
},
updateComment: function($comment, comment, top) {
if(!$comment || $comment.length == 0) {
$comment = $("#wiki_page_comment_blank").clone(true).removeAttr('id');
@ -126,7 +126,7 @@ define([
$comment.toggleClass('deletable_comment', !!(comment.permissions && comment.permissions['delete']));
}
};
// miscellaneous things to do on domready
$(document).ready(function() {
$(document).fragmentChange(function(event, hash){
@ -148,7 +148,7 @@ define([
var oldVersion = parseInt($("#wiki_page_version_number").text(), 10);
var newVersion = data && data && data.wiki_page && data.wiki_page.version_number;
var changed = oldVersion && newVersion && newVersion > oldVersion;
if(changed) {
if(changed) {
$(".someone_else_edited").slideDown();
setTimeout(checkForChanges, 240000);
} else {

View File

@ -1,90 +0,0 @@
define ['compiled/grade_calculator', 'underscore'], (GradeCalculator, _) ->
# convenient way to test GradeCalculator.calculate results
# if currentOrFinal is 'current', ungraded assignments will be ignored
assertGrade = (result, currentOrFinal, score, possible) ->
equal result[currentOrFinal].score, score
equal result[currentOrFinal].possible, possible
# asserts sure that all the given submissions were dropped
assertDropped = (submissions, grades...) ->
_(grades).each ([score, possible]) ->
submission = _(submissions).find (s) ->
s.score == score && s.possible == possible
equal submission.drop, true
module "GradeCalculator",
setup: ->
# submission grades (100/100, 3/38, etc). null means ungraded.
# if these grades are changed or added to, be careful to respect the
# weird drop behavior (see the test for drop_lowest below).
grades = [[100,100], [42,91], [14,55], [3,38], [null,1000]]
@assignments = grades.map ([z,possible], i) ->
assignment =
points_possible: possible,
id: i
@submissions = grades.map ([score,z], i) ->
submission =
assignment_id: i
score: score
@group =
id: 1
assignments: @assignments
test "without submissions or assignments", ->
group =
id: 1
rules: {}
assignments: []
result = GradeCalculator.calculate [], [group]
assertGrade result, 'current', 0, 0
assertGrade result, 'final', 0, 0
test "without submissions", ->
@group.rules = {}
result = GradeCalculator.calculate [], [@group]
assertGrade result, 'current', 0, 0
assertGrade result, 'final', 0, 1284
test "no drop rules", ->
@group.rules = {}
result = GradeCalculator.calculate @submissions, [@group]
assertGrade result, 'current', 159, 284
assertGrade result, 'final', 159, 1284
test "drop lowest", ->
@group.rules = drop_lowest: 1
result = GradeCalculator.calculate @submissions, [@group]
assertGrade result, 'current', 156, 246
assertDropped result.group_sums[0].current.submissions, [3, 38]
assertGrade result, 'final', 159, 284
assertDropped result.group_sums[0]['final'].submissions, [0, 1000]
# NOTE: this example illustrates that even though 3/38 was the optimal
# grade to drop when just one assignment was dropped, it is no longer
# optimal to drop that assignment when 2 grades are dropped
@group.rules = drop_lowest: 2
result = GradeCalculator.calculate @submissions, [@group]
assertGrade result, 'current', 103, 138
assertDropped result.group_sums[0].current.submissions, [42, 91], [14, 55]
assertGrade result, 'final', 156, 246
assertDropped result.group_sums[0]['final'].submissions, [0, 1000], [3, 38]
test "drop highest", ->
@group.rules = drop_highest: 1
result = GradeCalculator.calculate @submissions, [@group]
assertGrade result, 'current', 59, 184
assertDropped result.group_sums[0].current.submissions, [100, 100]
assertGrade result, 'final', 59, 1184
assertDropped result.group_sums[0]['final'].submissions, [100, 100]
test "never drop", ->
@group.rules =
drop_lowest: 1
never_drop: [3] # 3/38
result = GradeCalculator.calculate @submissions, [@group]
assertGrade result, 'current', 145, 229
assertDropped result.group_sums[0].current.submissions, [14, 55]
assertGrade result, 'final', 159, 284
assertDropped result.group_sums[0]['final'].submissions, [0, 1000]

View File

@ -42,10 +42,6 @@ describe GradebooksController do
course_with_student_logged_in(:active_all => true)
get 'grade_summary', :course_id => @course.id
response.should render_template('grade_summary')
end
it "should render with specified user_id" do
course_with_student_logged_in(:active_all => true)
get 'grade_summary', :course_id => @course.id, :id => @user.id
response.should render_template('grade_summary')
assigns[:courses_with_grades].should_not be_nil

View File

@ -364,72 +364,7 @@ describe GradeCalculator do
@user.enrollments.first.computed_current_score.should eql(52.5)
@user.enrollments.first.computed_final_score.should eql(43.6)
end
end
# We should keep this in sync with GradeCalculatorSpec.coffee
context "GradeCalculatorSpec.coffee examples" do
before do
course_with_student
@grades = [[100,100], [42,91], [14,55], [3,38], [nil,1000]]
@group = @course.assignment_groups.create! :name => 'group 1'
@assignments = @grades.map do |score,possible|
@course.assignments.create! :title => 'homework',
:points_possible => possible,
:assignment_group => @group
end
end
def set_default_grades
@assignments.each_with_index do |a,i|
score = @grades[i].first
next unless score # don't grade nil submissions
a.grade_student @student, :grade => score
end
end
def check_grades(current, final)
GradeCalculator.recompute_final_score(@student.id, @course.id)
@enrollment.reload
@enrollment.computed_current_score.should == current
@enrollment.computed_final_score.should == final
end
it "should work without assignments or submissions" do
@group.assignments.clear
check_grades(nil, nil)
end
it "should work without submissions" do
check_grades(nil, 0)
end
it "should work with no drop rules" do
set_default_grades
check_grades(56.0, 12.4)
end
it "should support drop_lowest" do
set_default_grades
@group.update_attribute(:rules, 'drop_lowest:1')
check_grades(63.4, 56.0)
@group.update_attribute(:rules, 'drop_lowest:2')
check_grades(74.6, 63.4)
end
it "should support drop_highest" do
set_default_grades
@group.update_attribute(:rules, 'drop_highest:1')
check_grades(32.1, 5.0)
end
it "should support never_drop" do
set_default_grades
rules = "drop_lowest:1\nnever_drop:#{@assignments[3].id}" # 3/38
@group.update_attribute(:rules, rules)
check_grades(63.3, 56.0)
end
end
end

View File

@ -127,29 +127,6 @@ describe "edititing grades" do
f('#gradebook_grid [row="0"] .l1').should have_class('editable')
end
it "should display dropped grades correctly after editing a grade" do
@course.assignment_groups.first.update_attribute :rules, 'drop_lowest:1'
get "/courses/#{@course.id}/gradebook2"
wait_for_ajaximations
assignment_1_sel = '#gradebook_grid [row="0"] .l1'
assignment_2_sel= '#gradebook_grid [row="0"] .l2'
a1 = f(assignment_1_sel)
a2 = f(assignment_2_sel)
a1['class'].should include 'dropped'
a2['class'].should_not include 'dropped'
grade_input = keep_trying_until do
a2.click
a2.find_element(:css, '.grade')
end
set_value(grade_input, 3)
a1.send_keys(:tab)
wait_for_ajax_requests
f(assignment_1_sel)['class'].should_not include 'dropped'
f(assignment_2_sel)['class'].should include 'dropped'
end
it "should update a grade when clicking outside of slickgrid" do
get "/courses/#{@course.id}/gradebook2"
wait_for_ajaximations

View File

@ -124,8 +124,9 @@ describe "grading standards" do
get "/courses/#{@course.id}/grades/#{student.id}"
grading_scheme = driver.execute_script "return grading_scheme"
grading_scheme[2][0].should == 'B+'
f('#right-side .final_grade .grade').text.should == '89.9'
f('#final_letter_grade_text').text.should == 'B+'
driver.execute_script("return INST.GradeCalculator.letter_grade(grading_scheme, 89.9)").should == 'B+'
driver.find_element(:css, '#right-side .final_grade .grade').text.should == '89.9'
driver.find_element(:css, '#final_letter_grade_text').text.should == 'B+'
end
it "should allow editing the standard again without reloading the page" do