Quiz Stats - Refactor/get rid of QuestionAnalyzer
- removed the question analyzer - each answer analyzer is now expected to calculate its own "responses" field as opposed to doing it in a generic manner - made the Essay analyzer generate its "responses" field - a self-documenting DSL for defining analyzer output metrics Closes CNVS-13157 TEST PLAN ---- ---- It's all code movement/refactor so nothing new to test. The plan is to verify the Essay question stats are unaffected (which is basically all the gem supports at this point:) - create a quiz with an essay question - take the quiz by a few students - grade a submission - retrieve the stats via the API: GET /api/v1/courses/:course_id/quizzes/:quiz_id/statistics - verify the essay stats in the API are still functional, the "responses" field in particular Change-Id: I42b9552c60ccb56f7c6912fed7cc1173da71852d Reviewed-on: https://gerrit.instructure.com/35096 Reviewed-by: Jason Madsen <jmadsen@instructure.com> QA-Review: Caleb Guanzon <cguanzon@instructure.com> Tested-by: Jenkins <jenkins@instructure.com> Product-Review: Ahmad Amireh <ahmad@instructure.com>
This commit is contained in:
parent
dea0db86d4
commit
457e55d6a2
|
@ -19,6 +19,8 @@
|
|||
require 'csv'
|
||||
|
||||
class Quizzes::QuizStatistics::StudentAnalysis < Quizzes::QuizStatistics::Report
|
||||
CQS = CanvasQuizStatistics
|
||||
|
||||
include HtmlTextHelper
|
||||
|
||||
def readable_type
|
||||
|
@ -357,9 +359,9 @@ class Quizzes::QuizStatistics::StudentAnalysis < Quizzes::QuizStatistics::Report
|
|||
# "user_ids"=>[1,2,3],
|
||||
# "multiple_responses"=>false}],
|
||||
def stats_for_question(question, responses)
|
||||
if [ 'essay_question' ].include?(question[:question_type].to_s)
|
||||
analyzer = CanvasQuizStatistics::QuestionAnalyzer.new(question)
|
||||
analysis = analyzer.run(responses)
|
||||
if CQS.can_analyze?(question)
|
||||
analysis = CQS.analyze(question, responses)
|
||||
|
||||
return question.to_hash.merge(analysis).with_indifferent_access
|
||||
end
|
||||
|
||||
|
|
|
@ -1,5 +1,13 @@
|
|||
module CanvasQuizStatistics
|
||||
require 'canvas_quiz_statistics/version'
|
||||
require 'canvas_quiz_statistics/answer_analyzers'
|
||||
require 'canvas_quiz_statistics/question_analyzer'
|
||||
require 'canvas_quiz_statistics/analyzers'
|
||||
|
||||
def self.can_analyze?(question_data)
|
||||
Analyzers[question_data[:question_type]] != Analyzers::Base
|
||||
end
|
||||
|
||||
def self.analyze(question_data, responses)
|
||||
analyzer = Analyzers[question_data[:question_type]].new(question_data)
|
||||
analyzer.run(responses)
|
||||
end
|
||||
end
|
||||
|
|
|
@ -0,0 +1,46 @@
|
|||
#
|
||||
# Copyright (C) 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/>.
|
||||
#
|
||||
require 'active_support/core_ext'
|
||||
|
||||
module CanvasQuizStatistics::Analyzers
|
||||
class << self
|
||||
attr_accessor :available_analyzers
|
||||
|
||||
# Convenient access to analyzer for a given question type, e.g:
|
||||
#
|
||||
# Analyzers['multiple_choice_question'].new
|
||||
#
|
||||
# If the question type is not supported, you will be given the Base
|
||||
# analyzer which really does nothing.
|
||||
def [](question_type)
|
||||
self.available_analyzers ||= {}
|
||||
self.available_analyzers[question_type.to_sym] || Base
|
||||
end
|
||||
end
|
||||
|
||||
class Base
|
||||
def self.inherited(klass)
|
||||
namespace = CanvasQuizStatistics::Analyzers
|
||||
namespace.available_analyzers ||= {}
|
||||
namespace.available_analyzers[klass.question_type] = klass
|
||||
end
|
||||
end
|
||||
|
||||
require 'canvas_quiz_statistics/analyzers/base'
|
||||
require 'canvas_quiz_statistics/analyzers/essay'
|
||||
end
|
|
@ -0,0 +1,61 @@
|
|||
#
|
||||
# Copyright (C) 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/>.
|
||||
#
|
||||
require 'canvas_quiz_statistics/analyzers/base/dsl'
|
||||
|
||||
module CanvasQuizStatistics::Analyzers
|
||||
class Base
|
||||
extend DSL
|
||||
|
||||
attr_reader :question_data
|
||||
|
||||
def initialize(question_data)
|
||||
@question_data = question_data
|
||||
@metrics = self.class.metrics[self.class.question_type]
|
||||
end
|
||||
|
||||
def run(responses)
|
||||
context = build_context(responses)
|
||||
|
||||
{}.tap do |stats|
|
||||
@metrics.map do |metric|
|
||||
params = [ responses ]
|
||||
|
||||
if metric[:context].any?
|
||||
params += metric[:context].map { |var| context[var] }
|
||||
end
|
||||
|
||||
stats[metric[:key]] = instance_exec(*params, &metric[:calculator])
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def self.question_type
|
||||
(self.name.demodulize.underscore + '_question').to_sym
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
# This is the place to prepare any shared context that's needed by the
|
||||
# metric calculations. See DSL for more info on stateful metrics.
|
||||
#
|
||||
# @return [Hash] You must return a Hash with symbolized keys for a context.
|
||||
def build_context(responses)
|
||||
{}
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,85 @@
|
|||
#
|
||||
# Copyright (C) 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/>.
|
||||
#
|
||||
|
||||
# A custom DSL for writing metric calculators.
|
||||
#
|
||||
# Here's a full example showing how to define a context-free metric calculator,
|
||||
# and a stateful one that requires a shared, pre-calculated variable:
|
||||
#
|
||||
# module CanvasQuizStatistics::Analyzers
|
||||
# class MultipleChoice < Base
|
||||
# # A basic metric calculator. Your calculator block will be passed the
|
||||
# # set of responses, and needs to return the value of the metric.
|
||||
# #
|
||||
# # The key you specify will be written to the output, e.g:
|
||||
# # { "missing_answers": 1 }
|
||||
# metric :missing_answers do |responses|
|
||||
# responses.select { |r| r[:text].blank? }.length
|
||||
# end
|
||||
#
|
||||
# # Let's say you need some pre-calculated variable for a bunch of metrics,
|
||||
# # call it "grades", we can prepare it in the special #build_context
|
||||
# # method and explicitly declare it as a dependency of each metric:
|
||||
# def build_context(responses)
|
||||
# ctx = {}
|
||||
# ctx[:grades] = responses.map { |r| r[:grade] }
|
||||
# ctx
|
||||
# end
|
||||
#
|
||||
# # Notice how our metric definition now states that it requires the
|
||||
# # "grades" context variable to run, and it receives it as a block arg:
|
||||
# metric :graded_correctly => [ :grades ] do |responses, grades|
|
||||
# grades.select { |grade| grade == 'correct' }.length
|
||||
# end
|
||||
# end
|
||||
# end
|
||||
module CanvasQuizStatistics::Analyzers::Base::DSL
|
||||
def metric(key, &calculator)
|
||||
deps = []
|
||||
|
||||
if key.is_a?(Hash)
|
||||
deps, key = key.values.flatten, key.keys.first
|
||||
end
|
||||
|
||||
self.metrics[question_type] << {
|
||||
key: key.to_sym,
|
||||
context: deps,
|
||||
calculator: calculator
|
||||
}
|
||||
end
|
||||
|
||||
# You will need to do this if you're subclassing a concrete analyzer and would
|
||||
# like to inherit the metric calculators it defined, as the calculators are
|
||||
# scoped per question type and not the Ruby class.
|
||||
#
|
||||
# Example:
|
||||
#
|
||||
# module CanvasQuizStatistics::Analyzers
|
||||
# class TrueFalse < MultipleChoice
|
||||
# inherit_metrics :multiple_choice_question
|
||||
# end
|
||||
# end
|
||||
#
|
||||
def inherit_metrics(question_type)
|
||||
self.metrics[self.question_type] += self.metrics[question_type].clone
|
||||
end
|
||||
|
||||
def metrics
|
||||
@@metrics ||= Hash.new { |hsh, key| hsh[key] = [] }
|
||||
end
|
||||
end
|
|
@ -15,7 +15,7 @@
|
|||
# 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 CanvasQuizStatistics::AnswerAnalyzers
|
||||
module CanvasQuizStatistics::Analyzers
|
||||
# Generates statistics for a set of student responses to an essay question.
|
||||
class Essay < Base
|
||||
# @param [Array<Hash>] responses
|
||||
|
@ -30,6 +30,9 @@ module CanvasQuizStatistics::AnswerAnalyzers
|
|||
#
|
||||
# @example output
|
||||
# {
|
||||
# // Number of students who have answered this question.
|
||||
# "responses": 2,
|
||||
#
|
||||
# // The number of students whose responses were graded by the teacher so
|
||||
# // far.
|
||||
# "graded": 1,
|
||||
|
@ -44,8 +47,8 @@ module CanvasQuizStatistics::AnswerAnalyzers
|
|||
# { "score": 3, "count": 1 }
|
||||
# ]
|
||||
# }
|
||||
def run(question_data, responses)
|
||||
full_credit = question_data[:points_possible].to_f
|
||||
def run(responses)
|
||||
full_credit = @question_data[:points_possible].to_f
|
||||
|
||||
stats = {}
|
||||
stats[:graded] = responses.select { |r| r[:correct] == 'defined' }.length
|
||||
|
@ -60,11 +63,20 @@ module CanvasQuizStatistics::AnswerAnalyzers
|
|||
|
||||
stats[:point_distribution] = point_distribution.keys.map do |score|
|
||||
{ score: score, count: point_distribution[score] }
|
||||
end
|
||||
end.sort_by { |v| v[:score] || -1 }
|
||||
|
||||
stats[:point_distribution].sort_by! { |v| v[:score] || -1 }
|
||||
stats[:responses] = responses.select(&method(:answer_present?)).length
|
||||
|
||||
stats
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
# Test whether the response contains an answer to the question.
|
||||
#
|
||||
# Default behavior is to text whether the "text" field is populated.
|
||||
def answer_present?(response)
|
||||
response[:text].present?
|
||||
end
|
||||
end
|
||||
end
|
|
@ -1,39 +0,0 @@
|
|||
#
|
||||
# Copyright (C) 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/>.
|
||||
#
|
||||
require 'canvas_quiz_statistics/answer_analyzers/util'
|
||||
require 'canvas_quiz_statistics/answer_analyzers/base'
|
||||
require 'canvas_quiz_statistics/answer_analyzers/essay'
|
||||
|
||||
module CanvasQuizStatistics::AnswerAnalyzers
|
||||
# Convenient access to analyzer for a given question type, e.g:
|
||||
#
|
||||
# AnswerAnalyzers['multiple_choice_question'].new
|
||||
#
|
||||
# If the question type is not supported, you will be given the Base
|
||||
# analyzer which really does nothing.
|
||||
def self.[](question_type)
|
||||
AVAILABLE_ANALYZERS[question_type] || Base
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
# for fast lookup without having to use #constantize or anything
|
||||
AVAILABLE_ANALYZERS = {
|
||||
'essay_question' => Essay
|
||||
}
|
||||
end
|
|
@ -1,31 +0,0 @@
|
|||
#
|
||||
# Copyright (C) 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/>.
|
||||
#
|
||||
module CanvasQuizStatistics::AnswerAnalyzers
|
||||
class Base
|
||||
def run(question_data, responses)
|
||||
{}
|
||||
end
|
||||
|
||||
# Test whether the response contains an answer to the question.
|
||||
#
|
||||
# Default behavior is to text whether the "text" field is populated.
|
||||
def answer_present?(response)
|
||||
response[:text].present?
|
||||
end
|
||||
end
|
||||
end
|
|
@ -1,24 +0,0 @@
|
|||
#
|
||||
# Copyright (C) 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/>.
|
||||
#
|
||||
module CanvasQuizStatistics::AnswerAnalyzers
|
||||
module Util
|
||||
def self.digest(str)
|
||||
Digest::MD5.hexdigest((str || '').to_s.strip)
|
||||
end
|
||||
end
|
||||
end
|
|
@ -1,61 +0,0 @@
|
|||
#
|
||||
# Copyright (C) 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/>.
|
||||
#
|
||||
|
||||
require 'active_support/core_ext'
|
||||
|
||||
class CanvasQuizStatistics::QuestionAnalyzer
|
||||
attr_reader :question_data
|
||||
|
||||
def initialize(question_data)
|
||||
@question_data = question_data
|
||||
@answer_analyzer = AnswerAnalyzers[question_data[:question_type].to_s].new
|
||||
end
|
||||
|
||||
# Gathers all types of stats from a set of student responses.
|
||||
#
|
||||
# The output will contain the output of an answer analyzer.
|
||||
#
|
||||
# @return [Hash]
|
||||
# {
|
||||
# // Number of students who have answered this question.
|
||||
# "responses": 2,
|
||||
#
|
||||
# // IDs of those students.
|
||||
# "user_ids": [ 1, 133 ]
|
||||
# }
|
||||
def run(responses)
|
||||
{}.tap do |stats|
|
||||
stats.merge! @answer_analyzer.run(question_data, responses)
|
||||
stats.merge! count_filled_responses(responses)
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
AnswerAnalyzers = CanvasQuizStatistics::AnswerAnalyzers
|
||||
|
||||
# Returns the number of and IDs of students who provided any kind of answer,
|
||||
# regardless of whether it's correct or not.
|
||||
def count_filled_responses(responses)
|
||||
answers = responses.select do |response|
|
||||
@answer_analyzer.answer_present?(response)
|
||||
end
|
||||
|
||||
{ responses: answers.size, user_ids: answers.map { |a| a[:user_id] }.uniq }
|
||||
end
|
||||
end
|
|
@ -0,0 +1,91 @@
|
|||
require 'spec_helper'
|
||||
|
||||
describe CanvasQuizStatistics::Analyzers::Base do
|
||||
Base = CanvasQuizStatistics::Analyzers::Base
|
||||
subject { described_class.new({}) }
|
||||
|
||||
describe 'DSL' do
|
||||
def unset(*klasses)
|
||||
klasses.each do |klass|
|
||||
Object.send(:remove_const, klass.name.demodulize)
|
||||
Base.metrics[klass.question_type] = []
|
||||
end
|
||||
end
|
||||
|
||||
describe '#metric' do
|
||||
it 'should define a metric calculator' do
|
||||
class Apple < Base
|
||||
metric :something do |responses|
|
||||
responses.size
|
||||
end
|
||||
end
|
||||
|
||||
Apple.new({}).run([ {}, {} ]).should == { something: 2 }
|
||||
|
||||
unset Apple
|
||||
end
|
||||
|
||||
it 'should not conflict with other analyzer metrics' do
|
||||
class Apple < Base
|
||||
metric :something do |responses|
|
||||
responses.size
|
||||
end
|
||||
end
|
||||
|
||||
class Orange < Base
|
||||
metric :something_else do |responses|
|
||||
responses.size
|
||||
end
|
||||
end
|
||||
|
||||
Apple.new({}).run([{}]).should == { something: 1 }
|
||||
Orange.new({}).run([{}]).should == { something_else: 1 }
|
||||
|
||||
unset Apple, Orange
|
||||
end
|
||||
|
||||
describe 'with context dependencies' do
|
||||
it 'should invoke the context builder and parse dependency' do
|
||||
class Apple < Base
|
||||
def build_context(responses)
|
||||
{ colors: responses.map { |r| r[:color] } }
|
||||
end
|
||||
|
||||
metric something: [ :colors ] do |responses, colors|
|
||||
colors.join(', ')
|
||||
end
|
||||
end
|
||||
|
||||
responses = [{ color: 'Red' }, { color: 'Green' }]
|
||||
|
||||
Apple.new({}).run(responses).should == { something: 'Red, Green' }
|
||||
|
||||
unset Apple
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#inherit_metrics' do
|
||||
it 'should inherit a parent class metrics' do
|
||||
class Apple < Base
|
||||
metric :something do |responses|
|
||||
responses.size
|
||||
end
|
||||
end
|
||||
|
||||
class Orange < Apple
|
||||
inherit_metrics :apple_question
|
||||
|
||||
metric :something_else do |responses|
|
||||
responses.size
|
||||
end
|
||||
end
|
||||
|
||||
Apple.new({}).run([{}]).should == { something: 1 }
|
||||
Orange.new({}).run([{}]).should == { something: 1, something_else: 1 }
|
||||
|
||||
unset Apple, Orange
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -1,23 +1,43 @@
|
|||
require 'spec_helper'
|
||||
|
||||
describe CanvasQuizStatistics::AnswerAnalyzers::Essay do
|
||||
describe CanvasQuizStatistics::Analyzers::Essay do
|
||||
let(:question_data) { QuestionHelpers.fixture('essay_question') }
|
||||
subject { described_class.new(question_data) }
|
||||
|
||||
it 'should not blow up when no responses are provided' do
|
||||
expect {
|
||||
subject.run(question_data, []).should be_present
|
||||
subject.run([]).should be_present
|
||||
}.to_not raise_error
|
||||
end
|
||||
|
||||
it "should not consider an answer to be present if it's empty" do
|
||||
subject.answer_present?({ text: nil }).should be_false
|
||||
subject.answer_present?({ text: '' }).should be_false
|
||||
end
|
||||
|
||||
describe 'output [#run]' do
|
||||
describe '[:responses]' do
|
||||
it 'should count students who have written anything' do
|
||||
subject.run([{ text: 'foo' }])[:responses].should == 1
|
||||
end
|
||||
|
||||
it 'should not count students who have written a blank response' do
|
||||
subject.run([{ }])[:responses].should == 0
|
||||
subject.run([{ text: nil }])[:responses].should == 0
|
||||
subject.run([{ text: '' }])[:responses].should == 0
|
||||
end
|
||||
end
|
||||
|
||||
it ':graded - should reflect the number of graded answers' do
|
||||
output = subject.run([
|
||||
{ correct: 'defined' }, { correct: 'undefined' }
|
||||
])
|
||||
|
||||
output[:graded].should == 1
|
||||
end
|
||||
|
||||
describe ':full_credit' do
|
||||
let :question_data do
|
||||
{ points_possible: 3 }
|
||||
end
|
||||
|
||||
it 'should count all students who received full credit' do
|
||||
output = subject.run(question_data, [
|
||||
output = subject.run([
|
||||
{ points: 3 }, { points: 2 }, { points: 3 }
|
||||
])
|
||||
|
||||
|
@ -25,7 +45,7 @@ describe CanvasQuizStatistics::AnswerAnalyzers::Essay do
|
|||
end
|
||||
|
||||
it 'should count students who received more than full credit' do
|
||||
output = subject.run(question_data, [
|
||||
output = subject.run([
|
||||
{ points: 3 }, { points: 2 }, { points: 5 }
|
||||
])
|
||||
|
||||
|
@ -33,25 +53,22 @@ describe CanvasQuizStatistics::AnswerAnalyzers::Essay do
|
|||
end
|
||||
|
||||
it 'should be 0 otherwise' do
|
||||
output = subject.run(question_data, [
|
||||
output = subject.run([
|
||||
{ points: 1 }
|
||||
])
|
||||
|
||||
output[:full_credit].should == 0
|
||||
end
|
||||
end
|
||||
|
||||
it ':graded - should reflect the number of graded answers' do
|
||||
output = subject.run(question_data, [
|
||||
{ correct: 'defined' }, { correct: 'undefined' }
|
||||
])
|
||||
|
||||
output[:graded].should == 1
|
||||
it 'should count those who exceed the maximum points possible' do
|
||||
output = subject.run([{ points: 5 }])
|
||||
output[:full_credit].should == 1
|
||||
end
|
||||
end
|
||||
|
||||
describe ':point_distribution' do
|
||||
it 'should map each score to the number of receivers' do
|
||||
output = subject.run(question_data, [
|
||||
output = subject.run([
|
||||
{ points: 1, user_id: 1 },
|
||||
{ points: 3, user_id: 2 }, { points: 3, user_id: 3 },
|
||||
{ points: nil, user_id: 5 }
|
||||
|
@ -63,7 +80,7 @@ describe CanvasQuizStatistics::AnswerAnalyzers::Essay do
|
|||
end
|
||||
|
||||
it 'should sort them in score ascending mode' do
|
||||
output = subject.run(question_data, [
|
||||
output = subject.run([
|
||||
{ points: 3, user_id: 2 }, { points: 3, user_id: 3 },
|
||||
{ points: 1, user_id: 1 },
|
||||
{ points: nil, user_id: 5 }
|
|
@ -1,17 +0,0 @@
|
|||
require 'spec_helper'
|
||||
|
||||
describe CanvasQuizStatistics::AnswerAnalyzers::Base do
|
||||
describe '#run' do
|
||||
it 'returns an empty set' do
|
||||
subject.run({}, []).should == {}
|
||||
end
|
||||
end
|
||||
|
||||
describe '#answer_present?' do
|
||||
it 'defaults to testing whether :text is present' do
|
||||
subject.answer_present?({ text: 'foo' }).should be_true
|
||||
subject.answer_present?({}).should be_false
|
||||
subject.answer_present?({ text: nil }).should be_false
|
||||
end
|
||||
end
|
||||
end
|
|
@ -1,7 +1,7 @@
|
|||
require 'spec_helper'
|
||||
|
||||
describe CanvasQuizStatistics::AnswerAnalyzers do
|
||||
Analyzers = CanvasQuizStatistics::AnswerAnalyzers
|
||||
describe CanvasQuizStatistics::Analyzers do
|
||||
Analyzers = CanvasQuizStatistics::Analyzers
|
||||
|
||||
describe '[]' do
|
||||
it 'should locate an analyzer' do
|
||||
|
|
|
@ -1,43 +0,0 @@
|
|||
require 'spec_helper'
|
||||
|
||||
describe CanvasQuizStatistics::QuestionAnalyzer do
|
||||
subject do
|
||||
described_class.new(@question_data || { })
|
||||
end
|
||||
|
||||
it 'should not break with no responses' do
|
||||
expect { subject.run([]) }.to_not raise_error
|
||||
end
|
||||
|
||||
it 'should embed the output of the answer analyzer' do
|
||||
@question_data = {
|
||||
question_type: 'stubbed_question'
|
||||
}
|
||||
responses = [{ user_id: 1, text: 'foobar' }]
|
||||
answer_analysis = { some_metric: 12 }
|
||||
|
||||
analyzer = double
|
||||
analyzer.should_receive(:run).with(@question_data, responses).and_return(answer_analysis)
|
||||
analyzer.should_receive(:answer_present?).and_return(true)
|
||||
|
||||
analyzer_generator = double
|
||||
analyzer_generator.should_receive(:new).and_return(analyzer)
|
||||
CanvasQuizStatistics::AnswerAnalyzers.stub :[] => analyzer_generator
|
||||
|
||||
output = subject.run(responses)
|
||||
output[:some_metric].should == 12
|
||||
end
|
||||
|
||||
describe ':answered' do
|
||||
it ':count - the number of students who provided an answer' do
|
||||
output = subject.run([
|
||||
{ text: 'hi', user_id: 1 },
|
||||
{ text: 'hello', user_id: 3 }
|
||||
])
|
||||
|
||||
# answer_analyzer.stub(:answer_present?).and_return(true)
|
||||
output[:responses].should == 2
|
||||
output[:user_ids].should == [1,3]
|
||||
end
|
||||
end
|
||||
end
|
|
@ -319,21 +319,19 @@ describe Quizzes::QuizStatistics::StudentAnalysis do
|
|||
describe 'question statistics' do
|
||||
subject { Quizzes::QuizStatistics::StudentAnalysis.new({}) }
|
||||
|
||||
it 'should proxy to the gem for Essay question stats' do
|
||||
it 'should proxy to CanvasQuizStatistics for supported questions' do
|
||||
question_data = { question_type: 'essay_question' }
|
||||
responses = []
|
||||
stats = { foo: 'bar' }
|
||||
analyzer = stub
|
||||
|
||||
CanvasQuizStatistics::QuestionAnalyzer.
|
||||
expects(:new).with(question_data).
|
||||
returns(analyzer)
|
||||
CanvasQuizStatistics.
|
||||
expects(:analyze).
|
||||
with(question_data, responses).
|
||||
returns({ some_metric: 5 })
|
||||
|
||||
analyzer.expects(:run).with(responses).returns(stats)
|
||||
output = subject.send(:stats_for_question, question_data, responses)
|
||||
output.should == {
|
||||
question_type: 'essay_question',
|
||||
foo: 'bar'
|
||||
some_metric: 5
|
||||
}.with_indifferent_access
|
||||
end
|
||||
end
|
||||
|
|
Loading…
Reference in New Issue