1047 lines
41 KiB
Ruby
1047 lines
41 KiB
Ruby
#
|
|
# Copyright (C) 2011 - present Instructure, Inc.
|
|
#
|
|
# This file is part of Canvas.
|
|
#
|
|
# Canvas is free software: you can redistribute it and/or modify it under
|
|
# the terms of the GNU Affero General Public License as published by the Free
|
|
# Software Foundation, version 3 of the License.
|
|
#
|
|
# Canvas is distributed in the hope that it will be useful, but WITHOUT ANY
|
|
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
|
|
# A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
|
|
# details.
|
|
#
|
|
# You should have received a copy of the GNU Affero General Public License along
|
|
# with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
#
|
|
|
|
class GradebooksController < ApplicationController
|
|
include ActionView::Helpers::NumberHelper
|
|
include GradebooksHelper
|
|
include KalturaHelper
|
|
include Api::V1::AssignmentGroup
|
|
include Api::V1::Submission
|
|
include Api::V1::CustomGradebookColumn
|
|
include Api::V1::Section
|
|
include Api::V1::Rubric
|
|
include Api::V1::RubricAssessment
|
|
|
|
before_action :require_context
|
|
before_action :require_user, only: [:speed_grader, :speed_grader_settings, :grade_summary]
|
|
|
|
batch_jobs_in_actions :only => :update_submission, :batch => { :priority => Delayed::LOW_PRIORITY }
|
|
|
|
add_crumb(proc { t '#crumbs.grades', "Grades" }) { |c| c.send :named_context_url, c.instance_variable_get("@context"), :context_grades_url }
|
|
before_action { |c| c.active_tab = "grades" }
|
|
|
|
MAX_POST_GRADES_TOOLS = 10
|
|
|
|
def grade_summary
|
|
set_current_grading_period if grading_periods?
|
|
@presenter = grade_summary_presenter
|
|
# 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 @presenter.user_needs_redirection?
|
|
return redirect_to polymorphic_url([@context, 'gradebook'])
|
|
end
|
|
|
|
if !@presenter.student || !@presenter.student_enrollment
|
|
return render_unauthorized_action
|
|
end
|
|
|
|
return unless authorized_action(@context, @current_user, :read) &&
|
|
authorized_action(@presenter.student_enrollment, @current_user, :read_grades)
|
|
|
|
log_asset_access([ "grades", @context ], "grades", "other")
|
|
|
|
return render :grade_summary_list unless @presenter.student
|
|
|
|
add_crumb(@presenter.student_name, named_context_url(@context, :context_student_grades_url,
|
|
@presenter.student_id))
|
|
gp_id = nil
|
|
if grading_periods?
|
|
@grading_periods = active_grading_periods_json
|
|
gp_id = @current_grading_period_id unless view_all_grading_periods?
|
|
|
|
effective_due_dates =
|
|
Submission.active.
|
|
where(user_id: @presenter.student_id, assignment_id: @context.assignments.active).
|
|
select(:cached_due_date, :grading_period_id, :assignment_id, :user_id).
|
|
each_with_object({}) do |submission, hsh|
|
|
hsh[submission.assignment_id] = {
|
|
submission.user_id => {
|
|
due_at: submission.cached_due_date,
|
|
grading_period_id: submission.grading_period_id,
|
|
}
|
|
}
|
|
end
|
|
end
|
|
|
|
@exclude_total = exclude_total?(@context)
|
|
|
|
Shackles.activate(:slave) do
|
|
# run these queries on the slave database for speed
|
|
@presenter.assignments
|
|
aggregate_assignments
|
|
@presenter.submissions
|
|
@presenter.assignment_stats
|
|
end
|
|
|
|
submissions_json = @presenter.submissions.
|
|
select { |s| s.user_can_read_grade?(@current_user) }.
|
|
map do |s|
|
|
{
|
|
assignment_id: s.assignment_id,
|
|
score: s.score,
|
|
excused: s.excused?,
|
|
workflow_state: s.workflow_state,
|
|
}
|
|
end
|
|
|
|
grading_period = @grading_periods && @grading_periods.find { |period| period[:id] == gp_id }
|
|
|
|
ags_json = light_weight_ags_json(@presenter.groups, {student: @presenter.student})
|
|
|
|
js_env(
|
|
submissions: submissions_json,
|
|
assignment_groups: ags_json,
|
|
assignment_sort_options: @presenter.sort_options,
|
|
group_weighting_scheme: @context.group_weighting_scheme,
|
|
show_total_grade_as_points: @context.show_total_grade_as_points?,
|
|
grading_scheme: @context.grading_standard_or_default.data,
|
|
current_grading_period_id: @current_grading_period_id,
|
|
current_assignment_sort_order: @presenter.assignment_order,
|
|
grading_period_set: grading_period_group_json,
|
|
grading_period: grading_period,
|
|
grading_periods: @grading_periods,
|
|
courses_with_grades: courses_with_grades_json,
|
|
effective_due_dates: effective_due_dates,
|
|
exclude_total: @exclude_total,
|
|
non_scoring_rubrics_enabled: @context.root_account.feature_enabled?(:non_scoring_rubrics),
|
|
rubric_assessments: rubric_assessments_json(@presenter.rubric_assessments, @current_user, session, style: 'full'),
|
|
rubrics: rubrics_json(@presenter.rubrics, @current_user, session, style: 'full'),
|
|
save_assignment_order_url: course_save_assignment_order_url(@context),
|
|
student_outcome_gradebook_enabled: @context.feature_enabled?(:student_outcome_gradebook),
|
|
student_id: @presenter.student_id,
|
|
students: @presenter.students.as_json(include_root: false),
|
|
outcome_proficiency: outcome_proficiency
|
|
)
|
|
end
|
|
|
|
def save_assignment_order
|
|
if authorized_action(@context, @current_user, :read)
|
|
whitelisted_orders = {
|
|
'due_at' => :due_at, 'title' => :title,
|
|
'module' => :module, 'assignment_group' => :assignment_group
|
|
}
|
|
assignment_order = whitelisted_orders.fetch(params.fetch(:assignment_order), :due_at)
|
|
@current_user.preferences[:course_grades_assignment_order] ||= {}
|
|
@current_user.preferences[:course_grades_assignment_order][@context.id] = assignment_order
|
|
@current_user.save!
|
|
redirect_back(fallback_location: course_grades_url(@context))
|
|
end
|
|
end
|
|
|
|
def light_weight_ags_json(assignment_groups, opts={})
|
|
assignment_groups.map do |ag|
|
|
visible_assignments = ag.visible_assignments(opts[:student] || @current_user).to_a
|
|
|
|
if grading_periods? && @current_grading_period_id && !view_all_grading_periods?
|
|
current_period = GradingPeriod.for(@context).find_by(id: @current_grading_period_id)
|
|
visible_assignments = current_period.assignments_for_student(visible_assignments, opts[:student])
|
|
end
|
|
|
|
visible_assignments.map! do |a|
|
|
{
|
|
id: a.id,
|
|
submission_types: a.submission_types_array,
|
|
points_possible: a.points_possible,
|
|
due_at: a.due_at,
|
|
omit_from_final_grade: a.omit_from_final_grade?,
|
|
muted: a.muted?
|
|
}
|
|
end
|
|
|
|
{
|
|
id: ag.id,
|
|
rules: ag.rules_hash({stringify_json_ids: true}),
|
|
group_weight: ag.group_weight,
|
|
assignments: visible_assignments,
|
|
}
|
|
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.map{ |r| r.as_json(methods: [:context_name], include: {:rubric => {:include_root => false}}) }
|
|
else
|
|
render :json => @rubric_contexts
|
|
end
|
|
end
|
|
|
|
def show
|
|
if authorized_action(@context, @current_user, [:manage_grades, :view_all_grades])
|
|
@last_exported_gradebook_csv = GradebookCsv.last_successful_export(course: @context, user: @current_user)
|
|
set_current_grading_period if grading_periods?
|
|
set_gradebook_env
|
|
set_tutorial_js_env
|
|
@course_is_concluded = @context.completed?
|
|
@post_grades_tools = post_grades_tools
|
|
|
|
render_gradebook
|
|
end
|
|
end
|
|
|
|
def post_grades_ltis
|
|
@post_grades_ltis ||= self.external_tools.map { |tool| external_tool_detail(tool) }
|
|
end
|
|
|
|
def post_grades_tools
|
|
tool_limit = @context.feature_enabled?(:post_grades) ? MAX_POST_GRADES_TOOLS - 1 : MAX_POST_GRADES_TOOLS
|
|
tools = post_grades_ltis[0...tool_limit]
|
|
tools.push(type: :post_grades) if @context.feature_enabled?(:post_grades) && tools.size == 0
|
|
tools
|
|
end
|
|
|
|
def external_tool_detail(tool)
|
|
post_grades_placement = tool[:placements][:post_grades]
|
|
{
|
|
id: tool[:definition_id],
|
|
data_url: post_grades_placement[:canvas_launch_url],
|
|
name: tool[:name],
|
|
type: :lti,
|
|
data_width: post_grades_placement[:launch_width],
|
|
data_height: post_grades_placement[:launch_height]
|
|
}
|
|
end
|
|
|
|
def external_tools
|
|
bookmarked_collection = Lti::AppLaunchCollator.bookmarked_collection(@context, [:post_grades])
|
|
tools = bookmarked_collection.paginate(per_page: MAX_POST_GRADES_TOOLS + 1).to_a
|
|
launch_definitions = Lti::AppLaunchCollator.launch_definitions(tools, [:post_grades])
|
|
launch_definitions.each do |launch_definition|
|
|
case launch_definition[:definition_type]
|
|
when 'ContextExternalTool'
|
|
url = external_tool_url_for_lti1(launch_definition)
|
|
when 'Lti::MessageHandler'
|
|
url = external_tool_url_for_lti2(launch_definition)
|
|
end
|
|
launch_definition[:placements][:post_grades][:canvas_launch_url] = url
|
|
end
|
|
launch_definitions
|
|
end
|
|
|
|
def external_tool_url_for_lti1(launch_definition)
|
|
polymorphic_url(
|
|
[@context, :external_tool],
|
|
id: launch_definition[:definition_id],
|
|
display: 'borderless',
|
|
launch_type: 'post_grades',
|
|
)
|
|
end
|
|
|
|
def external_tool_url_for_lti2(launch_definition)
|
|
polymorphic_url(
|
|
[@context, :basic_lti_launch_request],
|
|
message_handler_id: launch_definition[:definition_id],
|
|
display: 'borderless',
|
|
)
|
|
end
|
|
|
|
def set_current_grading_period
|
|
if params[:grading_period_id].present?
|
|
@current_grading_period_id = params[:grading_period_id].to_i
|
|
else
|
|
return if view_all_grading_periods?
|
|
current = GradingPeriod.current_period_for(@context)
|
|
@current_grading_period_id = current ? current.id : 0
|
|
end
|
|
end
|
|
|
|
def view_all_grading_periods?
|
|
@current_grading_period_id == 0
|
|
end
|
|
|
|
def grading_period_group
|
|
return @grading_period_group if defined? @grading_period_group
|
|
|
|
@grading_period_group = active_grading_periods.first&.grading_period_group
|
|
end
|
|
|
|
def active_grading_periods
|
|
@active_grading_periods ||= GradingPeriod.for(@context).order(:start_date)
|
|
end
|
|
|
|
def grading_period_group_json
|
|
return @grading_period_group_json if defined? @grading_period_group_json
|
|
return @grading_period_group_json = nil unless grading_period_group.present?
|
|
|
|
@grading_period_group_json = grading_period_group
|
|
.as_json
|
|
.fetch(:grading_period_group)
|
|
.merge(grading_periods: active_grading_periods_json)
|
|
end
|
|
|
|
def active_grading_periods_json
|
|
@agp_json ||= GradingPeriod.periods_json(active_grading_periods, @current_user)
|
|
end
|
|
|
|
def old_gradebook_env
|
|
@gradebook_is_editable = @context.grants_right?(@current_user, session, :manage_grades)
|
|
per_page = Setting.get('api_max_per_page', '50').to_i
|
|
teacher_notes = @context.custom_gradebook_columns.not_deleted.where(teacher_notes: true).first
|
|
ag_includes = [:assignments, :assignment_visibility, :grades_published]
|
|
last_exported_attachment = @last_exported_gradebook_csv.try(:attachment)
|
|
grading_standard = @context.grading_standard_or_default
|
|
{
|
|
STUDENT_CONTEXT_CARDS_ENABLED: @domain_root_account.feature_enabled?(:student_context_cards),
|
|
GRADEBOOK_OPTIONS: {
|
|
api_max_per_page: per_page,
|
|
chunk_size: Setting.get('gradebook2.submissions_chunk_size', '10').to_i,
|
|
assignment_groups_url: api_v1_course_assignment_groups_url(
|
|
@context,
|
|
include: ag_includes,
|
|
override_assignment_dates: "false",
|
|
exclude_assignment_submission_types: ['wiki_page']
|
|
),
|
|
context_modules_url: api_v1_course_context_modules_url(@context),
|
|
sections_url: api_v1_course_sections_url(@context),
|
|
course_url: api_v1_course_url(@context),
|
|
effective_due_dates_url: api_v1_course_effective_due_dates_url(@context),
|
|
enrollments_url: custom_course_enrollments_api_url(per_page: per_page),
|
|
enrollments_with_concluded_url:
|
|
custom_course_enrollments_api_url(include_concluded: true, per_page: per_page),
|
|
enrollments_with_inactive_url:
|
|
custom_course_enrollments_api_url(include_inactive: true, per_page: per_page),
|
|
enrollments_with_concluded_and_inactive_url:
|
|
custom_course_enrollments_api_url(include_concluded: true, include_inactive: true, per_page: per_page),
|
|
students_url: custom_course_users_api_url(per_page: per_page),
|
|
students_stateless_url: custom_course_users_api_url(exclude_states: true, per_page: per_page),
|
|
students_with_concluded_enrollments_url:
|
|
custom_course_users_api_url(include_concluded: true, per_page: per_page),
|
|
students_with_inactive_enrollments_url:
|
|
custom_course_users_api_url(include_inactive: true, per_page: per_page),
|
|
students_with_concluded_and_inactive_enrollments_url:
|
|
custom_course_users_api_url(include_concluded: true, include_inactive: true, per_page: per_page),
|
|
submissions_url: api_v1_course_student_submissions_url(@context, grouped: '1'),
|
|
outcome_links_url: api_v1_course_outcome_group_links_url(@context, outcome_style: :full),
|
|
outcome_rollups_url: api_v1_course_outcome_rollups_url(@context, per_page: 100),
|
|
change_grade_url:
|
|
api_v1_course_assignment_submission_url(@context, ":assignment", ":submission", include: [:visibility]),
|
|
context_url: named_context_url(@context, :context_url),
|
|
download_assignment_submissions_url:
|
|
named_context_url(@context, :context_assignment_submissions_url, "{{ assignment_id }}", zip: 1),
|
|
re_upload_submissions_url:
|
|
named_context_url(@context, :submissions_upload_context_gradebook_url, "{{ assignment_id }}"),
|
|
context_id: @context.id.to_s,
|
|
context_code: @context.asset_string,
|
|
context_sis_id: @context.sis_source_id,
|
|
group_weighting_scheme: @context.group_weighting_scheme,
|
|
grading_standard: @context.grading_standard_enabled? && grading_standard.data,
|
|
default_grading_standard: grading_standard.data,
|
|
course_is_concluded: @context.completed?,
|
|
course_name: @context.name,
|
|
gradebook_is_editable: @gradebook_is_editable,
|
|
context_allows_gradebook_uploads: @context.allows_gradebook_uploads?,
|
|
gradebook_import_url: new_course_gradebook_upload_path(@context),
|
|
setting_update_url: api_v1_course_settings_url(@context),
|
|
show_total_grade_as_points: @context.show_total_grade_as_points?,
|
|
publish_to_sis_enabled: (
|
|
!!@context.sis_source_id && @context.allows_grade_publishing_by(@current_user) && @gradebook_is_editable
|
|
),
|
|
publish_to_sis_url: context_url(@context, :context_details_url, anchor: 'tab-grade-publishing'),
|
|
speed_grader_enabled: @context.allows_speed_grader?,
|
|
active_grading_periods: active_grading_periods_json,
|
|
grading_period_set: grading_period_group_json,
|
|
current_grading_period_id: @current_grading_period_id,
|
|
outcome_gradebook_enabled: @context.feature_enabled?(:outcome_gradebook),
|
|
custom_columns_url: api_v1_course_custom_gradebook_columns_url(@context),
|
|
custom_column_url: api_v1_course_custom_gradebook_column_url(@context, ":id"),
|
|
custom_column_data_url: api_v1_course_custom_gradebook_column_data_url(@context, ":id", per_page: per_page),
|
|
custom_column_datum_url: api_v1_course_custom_gradebook_column_datum_url(@context, ":id", ":user_id"),
|
|
reorder_custom_columns_url: api_v1_custom_gradebook_columns_reorder_url(@context),
|
|
teacher_notes: teacher_notes && custom_gradebook_column_json(teacher_notes, @current_user, session),
|
|
change_gradebook_version_url: context_url(
|
|
@context,
|
|
:change_gradebook_version_context_gradebook_url,
|
|
version: 2
|
|
),
|
|
export_gradebook_csv_url: course_gradebook_csv_url,
|
|
gradebook_csv_progress: @last_exported_gradebook_csv.try(:progress),
|
|
attachment_url: authenticated_download_url(last_exported_attachment),
|
|
attachment: last_exported_attachment,
|
|
sis_app_url: Setting.get('sis_app_url', nil),
|
|
sis_app_token: Setting.get('sis_app_token', nil),
|
|
list_students_by_sortable_name_enabled: @context.list_students_by_sortable_name?,
|
|
gradebook_column_size_settings: @current_user.preferences[:gradebook_column_size],
|
|
gradebook_column_size_settings_url: change_gradebook_column_size_course_gradebook_url,
|
|
gradebook_column_order_settings: @current_user.preferences[:gradebook_column_order].try(:[], @context.id),
|
|
gradebook_column_order_settings_url: save_gradebook_column_order_course_gradebook_url,
|
|
post_grades_ltis: post_grades_ltis,
|
|
post_grades_feature: post_grades_feature?,
|
|
sections: sections_json(@context.active_course_sections, @current_user, session, [], allow_sis_ids: true),
|
|
settings_update_url: api_v1_course_gradebook_settings_update_url(@context),
|
|
settings: gradebook_settings.fetch(@context.id, {}),
|
|
login_handle_name: @context.root_account.settings[:login_handle_name],
|
|
sis_name: @context.root_account.settings[:sis_name],
|
|
version: params.fetch(:version, nil),
|
|
outcome_proficiency: outcome_proficiency
|
|
}
|
|
}
|
|
end
|
|
|
|
def set_gradebook_env
|
|
env = old_gradebook_env
|
|
|
|
if new_gradebook_enabled?
|
|
env = env.deep_merge(new_gradebook_env)
|
|
end
|
|
|
|
js_env(env)
|
|
end
|
|
|
|
def post_grades_feature?
|
|
@context.feature_enabled?(:post_grades) &&
|
|
@context.allows_grade_publishing_by(@current_user) &&
|
|
can_do(@context, @current_user, :manage_grades)
|
|
end
|
|
|
|
def history
|
|
if authorized_action(@context, @current_user, %i[manage_grades view_all_grades])
|
|
crumbs.delete_if { |crumb| crumb[0] == "Grades" }
|
|
add_crumb(t("Gradebook History"),
|
|
context_url(@context, controller: :gradebooks, action: :history))
|
|
@page_title = t("Gradebook History")
|
|
@body_classes << "full-width padless-content"
|
|
js_bundle :gradebook_history
|
|
js_env(
|
|
COURSE_IS_CONCLUDED: @context.is_a?(Course) && @context.completed?
|
|
)
|
|
|
|
render html: "", layout: true
|
|
end
|
|
end
|
|
|
|
def update_submission
|
|
if authorized_action(@context, @current_user, :manage_grades)
|
|
if params[:submissions].blank? && params[:submission].blank?
|
|
render nothing: true, status: 400
|
|
return
|
|
end
|
|
|
|
submissions = if params[:submissions]
|
|
params[:submissions].values.map { |s| ActionController::Parameters.new(s) }
|
|
else
|
|
[params[:submission]]
|
|
end
|
|
|
|
# decorate submissions with user_ids if not present
|
|
submissions_without_user_ids = submissions.select {|s| s[:user_id].blank?}
|
|
if submissions_without_user_ids.present?
|
|
submissions = populate_user_ids(submissions_without_user_ids)
|
|
end
|
|
|
|
valid_user_ids = Set.new(@context.students_visible_to(@current_user, include: :inactive).pluck(:id))
|
|
submissions.select! { |submission| valid_user_ids.include? submission[:user_id].to_i }
|
|
|
|
user_ids = submissions.map { |submission| submission[:user_id] }
|
|
assignment_ids = submissions.map { |submission| submission[:assignment_id] }
|
|
users = @context.admin_visible_students.distinct.find(user_ids).index_by(&:id)
|
|
assignments = @context.assignments.active.find(assignment_ids).index_by(&:id)
|
|
|
|
request_error_status = nil
|
|
error = nil
|
|
@submissions = []
|
|
submissions.each do |submission|
|
|
@assignment = assignments[submission[:assignment_id].to_i]
|
|
@user = users[submission[:user_id].to_i]
|
|
|
|
submission = submission.permit(:grade, :score, :excuse, :excused,
|
|
:graded_anonymously, :provisional, :final,
|
|
:comment, :media_comment_id, :media_comment_type, :group_comment).to_unsafe_h
|
|
|
|
submission[:grader] = @current_user
|
|
submission.delete(:provisional) unless @assignment.moderated_grading?
|
|
if params[:attachments]
|
|
submission[:comment_attachments] = params[:attachments].keys.map do |idx|
|
|
attachment_json = params[:attachments][idx].permit(Attachment.permitted_attributes)
|
|
attachment_json[:user] = @current_user
|
|
attachment = @assignment.attachments.new(attachment_json.except(:uploaded_data))
|
|
Attachments::Storage.store_for_attachment(attachment, attachment_json[:uploaded_data])
|
|
attachment.save!
|
|
attachment
|
|
end
|
|
end
|
|
begin
|
|
if [:grade, :score, :excuse, :excused].any? { |k| submission.key? k }
|
|
# 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
|
|
|
|
submission[:dont_overwrite_grade] = value_to_boolean(params[:dont_overwrite_grades])
|
|
submission.delete(:final) if submission[:final] && !@assignment.permits_moderation?(@current_user)
|
|
subs = @assignment.grade_student(@user, submission)
|
|
apply_provisional_grade_filters!(submissions: subs, final: submission[:final]) if submission[:provisional]
|
|
@submissions += subs
|
|
end
|
|
if [:comment, :media_comment_id, :comment_attachments].any? { |k| submission.key? k }
|
|
submission[:commenter] = @current_user
|
|
submission[:hidden] = @assignment.muted?
|
|
|
|
subs = @assignment.update_submission(@user, submission)
|
|
apply_provisional_grade_filters!(submissions: subs, final: submission[:final]) if submission[:provisional]
|
|
@submissions += subs
|
|
end
|
|
rescue Assignment::GradeError => e
|
|
logger.info "GRADES: grade_student failed because '#{e.message}'"
|
|
error = e
|
|
end
|
|
end
|
|
@submissions = @submissions.reverse.uniq.reverse
|
|
@submissions = nil if submissions.empty? # no valid submissions
|
|
|
|
respond_to do |format|
|
|
if @submissions && error.nil?
|
|
flash[:notice] = t('notices.updated', 'Assignment submission was successfully updated.')
|
|
format.html { redirect_to course_gradebook_url(@assignment.context) }
|
|
format.json do
|
|
render(
|
|
json: submissions_json(submissions: @submissions, assignments: assignments),
|
|
status: :created,
|
|
location: course_gradebook_url(@assignment.context)
|
|
)
|
|
end
|
|
format.text do
|
|
render(
|
|
json: submissions_json(submissions: @submissions, assignments: assignments),
|
|
status: :created,
|
|
location: course_gradebook_url(@assignment.context),
|
|
as_text: true
|
|
)
|
|
end
|
|
else
|
|
error_message = error&.to_s
|
|
flash[:error] = t(
|
|
'errors.submission_failed',
|
|
"Submission was unsuccessful: %{error}",
|
|
error: error_message || t('errors.submission_failed_default', 'Submission Failed')
|
|
)
|
|
request_error_status = error&.status_code || :bad_request
|
|
|
|
error_json = {base: error_message}
|
|
error_json[:error_code] = error.error_code if error
|
|
|
|
format.html { render :show, course_id: @assignment.context.id }
|
|
format.json { render json: { errors: error_json }, status: request_error_status }
|
|
format.text { render json: { errors: error_json }, status: request_error_status }
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
def submissions_json(submissions:, assignments:)
|
|
submissions.map do |sub|
|
|
assignment = assignments[sub[:assignment_id].to_i]
|
|
omitted_field = assignment.anonymize_students? ? :user_id : :anonymous_id
|
|
json_params = {
|
|
include: { submission_history: { methods: %i[late missing], except: omitted_field } },
|
|
except: omitted_field
|
|
}
|
|
json = sub.as_json(Submission.json_serialization_full_parameters.merge(json_params))
|
|
json['submission']['assignment_visible'] = sub.assignment_visible_to_user?(sub.user)
|
|
json['submission']['provisional_grade_id'] = sub.provisional_grade_id if sub.provisional_grade_id
|
|
json
|
|
end
|
|
end
|
|
|
|
def submissions_zip_upload
|
|
return unless authorized_action(@context, @current_user, :manage_grades)
|
|
unless @context.allows_gradebook_uploads?
|
|
flash[:error] = t('errors.not_allowed', "This course does not allow score uploads.")
|
|
redirect_to named_context_url(@context, :context_assignment_url, @assignment.id)
|
|
return
|
|
end
|
|
@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 submission",
|
|
:other => "Files and comments created for %{count} submissions" },
|
|
:count => @comments.length)
|
|
end
|
|
|
|
def speed_grader
|
|
if !@context.allows_speed_grader?
|
|
flash[:notice] = t(:speed_grader_disabled, 'SpeedGrader is disabled for this course')
|
|
return redirect_to(course_gradebook_path(@context))
|
|
end
|
|
|
|
return unless authorized_action(@context, @current_user, [:manage_grades, :view_all_grades])
|
|
|
|
@assignment = @context.assignments.active.find(params[:assignment_id])
|
|
|
|
unless @assignment.can_view_speed_grader?(@current_user)
|
|
flash[:notice] = t('The maximum number of graders for this assignment has been reached.')
|
|
return redirect_to(course_gradebook_path(@context))
|
|
end
|
|
|
|
if @assignment.unpublished?
|
|
flash[:notice] = t(:speedgrader_enabled_only_for_published_content,
|
|
'SpeedGrader is enabled only for published content.')
|
|
return redirect_to polymorphic_url([@context, @assignment])
|
|
end
|
|
|
|
grading_role = if moderated_grading_enabled_and_no_grades_published?
|
|
if @assignment.permits_moderation?(@current_user)
|
|
:moderator
|
|
else
|
|
:provisional_grader
|
|
end
|
|
else
|
|
:grader
|
|
end
|
|
|
|
if @assignment.moderated_grading? && !@assignment.user_is_moderation_grader?(@current_user)
|
|
@assignment.create_moderation_grader(@current_user, occupy_slot: false)
|
|
end
|
|
|
|
@can_comment_on_submission = !@context.completed? && !@context_enrollment.try(:completed?)
|
|
@disable_unmute_assignment = @assignment.muted && !@assignment.grades_published?
|
|
respond_to do |format|
|
|
|
|
format.html do
|
|
rubric = @assignment&.rubric_association&.rubric
|
|
@headers = false
|
|
@outer_frame = true
|
|
log_asset_access([ "speed_grader", @context ], "grades", "other")
|
|
env = {
|
|
CONTEXT_ACTION_SOURCE: :speed_grader,
|
|
settings_url: speed_grader_settings_course_gradebook_path,
|
|
new_gradebook_enabled: new_gradebook_enabled?,
|
|
force_anonymous_grading: force_anonymous_grading?(@assignment),
|
|
grading_role: grading_role,
|
|
grading_type: @assignment.grading_type,
|
|
lti_retrieve_url: retrieve_course_external_tools_url(
|
|
@context.id, assignment_id: @assignment.id, display: 'borderless'
|
|
),
|
|
course_id: @context.id,
|
|
assignment_id: @assignment.id,
|
|
assignment_title: @assignment.title,
|
|
rubric: rubric ? rubric_json(rubric, @current_user, session, style: 'full') : nil,
|
|
nonScoringRubrics: @domain_root_account.feature_enabled?(:non_scoring_rubrics),
|
|
outcome_extra_credit_enabled: @context.feature_enabled?(:outcome_extra_credit),
|
|
can_comment_on_submission: @can_comment_on_submission,
|
|
show_help_menu_item: show_help_link?,
|
|
help_url: help_link_url,
|
|
outcome_proficiency: outcome_proficiency
|
|
}
|
|
if grading_role == :moderator
|
|
env[:provisional_copy_url] = api_v1_copy_to_final_mark_path(@context.id, @assignment.id, "{{provisional_grade_id}}")
|
|
env[:provisional_select_url] = api_v1_select_provisional_grade_path(@context.id, @assignment.id, "{{provisional_grade_id}}")
|
|
end
|
|
|
|
if new_gradebook_enabled?
|
|
env[:selected_section_id] = gradebook_settings.dig(@context.id, 'filter_rows_by', 'section_id')
|
|
end
|
|
|
|
if @assignment.quiz
|
|
env[:quiz_history_url] = course_quiz_history_path @context.id,
|
|
@assignment.quiz.id,
|
|
:user_id => "{{user_id}}"
|
|
end
|
|
append_sis_data(env)
|
|
js_env(env)
|
|
|
|
render :speed_grader, locals: { anonymize_students: @assignment.anonymize_students? }
|
|
end
|
|
|
|
format.json do
|
|
render json: Assignment::SpeedGrader.new(
|
|
@assignment,
|
|
@current_user,
|
|
avatars: service_enabled?(:avatars),
|
|
grading_role: grading_role
|
|
).json
|
|
end
|
|
end
|
|
end
|
|
|
|
def speed_grader_settings
|
|
if params[:enable_speedgrader_grade_by_question]
|
|
grade_by_question = value_to_boolean(params[:enable_speedgrader_grade_by_question])
|
|
@current_user.preferences[:enable_speedgrader_grade_by_question] = grade_by_question
|
|
@current_user.save!
|
|
end
|
|
|
|
if params[:selected_section_id]
|
|
section_to_show = if params[:selected_section_id] == 'all'
|
|
nil
|
|
elsif @context.active_course_sections.exists?(id: params[:selected_section_id])
|
|
params[:selected_section_id]
|
|
end
|
|
|
|
gradebook_settings.deep_merge!({
|
|
@context.id => {
|
|
'filter_rows_by' => {
|
|
'section_id' => section_to_show
|
|
}
|
|
}
|
|
})
|
|
@current_user.save!
|
|
end
|
|
|
|
head :ok
|
|
end
|
|
|
|
def blank_submission
|
|
@headers = false
|
|
end
|
|
|
|
def change_gradebook_column_size
|
|
if authorized_action(@context, @current_user, :manage_grades)
|
|
unless @current_user.preferences.key?(:gradebook_column_size)
|
|
@current_user.preferences[:gradebook_column_size] = {}
|
|
end
|
|
|
|
@current_user.preferences[:gradebook_column_size][params[:column_id]] = params[:column_size]
|
|
@current_user.save!
|
|
render json: nil
|
|
end
|
|
end
|
|
|
|
def save_gradebook_column_order
|
|
if authorized_action(@context, @current_user, :manage_grades)
|
|
unless @current_user.preferences.key?(:gradebook_column_order)
|
|
@current_user.preferences[:gradebook_column_order] = {}
|
|
end
|
|
|
|
@current_user.preferences[:gradebook_column_order][@context.id] = params[:column_order].to_unsafe_h
|
|
@current_user.save!
|
|
render json: nil
|
|
end
|
|
end
|
|
|
|
def user_ids
|
|
return unless authorized_action(@context, @current_user, [:manage_grades, :view_all_grades])
|
|
|
|
gradebook_user_ids = GradebookUserIds.new(@context, @current_user)
|
|
render json: { user_ids: gradebook_user_ids.user_ids }
|
|
end
|
|
|
|
def grading_period_assignments
|
|
return unless authorized_action(@context, @current_user, [:manage_grades, :view_all_grades])
|
|
|
|
grading_period_assignments = GradebookGradingPeriodAssignments.new(@context, gradebook_settings)
|
|
render json: { grading_period_assignments: grading_period_assignments.to_h }
|
|
end
|
|
|
|
def change_gradebook_version
|
|
@current_user.preferences[:gradebook_version] = params[:version]
|
|
@current_user.save!
|
|
redirect_to polymorphic_url([@context, 'gradebook'])
|
|
end
|
|
|
|
def visible_modules?
|
|
@visible_modules ||= @context.modules_visible_to(@current_user).any?
|
|
end
|
|
helper_method :visible_modules?
|
|
|
|
def multiple_sections?
|
|
@multiple_sections ||= @context.multiple_sections?
|
|
end
|
|
helper_method :multiple_sections?
|
|
|
|
def multiple_assignment_groups?
|
|
@multiple_assignment_groups ||= @context.assignment_groups.many?
|
|
end
|
|
helper_method :multiple_assignment_groups?
|
|
|
|
private
|
|
|
|
def outcome_proficiency
|
|
if @context.root_account.feature_enabled?(:non_scoring_rubrics)
|
|
@context.account.resolved_outcome_proficiency&.as_json
|
|
end
|
|
end
|
|
|
|
def new_gradebook_env
|
|
graded_late_submissions_exist = @context.submissions.graded.late.exists?
|
|
|
|
{
|
|
GRADEBOOK_OPTIONS: {
|
|
colors: gradebook_settings.fetch(:colors, {}),
|
|
graded_late_submissions_exist: graded_late_submissions_exist,
|
|
grading_schemes: GradingStandard.for(@context).as_json(include_root: false),
|
|
gradezilla: true,
|
|
new_gradebook_development_enabled: new_gradebook_development_enabled?,
|
|
late_policy: @context.late_policy.as_json(include_root: false)
|
|
}
|
|
}
|
|
end
|
|
|
|
def gradebook_version
|
|
# params[:version] is a development-only convenience for engineers.
|
|
# This param should never be used outside of development.
|
|
if Rails.env.development? && params.include?(:version)
|
|
params[:version]
|
|
else
|
|
@current_user.preferred_gradebook_version
|
|
end
|
|
end
|
|
|
|
def new_gradebook_enabled?
|
|
# params[:new_gradebook] is a development-only convenience for engineers.
|
|
# This param should never be used outside of development.
|
|
if Rails.env.development? && params.include?(:new_gradebook)
|
|
params[:new_gradebook] == "true"
|
|
else
|
|
@context.feature_enabled?(:new_gradebook)
|
|
end
|
|
end
|
|
|
|
def new_gradebook_development_enabled?
|
|
# params[:new_gradebook_development] is a development-only convenience for engineers.
|
|
# This param should never be used outside of development.
|
|
if Rails.env.development? && params.include?(:new_gradebook_development)
|
|
params[:new_gradebook_development] == "true"
|
|
else
|
|
!!ENV['GRADEBOOK_DEVELOPMENT']
|
|
end
|
|
end
|
|
|
|
def render_gradebook
|
|
if ["srgb", "individual"].include?(gradebook_version)
|
|
render_individual_gradebook
|
|
else
|
|
render_default_gradebook
|
|
end
|
|
end
|
|
|
|
def render_default_gradebook
|
|
if new_gradebook_enabled?
|
|
render "gradebooks/gradezilla/gradebook"
|
|
else
|
|
render :gradebook
|
|
end
|
|
end
|
|
|
|
def render_individual_gradebook
|
|
if new_gradebook_enabled?
|
|
render "gradebooks/gradezilla/individual"
|
|
else
|
|
render :screenreader
|
|
end
|
|
end
|
|
|
|
def percentage(weight)
|
|
I18n.n(weight, percentage: true)
|
|
end
|
|
|
|
def points_possible(weight, options)
|
|
return unless options[:weighting]
|
|
return t("%{weight} of Final", weight: percentage(weight)) if options[:out_of_final]
|
|
percentage(weight)
|
|
end
|
|
|
|
def aggregate_by_grading_period?
|
|
view_all_grading_periods? && @context.weighted_grading_periods?
|
|
end
|
|
|
|
def aggregate_assignments
|
|
if aggregate_by_grading_period?
|
|
@presenter.periods_assignments = periods_as_assignments(@presenter.grading_periods,
|
|
out_of_final: true,
|
|
exclude_total: @exclude_total)
|
|
else
|
|
@presenter.groups_assignments = groups_as_assignments(@presenter.groups,
|
|
out_of_final: true,
|
|
exclude_total: @exclude_total)
|
|
end
|
|
end
|
|
|
|
def groups_as_assignments(groups=nil, options = {})
|
|
as_assignments(
|
|
groups || @context.assignment_groups.active,
|
|
options.merge!(weighting: @context.group_weighting_scheme == 'percent')
|
|
) { |group| group_as_assignment(group, options) }
|
|
end
|
|
|
|
def periods_as_assignments(periods=nil, options = {})
|
|
as_assignments(
|
|
periods || @context.grading_periods.active,
|
|
options.merge!(weighting: @context.weighted_grading_periods?)
|
|
) { |period| period_as_assignment(period, options) }
|
|
end
|
|
|
|
def as_assignments(objects=nil, options={})
|
|
fakes = []
|
|
fakes.concat(objects.map { |object| yield(object) }) if objects && block_given?
|
|
fakes << total_as_assignment(options) unless options[:exclude_total]
|
|
fakes
|
|
end
|
|
|
|
def group_as_assignment(group, options)
|
|
OpenObject.build('assignment',
|
|
id: "group-#{group.id}",
|
|
rules: group.rules,
|
|
title: group.name,
|
|
points_possible: points_possible(group.group_weight, options),
|
|
hard_coded: true,
|
|
special_class: 'group_total',
|
|
assignment_group_id: group.id,
|
|
group_weight: group.group_weight,
|
|
asset_string: "group_total_#{group.id}")
|
|
end
|
|
|
|
def period_as_assignment(period, options)
|
|
OpenObject.build('assignment',
|
|
id: "period-#{period.id}",
|
|
rules: [],
|
|
title: period.title,
|
|
points_possible: points_possible(period.weight, options),
|
|
hard_coded: true,
|
|
special_class: 'group_total',
|
|
assignment_group_id: period.id,
|
|
group_weight: period.weight,
|
|
asset_string: "period_total_#{period.id}")
|
|
end
|
|
|
|
def total_as_assignment(options = {})
|
|
OpenObject.build('assignment',
|
|
id: 'final-grade',
|
|
title: t('Total'),
|
|
points_possible: (options[:out_of_final] ? '' : percentage(100)),
|
|
hard_coded: true,
|
|
special_class: 'final_grade',
|
|
asset_string: "final_grade_column")
|
|
end
|
|
|
|
def moderated_grading_enabled_and_no_grades_published?
|
|
@assignment.moderated_grading? && !@assignment.grades_published?
|
|
end
|
|
|
|
def exclude_total?(context)
|
|
return true if context.hide_final_grades
|
|
return false unless grading_periods? && view_all_grading_periods?
|
|
|
|
grading_period_group.present? && !grading_period_group.display_totals_for_all_grading_periods?
|
|
end
|
|
|
|
def grade_summary_presenter
|
|
options = presenter_options
|
|
if options.key?(:grading_period_id)
|
|
GradingPeriodGradeSummaryPresenter.new(@context, @current_user, params[:id], options)
|
|
else
|
|
GradeSummaryPresenter.new(@context, @current_user, params[:id], options)
|
|
end
|
|
end
|
|
|
|
def presenter_options
|
|
options = {}
|
|
return options unless @context.present?
|
|
|
|
if @current_grading_period_id.present? && !view_all_grading_periods? && grading_periods?
|
|
options[:grading_period_id] = @current_grading_period_id
|
|
end
|
|
|
|
return options unless @current_user.present?
|
|
|
|
order_preferences = @current_user.preferences[:course_grades_assignment_order]
|
|
saved_order = order_preferences && order_preferences[@context.id]
|
|
options[:assignment_order] = saved_order if saved_order.present?
|
|
options
|
|
end
|
|
|
|
def custom_course_users_api_url(include_concluded: false, include_inactive: false, exclude_states: false, per_page:)
|
|
state = %w[active invited]
|
|
state << 'completed' if include_concluded
|
|
state << 'inactive' if include_inactive
|
|
state = [] if exclude_states
|
|
|
|
api_v1_course_users_url(
|
|
@context,
|
|
include: %i[avatar_url group_ids enrollments],
|
|
enrollment_type: %w[student student_view],
|
|
enrollment_state: state,
|
|
per_page: per_page
|
|
)
|
|
end
|
|
|
|
def custom_course_enrollments_api_url(include_concluded: false, include_inactive: false, per_page:)
|
|
state = %w[active invited]
|
|
state << 'completed' if include_concluded
|
|
state << 'inactive' if include_inactive
|
|
api_v1_course_enrollments_url(
|
|
@context,
|
|
include: %i[avatar_url group_ids],
|
|
type: %w[StudentEnrollment StudentViewEnrollment],
|
|
state: state,
|
|
per_page: per_page
|
|
)
|
|
end
|
|
|
|
def gradebook_settings
|
|
@current_user.preferences.fetch(:gradebook_settings, {})
|
|
end
|
|
|
|
def courses_with_grades_json
|
|
courses = @presenter.courses_with_grades
|
|
courses << @context if courses.empty?
|
|
|
|
courses.map do |course|
|
|
grading_period_set_id = GradingPeriodGroup.for_course(course)&.id
|
|
|
|
{
|
|
id: course.id,
|
|
nickname: course.nickname_for(@current_user),
|
|
url: context_url(course, :context_grades_url),
|
|
grading_period_set_id: grading_period_set_id.try(:to_s)
|
|
}
|
|
end.as_json
|
|
end
|
|
|
|
def populate_user_ids(submissions)
|
|
anonymous_ids = submissions.map {|submission| submission.fetch(:anonymous_id)}
|
|
submission_ids_map = Submission.select(:user_id, :anonymous_id).
|
|
where(assignment: @context.assignments, anonymous_id: anonymous_ids).
|
|
index_by(&:anonymous_id)
|
|
|
|
# merge back into submissions
|
|
submissions.map do |submission|
|
|
submission[:user_id] = submission_ids_map[submission.fetch(:anonymous_id)].user_id
|
|
submission
|
|
end
|
|
end
|
|
|
|
def apply_provisional_grade_filters!(submissions:, final:)
|
|
preloaded_grades = ModeratedGrading::ProvisionalGrade.where(submission: submissions)
|
|
grades_by_submission_id = preloaded_grades.group_by(&:submission_id)
|
|
|
|
submissions.each do |submission|
|
|
provisional_grade = submission.provisional_grade(
|
|
@current_user,
|
|
preloaded_grades: grades_by_submission_id,
|
|
final: final,
|
|
default_to_null_grade: false
|
|
)
|
|
submission.apply_provisional_grade_filter!(provisional_grade) if provisional_grade
|
|
end
|
|
end
|
|
end
|