LMGB outcome details includes all results

closes OUT-2550

test plan:
  - create more than 20 student users in a course.
    you can bulk create users by modifying the sample
    file of 10 users under "users.csv" on
    https://community.canvaslms.com/docs/DOC-12585-4214164118
    and then importing them through an account's SIS Import
    page (you may need to enable "SIS imports" on the account
    features section on its settings page).
    afterwards, add the students to a course
  - create a course outcome
  - create an assignment, aligned to the outcome using a rubric
  - as all the students, submit to the assignment
  - as a teacher, in SpeedGrader, create rubric assessments for
    all submissions, selecting different criterion ratings
  - load the LMGB page
  - hover over the outcome header, and confirm the outcome details
    summarize the correct percentages based on the outcome scores
  - navigate to the 2nd page of results
  - hover over the outcome header, and confirm the outcomes details,
    like described two steps above
  - enable the New Gradebook
  - repeat the steps above, starting at loading the LMGB page

Change-Id: I52815eb2e05a04a0ad70c9c53e13726dbc626b64
Reviewed-on: https://gerrit.instructure.com/173291
Tested-by: Jenkins
Reviewed-by: Neil Gupta <ngupta@instructure.com>
Reviewed-by: Frank Murphy III <fmurphy@instructure.com>
QA-Review: Brian Watson <bwatson@instructure.com>
Product-Review: Neil Gupta <ngupta@instructure.com>
This commit is contained in:
Augusto Callejas 2018-11-21 13:50:36 -10:00
parent b8c4c8f799
commit 4d2bedefb3
11 changed files with 84 additions and 22 deletions

View File

@ -351,8 +351,3 @@ define [
results = Grid.View.getColumnResults(grid.getData(), column)
ratings = column.outcome.ratings || []
ratings.result_count = results.length
points = _.pluck ratings, 'points'
counts = _.countBy results, (result) ->
_.find points, (x) -> result && x <= result.score
_.each ratings, (rating) ->
rating.percent = Math.round((counts[rating.points] || 0) / results.length * 100)

View File

@ -351,8 +351,3 @@ define [
results = Grid.View.getColumnResults(grid.getData(), column)
ratings = column.outcome.ratings || []
ratings.result_count = results.length
points = _.pluck ratings, 'points'
counts = _.countBy results, (result) ->
_.find points, (x) -> result && x <= result.score
_.each ratings, (rating) ->
rating.percent = Math.round((counts[rating.points] || 0) / results.length * 100)

View File

@ -281,7 +281,7 @@ define [
sortParams = "#{sortParams}&sort_outcome_id=#{sortOutcomeId}" if sortOutcomeId
sortParams = "#{sortParams}&sort_order=desc" if !@sortOrderAsc
sectionParam = if Grid.section then "&section_id=#{Grid.section}" else ""
"/api/v1/courses/#{course}/outcome_rollups?per_page=20&include[]=outcomes&include[]=users&include[]=outcome_paths#{excluding}&page=#{page}#{sortParams}#{sectionParam}"
"/api/v1/courses/#{course}/outcome_rollups?rating_percents=true&per_page=20&include[]=outcomes&include[]=users&include[]=outcome_paths#{excluding}&page=#{page}#{sortParams}#{sectionParam}"
_loadOutcomes: (page = 1) =>
exclude = if @$('#no_results_students').prop('checked') then 'missing_user_rollups' else ''

View File

@ -304,7 +304,7 @@ define [
sortParams = "#{sortParams}&sort_outcome_id=#{sortOutcomeId}" if sortOutcomeId
sortParams = "#{sortParams}&sort_order=desc" if !@sortOrderAsc
sectionParam = if Grid.section and Grid.section != "0" then "&section_id=#{Grid.section}" else ""
"/api/v1/courses/#{course}/outcome_rollups?per_page=20&include[]=outcomes&include[]=users&include[]=outcome_paths#{excluding}&page=#{page}#{sortParams}#{sectionParam}"
"/api/v1/courses/#{course}/outcome_rollups?rating_percents=true&per_page=20&include[]=outcomes&include[]=users&include[]=outcome_paths#{excluding}&page=#{page}#{sortParams}#{sectionParam}"
_loadOutcomes: (page = 1) =>
exclude = if @$('#no_results_students').prop('checked') then 'missing_user_rollups' else ''

View File

@ -336,12 +336,18 @@ class OutcomeResultsController < ApplicationController
private
def find_results(opts = {})
find_outcome_results(@current_user, users: @users, context: @context, outcomes: @outcomes, **opts)
find_outcome_results(
@current_user,
users: opts[:all_users] ? @all_users : @users,
context: @context,
outcomes: @outcomes,
**opts
)
end
def user_rollups(_opts = {})
def user_rollups(opts = {})
excludes = Api.value_to_array(params[:exclude]).uniq
@results = find_results.preload(:user)
@results = find_results(opts).preload(:user)
outcome_results_rollups(@results, @users, excludes)
end
@ -412,7 +418,9 @@ class OutcomeResultsController < ApplicationController
end
def include_outcomes
outcome_results_include_outcomes_json(@outcomes)
percents = {}
percents = rating_percents(user_rollups(all_users: true)) if params[:rating_percents] == 'true'
outcome_results_include_outcomes_json(@outcomes, percents)
end
def include_outcome_groups
@ -564,6 +572,9 @@ class OutcomeResultsController < ApplicationController
end
@users ||= users_for_outcome_context.to_a
@users.sort! {|a,b| a.id <=> b.id} unless params[:sort_by]
# cache all users, since pagination in #user_rollups_json may remove some
# when we need all users when calculating rating percents
@all_users = @users
end
def users_for_outcome_context

View File

@ -57,7 +57,10 @@ module Api::V1::Outcome
hash['points_possible'] = outcome.rubric_criterion[:points_possible]
hash['mastery_points'] = outcome.rubric_criterion[:mastery_points]
hash['ratings'] = outcome.rubric_criterion[:ratings]
hash['ratings'] = outcome.rubric_criterion[:ratings]&.clone
hash['ratings']&.each_with_index do |rating, i|
rating[:percent] = opts[:rating_percents][i] if i < opts[:rating_percents].length
end if opts[:rating_percents]
if opts[:assessed_outcomes] && outcome.context_type != "Account"
hash['assessed'] = opts[:assessed_outcomes].include?(outcome.id)
else

View File

@ -60,7 +60,7 @@ module Api::V1::OutcomeResults
# Public: Serializes outcomes in a hash that can be added to the linked hash.
#
# Returns a Hash containing serialized outcomes.
def outcome_results_include_outcomes_json(outcomes)
def outcome_results_include_outcomes_json(outcomes, percents = {})
alignment_asset_string_map = {}
outcomes.each_slice(50).each do |outcomes_slice|
ActiveRecord::Associations::Preloader.new.preload(outcomes_slice, [:context])
@ -74,7 +74,10 @@ module Api::V1::OutcomeResults
assessed_outcomes += LearningOutcomeResult.distinct.where(learning_outcome_id: outcome_ids).pluck(:learning_outcome_id)
end
outcomes.map do |o|
hash = outcome_json(o, @current_user, session, assessed_outcomes: assessed_outcomes)
hash = outcome_json(o,
@current_user, session,
assessed_outcomes: assessed_outcomes,
rating_percents: percents[o.id])
hash.merge!(alignments: alignment_asset_string_map[o.id])
hash
end

View File

@ -129,6 +129,33 @@ module Outcomes
rollups + missing_users.map { |u| Rollup.new(u, []) }
end
# Public: Gets rating percents for outcomes based on rollup
#
# Returns a hash of outcome id to array of rating percents
def rating_percents(rollups)
counts = {}
rollups.each do |rollup|
rollup.scores.each do |score|
next unless score.score
outcome = score.outcome
next unless outcome
ratings = outcome.rubric_criterion[:ratings]
next unless ratings
counts[outcome.id] = Array.new(ratings.length, 0) unless counts[outcome.id]
idx = ratings.find_index { |rating| rating[:points] <= score.score }
counts[outcome.id][idx] = counts[outcome.id][idx] + 1 if idx
end
end
counts.each {|k, v| counts[k] = to_percents(v)}
counts
end
def to_percents(count_arr)
total = count_arr.sum
return count_arr if total.zero?
count_arr.map {|v| (100.0 * v / total).round}
end
class << self
include ResultAnalytics
end

View File

@ -258,6 +258,11 @@ describe OutcomeResultsController do
format: "json"
end
it 'includes rating percents' do
json = parse_response(get_rollups(rating_percents: true, include: ['outcomes']))
expect(json['linked']['outcomes'][0]['ratings'].map { |r| r['percent'] }).to eq [50, 50]
end
context 'sorting' do
it 'should validate sort_by parameter' do
get_rollups(sort_by: 'garbage')

View File

@ -66,6 +66,10 @@ RSpec.describe "Api::V1::Outcome" do
end
context "outcome json" do
let(:opts) do
{ rating_percents: [30, 40, 30] }
end
let(:check_outcome_json) do
->(outcome) do
expect(outcome['title']).to eq(outcome_params[:title])
@ -79,17 +83,18 @@ RSpec.describe "Api::V1::Outcome" do
LearningOutcome.find(outcome['id']).updateable_rubrics?
)
expect(outcome['ratings'].length).to eq 3
expect(outcome['ratings'].map { |r| r['percent'] }).to eq [30, 40, 30]
end
end
it "returns the json for an outcome" do
check_outcome_json.call(lib.outcome_json(new_outcome(outcome_params), nil, nil))
check_outcome_json.call(lib.outcome_json(new_outcome(outcome_params), nil, nil, opts))
end
it "returns the json for multiple outcomes" do
outcomes = []
10.times{ outcomes.push(new_outcome) }
lib.outcomes_json(outcomes, nil, nil).each { |o| check_outcome_json.call(o) }
lib.outcomes_json(outcomes, nil, nil, opts).each { |o| check_outcome_json.call(o) }
end
end

View File

@ -58,7 +58,7 @@ describe Outcomes::ResultAnalytics do
# outcomes that predate the newer calculation methods
id = args[:id] || 80
method = args[:method] || "highest"
criterion = args[:criterion] || {mastery_points: 3.0}
criterion = args[:criterion] || LearningOutcome.default_rubric_criterion
MockOutcome[id, method, args[:calc_int], criterion]
end
@ -375,6 +375,24 @@ describe Outcomes::ResultAnalytics do
end
end
describe '#rating_percents' do
before do
allow_any_instance_of(ActiveRecord::Associations::Preloader).to receive(:preload)
end
it 'computes percents' do
results = [
outcome_from_score(4.0, {}),
outcome_from_score(5.0, {user: MockUser[20, 'b']}),
outcome_from_score(3.0, {user: MockUser[20, 'b']})
]
users = [MockUser[10, 'a'], MockUser[30, 'c']]
rollups = ra.outcome_results_rollups(results, users)
percents = ra.rating_percents(rollups)
expect(percents).to eq({ 80 => [50, 50, 0] })
end
end
describe "handling quiz outcome results objects" do
it "scales quiz scores to rubric score" do
o1 = MockOutcome[80, 'decaying_average', 65, {points_possible: 5}]