Updates teacher learning mastery gradebook for fancy math

fixes CNVS-11656

test plan:
- enable the teacher and student learning mastery gradebook feature flags
- go to gradebook and select the Learning Mastery tab
- outcome scores per student and aggregate totals should appear the same as
  they do on master
- hover mouse over outcome title to trigger popover
- popover should include a path to the outcome, as well as a pie graph with a
  key to the right of it that shows colors for 'exceeds expectations',
  'meets expectation', and 'does not meet expectations'
- popver should then show mastery points needed, outcome description,
  then a content box that includes the calculation method, along with
  method description/example.
- click a student name to go to student learning mastery gradebook
- all things should appear the same was as they did on
  https://gerrit.instructure.com/#/c/45777/ excluding a minor change to
  the wording for most recent assessment
- masquerade as a student and go to /grades in a course
- all popover content should still be the same as when viewing this page as a
  teacher

Change-Id: Ibe4214a4217c4fb42824e66934b49dccff13fe17
Reviewed-on: https://gerrit.instructure.com/48004
Reviewed-by: Simon Williams <simon@instructure.com>
QA-Review: Adam Stone <astone@instructure.com>
Product-Review: Hilary Scharton <hilary@instructure.com>
Tested-by: Jenkins
This commit is contained in:
Matthew Berns 2015-01-28 16:06:12 -07:00 committed by Matt Berns
parent b68984052d
commit aa8b5f6dc8
15 changed files with 373 additions and 285 deletions

View File

@ -57,8 +57,8 @@ define [
parent = @rawCollections.groups.get(link.get('outcome_group').id)
rollup = rollups[outcome.id]
outcome.set('score', rollup?.score)
outcome.set('resultTitle', rollup?.title)
outcome.set('submissionTime', rollup?.submitted_at)
outcome.set('result_title', rollup?.title)
outcome.set('submission_time', rollup?.submitted_at)
outcome.set('count', rollup?.count || 0)
outcome.group = parent
parent.get('outcomes').add(outcome)

View File

@ -392,7 +392,7 @@ define [
calculateRatingsTotals: (grid, column) ->
results = Grid.View.getColumnResults(grid.getData(), column)
ratings = column.outcome.ratings
ratings = column.outcome.ratings || []
ratings.result_count = results.length
points = _.pluck ratings, 'points'
counts = _.countBy results, (result) ->

View File

@ -0,0 +1,38 @@
define [
'i18n!outcomes'
], (I18n) ->
class CalculationMethodContent
constructor: (@model) ->
@calculation_method = @model.get('calculation_method')
@calculation_int = @model.get('calculation_int')
present: ->
@toJSON()[@calculation_method]
toJSON: ->
decaying_average:
method: I18n.t("%{recentInt}/%{remainderInt} Decaying Average", {
recentInt: @calculation_int
remainderInt: 100 - @calculation_int
})
exampleText: I18n.t("Most recent score counts as 75% of mastery weight, average of all other scores count as 25% of weight."),
exScores: "1, 3, 2, 4, 5, 3, 6",
exResult: "5.25"
n_mastery:
method: I18n.t("Achieve mastery %{count} times", {
count: @calculation_int
})
exampleText: I18n.t("Must achieve mastery at least 2 times. Scores above mastery will be averaged to calculate final score."),
exScores: "1, 3, 2, 4, 5, 3, 6",
exResult: "5.5"
latest:
method: I18n.t("Latest Score")
exampleText: I18n.t("Mastery score reflects the most recent graded assigment or quiz."),
exScores: "2, 4, 5, 3",
exResult: "3"
highest:
method: I18n.t("Highest Score")
exampleText: I18n.t("Mastery scrore reflects the highest score of a graded assignment or quiz."),
exScores: "5, 3, 4, 2",
exResult: "5"

View File

@ -1,8 +1,13 @@
define [
'underscore'
'Backbone'
], (_, {Model, Collection}) ->
'compiled/models/grade_summary/CalculationMethodContent'
], (_, {Model, Collection}, CalculationMethodContent) ->
class Outcome extends Model
defaults:
calculation_method: "highest"
initialize: ->
super
@set 'friendly_name', @get('display_name') || @get('title')
@ -40,6 +45,9 @@ define [
masteryPercent: ->
@get('mastery_points')/@get('points_possible') * 100
present: ->
_.extend({}, @toJSON(), new CalculationMethodContent(@).present())
toJSON: ->
_.extend super,
status: @status()

View File

@ -105,11 +105,15 @@ define [
my: 'center '+(if @options.verticalSide == 'bottom' then 'top' else 'bottom'),
at: 'center '+(@options.verticalSide || 'top'),
of: @trigger,
offset: if @options.verticalSide == 'bottom' then '0px 10px' else '0px -10px',
offset: "0px #{@offsetPx()}px",
within: 'body',
collision: 'flipfit '+(if @options.verticalSide then 'none' else 'flipfit')
using: using
offsetPx: ->
offset = if @options.verticalSide == 'bottom' then 10 else -10
if @options.invertOffset then (offset * -1) else offset
restoreFocus: ->
# set focus back to the previously focused item.
@previousTarget.focus() if @previousTarget and $(@previousTarget).is(':visible')

View File

@ -0,0 +1,67 @@
define [
'i18n!outcomes'
'underscore'
'Backbone'
'compiled/util/Popover'
], (I18n, _, Backbone, Popover) ->
class OutcomePopoverView extends Backbone.View
TIMEOUT_LENGTH: 50
@optionProperty 'el'
@optionProperty 'model'
@optionProperty 'template'
events:
'keydown' : 'togglePopover'
'mouseenter': 'mouseenter'
'mouseleave': 'mouseleave'
inside: false
initialize: ->
super
# Overrides
render: ->
@template(@toJSON())
# Instance methods
closePopover: (e) ->
e?.preventDefault()
return true unless @popover?
@popover.hide()
delete @popover
mouseenter: (e) =>
@openPopover(e)
@inside = true
mouseleave: (e) =>
@inside = false
setTimeout =>
return if @inside || !@popover
@closePopover()
, @TIMEOUT_LENGTH
openPopover: (e) ->
if @closePopover()
@popover = new Popover(e, @render(), {
verticalSide: 'bottom'
manualOffset: 14
})
togglePopover: (e) =>
keyPressed = @_getKey(e.keyCode)
if keyPressed == "spacebar"
@openPopover(e)
else if keyPressed == "escape"
@closePopover(e)
# Private
_getKey: (keycode) =>
keys = {
32 : "spacebar"
27 : "escape"
}
keys[keycode]

View File

@ -2,24 +2,22 @@ define [
'i18n!outcomes'
'underscore'
'Backbone'
'timezone'
'compiled/views/grade_summary/ProgressBarView'
'compiled/util/Popover'
'compiled/views/grade_summary/OutcomePopoverView'
'jst/grade_summary/outcome'
'jst/grade_summary/mastery_hover'
], (I18n, _, Backbone, tz, ProgressBarView, Popover, template, mastery_hover) ->
'jst/outcomes/outcomePopover'
], (I18n, _, Backbone, ProgressBarView, OutcomePopoverView, template, popover_template) ->
class OutcomeView extends Backbone.View
tagName: 'li'
className: 'outcome'
template: template
mastery_hover: mastery_hover
TIMEOUT_LENGTH: 50
events:
'keydown .alignment-info i' : 'togglePopover'
'mouseenter .alignment-info i': 'mouseenter'
'mouseleave .alignment-info i': 'mouseleave'
afterRender: ->
@popover = new OutcomePopoverView({
el: @$('.alignment-info i')
model: @model
template: popover_template
})
initialize: ->
super
@ -31,103 +29,9 @@ define [
statusTooltip: @statusTooltip()
progress: @progress
createPopover: (e) ->
methodContent = @getMethodContent()
attributes = {
scoreDefined: @model.scoreDefined()
score: @model.get('score')
latestTitle: @model.get('resultTitle') || I18n.t("N/A")
masteryPoints: @model.get('mastery_points')
masteryLevel: @model.status()
method: methodContent.method
exampleText: methodContent.exampleText
exScores: methodContent.exScores
exResult: methodContent.exResult
submissionTime: @getSubmissionTime(@model.get('submissionTime'))
}
popover = new Popover(e, @mastery_hover(attributes), verticalSide: 'bottom', manualOffset: 14)
popover.el.on('mouseenter', @mouseenter)
popover.el.on('mouseleave', @mouseleave)
popover.show(e)
popover
statusTooltip: ->
switch @model.status()
when 'undefined' then I18n.t 'undefined', 'Unstarted'
when 'remedial' then I18n.t 'remedial', 'Remedial'
when 'near' then I18n.t 'near', 'Near mastery'
when 'mastery' then I18n.t 'mastery', 'Mastery'
getSubmissionTime: (time) ->
if time
@model.get('submissionTime')
getMethodContent: ->
#highest for outcomes that pre-date change without a method set
#so they keep their original behavior
currentMethod = @model.get('calculation_method') || 'highest'
methodInt = @model.get('calculation_int')
switch currentMethod
when "decaying_average" then {
method: I18n.t("%{recentInt}/%{remainderInt} Decaying Average", {recentInt: methodInt, remainderInt: 100 - methodInt}),
exampleText: I18n.t("Most recent score counts as 75% of mastery weight, average of all other scores count as 25% of weight."),
exScores: "1, 3, 2, 4, 5, 3, 6",
exResult: "5.25"
}
when 'n_mastery' then {
method: I18n.t("Achieve mastery %{count} times", {count: methodInt}),
exampleText: I18n.t("Must achieve mastery at least 2 times. Scores above mastery will be averaged to calculate final score."),
exScores: "1, 3, 2, 4, 5, 3, 6",
exResult: "5.5"
}
when 'latest' then {
method: I18n.t("Latest Score"),
exampleText: I18n.t("Mastery score reflects the most recent graded assigment or quiz."),
exScores: "2, 4, 5, 3",
exResult: "3"
}
when 'highest' then {
method: I18n.t("Highest Score"),
exampleText: I18n.t("Mastery scrore reflects the highest score of a graded assignment or quiz."),
exScores: "5, 3, 4, 2",
exResult: "5"
}
togglePopover: (e) =>
keyPressed = @getKey(e.keyCode)
if keyPressed == "spacebar"
@openPopover(e)
else if keyPressed == "escape"
@closePopover(e)
openPopover: (e) =>
e.preventDefault()
if @popover
@popover.hide()
delete @popover
@popover = @createPopover(e)
closePopover: (e) =>
return unless @popover
e.preventDefault()
@popover.hide()
delete @popover
mouseenter: (e) =>
@popover = @createPopover(e) unless @popover
@inside = true
mouseleave: (e) =>
@inside = false
setTimeout =>
return if @inside || !@popover
@popover.hide()
delete @popover
, @TIMEOUT_LENGTH
getKey: (keycode) =>
keys = {
32 : "spacebar"
27 : "escape"
}
keys[keycode]

View File

@ -1,11 +1,11 @@
define [
'i18n!gradebook2'
'underscore'
'Backbone'
'compiled/util/Popover'
'compiled/models/grade_summary/Outcome'
'vendor/d3.v3'
'jst/gradebook2/outcome_popover'
], (I18n, _, {View}, Popover, d3, popover_template) ->
'jst/outcomes/outcomePopover'
], (_, {View}, Popover, Outcome, d3, popover_template) ->
class OutcomeColumnView extends View
@ -24,7 +24,8 @@ define [
createPopover: (e) ->
@totalsFn()
@pickColors()
popover = new Popover(e, @popover_template(@attributes), verticalSide: 'bottom')
attributes = new Outcome(@attributes)
popover = new Popover(e, @popover_template(attributes.present()), verticalSide: 'bottom', invertOffset: true)
popover.el.on('mouseenter', @mouseenter)
popover.el.on('mouseleave', @mouseleave)
@renderChart()
@ -45,6 +46,7 @@ define [
pickColors: ->
data = @attributes.ratings
return unless data
last = data.length - 1
mastery = @attributes.mastery_points
mastery_pos = data.indexOf(

View File

@ -0,0 +1,71 @@
#
# Copyright (C) 2011 - 2014 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 RollupScore
attr_reader :outcome_results, :outcome, :score, :count, :title, :submitted_at
def initialize(outcome_results, opts={})
@outcome_results = outcome_results
@outcome = @outcome_results.first.learning_outcome
@count = @outcome_results.size
@calculation_method = @outcome.calculation_method || "highest"
@calculation_int = @outcome.calculation_int
@score = opts[:aggregate_score] ? get_aggregate_score : calculate_results
get_latest_result unless opts[:aggregate_score]
end
#todo - do send(@calculation_method) instead of the case to streamline this more
def calculate_results
# decaying average is default for new outcomes
case @calculation_method
when 'decaying_average'
return nil if @outcome_results.length < 2
decaying_average
when 'n_mastery'
return nil if @outcome_results.length < @calculation_int
n_mastery
when 'latest'
@outcome_results.max_by{|result| result.submitted_at.to_i}.score
when 'highest'
@outcome_results.max_by{|result| result.score}.score
end
end
def n_mastery
scores = @outcome_results.map(&:score).sort.last(@calculation_int)
(scores.sum.to_f / scores.size).round(2)
end
def decaying_average
#default grading method with weight of 65 if none selected.
weight = @calculation_int || 65
scores = @outcome_results.sort_by{|result| result.submitted_at.to_i}.map(&:score)
latestWeighted = scores.pop * (0.01 * weight)
olderAvgWeighted = (scores.sum / scores.length) * (0.01 * (100 - weight)).round(2)
latestWeighted + olderAvgWeighted
end
def get_latest_result
latest_result = @outcome_results.max_by{|result| result.submitted_at.to_i}
@submitted_at = latest_result.submitted_at
@title = @submitted_at ? latest_result.title.split(", ")[1] : nil
end
def get_aggregate_score
scores = @outcome_results.map(&:score)
(scores.sum.to_f / scores.size).round(2)
end
end

View File

@ -0,0 +1,93 @@
@import 'base/environment';
.outcome-details {
position: absolute;
z-index: 1000;
background-color: #fff;
border: 1px solid #b0afaf;
box-shadow: 2px 2px 10px rgba(0, 0, 0, 0.6);
max-width: 500px;
min-width: 250px;
padding: 18px 12px;
border-radius: 8px;
font-weight: 500;
color: #4f5f6e;
.title {
font-size: 14px;
padding-bottom: 10px;
border-bottom: 1px solid #b0afaf;
margin-bottom: 10px;
}
.chart {
font-size: 11px;
text-align: center;
height: 150px
}
.ratings {
font-weight: normal;
display: inline-block;
text-align: left;
margin-bottom: 0px;
margin-top: 25px;
.rating {
list-style-type: none;
}
.legend-color {
width: 13px;
height: 13px;
display: inline-block;
margin-right: 3px;
vertical-align: sub;
}
}
.outcome-info {
clear: both;
.description {
font-weight: 200;
}
}
.mastery {
color: #555;
}
.last-assessment {
color: $grayLight;
font-size: $fontSizeSmall;
}
.mastery-details i:before {
font-size: 1.5em;
}
.mastery:before {
color: #5DC28F;
}
.near-mastery {
color: #DFB056;
}
.remedial:before {
color: #F76A66;
}
.method-details {
background-color: $ic-color-neutral;
margin-bottom: 0px;
margin-top: 10px;
div {
padding-bottom: 10px;
}
span {
display: inline-block;
font-style: italic;
}
.example {
color: $grayLight;
font-size: $fontSizeSmall;
}
.left-content {
float: left;
}
.right-content {
padding-left: 10px;
}
.method-description {
width: 320px;
}
}
}

View File

@ -355,53 +355,3 @@ $page-dark: #ebeff2;
&.near-mastery { background: $mastery-yellow; }
&.remedial { background: $mastery-red; }
}
.outcome-details {
position: absolute;
z-index: 1000;
background-color: #fff;
border: 1px solid #b0afaf;
box-shadow: 2px 2px 10px rgba(0, 0, 0, 0.6);
max-width: 500px;
min-width: 250px;
padding: 18px 12px;
border-radius: 8px;
font-weight: 500;
color: #4f5f6e;
.title {
font-size: 14px;
padding-bottom: 10px;
border-bottom: 1px solid #b0afaf;
margin-bottom: 10px;
}
.chart {
font-size: 11px;
text-align: center;
}
.ratings {
font-weight: normal;
display: inline-block;
text-align: left;
.rating {
list-style-type: none;
}
.legend-color {
width: 13px;
height: 13px;
display: inline-block;
margin-right: 3px;
vertical-align: sub;
}
}
.mastery {
color: #555;
}
.description {
margin: 10px 0;
font-size: 11px;
font-weight: normal;
}
.view-more {
text-align: right;
}
}

View File

@ -1,19 +0,0 @@
<div class="outcome-details carat-bottom">
<div class="title">{{path}}</div>
<div class="chart">
{{#if ratings.result_count}}
<div class="chart-image"></div>
<ol class="ratings">
{{#each ratings}}
<li class="rating"><span class="legend-color" style="background-color: {{color}}" /> {{description}}</li>
{{/each}}
</ol>
{{else}}
<p><img src="/images/pie-chart-disabled.png" /></p>
<p>{{#t "no_results"}}(no results){{/t}}</p>
{{/if}}
</div>
<div class="mastery">{{#t "mastery_set_at"}}Mastery set at{{/t}}: {{mastery_points}}</div>
<div class="description">{{{description}}}</div>
<div class="view-more"><a href="{{url}}">{{#t "view_more_details"}}view more details{{/t}}</a></div>
</div>

View File

@ -1,12 +1,32 @@
<div class="outcome-details carat-right">
<div class="outcome-details">
<div class="head-content" tabindex="0">
{{#if path}}
<div class="title">{{path}}</div>
<div class="chart">
{{#if ratings.result_count}}
<div class="chart-image pull-left"></div>
<ol class="ratings pull-left">
{{#each ratings}}
<li class="rating"><span class="legend-color" style="background-color: {{color}}" /> {{description}}</li>
{{/each}}
</ol>
{{else}}
<p><img src="/images/pie-chart-disabled.png" /></p>
<p>{{#t "no_results"}}(no results){{/t}}</p>
{{/if}}
</div>
<div class="outcome-info">
<div class="mastery">{{#t "mastery_set_at"}}Mastery set at{{/t}}: {{mastery_points}}</div>
{{#if description}}<div class="description">{{{description}}}</div>{{/if}}
</div>
{{else}}
<div class="mastery-details pull-right" aria-hidden="true">
{{#if scoreDefined}}
<span>{{score}} / {{masteryPoints}}</span>
{{#ifEqual masteryLevel 'mastery'}}
{{#if score}}
<span>{{score}} / {{mastery_points}}</span>
{{#ifEqual status 'mastery'}}
<i class="icon-check mastery"></i>
{{else}}
{{#ifEqual masteryLevel 'near'}}
{{#ifEqual status 'near'}}
<i class="icon-plus near-mastery"></i>
{{else}}
<i class="icon-x remedial"></i>
@ -17,16 +37,23 @@
{{/if}}
</div>
<div class="recent-details">
<div class="last-assessment">{{#t}}Last Assessment: {{latestTitle}}{{/t}}</div>
<div class="submission-time">{{datetimeFormatted submissionTime}}</div>
<div class="last-assessment">{{#t}}Latest Assessment{{/t}}:&nbsp;
{{#if result_title}}
{{result_title}}
{{else}}
{{#t}}Not available until next submission{{/t}}
{{/if}}
</div>
<div class="submission-time">{{datetimeFormatted submission_time}}</div>
</div>
<div class="screenreader-only">
{{#if scoreDefined}}
{{#if score}}
{{#t}}Current Mastery Score: {{score}} out of {{masteryPoints}}{{/t}}
{{else}}
{{#t}}No score yet{{/t}}
{{/if}}
</div>
{{/if}}
</div>
<div class="method-details content-box pad-box-mini border border-trbl border-round">
<div>

View File

@ -20,7 +20,7 @@ module Outcomes
module ResultAnalytics
Rollup = Struct.new(:context, :scores)
RollupScore = Struct.new(:outcome, :score, :count, :title, :submitted_at)
Result = Struct.new(:learning_outcome, :score, :count)
# Public: Queries learning_outcome_results for rollup.
#
@ -79,13 +79,12 @@ module Outcomes
def aggregate_outcome_results_rollup(results, context)
rollups = outcome_results_rollups(results)
rollup_scores = rollups.map(&:scores).flatten
outcome_scores = rollup_scores.group_by(&:outcome)
aggregate_scores = outcome_scores.map do |outcome, scores|
aggregate_score = scores.map(&:score).sum.to_f / scores.size
RollupScore.new(outcome, aggregate_score, scores.size)
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)}
end
Rollup.new(context, aggregate_scores)
aggregate_rollups = aggregate_results.map{|result| RollupScore.new(result,{aggregate_score: true})}
Rollup.new(context, aggregate_rollups)
end
# Internal: Generates a rollup of the outcome results, Assuming all the
@ -97,22 +96,7 @@ module Outcomes
# Returns an Array of RollupScore objects
def rollup_user_results(user_results)
outcome_scores = user_results.chunk(&:learning_outcome_id).map do |_, outcome_results|
outcome = outcome_results.first.learning_outcome
user_rollup_score = calculate_results(outcome_results, {
# || 'highest' is for outcomes created prior to this update with no
# method set so they'll keep their initial behavior
calculation_method: outcome.calculation_method || "highest",
calculation_int: outcome.calculation_int
})
latest_result = outcome_results.max_by{|result| result.submitted_at.to_i}
if !latest_result.submitted_at
# don't pass in a title for comparison if there are no submissions with timestamps
# otherwise grab the portion of the title that has the assignment/quiz's name
latest_result.title = nil
else
latest_result.title = latest_result.title.split(", ")[1]
end
RollupScore.new(outcome, user_rollup_score, outcome_results.size, latest_result.title, latest_result.submitted_at)
RollupScore.new(outcome_results)
end
end
@ -128,42 +112,6 @@ module Outcomes
rollups + missing_users.map { |u| Rollup.new(u, []) }
end
def calculate_results(results, method)
# decaying average is default for new outcomes
score = case method[:calculation_method]
when 'decaying_average'
decaying_average(results, method[:calculation_int])
when 'n_mastery'
n_mastery(results, method[:calculation_int])
when 'latest'
latest(results)
when 'highest'
results.max_by{|result| result.score}.score
end
score
end
#scoring methods
def latest(results)
results.max_by{|result| result.submitted_at.to_i}.score
end
def n_mastery(results, n_of_scores)
return nil if results.length < n_of_scores
scores = results.map(&:score).sort.last(n_of_scores)
(scores.sum / scores.length).round(2)
end
def decaying_average(results, weight = 75)
#default grading method with weight of 75 if none selected
return nil if results.length < 2
scores = results.sort_by{|result| result.submitted_at.to_i}.map(&:score)
latestWeighted = scores.pop * (0.01 * weight)
olderAvgWeighted = (scores.sum / scores.length) * (0.01 * (100 - weight)).round(2)
latestWeighted + olderAvgWeighted
end
class << self
include ResultAnalytics
end

View File

@ -24,7 +24,6 @@ describe Outcomes::ResultAnalytics do
let(:ra) { Outcomes::ResultAnalytics }
let(:time) { Time.now }
Rollup = Outcomes::ResultAnalytics::Rollup
RollupScore = Outcomes::ResultAnalytics::RollupScore
# ResultAnalytics only uses a few fields, so use some mock stuff to avoid all
# the surrounding database logic
@ -46,10 +45,11 @@ describe Outcomes::ResultAnalytics do
MockOutcomeResult[MockUser[10, 'a'], MockOutcome[80], 2.0],
MockOutcomeResult[MockUser[10, 'a'], MockOutcome[81], 3.0],
]
expect(ra.rollup_user_results(results)).to eq [
RollupScore.new(MockOutcome[80], 2.0, 1),
RollupScore.new(MockOutcome[81], 3.0, 1),
]
rollup = ra.rollup_user_results(results)
expect(rollup.size).to eq 2
rollup.each.with_index do |ru, i|
expect(ru.outcome_results.first).to eq results[i]
end
end
end
@ -61,9 +61,10 @@ describe Outcomes::ResultAnalytics do
MockOutcomeResult[MockUser[10, 'a'], MockOutcome[80], 3.0],
MockOutcomeResult[MockUser[10, 'a'], MockOutcome[80], 1.0],
]
expect(ra.rollup_user_results(results)).to eq [
RollupScore.new(MockOutcome[80], 3.0, 2)
]
rollup = ra.rollup_user_results(results)
expect(rollup.size).to eq 1
expect(rollup[0].count).to eq 2
expect(rollup[0].score).to eq 3.0
end
it 'returns maximum score when highest score method is selected' do
@ -71,9 +72,9 @@ describe Outcomes::ResultAnalytics do
MockOutcomeResult[MockUser[10, 'a'], MockOutcome[80, 'highest'], 3.0],
MockOutcomeResult[MockUser[10, 'a'], MockOutcome[80, 'highest'], 1.0],
]
expect(ra.rollup_user_results(results)).to eq [
RollupScore.new(MockOutcome[80, 'highest'], 3.0, 2)
]
rollup = ra.rollup_user_results(results)
expect(rollup[0].score).to eq 3.0
expect(rollup[0].outcome.calculation_method).to eq "highest"
end
it 'returns correct score when latest score method is selected' do
@ -82,9 +83,8 @@ describe Outcomes::ResultAnalytics do
MockOutcomeResult[MockUser[10, 'a'], MockOutcome[80, 'latest'], 3.0, "name, o1", time],
MockOutcomeResult[MockUser[10, 'a'], MockOutcome[80, 'latest'], 1.0, "name, o1", time - 1.day]
]
expect(ra.rollup_user_results(results)).to eq [
RollupScore.new(MockOutcome[80, 'latest'], 3.0, 3, "o1", time)
]
rollups = ra.rollup_user_results(results)
expect(rollups[0].score).to eq 3.0
end
it 'properly calculates results when method is n# of scores for mastery' do
@ -102,11 +102,9 @@ describe Outcomes::ResultAnalytics do
MockOutcomeResult[MockUser[10, 'a'], MockOutcome[82, 'n_mastery', 4], 1.0],
MockOutcomeResult[MockUser[10, 'a'], MockOutcome[82, 'n_mastery', 4], 3.0],
]
expect(ra.rollup_user_results(results)).to eq [
RollupScore.new(MockOutcome[80, 'n_mastery', 3], nil, 2),
RollupScore.new(MockOutcome[81, 'n_mastery', 5], nil, 3),
RollupScore.new(MockOutcome[82, 'n_mastery', 4], 3.25, 4),
]
rollups = ra.rollup_user_results(results)
expect(rollups.size).to eq 3
expect(rollups.map(&:score)).to eq [nil, nil, 3.25]
end
it 'properly calculates results when method is decaying average' do
@ -119,10 +117,9 @@ describe Outcomes::ResultAnalytics do
MockOutcomeResult[MockUser[10, 'a'], MockOutcome[81, 'decaying_average', 75], 1.0, "name, o2", time - 2.days],
MockOutcomeResult[MockUser[10, 'a'], MockOutcome[81, 'decaying_average', 75], 3.0, "name, o2", time - 3.days],
]
expect(ra.rollup_user_results(results)).to eq [
RollupScore.new(MockOutcome[80, 'decaying_average', 75], nil, 1, "o1", time),
RollupScore.new(MockOutcome[81, 'decaying_average', 75], 3.75, 4, "o2", time),
]
rollups = ra.rollup_user_results(results)
expect(rollups.size).to eq 2
expect(rollups.map(&:score)).to eq [nil, 3.75]
end
end
@ -131,13 +128,14 @@ describe Outcomes::ResultAnalytics do
results = [
MockOutcomeResult[MockUser[10, 'a'], MockOutcome[80], 4.0],
MockOutcomeResult[MockUser[20, 'b'], MockOutcome[80], 5.0],
MockOutcomeResult[MockUser[20, 'b'], MockOutcome[80], 3.0],
]
users = [MockUser[10, 'a'], MockUser[30, 'c']]
expect(ra.outcome_results_rollups(results, users)).to eq [
Rollup.new(MockUser[10, 'a'], [ RollupScore.new(MockOutcome[80], 4.0, 1) ]),
Rollup.new(MockUser[20, 'b'], [ RollupScore.new(MockOutcome[80], 5.0, 1) ]),
Rollup.new(MockUser[30, 'c'], []),
]
rollups = ra.outcome_results_rollups(results, users)
rollup_scores = ra.rollup_user_results(results).map(&:outcome_results).flatten
rollups.each.with_index do |rollup, i|
expect(rollup.scores.map(&:outcome_results).flatten).to eq rollup_scores.find_all{|score| score.user.id == rollup.context.id}
end
end
end
@ -155,13 +153,10 @@ describe Outcomes::ResultAnalytics do
MockOutcomeResult[MockUser[40, 'd'], MockOutcome[81], 7.0],
]
aggregate_result = ra.aggregate_outcome_results_rollup(results, fake_context)
expect(aggregate_result).to eq Rollup.new(
fake_context,
[
RollupScore.new(MockOutcome[80], 2.5, 4),
RollupScore.new(MockOutcome[81], 6.0, 3),
]
)
expect(aggregate_result.size).to eq 2
expect(aggregate_result.scores.map(&:score)).to eq [2.5, 6.0]
expect(aggregate_result.scores[0].outcome_results.size).to eq 4
expect(aggregate_result.scores[1].outcome_results.size).to eq 3
end
end