Ember Quiz Statistics - Base/Skeleton
A starting point for the implementation of Ember quiz statistics that includes a route with the required data properly loaded, the necessary serializers and adapters, and base stylesheet/template to start from. This patch also adds a new submission statistic called "submission_scores" that's basically a map between a score percentile and the count of students who received it. Closes CNVS-12171 TEST PLAN ---- ---- - with fabulous quizzes on, go to a quiz show page - click the Statistics tab - verify that you see the blank page, and the tab is activated - verify that no errors are thrown in the console Testing the new metric: - create a quiz with a certain number of points possible - take it by a number of students and score diversely - also let more than one student have the same score - perform an API request to retrieve the statistics and: - verify you get the new score distribution statistic and that it is correct - the metric should contain an entry for each distinct percentile - the metric entry should really reflect how many students got that score - check out the Quiz Statistics API docs and: - verify the new "scores" statistic under SubmissionStatistics is documented - verify that the documentation is clear enough Change-Id: I1f00bd4c18a0767d6a50767c5d8868f1d6e561ac Reviewed-on: https://gerrit.instructure.com/32732 Tested-by: Jenkins <jenkins@instructure.com> QA-Review: Caleb Guanzon <cguanzon@instructure.com> Reviewed-by: Jason Madsen <jmadsen@instructure.com> Product-Review: Derek DeVries <ddevries@instructure.com>
This commit is contained in:
parent
87cd6d57b3
commit
e471722b26
|
@ -0,0 +1,6 @@
|
|||
define [
|
||||
'./jsonapi_adapter'
|
||||
], (JSONAPIAdapter) ->
|
||||
|
||||
JSONAPIAdapter.extend
|
||||
namespace: "api/v1"
|
|
@ -0,0 +1,8 @@
|
|||
define [
|
||||
'ember'
|
||||
'./jsonapi_adapter'
|
||||
], ({get,set}, JSONAPIAdapter) ->
|
||||
JSONAPIAdapter.extend
|
||||
buildURL: (type, id) ->
|
||||
store = @container.lookup('store:main')
|
||||
store.getById('quizReport', id).get('url')
|
|
@ -12,6 +12,10 @@ define [
|
|||
initialize: (container, application) ->
|
||||
env.setEnv(window.ENV)
|
||||
|
||||
Ember.Inflector.inflector.irregular('quizStatistics', 'quizStatistics');
|
||||
Ember.Inflector.inflector.irregular('questionStatistics', 'questionStatistics');
|
||||
Ember.Inflector.inflector.irregular('progress', 'progress');
|
||||
|
||||
Ember.$.ajaxPrefilter 'json', (options, originalOptions, xhr) ->
|
||||
options.dataType = 'json'
|
||||
options.headers = 'Accept': 'application/vnd.api+json'
|
||||
|
|
|
@ -5,3 +5,4 @@ define ->
|
|||
@resource 'quiz', path: '/:quiz_id', ->
|
||||
@route 'show', path: '/'
|
||||
@route 'moderate', path: '/moderate'
|
||||
@route 'statistics', path: '/statistics'
|
|
@ -0,0 +1,51 @@
|
|||
define [
|
||||
'ember'
|
||||
'ember-data'
|
||||
], ({Deferred}, {Model, attr}) ->
|
||||
# The Progress model represents the progress of an async operation happening
|
||||
# in the back-end that may take some time to complete. The model provides an
|
||||
# interface to track the completion of this operation.
|
||||
Model.extend
|
||||
tag: attr('string')
|
||||
completion: attr('number')
|
||||
# workflowState can be any one of:
|
||||
#
|
||||
# - undefined: the operation hasn't started
|
||||
# - "queued": the operation is pending and will start soon
|
||||
# - "running": the operation is being performed
|
||||
# - "completed": the operation was completed successfully
|
||||
# - "failed": the operation failed
|
||||
workflowState: attr('string')
|
||||
message: attr('string')
|
||||
createdAt: attr('date')
|
||||
updatedAt: attr('date')
|
||||
|
||||
# Kick off a poller that will track the completion of the operation.
|
||||
#
|
||||
# @param [Integer] [pollingInterval=1000]
|
||||
# How often to poll the operation for its completion, in milliseconds.
|
||||
#
|
||||
# @return [Ember.Deferred]
|
||||
# A promise that will yield only when the operation is complete.
|
||||
trackCompletion: (pollingInterval) ->
|
||||
service = new Deferred()
|
||||
|
||||
Ember.run.later this, ->
|
||||
poll = null
|
||||
timeout = null
|
||||
|
||||
# don't try to do any ajax when we're leaving the page
|
||||
# workaround for https://code.google.com/p/chromium/issues/detail?id=263981
|
||||
$(window).on 'beforeunload', -> clearTimeout(timeout)
|
||||
|
||||
poll = =>
|
||||
@reload().then =>
|
||||
if @get('workflowState') == 'failed'
|
||||
service.reject()
|
||||
else if @get('workflowState') == 'completed'
|
||||
service.resolve()
|
||||
else
|
||||
timeout = setTimeout poll, pollingInterval || 1000
|
||||
poll()
|
||||
|
||||
service
|
|
@ -0,0 +1,30 @@
|
|||
define [
|
||||
'ember'
|
||||
'ember-data'
|
||||
'i18n!quizzes'
|
||||
], (Em, DS, I18n) ->
|
||||
|
||||
{alias} = Em.computed
|
||||
{Model, attr, belongsTo} = DS
|
||||
|
||||
Model.extend
|
||||
quizStatistics: belongsTo 'quizStatistics', async: false
|
||||
questionType: attr()
|
||||
questionName: attr()
|
||||
questionText: attr()
|
||||
position: attr()
|
||||
answers: attr()
|
||||
pointBiserials: attr()
|
||||
responses: attr()
|
||||
responseValues: attr()
|
||||
unexpectedResponseValues: attr()
|
||||
topStudentCount: attr()
|
||||
middleStudentCount: attr()
|
||||
bottomStudentCount: attr()
|
||||
correctStudentCount: attr()
|
||||
incorrectStudentCount: attr()
|
||||
correctStudentRatio: attr()
|
||||
incorrectStudentRatio: attr()
|
||||
correctTopStudentCount: attr()
|
||||
correctMiddleStudentCount: attr()
|
||||
correctBottomStudentCount: attr()
|
|
@ -6,15 +6,15 @@ define [
|
|||
], (Em, DS, I18n, ajax) ->
|
||||
|
||||
{alias, equal, any} = Em.computed
|
||||
{belongsTo, PromiseObject} = DS
|
||||
{belongsTo, PromiseObject, hasMany, Model, attr} = DS
|
||||
|
||||
Em.onerror = (error) ->
|
||||
console.log 'ERR', error, error.stack
|
||||
|
||||
{Model, attr} = DS
|
||||
Model.extend
|
||||
title: attr()
|
||||
quizType: attr()
|
||||
links: attr()
|
||||
htmlURL: attr()
|
||||
# editURL is temporary until we have a real ember route for it
|
||||
editURL: (->
|
||||
|
@ -86,3 +86,5 @@ define [
|
|||
{ html: html }
|
||||
PromiseObject.create promise: promise
|
||||
).property('quizSubmissionHtmlURL')
|
||||
quizStatistics: hasMany 'quiz_statistics', async: true
|
||||
quizReports: hasMany 'quiz_report', async: true
|
||||
|
|
|
@ -0,0 +1,18 @@
|
|||
define [
|
||||
'ember'
|
||||
'ember-data'
|
||||
], (Em, DS) ->
|
||||
{Model, attr, belongsTo} = DS
|
||||
|
||||
Model.extend
|
||||
quiz: belongsTo 'quiz', async: false
|
||||
|
||||
anonymous: attr()
|
||||
includesAllVersions: attr()
|
||||
reportType: attr()
|
||||
createdAt: attr('date')
|
||||
updatedAt: attr('date')
|
||||
file: attr()
|
||||
progress: attr()
|
||||
progressUrl: attr()
|
||||
url: attr()
|
|
@ -0,0 +1,26 @@
|
|||
define [
|
||||
'ember'
|
||||
'ember-data'
|
||||
], (Em, DS) ->
|
||||
|
||||
{alias} = Em.computed
|
||||
{Model, attr, belongsTo, hasMany} = DS
|
||||
|
||||
Model.extend
|
||||
quiz: belongsTo 'quiz', async: false
|
||||
questionStatistics: hasMany 'questionStatistics', async: false, embedded: 'load'
|
||||
|
||||
generatedAt: attr('date')
|
||||
multipleAttemptsExist: attr('boolean')
|
||||
|
||||
submissionStatistics: attr()
|
||||
|
||||
avgCorrect: alias 'submissionStatistics.correct_count_average'
|
||||
avgIncorrect: alias 'submissionStatistics.incorrect_count_average'
|
||||
avgDuration: alias 'submissionStatistics.duration_average'
|
||||
loggedOutUsers: alias 'submissionStatistics.logged_out_users'
|
||||
avgScore: alias 'submissionStatistics.score_average'
|
||||
highScore: alias 'submissionStatistics.score_high'
|
||||
lowScore: alias 'submissionStatistics.score_low'
|
||||
scoreStdev: alias 'submissionStatistics.score_stdev'
|
||||
uniqueCount: alias 'submissionStatistics.unique_count'
|
|
@ -0,0 +1,21 @@
|
|||
define [ 'ember', 'underscore' ], (Ember, _) ->
|
||||
{RSVP} = Ember
|
||||
|
||||
Ember.Route.extend
|
||||
model: (transition, options) ->
|
||||
quiz = @modelFor('quiz')
|
||||
quiz.get('quizStatistics').then((items)->
|
||||
# use the latest statistics report available:
|
||||
items.sortBy('createdAt').get('lastObject')
|
||||
).then (latestStatistics)->
|
||||
# load the reports, we need these to be able to generate if requested
|
||||
quiz.get('quizReports').then ->
|
||||
latestStatistics
|
||||
|
||||
afterModel: ->
|
||||
# for some reason, the quiz is not associating with the quiz_questions,
|
||||
# although the inverse is true (quiz questions *are* associated to the quiz),
|
||||
# anyway, do it manually:
|
||||
set = @modelFor('quizStatistics').get('questionStatistics')
|
||||
set.clear()
|
||||
set.pushObjects(@store.all('questionStatistics'))
|
|
@ -0,0 +1,4 @@
|
|||
define [ 'ember-data' ], (DS) ->
|
||||
DS.ActiveModelSerializer.extend
|
||||
extractSingle: (store, type, payload, id, requestType) ->
|
||||
@_super store, type, { progress: [ payload ] }, id, requestType
|
|
@ -0,0 +1,4 @@
|
|||
define [ 'ember-data' ], (DS) ->
|
||||
DS.ActiveModelSerializer.extend
|
||||
extractSingle: (store, type, payload, id, requestType) ->
|
||||
@_super store, type, { quiz_reports: [ payload ] }, id, requestType
|
|
@ -0,0 +1,12 @@
|
|||
define [ 'ember-data', 'underscore' ], (DS, _) ->
|
||||
DS.ActiveModelSerializer.extend
|
||||
normalizePayload: (type, hash, prop) ->
|
||||
# how can we add query parameters to model.find('quiz_statistics') ??
|
||||
if hash.quizzes
|
||||
_(hash.quizzes).each (quiz) ->
|
||||
if quiz.links
|
||||
if quiz.links.quiz_statistics
|
||||
quiz.links.quiz_statistics += '?include=quiz_questions'
|
||||
if quiz.links.quiz_reports
|
||||
quiz.links.quiz_reports += '?includes_all_versions=true'
|
||||
hash
|
|
@ -0,0 +1,6 @@
|
|||
define [ 'ember-data', 'underscore' ], (DS, _) ->
|
||||
DS.ActiveModelSerializer.extend
|
||||
extractArray: (store, type, payload, id, requestType) ->
|
||||
payload.question_statistics = payload.quiz_statistics[0].question_statistics
|
||||
|
||||
@_super(store, type, payload, id, requestType)
|
|
@ -42,7 +42,11 @@
|
|||
{{/link-to}}
|
||||
{{/link-to}}
|
||||
|
||||
<li><a href="#">Statistics</a></li>
|
||||
{{#link-to 'quiz.statistics' this tagName='li'}}
|
||||
{{#link-to 'quiz.statistics' this.id}}
|
||||
{{#t 'statistics'}}Statistics{{/t}}
|
||||
{{/link-to}}
|
||||
{{/link-to}}
|
||||
</ol>
|
||||
</div>
|
||||
|
||||
|
|
|
@ -0,0 +1,2 @@
|
|||
<div id="quiz-statistics">
|
||||
</div>
|
|
@ -0,0 +1,21 @@
|
|||
define [
|
||||
'ember'
|
||||
'../start_app'
|
||||
'../environment_setup'
|
||||
'../shared_ajax_fixtures'
|
||||
'../../adapters/progress_adapter'
|
||||
], (Ember, startApp, env, fixtures, Subject) ->
|
||||
App = null
|
||||
subject = null
|
||||
|
||||
module 'Quiz Report Adapter',
|
||||
setup: ->
|
||||
App = startApp()
|
||||
fixtures.create()
|
||||
subject = App.__container__.lookup('adapter:progress')
|
||||
|
||||
teardown: ->
|
||||
Ember.run App, 'destroy'
|
||||
|
||||
test 'it builds the proper URL', ->
|
||||
strictEqual subject.buildURL('progress', 1), '/api/v1/progress/1'
|
|
@ -0,0 +1,23 @@
|
|||
define [
|
||||
'ember'
|
||||
'../start_app'
|
||||
'../environment_setup'
|
||||
'../shared_ajax_fixtures'
|
||||
'../../adapters/quiz_report_adapter'
|
||||
], (Ember, startApp, env, fixtures, Subject) ->
|
||||
App = null
|
||||
subject = null
|
||||
|
||||
module 'Quiz Report Adapter',
|
||||
setup: ->
|
||||
App = startApp()
|
||||
fixtures.create()
|
||||
subject = App.__container__.lookup('adapter:quizReport')
|
||||
visit('/1/statistics')
|
||||
|
||||
teardown: ->
|
||||
Ember.run App, 'destroy'
|
||||
|
||||
test 'it uses the report URL', ->
|
||||
url = subject.buildURL 'quizReport', 14
|
||||
ok url.match('/api/v1/courses/1/quizzes/1/reports/14')
|
|
@ -0,0 +1,31 @@
|
|||
define [
|
||||
'ember'
|
||||
'../start_app'
|
||||
'../environment_setup'
|
||||
'../shared_ajax_fixtures'
|
||||
], (Ember, startApp, env, fixtures) ->
|
||||
App = null
|
||||
|
||||
{$} = Ember
|
||||
|
||||
module "Quiz Statistics Integration",
|
||||
setup: ->
|
||||
App = startApp()
|
||||
fixtures.create()
|
||||
|
||||
teardown: ->
|
||||
Ember.run App, 'destroy'
|
||||
|
||||
testPage = (desc, callback) ->
|
||||
test desc, ->
|
||||
visit('/1/statistics').then callback
|
||||
|
||||
testPage 'it renders', ->
|
||||
equal find('#quiz-statistics').length, 1
|
||||
|
||||
testPage 'it loads the quiz, statistics, and question statistics', ->
|
||||
route = App.__container__.lookup('route:quizStatistics')
|
||||
ok q = route.modelFor('quiz'), 'loads the quiz'
|
||||
equal q.get('quizReports.length'), 2, 'loads quiz reports'
|
||||
ok qs = route.modelFor('quizStatistics'), 'loads quiz statistics'
|
||||
equal qs.get('questionStatistics.length'), 11, 'loads question statistics'
|
|
@ -0,0 +1,83 @@
|
|||
define [
|
||||
'ic-ajax'
|
||||
'ember'
|
||||
'underscore'
|
||||
'../start_app'
|
||||
], (ajax, Ember, _, startApp) ->
|
||||
|
||||
{run} = Ember
|
||||
App = null
|
||||
subject = null
|
||||
timeout = null
|
||||
|
||||
progressFixture = (attrs) ->
|
||||
_.extend {
|
||||
"completion": 0,
|
||||
"context_id": 1,
|
||||
"context_type": "Quizzes::QuizStatistics",
|
||||
"created_at": "2014-04-02T09:40:32Z",
|
||||
"id": 1,
|
||||
"message": null,
|
||||
"tag": "Quizzes::QuizStatistics",
|
||||
"updated_at": "2014-04-02T09:40:36Z",
|
||||
"user_id": null,
|
||||
"workflow_state": "running",
|
||||
"url": "http://localhost:3000/api/v1/progress/1"
|
||||
}, attrs
|
||||
|
||||
module "Progress",
|
||||
setup: ->
|
||||
App = startApp()
|
||||
run ->
|
||||
container = App.__container__
|
||||
store = container.lookup 'store:main'
|
||||
subject = store.createRecord 'progress', progressFixture()
|
||||
# need to modify the adapter to use ic-ajax, which for some reason it's
|
||||
# not in the spec...
|
||||
adapter = container.lookup 'adapter:progress'
|
||||
adapter.ajax = (url, method) ->
|
||||
ajax({ url: url, type: method })
|
||||
teardown: ->
|
||||
clearTimeout timeout
|
||||
run App, 'destroy'
|
||||
|
||||
testWithTimeout = (desc, callback) ->
|
||||
asyncTest desc, ->
|
||||
timeout = setTimeout (->
|
||||
ok false, "timed out"
|
||||
start()
|
||||
), 1000
|
||||
|
||||
callback()
|
||||
|
||||
testWithTimeout '#trackCompletion: it polls until complete', ->
|
||||
expect 1
|
||||
|
||||
ajax.defineFixture '/api/v1/progress/1',
|
||||
response: progressFixture(workflow_state: 'completed'),
|
||||
jqXHR: {}
|
||||
testStatus: 200
|
||||
textStatus: 'success'
|
||||
|
||||
run ->
|
||||
subject.trackCompletion(5).then ->
|
||||
ok true, "notifies me when it's done"
|
||||
start()
|
||||
|
||||
testWithTimeout '#trackCompletion: it reports failures', ->
|
||||
expect 1
|
||||
|
||||
ajax.defineFixture '/api/v1/progress/1',
|
||||
response: progressFixture(workflow_state: 'failed'),
|
||||
jqXHR: {}
|
||||
testStatus: 200
|
||||
textStatus: 'success'
|
||||
|
||||
run ->
|
||||
subject.trackCompletion(5).then(->
|
||||
ok false, "progress success callback should never be called"
|
||||
start()
|
||||
, ->
|
||||
ok true, "progress failure callback should be called"
|
||||
start()
|
||||
)
|
|
@ -0,0 +1,22 @@
|
|||
define [
|
||||
'ember'
|
||||
'../start_app'
|
||||
'i18n!quizzes'
|
||||
], (Em, startApp, I18n) ->
|
||||
|
||||
{run} = Em
|
||||
App = null
|
||||
statistics = null
|
||||
store = null
|
||||
|
||||
module "Quiz",
|
||||
|
||||
setup: ->
|
||||
App = startApp()
|
||||
container = App.__container__
|
||||
store = container.lookup 'store:main'
|
||||
run ->
|
||||
statistics = store.createRecord 'quiz_statistics', id: '1'
|
||||
|
||||
teardown: ->
|
||||
run App, 'destroy'
|
File diff suppressed because one or more lines are too long
|
@ -226,6 +226,11 @@
|
|||
# "example": 1.24721912892465,
|
||||
# "type": "number"
|
||||
# },
|
||||
# "scores": {
|
||||
# "description": "A percentile distribution of the student scores, each key is the percentile (ranges between 0 and 100%) while the value is the number of students who received that score.",
|
||||
# "example": { "50": 1, "34": 5, "100": 1 },
|
||||
# "type": "object"
|
||||
# },
|
||||
# "correct_count_average": {
|
||||
# "description": "The mean of the number of questions answered correctly by each student.",
|
||||
# "example": 3.66666666666667,
|
||||
|
|
|
@ -65,12 +65,14 @@ class Quizzes::QuizStatistics::StudentAnalysis < Quizzes::QuizStatistics::Report
|
|||
found_ids = {}
|
||||
score_counter = Stats::Counter.new
|
||||
questions_hash = {}
|
||||
quiz_points = [quiz.current_points_possible.to_f, 1.0].max
|
||||
stats[:questions] = []
|
||||
stats[:multiple_attempts_exist] = submissions.any? { |s|
|
||||
s.attempt && s.attempt > 1
|
||||
}
|
||||
stats[:submission_user_ids] = Set.new
|
||||
stats[:submission_logged_out_users] = []
|
||||
stats[:submission_scores] = Hash.new(0)
|
||||
stats[:unique_submission_count] = 0
|
||||
correct_cnt = incorrect_cnt = total_duration = 0
|
||||
submissions.each_with_index do |sub, index|
|
||||
|
@ -82,7 +84,9 @@ class Quizzes::QuizStatistics::StudentAnalysis < Quizzes::QuizStatistics::Report
|
|||
stats[:submission_logged_out_users] << temp_user
|
||||
end
|
||||
if !found_ids[sub.id]
|
||||
percentile = (sub.score.to_f / quiz_points * 100).round
|
||||
stats[:unique_submission_count] += 1
|
||||
stats[:submission_scores][percentile] += 1
|
||||
found_ids[sub.id] = true
|
||||
end
|
||||
answers = sub.submission_data || []
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
@import "environment.sass";
|
||||
@import "show";
|
||||
@import "statistics";
|
||||
$backgroundColor: #dfe9f0;
|
||||
$borderColor: #AAA;
|
||||
$questionHeaderBackground: #F5F5F5;
|
||||
|
|
|
@ -0,0 +1,40 @@
|
|||
$highlightColor: #3cb878;
|
||||
|
||||
#quiz-statistics {
|
||||
header {
|
||||
line-height: 34px;
|
||||
}
|
||||
|
||||
.inline {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
line-height: 1;
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
|
||||
.question-statistics {
|
||||
border: 1px solid #CAD0D7;
|
||||
border-radius: 6px;
|
||||
padding: 10px 20px 0;
|
||||
position: relative;
|
||||
margin-bottom: 20px;
|
||||
|
||||
section {
|
||||
display: inline-block;
|
||||
vertical-align: top;
|
||||
width: 320px;
|
||||
}
|
||||
|
||||
.question-attempts {
|
||||
font-size: 0.95em;
|
||||
}
|
||||
|
||||
.question-text {
|
||||
margin-top: -5px;
|
||||
margin-bottom: 20px;
|
||||
font-size: 1.25em;
|
||||
}
|
||||
}
|
|
@ -17,7 +17,13 @@ namespace :jst do
|
|||
desc 'precompile ember templates'
|
||||
task :ember do
|
||||
require 'handlebars_tasks'
|
||||
files = Dir.glob("app/coffeescripts/**/*.hbs")
|
||||
|
||||
files = if ENV['file'].present?
|
||||
[ ENV['file'] ]
|
||||
else
|
||||
Dir.glob("app/coffeescripts/**/*.hbs")
|
||||
end
|
||||
|
||||
files.each do |file|
|
||||
HandlebarsTasks::EmberHbs.compile_file(file)
|
||||
end
|
||||
|
|
|
@ -25,7 +25,17 @@ describe Quizzes::QuizStatistics::StudentAnalysis do
|
|||
|
||||
it 'should calculate mean/stddev as expected with a few submissions' do
|
||||
q = @course.quizzes.create!
|
||||
question = q.quiz_questions.create!({
|
||||
question_data: {
|
||||
name: 'q1',
|
||||
points_possible: 30,
|
||||
question_type: 'essay_question',
|
||||
question_text: 'ohai mark'
|
||||
}
|
||||
})
|
||||
q.generate_quiz_data
|
||||
q.save!
|
||||
|
||||
@user1 = User.create! :name => "some_user 1"
|
||||
@user2 = User.create! :name => "some_user 2"
|
||||
@user3 = User.create! :name => "some_user 2"
|
||||
|
@ -34,31 +44,37 @@ describe Quizzes::QuizStatistics::StudentAnalysis do
|
|||
student_in_course :course => @course, :user => @user3
|
||||
sub = q.generate_submission(@user1)
|
||||
sub.workflow_state = 'complete'
|
||||
sub.submission_data = [{ :points => 15, :text => "", :correct => "undefined", :question_id => -1 }]
|
||||
sub.submission_data = [{ :points => 15, :text => "", :correct => "undefined", :question_id => question.id }]
|
||||
sub.score = 15
|
||||
sub.with_versioning(true, &:save!)
|
||||
stats = q.statistics
|
||||
stats[:submission_score_average].should == 15
|
||||
stats[:submission_score_high].should == 15
|
||||
stats[:submission_score_low].should == 15
|
||||
stats[:submission_score_stdev].should == 0
|
||||
stats[:submission_scores].should == { 50 => 1 }
|
||||
sub = q.generate_submission(@user2)
|
||||
sub.workflow_state = 'complete'
|
||||
sub.submission_data = [{ :points => 17, :text => "", :correct => "undefined", :question_id => -1 }]
|
||||
sub.submission_data = [{ :points => 17, :text => "", :correct => "undefined", :question_id => question.id }]
|
||||
sub.score = 17
|
||||
sub.with_versioning(true, &:save!)
|
||||
stats = q.statistics
|
||||
stats[:submission_score_average].should == 16
|
||||
stats[:submission_score_high].should == 17
|
||||
stats[:submission_score_low].should == 15
|
||||
stats[:submission_score_stdev].should == 1
|
||||
stats[:submission_scores].should == { 50 => 1, 57 => 1 }
|
||||
sub = q.generate_submission(@user3)
|
||||
sub.workflow_state = 'complete'
|
||||
sub.submission_data = [{ :points => 20, :text => "", :correct => "undefined", :question_id => -1 }]
|
||||
sub.submission_data = [{ :points => 20, :text => "", :correct => "undefined", :question_id => question.id }]
|
||||
sub.score = 20
|
||||
sub.with_versioning(true, &:save!)
|
||||
stats = q.statistics
|
||||
stats[:submission_score_average].should be_close(17 + 1.0/3, 0.0000000001)
|
||||
stats[:submission_score_high].should == 20
|
||||
stats[:submission_score_low].should == 15
|
||||
stats[:submission_score_stdev].should be_close(Math::sqrt(4 + 2.0/9), 0.0000000001)
|
||||
stats[:submission_scores].should == { 50 => 1, 57 => 1, 67 => 1 }
|
||||
end
|
||||
|
||||
context "csv" do
|
||||
|
|
Loading…
Reference in New Issue