revert several grade calculation changes
Revert "make grade_summary page use GradeCalculator" This reverts commit4776cb2880
. Revert "fix gradebook bug in ie8" This reverts commitc927237458
. Revert "gb2: don't lie about which assignments are dropped" This reverts commitda37933079
. Revert "fix ruby calculation of grades (grade-dropping)" This reverts commit63f75597a2
. Revert "fix grade-dropping in gradebooks" This reverts commit4a4a2f53c1
. 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:
parent
6d2d60b3f1
commit
c835ccd39a
|
@ -1 +1 @@
|
|||
require ['grade_summary']
|
||||
require ['grade_summary', 'compiled/grade_calculator']
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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) ->
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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' }
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
|
@ -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)
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
});
|
||||
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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]
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue