2011-02-01 09:57:29 +08:00
|
|
|
#
|
2014-03-17 23:52:51 +08:00
|
|
|
# Copyright (C) 2011 - 2014 Instructure, Inc.
|
2011-02-01 09:57:29 +08:00
|
|
|
#
|
|
|
|
# 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/>.
|
|
|
|
#
|
|
|
|
|
|
|
|
class GradeCalculator
|
2014-01-15 08:29:38 +08:00
|
|
|
attr_accessor :submissions, :assignments, :groups
|
|
|
|
|
2013-02-09 05:39:18 +08:00
|
|
|
def initialize(user_ids, course, opts = {})
|
2012-12-22 07:02:45 +08:00
|
|
|
opts = opts.reverse_merge(:ignore_muted => true)
|
|
|
|
|
2015-02-13 11:08:45 +08:00
|
|
|
Rails.logger.info("GRADES: calc args: user_ids=#{user_ids.inspect}")
|
|
|
|
Rails.logger.info("GRADES: calc args: course=#{course.inspect}")
|
|
|
|
Rails.logger.info("GRADES: calc args: opts=#{opts.inspect}")
|
|
|
|
|
2014-12-06 05:49:27 +08:00
|
|
|
@course = course.is_a?(Course) ? course : Course.find(course)
|
|
|
|
@groups = @course.assignment_groups.active
|
|
|
|
@grading_period = opts[:grading_period]
|
|
|
|
|
|
|
|
assignment_scope = @course.assignments.published.gradeable
|
feature flag for 'All Grading Periods' totals
Add grading period dropdowns on the 'grades' page,
and add a "Display Totals for 'All Grading Periods'"
feature flag. By default, the feature will be turned
'off'.
When the feature is 'off':
- Totals will not display in the gradebook
or the 'student grades' page when the 'All
Grading Periods' option is selected.
- The grading period dropdowns on the 'grades'
page will not have an 'All Grading Periods'
option.
When the feature is 'on':
- Totals will display in the gradebook and the
'student grades' page when the 'All Grading
Periods' option is selected.
- The grading period dropdowns on the 'grades'
page will have an 'All Grading Periods' option.
closes CNVS-23995
test plan:
1) as a teacher, enable the 'multiple grading
periods' feature (do not enable the 'display
totals for all grading periods' feature yet).
a) verify the gradebook does not show totals
when the 'All Grading Periods' option is
selected.
b) verify the 'student grades page'
(courses/4/grades/9#tab-assignments) does
not show totals, and the calculation of
'what-if' grades is disabled when the
'All Grading Periods' option is selected.
c) turn on the 'display totals for all
grading periods' feature. repeat steps
a & b and verify that the totals now
show up (and you can calculate what-if
grades on the student grades page when
'All Grading Periods is selected')
2) sign in as a student that is enrolled in
3 courses: 1 course with MGP disabled, 1
course with MGP enabled and 'display all
grading periods totals' (DAGPT) disabled,
and 1 course with MGP enabled and DAGPT
enabled. go the the 'grades' page (/grades).
a) verify there is a grading period dropdown
next to the totals for courses that have
MGP enabled. verify there is not a grading
period dropdown next to the total for the
course with MGP disabled.
b) verify that the current grading period is
selected by default, if one exists. if a
current grading period does not exist, then:
- the dropdown next to the total for the
course with DAGPT disabled should show
'Select a grading period' and the total
grade should show as '--'.
- the dropdown next to the total for the
course with DAGPT enabled should show
'All Grading Periods' and the total grade
should be displayed.
c) verify clicking a grading period in the
dropdown changes the total, and shows
the correct total for that grading period.
3) repeat steps 2a-c, but sign in as an observer that
is observing at least 3 students in 3 different
courses(1 course with MGP disabled, 1 with MGP
enabled and DAGPT disabled, and 1 course with
MGP enabled + DAGPT enabled).
4) verify that the grading period dropdowns that were
added are accessible.
Note: The 'grades' page (/grades) will _always_
display the total for 'All Grading Periods' when
signed in as a teacher. We are aware of this
existing bug and we're working on a solution.
Change-Id: If501b47aa57121d17d4e6629d1dcdbc8676971a2
Reviewed-on: https://gerrit.instructure.com/65847
Tested-by: Jenkins
Reviewed-by: Strand McCutchen <smccutchen@instructure.com>
Reviewed-by: Dylan Ross <dross@instructure.com>
Reviewed-by: Derek Bender <djbender@instructure.com>
QA-Review: Jason Carter <jcarter@instructure.com>
Product-Review: Spencer Olson <solson@instructure.com>
2015-10-14 03:20:03 +08:00
|
|
|
@assignments = assignment_scope.to_a
|
2014-12-06 05:49:27 +08:00
|
|
|
|
2011-02-01 09:57:29 +08:00
|
|
|
@user_ids = Array(user_ids).map(&:to_i)
|
2014-03-17 23:52:51 +08:00
|
|
|
@current_updates = {}
|
|
|
|
@final_updates = {}
|
2012-12-22 07:02:45 +08:00
|
|
|
@ignore_muted = opts[:ignore_muted]
|
2011-02-01 09:57:29 +08:00
|
|
|
end
|
2013-04-30 08:29:45 +08:00
|
|
|
|
2014-01-15 08:29:38 +08:00
|
|
|
# recomputes the scores and saves them to each user's Enrollment
|
2011-04-13 01:03:21 +08:00
|
|
|
def self.recompute_final_score(user_ids, course_id)
|
2014-03-17 23:52:51 +08:00
|
|
|
user_ids = Array(user_ids).uniq.map(&:to_i)
|
|
|
|
return if user_ids.empty?
|
|
|
|
user_ids.in_groups_of(1000, false) do |user_ids_group|
|
|
|
|
calc = GradeCalculator.new user_ids_group, course_id
|
|
|
|
calc.compute_and_save_scores
|
|
|
|
end
|
2011-02-01 09:57:29 +08:00
|
|
|
end
|
2012-12-22 07:02:45 +08:00
|
|
|
|
|
|
|
def compute_scores
|
2013-06-04 03:39:32 +08:00
|
|
|
@submissions = @course.submissions.
|
2014-02-15 04:11:19 +08:00
|
|
|
except(:order, :select).
|
2013-06-04 03:39:32 +08:00
|
|
|
for_user(@user_ids).
|
2014-12-06 05:49:27 +08:00
|
|
|
where(assignment_id: @assignments.map(&:id)).
|
2015-04-17 07:24:31 +08:00
|
|
|
select("submissions.id, user_id, assignment_id, score, excused")
|
2013-02-09 05:39:18 +08:00
|
|
|
submissions_by_user = @submissions.group_by(&:user_id)
|
2014-08-18 23:55:26 +08:00
|
|
|
|
2014-10-28 02:19:36 +08:00
|
|
|
scores = []
|
|
|
|
@user_ids.each_slice(100) do |batched_ids|
|
|
|
|
load_assignment_visibilities_for_users(batched_ids)
|
|
|
|
batched_ids.each do |user_id|
|
|
|
|
user_submissions = submissions_by_user[user_id] || []
|
|
|
|
if differentiated_assignments_on?
|
|
|
|
user_submissions.select!{|s| assignment_ids_visible_to_user(user_id).include?(s.assignment_id)}
|
|
|
|
end
|
|
|
|
current, current_groups = calculate_current_score(user_id, user_submissions)
|
|
|
|
final, final_groups = calculate_final_score(user_id, user_submissions)
|
|
|
|
scores << [[current, current_groups], [final, final_groups]]
|
2014-08-18 23:55:26 +08:00
|
|
|
end
|
2014-10-28 02:19:36 +08:00
|
|
|
clear_assignment_visibilities_cache
|
2011-02-01 09:57:29 +08:00
|
|
|
end
|
2014-10-28 02:19:36 +08:00
|
|
|
scores
|
2012-12-22 07:02:45 +08:00
|
|
|
end
|
|
|
|
|
2014-03-17 23:52:51 +08:00
|
|
|
def compute_and_save_scores
|
|
|
|
compute_scores
|
|
|
|
save_scores
|
|
|
|
end
|
|
|
|
|
|
|
|
private
|
|
|
|
|
2014-08-18 23:55:26 +08:00
|
|
|
def differentiated_assignments_on?
|
|
|
|
@differentiated_assignments_enabled ||= @course.feature_enabled?(:differentiated_assignments)
|
|
|
|
end
|
|
|
|
|
2012-12-22 07:02:45 +08:00
|
|
|
def save_scores
|
2013-03-15 03:32:18 +08:00
|
|
|
raise "Can't save scores when ignore_muted is false" unless @ignore_muted
|
2011-07-08 05:53:21 +08:00
|
|
|
|
2013-03-19 23:49:31 +08:00
|
|
|
Course.where(:id => @course).update_all(:updated_at => Time.now.utc)
|
2015-02-26 14:17:17 +08:00
|
|
|
time = Enrollment.sanitize(Time.now.utc)
|
|
|
|
query = "updated_at=#{time}, graded_at=#{time}"
|
2014-03-17 23:52:51 +08:00
|
|
|
query += ", computed_current_score=CASE user_id #{@current_updates.map { |user_id, grade| "WHEN #{user_id} THEN #{grade || "NULL"}"}.
|
|
|
|
join(" ")} ELSE computed_current_score END"
|
|
|
|
query += ", computed_final_score=CASE user_id #{@final_updates.map { |user_id, grade| "WHEN #{user_id} THEN #{grade || "NULL"}"}.
|
|
|
|
join(" ")} ELSE computed_final_score END"
|
|
|
|
Enrollment.where(:user_id => @user_ids, :course_id => @course).update_all(query)
|
2011-02-01 09:57:29 +08:00
|
|
|
end
|
|
|
|
|
2011-04-13 01:03:21 +08:00
|
|
|
# The score ignoring unsubmitted assignments
|
|
|
|
def calculate_current_score(user_id, submissions)
|
2014-01-15 08:29:38 +08:00
|
|
|
calculate_score(submissions, user_id, @current_updates, true)
|
2011-02-01 09:57:29 +08:00
|
|
|
end
|
|
|
|
|
2011-04-13 01:03:21 +08:00
|
|
|
# The final score for the class, so unsubmitted assignments count as zeros
|
|
|
|
def calculate_final_score(user_id, submissions)
|
2014-01-15 08:29:38 +08:00
|
|
|
calculate_score(submissions, user_id, @final_updates, false)
|
|
|
|
end
|
|
|
|
|
2014-03-17 23:52:51 +08:00
|
|
|
def calculate_score(submissions, user_id, grade_updates, ignore_ungraded)
|
2014-08-18 23:55:26 +08:00
|
|
|
group_sums = create_group_sums(submissions, user_id, ignore_ungraded)
|
2013-04-30 08:29:45 +08:00
|
|
|
info = calculate_total_from_group_scores(group_sums)
|
2015-02-13 11:08:45 +08:00
|
|
|
Rails.logger.info "GRADES: calculated: #{info.inspect}"
|
2014-03-17 23:52:51 +08:00
|
|
|
grade_updates[user_id] = info[:grade]
|
2014-01-15 08:29:38 +08:00
|
|
|
[info, group_sums.index_by { |s| s[:id] }]
|
2011-02-01 09:57:29 +08:00
|
|
|
end
|
2014-01-15 08:29:38 +08:00
|
|
|
|
2012-12-22 04:31:53 +08:00
|
|
|
# returns information about assignments groups in the form:
|
2013-01-12 07:32:38 +08:00
|
|
|
# [
|
|
|
|
# {
|
|
|
|
# :id => 1
|
|
|
|
# :score => 5,
|
|
|
|
# :possible => 7,
|
2014-01-22 05:20:33 +08:00
|
|
|
# :grade => 71.42,
|
2013-01-12 07:32:38 +08:00
|
|
|
# :weight => 50},
|
|
|
|
# ...]
|
2012-12-22 04:31:53 +08:00
|
|
|
# each group
|
2014-08-18 23:55:26 +08:00
|
|
|
def create_group_sums(submissions, user_id, ignore_ungraded=true)
|
|
|
|
visible_assignments = @assignments
|
|
|
|
if differentiated_assignments_on?
|
|
|
|
visible_assignments = visible_assignments.select{|a| assignment_ids_visible_to_user(user_id).include?(a.id)}
|
|
|
|
end
|
feature flag for 'All Grading Periods' totals
Add grading period dropdowns on the 'grades' page,
and add a "Display Totals for 'All Grading Periods'"
feature flag. By default, the feature will be turned
'off'.
When the feature is 'off':
- Totals will not display in the gradebook
or the 'student grades' page when the 'All
Grading Periods' option is selected.
- The grading period dropdowns on the 'grades'
page will not have an 'All Grading Periods'
option.
When the feature is 'on':
- Totals will display in the gradebook and the
'student grades' page when the 'All Grading
Periods' option is selected.
- The grading period dropdowns on the 'grades'
page will have an 'All Grading Periods' option.
closes CNVS-23995
test plan:
1) as a teacher, enable the 'multiple grading
periods' feature (do not enable the 'display
totals for all grading periods' feature yet).
a) verify the gradebook does not show totals
when the 'All Grading Periods' option is
selected.
b) verify the 'student grades page'
(courses/4/grades/9#tab-assignments) does
not show totals, and the calculation of
'what-if' grades is disabled when the
'All Grading Periods' option is selected.
c) turn on the 'display totals for all
grading periods' feature. repeat steps
a & b and verify that the totals now
show up (and you can calculate what-if
grades on the student grades page when
'All Grading Periods is selected')
2) sign in as a student that is enrolled in
3 courses: 1 course with MGP disabled, 1
course with MGP enabled and 'display all
grading periods totals' (DAGPT) disabled,
and 1 course with MGP enabled and DAGPT
enabled. go the the 'grades' page (/grades).
a) verify there is a grading period dropdown
next to the totals for courses that have
MGP enabled. verify there is not a grading
period dropdown next to the total for the
course with MGP disabled.
b) verify that the current grading period is
selected by default, if one exists. if a
current grading period does not exist, then:
- the dropdown next to the total for the
course with DAGPT disabled should show
'Select a grading period' and the total
grade should show as '--'.
- the dropdown next to the total for the
course with DAGPT enabled should show
'All Grading Periods' and the total grade
should be displayed.
c) verify clicking a grading period in the
dropdown changes the total, and shows
the correct total for that grading period.
3) repeat steps 2a-c, but sign in as an observer that
is observing at least 3 students in 3 different
courses(1 course with MGP disabled, 1 with MGP
enabled and DAGPT disabled, and 1 course with
MGP enabled + DAGPT enabled).
4) verify that the grading period dropdowns that were
added are accessible.
Note: The 'grades' page (/grades) will _always_
display the total for 'All Grading Periods' when
signed in as a teacher. We are aware of this
existing bug and we're working on a solution.
Change-Id: If501b47aa57121d17d4e6629d1dcdbc8676971a2
Reviewed-on: https://gerrit.instructure.com/65847
Tested-by: Jenkins
Reviewed-by: Strand McCutchen <smccutchen@instructure.com>
Reviewed-by: Dylan Ross <dross@instructure.com>
Reviewed-by: Derek Bender <djbender@instructure.com>
QA-Review: Jason Carter <jcarter@instructure.com>
Product-Review: Spencer Olson <solson@instructure.com>
2015-10-14 03:20:03 +08:00
|
|
|
|
|
|
|
if @grading_period
|
|
|
|
user = @course.users.find(user_id)
|
|
|
|
visible_assignments = @grading_period.assignments_for_student(visible_assignments, user)
|
|
|
|
end
|
2014-08-18 23:55:26 +08:00
|
|
|
assignments_by_group_id = visible_assignments.group_by(&:assignment_group_id)
|
2012-12-22 04:31:53 +08:00
|
|
|
submissions_by_assignment_id = Hash[
|
|
|
|
submissions.map { |s| [s.assignment_id, s] }
|
|
|
|
]
|
|
|
|
|
2013-01-12 07:32:38 +08:00
|
|
|
@groups.map do |group|
|
2012-12-22 04:31:53 +08:00
|
|
|
assignments = assignments_by_group_id[group.id] || []
|
2014-01-15 08:29:38 +08:00
|
|
|
|
2012-12-22 04:31:53 +08:00
|
|
|
group_submissions = assignments.map do |a|
|
|
|
|
s = submissions_by_assignment_id[a.id]
|
|
|
|
|
|
|
|
# ignore submissions for muted assignments
|
2012-12-22 07:02:45 +08:00
|
|
|
s = nil if @ignore_muted && a.muted?
|
2012-12-22 04:31:53 +08:00
|
|
|
|
|
|
|
{
|
2015-04-17 07:24:31 +08:00
|
|
|
assignment: a,
|
|
|
|
submission: s,
|
|
|
|
score: s && s.score,
|
|
|
|
total: a.points_possible || 0,
|
|
|
|
excused: s && s.excused?,
|
2012-12-22 04:31:53 +08:00
|
|
|
}
|
2012-09-14 08:53:07 +08:00
|
|
|
end
|
2015-04-17 07:24:31 +08:00
|
|
|
|
2012-12-22 04:31:53 +08:00
|
|
|
group_submissions.reject! { |s| s[:score].nil? } if ignore_ungraded
|
2015-04-17 07:24:31 +08:00
|
|
|
group_submissions.reject! { |s| s[:excused] }
|
2012-12-22 04:31:53 +08:00
|
|
|
group_submissions.each { |s| s[:score] ||= 0 }
|
|
|
|
|
2015-02-13 11:08:45 +08:00
|
|
|
logged_submissions = group_submissions.map { |s| loggable_submission(s) }
|
|
|
|
Rails.logger.info "GRADES: calculating for assignment_group=#{group.global_id} user=#{user_id}"
|
|
|
|
Rails.logger.info "GRADES: calculating... ignore_ungraded=#{ignore_ungraded}"
|
|
|
|
Rails.logger.info "GRADES: calculating... submissions=#{logged_submissions.inspect}"
|
|
|
|
|
2012-12-22 04:31:53 +08:00
|
|
|
kept = drop_assignments(group_submissions, group.rules_hash)
|
|
|
|
|
2013-01-10 08:59:16 +08:00
|
|
|
score, possible = kept.reduce([0, 0]) { |(s_sum,p_sum),s|
|
2012-12-22 04:31:53 +08:00
|
|
|
[s_sum + s[:score], p_sum + s[:total]]
|
|
|
|
}
|
2013-01-12 07:32:38 +08:00
|
|
|
|
|
|
|
{
|
|
|
|
:id => group.id,
|
|
|
|
:score => score,
|
|
|
|
:possible => possible,
|
|
|
|
:weight => group.group_weight,
|
2014-01-15 08:29:38 +08:00
|
|
|
:grade => ((score.to_f / possible * 100).round(2) if possible > 0),
|
2015-02-13 11:08:45 +08:00
|
|
|
}.tap { |group_grade_info|
|
|
|
|
Rails.logger.info "GRADES: calculated #{group_grade_info.inspect}"
|
2012-12-22 04:31:53 +08:00
|
|
|
}
|
2011-02-01 09:57:29 +08:00
|
|
|
end
|
|
|
|
end
|
2012-12-22 04:31:53 +08:00
|
|
|
|
2014-10-28 02:19:36 +08:00
|
|
|
def load_assignment_visibilities_for_users(user_ids)
|
2014-08-18 23:55:26 +08:00
|
|
|
@assignment_ids_visible_to_user ||= begin
|
|
|
|
if differentiated_assignments_on?
|
2014-10-28 02:19:36 +08:00
|
|
|
AssignmentStudentVisibility.visible_assignment_ids_in_course_by_user(course_id: @course.id, user_id: user_ids)
|
2014-08-18 23:55:26 +08:00
|
|
|
else
|
|
|
|
assignment_ids = @assignments.map(&:id)
|
2014-10-28 02:19:36 +08:00
|
|
|
user_ids.reduce({}){|hash, id| hash[id] = assignment_ids; hash}
|
2014-08-18 23:55:26 +08:00
|
|
|
end
|
|
|
|
end
|
2014-10-28 02:19:36 +08:00
|
|
|
end
|
|
|
|
|
|
|
|
def clear_assignment_visibilities_cache
|
|
|
|
@assignment_ids_visible_to_user = nil
|
|
|
|
end
|
|
|
|
|
|
|
|
def assignment_ids_visible_to_user(user_id)
|
|
|
|
@assignment_ids_visible_to_user[user_id]
|
2014-08-18 23:55:26 +08:00
|
|
|
end
|
|
|
|
|
2012-12-22 04:31:53 +08:00
|
|
|
# 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?
|
|
|
|
|
2015-02-13 11:08:45 +08:00
|
|
|
Rails.logger.info "GRADES: dropping assignments! #{rules.inspect}"
|
|
|
|
|
2012-12-29 17:39:44 +08:00
|
|
|
cant_drop = []
|
|
|
|
if never_drop_ids.present?
|
2012-12-22 04:31:53 +08:00
|
|
|
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
|
|
|
|
|
2012-12-29 17:39:44 +08:00
|
|
|
keep_highest = submissions.size - drop_lowest
|
|
|
|
keep_lowest = keep_highest - drop_highest
|
|
|
|
|
2013-01-15 05:06:16 +08:00
|
|
|
submissions.sort! { |a,b| a[:assignment].id - b[:assignment].id }
|
|
|
|
|
2012-12-29 17:39:44 +08:00
|
|
|
# assignment groups that have no points possible have to be dropped
|
|
|
|
# differently (it's a simpler case, but not one that fits in with our
|
|
|
|
# usual bisection approach)
|
|
|
|
kept = (cant_drop + submissions).any? { |s| s[:total] > 0 } ?
|
2013-01-10 08:59:16 +08:00
|
|
|
drop_pointed(submissions, cant_drop, keep_highest, keep_lowest) :
|
2012-12-29 17:39:44 +08:00
|
|
|
drop_unpointed(submissions, keep_highest, keep_lowest)
|
|
|
|
|
2015-02-13 11:08:45 +08:00
|
|
|
(kept + cant_drop).tap do |all_kept|
|
|
|
|
loggable_kept = all_kept.map { |s| loggable_submission(s) }
|
|
|
|
Rails.logger.info "GRADES.calculating... kept=#{loggable_kept.inspect}"
|
|
|
|
end
|
2012-12-29 17:39:44 +08:00
|
|
|
end
|
|
|
|
|
|
|
|
def drop_unpointed(submissions, keep_highest, keep_lowest)
|
|
|
|
sorted_submissions = submissions.sort_by { |s| s[:score] }
|
|
|
|
sorted_submissions.last(keep_highest).first(keep_lowest)
|
|
|
|
end
|
2012-12-22 04:31:53 +08:00
|
|
|
|
2013-01-10 08:59:16 +08:00
|
|
|
def drop_pointed(submissions, cant_drop, n_highest, n_lowest)
|
|
|
|
max_total = (submissions + cant_drop).map { |s| s[:total] }.max
|
|
|
|
|
|
|
|
kept = keep_highest(submissions, cant_drop, n_highest, max_total)
|
|
|
|
kept = keep_lowest(kept, cant_drop, n_lowest, max_total)
|
2012-12-22 04:31:53 +08:00
|
|
|
end
|
|
|
|
|
2013-01-10 08:59:16 +08:00
|
|
|
def keep_highest(submissions, cant_drop, keep, max_total)
|
|
|
|
keep_helper(submissions, cant_drop, keep, max_total) { |*args| big_f_best(*args) }
|
2012-12-22 04:31:53 +08:00
|
|
|
end
|
|
|
|
|
2013-01-10 08:59:16 +08:00
|
|
|
def keep_lowest(submissions, cant_drop, keep, max_total)
|
|
|
|
keep_helper(submissions, cant_drop, keep, max_total) { |*args| big_f_worst(*args) }
|
2012-12-22 04:31:53 +08:00
|
|
|
end
|
|
|
|
|
2013-01-10 08:59:16 +08:00
|
|
|
# @submissions: set of droppable submissions
|
|
|
|
# @cant_drop: submissions that are not eligible for dropping
|
|
|
|
# @keep: number of submissions to keep from +submissions+
|
|
|
|
# @max_total: the highest number of points possible
|
|
|
|
# @big_f_blk: sorting block for the big_f function
|
|
|
|
# returns +keep+ +submissions+
|
|
|
|
def keep_helper(submissions, cant_drop, keep, max_total, &big_f_blk)
|
2012-12-22 04:31:53 +08:00
|
|
|
return submissions if submissions.size <= keep
|
|
|
|
|
2013-01-10 08:59:16 +08:00
|
|
|
unpointed, pointed = (submissions + cant_drop).partition { |s|
|
|
|
|
s[:total].zero?
|
|
|
|
}
|
2012-12-29 17:39:44 +08:00
|
|
|
grades = pointed.map { |s| s[:score].to_f / s[:total] }.sort
|
|
|
|
|
|
|
|
q_high = estimate_q_high(pointed, unpointed, grades)
|
2012-12-22 04:31:53 +08:00
|
|
|
q_low = grades.first
|
|
|
|
q_mid = (q_low + q_high) / 2
|
|
|
|
|
2013-01-10 08:59:16 +08:00
|
|
|
x, kept = big_f_blk.call(q_mid, submissions, cant_drop, keep)
|
2012-12-22 04:31:53 +08:00
|
|
|
threshold = 1 / (2 * keep * 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
|
2013-02-17 07:42:10 +08:00
|
|
|
|
|
|
|
# bail if we can't can't ever satisfy the threshold (floats!)
|
|
|
|
break if q_mid == q_high || q_mid == q_low
|
|
|
|
|
2013-01-10 08:59:16 +08:00
|
|
|
x, kept = big_f_blk.call(q_mid, submissions, cant_drop, keep)
|
2012-12-22 04:31:53 +08:00
|
|
|
end
|
|
|
|
|
|
|
|
kept
|
|
|
|
end
|
|
|
|
|
2013-01-10 08:59:16 +08:00
|
|
|
def big_f(q, submissions, cant_drop, keep, &sort_blk)
|
2012-12-22 04:31:53 +08:00
|
|
|
kept = submissions.map { |s|
|
|
|
|
rated_score = s[:score] - q * s[:total]
|
|
|
|
[rated_score, s]
|
|
|
|
}.sort(&sort_blk).first(keep)
|
|
|
|
|
|
|
|
q_kept = kept.reduce(0) { |sum,(rated_score,_)| sum + rated_score }
|
2013-01-10 08:59:16 +08:00
|
|
|
q_cant_drop = cant_drop.reduce(0) { |sum,s| sum + (s[:score] - q * s[:total]) }
|
|
|
|
|
|
|
|
[q_kept + q_cant_drop, kept.map(&:last)]
|
2012-12-22 04:31:53 +08:00
|
|
|
end
|
|
|
|
|
2012-12-29 17:39:44 +08:00
|
|
|
# we can't use the student's highest grade as an upper-bound for bisection
|
|
|
|
# when 0-points-possible assignments are present, so guess the best possible
|
|
|
|
# grade the student could have earned in that case
|
|
|
|
def estimate_q_high(pointed, unpointed, grades)
|
|
|
|
if unpointed.present?
|
|
|
|
points_possible = pointed.reduce(0) { |sum,s| sum + s[:total] }
|
|
|
|
best_pointed_score = [
|
|
|
|
points_possible, # 100%
|
|
|
|
pointed.reduce(0) { |sum,s| sum + s[:score] } # ... or extra credit
|
|
|
|
].max
|
|
|
|
unpointed_score = unpointed.reduce(0) { |sum,s| sum + s[:score] }
|
|
|
|
max_score = best_pointed_score + unpointed_score
|
|
|
|
max_score.to_f / points_possible
|
|
|
|
else
|
|
|
|
grades.last
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2012-12-22 04:31:53 +08:00
|
|
|
# determines the best +keep+ assignments from submissions for the given q
|
|
|
|
# (suitable for use with drop_lowest)
|
2013-01-10 08:59:16 +08:00
|
|
|
def big_f_best(q, submissions, cant_drop, keep)
|
|
|
|
big_f(q, submissions, cant_drop, keep) { |(a,_),(b,_)| b <=> a }
|
2012-12-22 04:31:53 +08:00
|
|
|
end
|
|
|
|
|
|
|
|
# determines the worst +keep+ assignments from submissions for the given q
|
|
|
|
# (suitable for use with drop_highest)
|
2013-01-10 08:59:16 +08:00
|
|
|
def big_f_worst(q, submissions, cant_drop, keep)
|
|
|
|
big_f(q, submissions, cant_drop, keep) { |(a,_),(b,_)| a <=> b }
|
2012-12-22 04:31:53 +08:00
|
|
|
end
|
2013-04-30 08:29:45 +08:00
|
|
|
|
|
|
|
# returns grade information from all the assignment groups
|
2013-01-12 07:32:38 +08:00
|
|
|
def calculate_total_from_group_scores(group_sums)
|
2011-02-01 09:57:29 +08:00
|
|
|
if @course.group_weighting_scheme == 'percent'
|
2013-01-12 07:32:38 +08:00
|
|
|
relevant_group_sums = group_sums.reject { |gs|
|
|
|
|
gs[:possible].zero? || gs[:possible].nil?
|
|
|
|
}
|
|
|
|
final_grade = relevant_group_sums.reduce(0) { |grade,gs|
|
|
|
|
grade + (gs[:score].to_f / gs[:possible]) * gs[:weight]
|
|
|
|
}
|
|
|
|
|
|
|
|
# scale the grade up if total weights don't add up to 100%
|
|
|
|
full_weight = relevant_group_sums.reduce(0) { |w,gs| w + gs[:weight] }
|
|
|
|
if full_weight.zero?
|
|
|
|
final_grade = nil
|
|
|
|
elsif full_weight < 100
|
|
|
|
final_grade *= 100.0 / full_weight
|
2011-02-01 09:57:29 +08:00
|
|
|
end
|
2013-01-12 07:32:38 +08:00
|
|
|
|
2015-02-28 03:21:51 +08:00
|
|
|
{:grade => final_grade.try(:round, 2)}
|
2011-02-01 09:57:29 +08:00
|
|
|
else
|
2013-01-12 07:32:38 +08:00
|
|
|
total, possible = group_sums.reduce([0,0]) { |(m,n),gs|
|
|
|
|
[m + gs[:score], n + gs[:possible]]
|
|
|
|
}
|
|
|
|
if possible > 0
|
|
|
|
final_grade = (total.to_f / possible) * 100
|
2013-04-30 08:29:45 +08:00
|
|
|
{
|
2015-02-28 03:21:51 +08:00
|
|
|
:grade => final_grade.round(2),
|
2013-04-30 08:29:45 +08:00
|
|
|
:total => total.to_f,
|
|
|
|
:possible => possible,
|
|
|
|
}
|
2013-01-12 07:32:38 +08:00
|
|
|
else
|
2013-04-30 08:29:45 +08:00
|
|
|
{:grade => nil, :total => total.to_f}
|
2011-02-01 09:57:29 +08:00
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
2015-02-13 11:08:45 +08:00
|
|
|
|
|
|
|
# this takes a wrapped submission (like from create_group_sums)
|
|
|
|
def loggable_submission(wrapped_submission)
|
|
|
|
{
|
|
|
|
assignment_id: wrapped_submission[:assignment].id,
|
|
|
|
score: wrapped_submission[:score],
|
|
|
|
total: wrapped_submission[:total],
|
|
|
|
}
|
|
|
|
end
|
2011-04-12 02:30:31 +08:00
|
|
|
end
|