Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 42 additions & 0 deletions app/controllers/components/course/gradebook_component.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
# frozen_string_literal: true
class Course::GradebookComponent < SimpleDelegator
include Course::ControllerComponentHost::Component

def self.display_name
'Gradebook'
end

def sidebar_items
main_sidebar_items + settings_sidebar_items
end

private

def main_sidebar_items
return [] unless can?(:read_gradebook, current_course)

[
{
key: self.class.key,
icon: :gradebook,
title: I18n.t('course.gradebook.component.sidebar_title'),
type: :normal,
weight: 9,
path: course_gradebook_path(current_course)
}
]
end

def settings_sidebar_items
return [] unless can?(:manage_gradebook_settings, current_course)

[
{
key: self.class.key,
type: :settings,
weight: 14,
path: course_admin_gradebook_path(current_course)
}
]
end
end
28 changes: 28 additions & 0 deletions app/controllers/course/admin/gradebook_settings_controller.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
# frozen_string_literal: true
class Course::Admin::GradebookSettingsController < Course::Admin::Controller
def edit
respond_to(&:json)
end

def update
if @settings.update(gradebook_settings_params) && current_course.save
render 'edit'
else
render json: { errors: @settings.errors }, status: :bad_request
end
end

private

def gradebook_settings_params
params.require(:settings_gradebook_component).permit(:weighted_view_enabled)
end

def component
current_component_host[:course_gradebook_component]
end

def authorize_admin
authorize! :manage_gradebook_settings, current_course
end
end
53 changes: 53 additions & 0 deletions app/controllers/course/external_assessment_imports_controller.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
# frozen_string_literal: true
class Course::ExternalAssessmentImportsController < Course::ComponentController
Service = Course::Gradebook::ExternalAssessmentImportService

def preview
authorize! :manage_gradebook_weights, current_course
@result = build_service.preview
render 'preview'
rescue Service::ImportError => e
render json: { errors: e.payload }, status: :unprocessable_entity
end

def create
authorize! :manage_gradebook_weights, current_course
@summary = build_service.commit(on_conflict: import_params[:onConflict])
render 'create'
rescue Service::ImportError => e
render json: { errors: e.payload }, status: :unprocessable_entity
end

private

def component
current_component_host[:course_gradebook_component]
end

def build_service
permitted_params = import_params
weighted_view_enabled = gradebook_settings.weighted_view_enabled
Service.new(
course: current_course,
actor: current_user,
components: permitted_params[:components].map do |c|
{ name: c[:name],
weightage: weighted_view_enabled ? c[:weightage].to_i : 0,
maximum_grade: c[:maximumGrade].to_f }
end,
identifier_mode: permitted_params[:identifierMode],
csv_data: permitted_params[:csvData]
)
end

def gradebook_settings
@gradebook_settings ||= Course::Settings::GradebookComponent.new(component)
end

def import_params
@import_params ||= params.slice(:identifierMode, :csvData, :onConflict, :components).permit(
:identifierMode, :csvData, :onConflict,
components: [:name, :weightage, :maximumGrade]
)
end
end
119 changes: 119 additions & 0 deletions app/controllers/course/external_assessments_controller.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
# frozen_string_literal: true
class Course::ExternalAssessmentsController < Course::ComponentController
before_action :load_external_assessment, only: [:update, :destroy, :grades]

def create
authorize! :manage_gradebook_weights, current_course
@weighted_view_enabled = gradebook_settings.weighted_view_enabled
@external_assessment = Course::ExternalAssessment.create_for_course!(
course: current_course,
title: create_params[:title],
maximum_grade: create_params[:maximumGrade],
weight: create_weight,
floor_at_zero: bound_flag(:floorAtZero, default: true),
cap_at_maximum: bound_flag(:capAtMaximum, default: true)
)
render 'create'
rescue ActiveRecord::RecordInvalid => e
render json: { errors: { base: e.message } }, status: :unprocessable_entity
end

def update
authorize! :manage_gradebook_weights, current_course
@weighted_view_enabled = gradebook_settings.weighted_view_enabled
@external_assessment.update!(update_params_attrs)
update_weight if @weighted_view_enabled && params.key?(:weight)
render 'update'
rescue ActiveRecord::RecordInvalid => e
render json: { errors: { base: e.message } }, status: :unprocessable_entity
end

def destroy
authorize! :manage_gradebook_weights, current_course
@external_assessment.destroy!
head :ok
end

def reorder
authorize! :manage_gradebook_weights, current_course
Course::ExternalAssessment.reorder!(course: current_course, ordered_ids: reorder_params)
head :ok
rescue ArgumentError
head :unprocessable_entity
end

def grades
authorize! :grade, @external_assessment
# The gradebook keys students by user_id (see index/update_grade jbuilders), so the
# `studentId` param is a user_id, not a course_user PK.
course_user = current_course.course_users.find_by!(user_id: grade_params[:studentId])
@grade = @external_assessment.external_assessment_grades.
find_or_initialize_by(course_user: course_user)
@grade.grade = normalized_grade(grade_params[:grade])
@grade.save!
render 'update_grade'
rescue ActiveRecord::RecordNotUnique
retry
rescue ActiveRecord::RecordNotFound
head :not_found
rescue ActiveRecord::RecordInvalid => e
render json: { errors: { base: e.message } }, status: :unprocessable_entity
end

private

def component
current_component_host[:course_gradebook_component]
end

def gradebook_settings
@gradebook_settings ||= Course::Settings::GradebookComponent.new(component)
end

def load_external_assessment
@external_assessment = Course::ExternalAssessment.for_course(current_course).find(params[:id])
rescue ActiveRecord::RecordNotFound
head :not_found
end

def create_params
params.permit(:title, :maximumGrade, :weight, :floorAtZero, :capAtMaximum)
end

def create_weight
@weighted_view_enabled ? (create_params[:weight].presence || 0).to_f : 0
end

def update_weight
@external_assessment.gradebook_contribution&.update!(weight: (params[:weight].presence || 0).to_f)
end

def update_params_attrs
attrs = {}
attrs[:title] = params[:title] if params.key?(:title)
attrs[:maximum_grade] = params[:maximumGrade] if params.key?(:maximumGrade)
attrs[:floor_at_zero] = bound_flag(:floorAtZero, default: true) if params.key?(:floorAtZero)
attrs[:cap_at_maximum] = bound_flag(:capAtMaximum, default: true) if params.key?(:capAtMaximum)
attrs
end

# Coerce a string/bool HTTP param into a Ruby boolean (defaults when absent).
def bound_flag(key, default:)
return default unless params.key?(key)

ActiveRecord::Type::Boolean.new.cast(params[key])
end

def grade_params
params.permit(:studentId, :grade)
end

# Blank cell clears the grade to null (ungraded), never zero (decision #7).
def normalized_grade(value)
value.blank? ? nil : value
end

def reorder_params
params.require(:orderedIds).map(&:to_i)
end
end
109 changes: 109 additions & 0 deletions app/controllers/course/gradebook_controller.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
# frozen_string_literal: true
class Course::GradebookController < Course::ComponentController
before_action :authorize_read_gradebook!

def index
respond_to do |format|
format.json do
@weighted_view_enabled = @settings.weighted_view_enabled
@published_assessments = fetch_published_assessments
@categories, @tabs = fetch_categories_and_tabs
@students = fetch_students
assessment_ids = @published_assessments.pluck(:id)
load_weighted_view_contributions(assessment_ids) if @weighted_view_enabled
@assessment_max_grades = Course::Assessment.max_grades(assessment_ids)
@submissions = Course::Assessment::Submission.grade_summary(
student_ids: @students.map(&:user_id),
assessment_ids: assessment_ids
)
load_externals
end
end
end

def update_weights
authorize! :manage_gradebook_weights, current_course
updates = update_weights_params[:weights].map { |entry| parse_weight_entry(entry) }
Course::Gradebook::Contribution.bulk_update(course: current_course, updates: updates)
render json: { weights: serialize_weight_updates(updates) }
rescue ActiveRecord::RecordInvalid, ActiveRecord::RecordNotFound => e
render json: { errors: { base: e.message } }, status: :unprocessable_entity
end

private

def authorize_read_gradebook!
authorize! :read_gradebook, current_course
end

def load_weighted_view_contributions(assessment_ids)
@tab_contributions = Course::Gradebook::Contribution.
where(tab_id: @tabs.map(&:id)).index_by(&:tab_id)
@assessment_contributions = Course::Gradebook::AssessmentContribution.
where(assessment_id: assessment_ids).index_by(&:assessment_id)
end

def parse_weight_entry(entry)
{
tab_id: entry[:tabId].to_i,
weight: entry[:weight].to_f,
weight_mode: entry[:weightMode] || 'equal',
excluded_assessment_ids: (entry[:excludedAssessmentIds] || []).map(&:to_i),
assessment_weights: (entry[:assessmentWeights] || []).map do |aw|
{ assessment_id: aw[:assessmentId].to_i, weight: aw[:weight].to_f }
end
}
end

def update_weights_params
params.permit(
weights: [:tabId, :weight, :weightMode,
excludedAssessmentIds: [], assessmentWeights: [:assessmentId, :weight]]
)
end

def serialize_weight_updates(updates)
updates.map do |u|
entry = { tabId: u[:tab_id], weight: u[:weight], weightMode: u[:weight_mode].to_s,
excludedAssessmentIds: u[:excluded_assessment_ids] }
if u[:weight_mode].to_s == 'custom'
entry[:assessmentWeights] = u[:assessment_weights].map do |aw|
{ assessmentId: aw[:assessment_id], weight: aw[:weight] }
end
end
entry
end
end

def component
current_component_host[:course_gradebook_component]
end

def fetch_categories_and_tabs
tabs = @published_assessments.map(&:tab).uniq(&:id)
[tabs.map(&:category).uniq(&:id), tabs]
end

def load_externals
@external_assessments = Course::ExternalAssessment.for_course(current_course).order(:position).
includes(:gradebook_contribution, external_assessment_grades: :course_user).to_a
@external_grades = @external_assessments.flat_map(&:external_assessment_grades)
@external_contributions = @external_assessments.
index_by(&:id).
transform_values(&:gradebook_contribution)
end

def fetch_students
current_course.levels.to_a
current_course.course_users.students.without_phantom_users.
calculated(:experience_points).includes(user: :emails).to_a
end

def fetch_published_assessments
current_course.assessments.
published.
includes(tab: :category).
joins(tab: :category).
reorder('course_assessment_categories.weight, course_assessment_tabs.weight, course_assessments.id')
end
end
7 changes: 0 additions & 7 deletions app/controllers/course/statistics/aggregate_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -47,13 +47,6 @@ def activity_get_help
@course_user_hash = current_course.course_users.index_by(&:user_id)
end

def download_score_summary
job = Course::Statistics::AssessmentsScoreSummaryDownloadJob.
perform_later(current_course, params[:assessment_ids]).job

render partial: 'jobs/submitted', locals: { job: job }
end

private

def sanitize_date_range(start_at_param, end_at_param)
Expand Down

This file was deleted.

Loading