2013-12-18 08:44:23 +08:00
|
|
|
#
|
2017-04-28 04:05:04 +08:00
|
|
|
# Copyright (C) 2013 - present Instructure, Inc.
|
2013-12-18 08:44:23 +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/>.
|
|
|
|
#
|
|
|
|
|
|
|
|
module Outcomes
|
|
|
|
module ResultAnalytics
|
|
|
|
|
2014-01-03 06:27:40 +08:00
|
|
|
Rollup = Struct.new(:context, :scores)
|
2015-01-29 07:06:12 +08:00
|
|
|
Result = Struct.new(:learning_outcome, :score, :count)
|
2013-12-18 08:44:23 +08:00
|
|
|
|
|
|
|
# Public: Queries learning_outcome_results for rollup.
|
|
|
|
#
|
|
|
|
# opts - The options for the query. In a later version of ruby, these would
|
|
|
|
# be named parameters.
|
|
|
|
# :users - The users to lookup results for (required)
|
|
|
|
# :context - The context to lookup results for (required)
|
|
|
|
# :outcomes - The outcomes to lookup results for (required)
|
|
|
|
#
|
|
|
|
# Returns a relation of the results, suitably ordered.
|
|
|
|
def find_outcome_results(opts)
|
|
|
|
required_opts = [:users, :context, :outcomes]
|
|
|
|
required_opts.each { |p| raise "#{p} option is required" unless opts[p] }
|
|
|
|
users, context, outcomes = opts.values_at(*required_opts)
|
2015-08-29 05:13:57 +08:00
|
|
|
order_results_for_rollup LearningOutcomeResult.active.where(
|
2013-12-18 08:44:23 +08:00
|
|
|
context_code: context.asset_string,
|
|
|
|
user_id: users.map(&:id),
|
2015-04-28 05:10:59 +08:00
|
|
|
learning_outcome_id: outcomes.map(&:id)
|
2013-12-18 08:44:23 +08:00
|
|
|
)
|
|
|
|
end
|
|
|
|
|
|
|
|
# Internal: Add an order clause to a relation so results are returned in an
|
|
|
|
# order suitable for rollup calculations.
|
|
|
|
#
|
|
|
|
# relation - The relation to add an order clause to.
|
|
|
|
#
|
|
|
|
# Returns the resulting relation
|
|
|
|
def order_results_for_rollup(relation)
|
|
|
|
relation.order(:user_id, :learning_outcome_id)
|
|
|
|
end
|
|
|
|
|
|
|
|
# Public: Generates a rollup of each outcome result for each user.
|
|
|
|
#
|
|
|
|
# results - An Enumeration of properly sorted LearningOutcomeResult objects.
|
|
|
|
# The results should be sorted by user id and then by outcome id.
|
|
|
|
#
|
2014-01-01 09:01:13 +08:00
|
|
|
# users - (Optional) Ensure rollups are included for users in this list.
|
|
|
|
# A listed user with no results will have an empty score array.
|
|
|
|
#
|
2014-01-03 06:27:40 +08:00
|
|
|
# Returns an Array of Rollup objects.
|
|
|
|
def outcome_results_rollups(results, users=[])
|
2015-12-19 05:47:46 +08:00
|
|
|
ActiveRecord::Associations::Preloader.new.preload(results, :learning_outcome)
|
2014-01-01 09:01:13 +08:00
|
|
|
rollups = results.chunk(&:user_id).map do |_, user_results|
|
2014-01-03 06:27:40 +08:00
|
|
|
Rollup.new(user_results.first.user, rollup_user_results(user_results))
|
2013-12-18 08:44:23 +08:00
|
|
|
end
|
2014-01-03 06:27:40 +08:00
|
|
|
add_missing_user_rollups(rollups, users)
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
|
|
# Public: Calculates an average rollup for the specified results
|
|
|
|
#
|
|
|
|
# results - An Enumeration of properly sorted LearningOutcomeResult objects.
|
|
|
|
# context - The context to use for the resulting rollup.
|
|
|
|
#
|
|
|
|
# Returns a Rollup.
|
|
|
|
def aggregate_outcome_results_rollup(results, context)
|
|
|
|
rollups = outcome_results_rollups(results)
|
|
|
|
rollup_scores = rollups.map(&:scores).flatten
|
2015-01-29 07:06:12 +08:00
|
|
|
outcome_results = rollup_scores.group_by(&:outcome).values
|
|
|
|
aggregate_results = outcome_results.map do |scores|
|
|
|
|
scores.map{|score| Result.new(score.outcome, score.score, score.count)}
|
2014-01-03 06:27:40 +08:00
|
|
|
end
|
2015-05-01 01:27:50 +08:00
|
|
|
aggregate_rollups = aggregate_results.map do |result|
|
|
|
|
RollupScore.new(result,{aggregate_score: true})
|
|
|
|
end
|
2015-01-29 07:06:12 +08:00
|
|
|
Rollup.new(context, aggregate_rollups)
|
2013-12-18 08:44:23 +08:00
|
|
|
end
|
|
|
|
|
|
|
|
# Internal: Generates a rollup of the outcome results, Assuming all the
|
|
|
|
# results are for the same user.
|
|
|
|
#
|
|
|
|
# user_results - An Enumeration of LearningOutcomeResult objects for a user
|
|
|
|
# sorted by outcome id.
|
|
|
|
#
|
2014-01-03 06:27:40 +08:00
|
|
|
# Returns an Array of RollupScore objects
|
2013-12-18 08:44:23 +08:00
|
|
|
def rollup_user_results(user_results)
|
fix outcome calcs for mix of assignments & quizzes
fixes OUT-460
test plan:
- create 2 outcomes, one with decaying average and one w/ n_mastery
- attach each outcome to an assignment and a quiz
- it's not reccomended to use exactly 5 questions for quiz testing
since this has the potential to obfuscate possible calc errors
- log in as a student and submit to the assignment/take the quiz
- as the teacher/admin, asses the outcome on the assignment in
speedgrader. It's reccomended to get a high score on at least
one quiz that's being tested, in order to ensure the mastery
score on the result does not exceed the max possible score
for the outcome
- view the students outcome scores in the lmgb and student lmgb
to confirm the score's accuracy
try various scores, but here's an initial example assuming
Outcome A is decaying avg, and Outcome B is n_mastery
Outcome A
- Attach Outcome A to two assignments and two quizzes
- Submit as the student, first to the two assignments,
scoring a 3.0 and a 2.0, then on a quiz in which you
get 90% on the aligned bank
- on the final/most recent bank, score a 40%
- the score for the Outcome should be 2.41
Outcome B
- Attach Outcome B to two assignments and three quizzes
- Submit as the student. Order does not matter, but ensure
scores of 3.0 and 3.5 on the assignments, and 20%, 50%,
and 80% on the quizzes.
- the score for the Outcome should be 3.5
Change-Id: If99d8ab6a3791137e407ab43fd8af2c0d69058d5
Reviewed-on: https://gerrit.instructure.com/93333
Reviewed-by: Augusto Callejas <acallejas@instructure.com>
Reviewed-by: Michael Brewer-Davis <mbd@instructure.com>
Tested-by: Jenkins
QA-Review: Cemal Aktas <caktas@instructure.com>
Product-Review: McCall Smith <mcsmith@instructure.com>
2016-10-21 04:39:59 +08:00
|
|
|
filtered_results = user_results.select{|r| !r.score.nil?}
|
|
|
|
filtered_results.group_by(&:learning_outcome_id).map do |_, outcome_results|
|
2015-01-29 07:06:12 +08:00
|
|
|
RollupScore.new(outcome_results)
|
2013-12-18 08:44:23 +08:00
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2014-01-03 06:27:40 +08:00
|
|
|
# Internal: Adds rollups rows for users that did not have any results
|
|
|
|
#
|
|
|
|
# rollups - The list of rollup objects based on existing results.
|
|
|
|
# users - The list of User objects that should have results.
|
|
|
|
#
|
|
|
|
# Returns the modified rollups list. Users without rollups will have a
|
|
|
|
# rollup row with an empty scores array.
|
|
|
|
def add_missing_user_rollups(rollups, users)
|
|
|
|
missing_users = users - rollups.map(&:context)
|
|
|
|
rollups + missing_users.map { |u| Rollup.new(u, []) }
|
|
|
|
end
|
|
|
|
|
2013-12-18 08:44:23 +08:00
|
|
|
class << self
|
|
|
|
include ResultAnalytics
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|