425 lines
20 KiB
Ruby
425 lines
20 KiB
Ruby
#
|
|
# Copyright (C) 2011 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/>.
|
|
#
|
|
|
|
class GradebooksController < ApplicationController
|
|
include ActionView::Helpers::NumberHelper
|
|
|
|
before_filter :require_context, :except => :public_feed
|
|
|
|
add_crumb("Grades", :except => :public_feed) { |c| c.send :named_context_url, c.instance_variable_get("@context"), :context_grades_url }
|
|
before_filter { |c| c.active_tab = "grades" }
|
|
|
|
def grade_summary
|
|
# do this as the very first thing, if the current user is a teacher in the course and they are not trying to view another user's grades, redirect them to the gradebook
|
|
if (@context.grants_right?(@current_user, nil, :manage_grades) || @context.grants_right?(@current_user, nil, :view_all_grades)) && !params[:id]
|
|
redirect_to_appropriate_gradebook_version
|
|
return
|
|
end
|
|
|
|
@observed_students = ObserverEnrollment.observed_students(@context, @current_user)
|
|
|
|
# always use id if given
|
|
if params[:id]
|
|
@student_enrollment = @context.all_student_enrollments.find_by_user_id(params[:id])
|
|
# otherwise try to find an observed student
|
|
elsif @observed_students.present?
|
|
# be consistent about which student we return by default
|
|
@student_enrollment = (@observed_students.to_a.sort_by {|e| e[0].sortable_name}.first)[1].first
|
|
# or just fall back to @current_user
|
|
else
|
|
@student_enrollment = @context.all_student_enrollments.find_by_user_id(@current_user.id)
|
|
end
|
|
|
|
@student = @student_enrollment && @student_enrollment.user
|
|
if !@student || !@student_enrollment
|
|
authorized_action(nil, @current_user, :permission_fail)
|
|
return
|
|
end
|
|
if authorized_action(@student_enrollment, @current_user, :read_grades)
|
|
log_asset_access("grades:#{@context.asset_string}", "grades", "other")
|
|
respond_to do |format|
|
|
if @student
|
|
add_crumb(@student.name, named_context_url(@context, :context_student_grades_url, @student.id))
|
|
|
|
@groups = @context.assignment_groups.active.all
|
|
@assignments = @context.assignments.active.gradeable.find(:all, :order => 'due_at, title')
|
|
groups_assignments =
|
|
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.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
|
|
# assignment in order to calculate grade distributions
|
|
@all_submissions = @context.submissions.all(:select => "submissions.assignment_id, submissions.score, submissions.grade, submissions.quiz_submission_id")
|
|
if @student == @current_user
|
|
@courses_with_grades = @student.available_courses.select{|c| c.grants_right?(@student, nil, :participate_as_student)}
|
|
end
|
|
format.html { render :action => 'grade_summary' }
|
|
else
|
|
format.html { render :action => 'grade_summary_list' }
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
def grading_rubrics
|
|
@rubric_contexts = @context.rubric_contexts(@current_user)
|
|
if params[:context_code]
|
|
context = @rubric_contexts.detect{|r| r[:context_code] == params[:context_code] }
|
|
@rubric_context = @context
|
|
if context
|
|
@rubric_context = Context.find_by_asset_string(params[:context_code])
|
|
end
|
|
@rubric_associations = @context.sorted_rubrics(@current_user, @rubric_context)
|
|
render :json => @rubric_associations.to_json(:methods => [:context_name], :include => :rubric)
|
|
else
|
|
render :json => @rubric_contexts.to_json
|
|
end
|
|
end
|
|
|
|
def submissions_json
|
|
updated = Time.parse(params[:updated]) rescue nil
|
|
updated ||= Time.parse("Jan 1 2000")
|
|
@submissions = @context.submissions.find(:all, :include => [:quiz_submission, :submission_comments, :attachments], :conditions => ['submissions.updated_at > ?', updated]).to_a
|
|
@new_submissions = @submissions
|
|
|
|
respond_to do |format|
|
|
if @new_submissions.empty?
|
|
format.json { render :json => [].to_json }
|
|
else
|
|
format.json { render :json => @new_submissions.to_json(:include => [:quiz_submission, :submission_comments, :attachments]) }
|
|
end
|
|
end
|
|
end
|
|
protected :submissions_json
|
|
|
|
def attendance
|
|
@enrollment = @context.all_student_enrollments.find_by_user_id(params[:user_id]) if params[:user_id].present?
|
|
@enrollment ||= @context.all_student_enrollments.find_by_user_id(@current_user.id) if !@context.grants_right?(@current_user, session, :manage_grades)
|
|
add_crumb t(:crumb, 'Attendance')
|
|
if !@enrollment && @context.grants_right?(@current_user, session, :manage_grades)
|
|
@assignments = @context.assignments.active.select{|a| a.submission_types == "attendance" }
|
|
@students = @context.students_visible_to(@current_user).order_by_sortable_name
|
|
@submissions = @context.submissions
|
|
@at_least_one_due_at = @assignments.any?{|a| a.due_at }
|
|
# Find which assignment group most attendance items belong to,
|
|
# it'll be a better guess for default assignment group than the first
|
|
# in the list...
|
|
@default_group_id = @assignments.to_a.count_per(&:assignment_group_id).sort_by{|id, cnt| cnt }.reverse.first[0] rescue nil
|
|
elsif @enrollment && @enrollment.grants_right?(@current_user, session, :read_grades)
|
|
@assignments = @context.assignments.active.select{|a| a.submission_types == "attendance" }
|
|
@students = @context.students_visible_to(@current_user).order_by_sortable_name
|
|
@submissions = @context.submissions.find_all_by_user_id(@enrollment.user_id)
|
|
@user = @enrollment.user
|
|
render :action => "student_attendance"
|
|
# render student_attendance, optional params[:assignment_id] to highlight and scroll to that particular assignment
|
|
else
|
|
flash[:notice] = t('notices.unauthorized', "You are not authorized to view attendance for this course")
|
|
redirect_to named_context_url(@context, :context_url)
|
|
# redirect
|
|
end
|
|
end
|
|
|
|
# GET /gradebooks/1
|
|
# GET /gradebooks/1.json
|
|
# GET /gradebooks/1.csv
|
|
def show
|
|
if authorized_action(@context, @current_user, [:manage_grades, :view_all_grades])
|
|
return submissions_json if params[:updated] && request.format == :json
|
|
return gradebook_init_json if params[:init] && request.format == :json
|
|
@context.require_assignment_group
|
|
|
|
log_asset_access("gradebook:#{@context.asset_string}", "grades", "other")
|
|
respond_to do |format|
|
|
format.html {
|
|
@groups = @context.assignment_groups.active
|
|
@groups_order = {}
|
|
@groups.each_with_index{|group, idx| @groups_order[group.id] = idx }
|
|
@just_assignments = @context.assignments.active.gradeable.find(:all, :order => 'due_at, title').select{|a| @groups_order[a.assignment_group_id] }
|
|
newest = Time.parse("Jan 1 2010")
|
|
@just_assignments = @just_assignments.sort_by{|a| [a.due_at || newest, @groups_order[a.assignment_group_id] || 0, a.position || 0] }
|
|
@assignments = @just_assignments.dup + groups_as_assignments(@groups)
|
|
@gradebook_upload = @context.build_gradebook_upload
|
|
@submissions = @context.submissions
|
|
@new_submissions = @submissions
|
|
if params[:updated]
|
|
d = DateTime.parse(params[:updated])
|
|
@new_submissions = @submissions.select{|s| s.updated_at > d}
|
|
end
|
|
@enrollments_hash = Hash.new{ |hash,key| hash[key] = [] }
|
|
@context.enrollments.sort_by{|e| [e.state_sortable, e.rank_sortable] }.each{ |e| @enrollments_hash[e.user_id] << e }
|
|
@students = @context.students_visible_to(@current_user).order_by_sortable_name.uniq
|
|
if params[:view] == "simple"
|
|
@headers = false
|
|
render :action => "show_simple"
|
|
else
|
|
render :action => "show"
|
|
end
|
|
}
|
|
format.csv {
|
|
cancel_cache_buster
|
|
Enrollment.recompute_final_score_if_stale @context
|
|
send_data(
|
|
@context.gradebook_to_csv(:include_sis_id => @context.grants_rights?(@current_user, session, :read_sis, :manage_sis).values.any?, :user => @current_user),
|
|
:type => "text/csv",
|
|
:filename => t('grades_filename', "Grades").gsub(/ /, "_") + "-" + @context.name.to_s.gsub(/ /, "_") + ".csv",
|
|
:disposition => "attachment"
|
|
)
|
|
}
|
|
format.json {
|
|
@submissions = @context.submissions
|
|
@new_submissions = @submissions
|
|
render :json => @new_submissions.to_json(:include => [:quiz_submission, :submission_comments, :attachments])
|
|
}
|
|
end
|
|
end
|
|
end
|
|
|
|
def gradebook_init_json
|
|
# res = "{"
|
|
if params[:assignments]
|
|
# you need to specify specifically which assignment fields you want returned to the gradebook via json here
|
|
# that makes it so we do a lot less querying to the db, which means less active record instantiation,
|
|
# which means less AR -> JSON serialization overhead which means less data transfer over the wire and faster request.
|
|
# (in this case, the worst part was the assignment 'description' which could be a massive wikipage)
|
|
render :json => @context.assignments.active.gradeable.scoped(
|
|
:select => ["id", "title", "due_at", "unlock_at", "lock_at",
|
|
"points_possible", "min_score", "max_score",
|
|
"mastery_score", "grading_type", "submission_types",
|
|
"assignment_group_id", "grading_scheme_id",
|
|
"grading_standard_id", "grade_group_students_individually",
|
|
"(select name from group_categories where
|
|
id=assignments.group_category_id) AS group_category"].join(", ")) + groups_as_assignments
|
|
elsif params[:students]
|
|
# you need to specify specifically which student fields you want returned to the gradebook via json here
|
|
render :json => @context.students_visible_to(@current_user).order_by_sortable_name.to_json(:only => ["id", "name", "sortable_name", "short_name"])
|
|
else
|
|
params[:user_ids] ||= params[:user_id]
|
|
user_ids = params[:user_ids].split(",").map(&:to_i) if params[:user_ids]
|
|
assignment_ids = params[:assignment_ids].split(",").map(&:to_i) if params[:assignment_ids]
|
|
# you need to specify specifically which submission fields you want returned to the gradebook here
|
|
scope_options = {
|
|
:select => ["assignment_id", "attachment_id", "grade", "grade_matches_current_submission", "group_id", "has_rubric_assessment", "id", "score", "submission_comments_count", "submission_type", "submitted_at", "url", "user_id"].join(" ,")
|
|
}
|
|
if user_ids && assignment_ids
|
|
@submissions = @context.submissions.scoped(scope_options).find(:all, :conditions => {:user_id => user_ids, :assignment_id => assignment_ids})
|
|
elsif user_ids
|
|
@submissions = @context.submissions.scoped(scope_options).find(:all, :conditions => {:user_id => user_ids})
|
|
else
|
|
@submissions = @context.submissions.scoped(scope_options)
|
|
end
|
|
render :json => @submissions.to_json(:include => [:attachments, :quiz_submission, :submission_comments])
|
|
end
|
|
end
|
|
protected :gradebook_init_json
|
|
|
|
def history
|
|
if authorized_action(@context, @current_user, :manage_grades)
|
|
#
|
|
# Temporary disabling of this page for large courses
|
|
# We need some reworking of the gradebook history to allow using it
|
|
# in large courses in a performant manner. Until that happens, we're
|
|
# disabling it over a certain threshold.
|
|
#
|
|
submissions_count = @context.submissions.count
|
|
submissions_limit = Setting.get('gradebook_history_submission_count_threshold', '0').to_i
|
|
if submissions_limit == 0 || submissions_count <= submissions_limit
|
|
# TODO this whole thing could go a LOT faster if you just got ALL the versions of ALL the submissions in this course then did a ruby sort_by day then grader
|
|
@days = SubmissionList.days(@context)
|
|
end
|
|
|
|
respond_to do |format|
|
|
format.html
|
|
end
|
|
end
|
|
end
|
|
|
|
def update_submission
|
|
if authorized_action(@context, @current_user, :manage_grades)
|
|
submissions = [params[:submission]]
|
|
if params[:submissions]
|
|
submissions = []
|
|
params[:submissions].each do |key, submission|
|
|
submissions << submission
|
|
end
|
|
end
|
|
@submissions = []
|
|
submissions.compact.each do |submission|
|
|
@assignment = @context.assignments.active.find(submission[:assignment_id])
|
|
begin
|
|
@user = @context.students_visible_to(@current_user).find(submission[:user_id].to_i)
|
|
rescue ActiveRecord::RecordNotFound
|
|
next
|
|
end
|
|
submission[:grader] = @current_user
|
|
submission.delete :comment_attachments
|
|
if params[:attachments]
|
|
attachments = []
|
|
params[:attachments].each do |idx, attachment|
|
|
attachment[:user] = @current_user
|
|
attachments << @assignment.attachments.create(attachment)
|
|
end
|
|
submission[:comment_attachments] = attachments
|
|
end
|
|
begin
|
|
# if it's a percentage graded assignment, we need to ensure there's a
|
|
# percent sign on the end. eventually this will probably be done in
|
|
# the javascript.
|
|
if @assignment.grading_type == "percent" && submission[:grade] && submission[:grade] !~ /%\z/
|
|
submission[:grade] = "#{submission[:grade]}%"
|
|
end
|
|
# requires: assignment_id, user_id, and grade or comment
|
|
@submissions += @assignment.grade_student(@user, submission)
|
|
rescue => e
|
|
@error_message = e.to_s
|
|
end
|
|
end
|
|
@submissions = @submissions.reverse.uniq.reverse
|
|
@submissions = nil if @submissions.empty?
|
|
|
|
respond_to do |format|
|
|
if @submissions && !@error_message#&& !@submission.errors || @submission.errors.empty?
|
|
flash[:notice] = t('notices.updated', 'Assignment submission was successfully updated.')
|
|
format.html { redirect_to course_gradebook_url(@assignment.context) }
|
|
format.json {
|
|
render :json => @submissions.to_json(Submission.json_serialization_full_parameters), :status => :created, :location => course_gradebook_url(@assignment.context)
|
|
}
|
|
format.text {
|
|
render :json => @submissions.to_json(Submission.json_serialization_full_parameters), :status => :created, :location => course_gradebook_url(@assignment.context),
|
|
:as_text => true
|
|
}
|
|
else
|
|
flash[:error] = t('errors.submission_failed', "Submission was unsuccessful: %{error}", :error => @error_message || t('errors.submission_failed_default', 'Submission Failed'))
|
|
format.html { render :action => "show", :course_id => @assignment.context.id }
|
|
format.json { render :json => {:errors => {:base => @error_message}}.to_json, :status => :bad_request }
|
|
format.text { render :json => {:errors => {:base => @error_message}}.to_json, :status => :bad_request }
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
def submissions_zip_upload
|
|
@assignment = @context.assignments.active.find(params[:assignment_id])
|
|
if !params[:submissions_zip] || params[:submissions_zip].is_a?(String)
|
|
flash[:error] = t('errors.missing_file', "Could not find file to upload")
|
|
redirect_to named_context_url(@context, :context_assignment_url, @assignment.id)
|
|
return
|
|
end
|
|
@comments, @failures = @assignment.generate_comments_from_files(params[:submissions_zip].path, @current_user)
|
|
flash[:notice] = t('notices.uploaded',
|
|
{ :one => "Files and comments created for 1 user submission",
|
|
:other => "Files and comments created for %{count} user submissions" },
|
|
:count => @comments.length)
|
|
end
|
|
|
|
def speed_grader
|
|
if authorized_action(@context, @current_user, [:manage_grades, :view_all_grades])
|
|
@assignment = @context.assignments.active.find(params[:assignment_id])
|
|
respond_to do |format|
|
|
format.html {
|
|
@headers = false
|
|
log_asset_access("speed_grader:#{@context.asset_string}", "grades", "other")
|
|
render :action => "speed_grader"
|
|
}
|
|
format.json { render :json => @assignment.speed_grader_json(@current_user, service_enabled?(:avatars)) }
|
|
end
|
|
end
|
|
end
|
|
|
|
def blank_submission
|
|
@headers = false
|
|
render :action => "blank_submission"
|
|
end
|
|
|
|
def public_feed
|
|
return unless get_feed_context(:only => [:course])
|
|
|
|
respond_to do |format|
|
|
feed = Atom::Feed.new do |f|
|
|
f.title = t('titles.feed_for_course', "%{course} Gradebook Feed", :course => @context.name)
|
|
f.links << Atom::Link.new(:href => course_gradebook_url(@context), :rel => 'self')
|
|
f.updated = Time.now
|
|
f.id = course_gradebook_url(@context)
|
|
end
|
|
@context.submissions.each do |e|
|
|
feed.entries << e.to_atom
|
|
end
|
|
format.atom { render :text => feed.to_xml }
|
|
end
|
|
end
|
|
|
|
def change_gradebook_version
|
|
if params[:reset]
|
|
@current_user.preferences.delete :use_gradebook2
|
|
else
|
|
@current_user.preferences[:use_gradebook2] = true
|
|
end
|
|
@current_user.save!
|
|
redirect_to_appropriate_gradebook_version
|
|
end
|
|
|
|
def redirect_to_appropriate_gradebook_version
|
|
gradebook_version_to_use = @current_user.preferences[:use_gradebook2] ? 'gradebook2' : 'gradebook'
|
|
redirect_to named_context_url(@context, "context_#{gradebook_version_to_use}_url")
|
|
end
|
|
protected :redirect_to_appropriate_gradebook_version
|
|
|
|
def groups_as_assignments(groups=nil, options = {})
|
|
groups ||= @context.assignment_groups.active
|
|
|
|
percentage = lambda do |weight|
|
|
# find the smallest precision necessary to capture up to two digits of
|
|
# significant decimals, but to avoid unnecessary zeros on the end. (so we
|
|
# can have 100%, but still have 33.33%, for example)
|
|
precision = sprintf('%.2f', weight % 1).sub(/^(?:0|1)\.(\d?[1-9])?0*$/, '\1').length
|
|
number_to_percentage(weight, :precision => precision)
|
|
end
|
|
|
|
points_possible =
|
|
(@context.group_weighting_scheme == "percent") ?
|
|
(options[:out_of_final] ?
|
|
lambda{ |group| t(:out_of_final, "%{weight} of Final", :weight => percentage[group.group_weight]) } :
|
|
lambda{ |group| percentage[group.group_weight] }) :
|
|
lambda{ |group| nil }
|
|
|
|
groups = groups.map{ |group|
|
|
OpenObject.build('assignment',
|
|
:id => 'group-' + group.id.to_s,
|
|
:rules => group.rules,
|
|
:title => group.name,
|
|
:points_possible => points_possible[group],
|
|
:hard_coded => true,
|
|
:special_class => 'group_total',
|
|
:assignment_group_id => group.id,
|
|
:group_weight => group.group_weight,
|
|
:asset_string => "group_total_#{group.id}")
|
|
}
|
|
groups << OpenObject.build('assignment',
|
|
:id => 'final-grade',
|
|
:title => t('titles.total', 'Total'),
|
|
:points_possible => (options[:out_of_final] ? '-' : percentage[100]),
|
|
:hard_coded => true,
|
|
:special_class => 'final_grade',
|
|
:asset_string => "final_grade_column") unless options[:exclude_total]
|
|
groups = [] if options[:exclude_total] && groups.length == 1
|
|
groups
|
|
end
|
|
end
|