make quiz_statistics model
refs CNVS-4887 This probably should have happened anyway, but the intermediate model will be necessary to handle attachments for the downloadable quiz_statistics.csv Test plan: * make sure the quiz statistics page still works * make sure downloading quiz statistics csv still works Change-Id: I9562a731d171dae24329fc52782a4f9efa4cf8bd Reviewed-on: https://gerrit.instructure.com/18977 Tested-by: Jenkins <jenkins@instructure.com> QA-Review: Myller de Araujo <myller@instructure.com> Reviewed-by: Simon Williams <simon@instructure.com> Product-Review: Simon Williams <simon@instructure.com>
This commit is contained in:
parent
6601753eb7
commit
7a3b4ec1c4
|
@ -40,6 +40,7 @@ class Quiz < ActiveRecord::Base
|
|||
has_many :quiz_questions, :dependent => :destroy, :order => 'position'
|
||||
has_many :quiz_submissions, :dependent => :destroy
|
||||
has_many :quiz_groups, :dependent => :destroy, :order => 'position'
|
||||
has_many :quiz_statistics, :class_name => 'QuizStatistics', :order => 'created_at'
|
||||
belongs_to :context, :polymorphic => true
|
||||
belongs_to :assignment
|
||||
belongs_to :cloned_item
|
||||
|
@ -875,293 +876,17 @@ class Quiz < ActiveRecord::Base
|
|||
end
|
||||
end
|
||||
|
||||
def submissions_for_statistics(include_all_versions=true)
|
||||
ActiveRecord::Base::ConnectionSpecification.with_environment(:slave) do
|
||||
for_users = context.student_ids
|
||||
scope = self.quiz_submissions.where(:user_id => for_users)
|
||||
scope = scope.includes(:versions) if include_all_versions
|
||||
scope.map { |qs|
|
||||
include_all_versions ?
|
||||
qs.submitted_versions :
|
||||
[qs.latest_submitted_version].compact
|
||||
}.flatten.
|
||||
select{ |s| s.completed? && s.submission_data.is_a?(Array) }.
|
||||
sort { |a,b| b.updated_at <=> a.updated_at }
|
||||
end
|
||||
def statistics(include_all_versions = true)
|
||||
quiz_statistics.build(
|
||||
:includes_all_versions => include_all_versions
|
||||
).generate
|
||||
end
|
||||
|
||||
def statistics_csv(options={})
|
||||
options ||= {}
|
||||
columns = []
|
||||
columns << t('statistics.csv_columns.name', 'name') unless options[:anonymous]
|
||||
columns << t('statistics.csv_columns.id', 'id') unless options[:anonymous]
|
||||
columns << t('statistics.csv_columns.sis_id', 'sis_id') unless options[:anonymous]
|
||||
columns << t('statistics.csv_columns.section', 'section')
|
||||
columns << t('statistics.csv_columns.section_id', 'section_id')
|
||||
columns << t('statistics.csv_columns.section_sis_id', 'section_sis_id')
|
||||
columns << t('statistics.csv_columns.submitted', 'submitted')
|
||||
columns << t('statistics.csv_columns.attempt', 'attempt') if options[:include_all_versions]
|
||||
first_question_index = columns.length
|
||||
submissions = submissions_for_statistics(options[:include_all_versions])
|
||||
found_question_ids = {}
|
||||
quiz_datas = [quiz_data] + submissions.map(&:quiz_data)
|
||||
quiz_datas.each do |quiz_data|
|
||||
quiz_data.each do |question|
|
||||
next if question['entry_type'] == 'quiz_group'
|
||||
if !found_question_ids[question[:id]]
|
||||
columns << "#{question[:id]}: #{strip_tags(question[:question_text])}"
|
||||
columns << question[:points_possible]
|
||||
found_question_ids[question[:id]] = true
|
||||
end
|
||||
end
|
||||
end
|
||||
last_question_index = columns.length - 1
|
||||
columns << t('statistics.csv_columns.n_correct', 'n correct')
|
||||
columns << t('statistics.csv_columns.n_incorrect', 'n incorrect')
|
||||
columns << t('statistics.csv_columns.score', 'score')
|
||||
rows = []
|
||||
submissions.each do |submission|
|
||||
row = []
|
||||
row << submission.user.name unless options[:anonymous]
|
||||
row << submission.user_id unless options[:anonymous]
|
||||
row << submission.user.sis_pseudonym_for(context.account).try(:sis_user_id) unless options[:anonymous]
|
||||
section_name = []
|
||||
section_id = []
|
||||
section_sis_id = []
|
||||
enrollments = submission.quiz.context.student_enrollments.active.where(:user_id => submission.user_id).each do |enrollment|
|
||||
section_name << enrollment.course_section.name
|
||||
section_id << enrollment.course_section.id
|
||||
section_sis_id << enrollment.course_section.try(:sis_source_id)
|
||||
end
|
||||
row << section_name.join(", ")
|
||||
row << section_id.join(", ")
|
||||
row << section_sis_id.join(", ")
|
||||
row << submission.finished_at
|
||||
row << submission.attempt if options[:include_all_versions]
|
||||
columns[first_question_index..last_question_index].each do |id|
|
||||
next unless id.is_a?(String)
|
||||
id = id.to_i
|
||||
answer = submission.submission_data.detect{|a| a[:question_id] == id }
|
||||
question = submission.quiz_data.detect{|q| q[:id] == id}
|
||||
unless question
|
||||
# if this submission didn't answer this question, fill in with blanks
|
||||
row << ''
|
||||
row << ''
|
||||
next
|
||||
end
|
||||
strip_html_answers(question)
|
||||
answer_item = question && question[:answers].detect{|a| a[:id] == answer[:answer_id]}
|
||||
answer_item ||= answer
|
||||
if question[:question_type] == 'fill_in_multiple_blanks_question'
|
||||
blank_ids = question[:answers].map{|a| a[:blank_id] }.uniq
|
||||
row << blank_ids.map{|blank_id| answer["answer_for_#{blank_id}".to_sym].try(:gsub, /,/, '\,') }.compact.join(',')
|
||||
elsif question[:question_type] == 'multiple_answers_question'
|
||||
row << question[:answers].map{|a| answer["answer_#{a[:id]}".to_sym] == '1' ? a[:text].gsub(/,/, '\,') : nil }.compact.join(',')
|
||||
elsif question[:question_type] == 'multiple_dropdowns_question'
|
||||
blank_ids = question[:answers].map{|a| a[:blank_id] }.uniq
|
||||
answer_ids = blank_ids.map{|blank_id| answer["answer_for_#{blank_id}".to_sym] }
|
||||
row << answer_ids.map{|id| (question[:answers].detect{|a| a[:id] == id } || {})[:text].try(:gsub, /,/, '\,' ) }.compact.join(',')
|
||||
elsif question[:question_type] == 'calculated_question'
|
||||
list = question[:answers][0][:variables].map{|a| [a[:name],a[:value].to_s].map{|str| str.gsub(/=>/, '\=>') }.join('=>') }
|
||||
list << answer[:text]
|
||||
row << list.map{|str| (str || '').gsub(/,/, '\,') }.join(',')
|
||||
elsif question[:question_type] == 'matching_question'
|
||||
answer_ids = question[:answers].map{|a| a[:id] }
|
||||
answer_and_matches = answer_ids.map{|id| [id, answer["answer_#{id}".to_sym].to_i] }
|
||||
row << answer_and_matches.map{|id, match_id|
|
||||
res = []
|
||||
res << (question[:answers].detect{|a| a[:id] == id } || {})[:text]
|
||||
match = question[:matches].detect{|m| m[:match_id] == match_id } || question[:answers].detect{|m| m[:match_id] == match_id} || {}
|
||||
res << (match[:right] || match[:text])
|
||||
res.map{|s| (s || '').gsub(/=>/, '\=>')}.join('=>').gsub(/,/, '\,')
|
||||
}.join(',')
|
||||
elsif question[:question_type] == 'numerical_question'
|
||||
row << (answer && answer[:text])
|
||||
else
|
||||
row << ((answer_item && answer_item[:text]) || '')
|
||||
end
|
||||
row << (answer ? answer[:points] : "")
|
||||
end
|
||||
row << submission.submission_data.select{|a| a[:correct] }.length
|
||||
row << submission.submission_data.reject{|a| a[:correct] }.length
|
||||
row << submission.score
|
||||
rows << row
|
||||
end
|
||||
FasterCSV.generate do |csv|
|
||||
columns.each_with_index do |val, idx|
|
||||
r = []
|
||||
r << val
|
||||
r << ''
|
||||
rows.each do |row|
|
||||
r << row[idx]
|
||||
end
|
||||
csv << r
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# returns a blob of stats junk like this:
|
||||
# {
|
||||
# :multiple_attempts_exist=>false,
|
||||
# :submission_user_ids=>#<Set: {2, ...}>,
|
||||
# :unique_submission_count=>50,
|
||||
# :submission_score_average=>5,
|
||||
# :submission_score_high=>10,
|
||||
# :submission_score_low=>0,
|
||||
# :submission_duration_average=>124,
|
||||
# :submission_score_stdev=>0,
|
||||
# :submission_incorrect_count_average=>3,
|
||||
# :submission_correct_count_average=>1,
|
||||
# :questions=>
|
||||
# [output of stats_for_question for every question in submission_data]
|
||||
def statistics(include_all_versions=true)
|
||||
submissions = submissions_for_statistics(include_all_versions)
|
||||
# questions: questions from quiz#quiz_data
|
||||
#{1022=>
|
||||
# {"id"=>1022,
|
||||
# "points_possible"=>1,
|
||||
# "question_type"=>"numerical_question",
|
||||
# "question_name"=>"Really Hard Question",
|
||||
# "name"=>"Really Hard Question",
|
||||
# "answers"=> [{"id"=>6782},...],
|
||||
# "assessment_question_id"=>1022,
|
||||
# }, ...}
|
||||
questions = Hash[
|
||||
(quiz_data || []).map { |q| q[:questions] || q }.
|
||||
flatten.
|
||||
select { |q| q[:answers] }.
|
||||
map { |q| [q[:id], q] }
|
||||
]
|
||||
stats = {}
|
||||
found_ids = {}
|
||||
score_counter = Stats::Counter.new
|
||||
questions_hash = {}
|
||||
stats[:questions] = []
|
||||
stats[:multiple_attempts_exist] = submissions.any?{|s| s.attempt && s.attempt > 1 }
|
||||
stats[:submission_user_ids] = Set.new
|
||||
stats[:unique_submission_count] = 0
|
||||
correct_cnt = incorrect_cnt = total_duration = 0
|
||||
submissions.each do |sub|
|
||||
stats[:submission_user_ids] << sub.user_id if sub.user_id > 0
|
||||
if !found_ids[sub.id]
|
||||
stats[:unique_submission_count] += 1
|
||||
found_ids[sub.id] = true
|
||||
end
|
||||
answers = sub.submission_data || []
|
||||
next unless answers.is_a?(Array)
|
||||
points = answers.map{|a| a[:points] }.sum
|
||||
score_counter << points
|
||||
correct_cnt += answers.count{|a| a[:correct] == true }
|
||||
incorrect_cnt += answers.count{|a| a[:correct] == false }
|
||||
total_duration += ((sub.finished_at - sub.started_at).to_i rescue 30)
|
||||
sub.quiz_data.each do |question|
|
||||
questions_hash[question[:id]] ||= question
|
||||
end
|
||||
end
|
||||
stats[:submission_score_average] = score_counter.mean
|
||||
stats[:submission_score_high] = score_counter.max
|
||||
stats[:submission_score_low] = score_counter.min
|
||||
stats[:submission_score_stdev] = score_counter.standard_deviation
|
||||
if submissions.size > 0
|
||||
stats[:submission_correct_count_average] = correct_cnt.to_f / submissions.size
|
||||
stats[:submission_incorrect_count_average] = incorrect_cnt.to_f / submissions.size
|
||||
stats[:submission_duration_average] = total_duration.to_f / submissions.size
|
||||
else
|
||||
stats[:submission_correct_count_average] =
|
||||
stats[:submission_incorrect_count_average] =
|
||||
stats[:submission_duration_average] = 0
|
||||
end
|
||||
|
||||
assessment_questions = if questions_hash.any? { |_,q| q[:assessment_question_id] }
|
||||
Hash[
|
||||
AssessmentQuestion.where(:id => questions_hash.keys).
|
||||
map { |aq| [aq.id, aq] }
|
||||
]
|
||||
else
|
||||
{}
|
||||
end
|
||||
responses_for_question = {}
|
||||
submissions.each do |s|
|
||||
s.submission_data.each do |a|
|
||||
q_id = a[:question_id]
|
||||
a[:user_id] = s.user_id
|
||||
responses_for_question[q_id] ||= []
|
||||
responses_for_question[q_id] << a
|
||||
end
|
||||
end
|
||||
|
||||
questions_hash.keys.each do |id|
|
||||
obj = questions[id]
|
||||
unless obj
|
||||
obj = questions_hash[id]
|
||||
if obj[:assessment_question_id]
|
||||
aq_name = assessment_questions[obj[:assessment_question_id]].try(:name)
|
||||
obj[:name] = aq_name if aq_name
|
||||
end
|
||||
end
|
||||
if obj[:answers] && obj[:question_type] != 'text_only_question'
|
||||
stat = stats_for_question(obj, responses_for_question[obj[:id]])
|
||||
stats[:questions] << ['question', stat]
|
||||
end
|
||||
end
|
||||
|
||||
stats
|
||||
end
|
||||
|
||||
# takes a question hash from Quiz/Submission#quiz_data, and a set of
|
||||
# responses (from Submission#submission_data)
|
||||
#
|
||||
# returns:
|
||||
# ["question",
|
||||
# {"points_possible"=>1,
|
||||
# "question_type"=>"multiple_choice_question",
|
||||
# "question_name"=>"Some Question",
|
||||
# "name"=>"Some Question",
|
||||
# "question_text"=>"<p>Blah blah blah?</p>",
|
||||
# "answers"=>
|
||||
# [{"text"=>"blah",
|
||||
# "comments"=>"",
|
||||
# "weight"=>100,
|
||||
# "id"=>8379,
|
||||
# "responses"=>2,
|
||||
# "user_ids"=>[2, 3]},
|
||||
# {"text"=>"blarb",
|
||||
# "weight"=>0,
|
||||
# "id"=>8153,
|
||||
# "responses"=>1,
|
||||
# "user_ids"=>[1]}],
|
||||
# "assessment_question_id"=>1017,
|
||||
# "id"=>1017,
|
||||
# "responses"=>3,
|
||||
# "response_values"=>[...],
|
||||
# "unexpected_response_values"=>[],
|
||||
# "user_ids"=>[1,2,3],
|
||||
# "multiple_responses"=>false}],
|
||||
def stats_for_question(question, responses)
|
||||
question[:responses] = 0
|
||||
question[:response_values] = []
|
||||
question[:unexpected_response_values] = []
|
||||
question[:user_ids] = []
|
||||
question[:answers].each { |a|
|
||||
a[:responses] = 0
|
||||
a[:user_ids] = []
|
||||
}
|
||||
strip_html_answers(question)
|
||||
|
||||
question[:user_ids] = responses.map { |r| r[:user_id] }
|
||||
question[:response_values] = responses.map { |r| r[:text] }
|
||||
question[:responses] = responses.size
|
||||
|
||||
question = QuizQuestion::Base.from_question_data(question).stats(responses)
|
||||
none = {
|
||||
:responses => question[:responses] - question[:answers].map{|a| a[:responses] || 0}.sum,
|
||||
:id => "none",
|
||||
:weight => 0,
|
||||
:text => t('statistics.no_answer', "No Answer"),
|
||||
:user_ids => question[:user_ids] - question[:answers].map{|a| a[:user_ids] }.flatten
|
||||
} rescue nil
|
||||
question[:answers] << none if none && none[:responses] > 0
|
||||
question
|
||||
quiz_statistics.create!(
|
||||
:includes_all_versions => options[:include_all_versions],
|
||||
:anonymous => options[:anonymous]
|
||||
).to_csv
|
||||
end
|
||||
|
||||
def unpublished_changes?
|
||||
|
|
|
@ -0,0 +1,326 @@
|
|||
#
|
||||
# Copyright (C) 2011 - 2012 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 QuizStatistics < ActiveRecord::Base
|
||||
include TextHelper
|
||||
|
||||
attr_accessible :includes_all_versions, :anonymous
|
||||
|
||||
belongs_to :quiz
|
||||
|
||||
set_table_name :quiz_statistics
|
||||
|
||||
# returns a blob of stats junk like this:
|
||||
# {
|
||||
# :multiple_attempts_exist=>false,
|
||||
# :submission_user_ids=>#<Set: {2, ...}>,
|
||||
# :unique_submission_count=>50,
|
||||
# :submission_score_average=>5,
|
||||
# :submission_score_high=>10,
|
||||
# :submission_score_low=>0,
|
||||
# :submission_duration_average=>124,
|
||||
# :submission_score_stdev=>0,
|
||||
# :submission_incorrect_count_average=>3,
|
||||
# :submission_correct_count_average=>1,
|
||||
# :questions=>
|
||||
# [output of stats_for_question for every question in submission_data]
|
||||
def generate
|
||||
submissions = submissions_for_statistics
|
||||
# questions: questions from quiz#quiz_data
|
||||
#{1022=>
|
||||
# {"id"=>1022,
|
||||
# "points_possible"=>1,
|
||||
# "question_type"=>"numerical_question",
|
||||
# "question_name"=>"Really Hard Question",
|
||||
# "name"=>"Really Hard Question",
|
||||
# "answers"=> [{"id"=>6782},...],
|
||||
# "assessment_question_id"=>1022,
|
||||
# }, ...}
|
||||
questions = Hash[
|
||||
(quiz.quiz_data || []).map { |q| q[:questions] || q }.
|
||||
flatten.
|
||||
select { |q| q[:answers] }.
|
||||
map { |q| [q[:id], q] }
|
||||
]
|
||||
stats = {}
|
||||
found_ids = {}
|
||||
score_counter = Stats::Counter.new
|
||||
questions_hash = {}
|
||||
stats[:questions] = []
|
||||
stats[:multiple_attempts_exist] = submissions.any?{ |s|
|
||||
s.attempt && s.attempt > 1
|
||||
}
|
||||
stats[:submission_user_ids] = Set.new
|
||||
stats[:unique_submission_count] = 0
|
||||
correct_cnt = incorrect_cnt = total_duration = 0
|
||||
submissions.each do |sub|
|
||||
stats[:submission_user_ids] << sub.user_id if sub.user_id > 0
|
||||
if !found_ids[sub.id]
|
||||
stats[:unique_submission_count] += 1
|
||||
found_ids[sub.id] = true
|
||||
end
|
||||
answers = sub.submission_data || []
|
||||
next unless answers.is_a?(Array)
|
||||
points = answers.map{ |a| a[:points] }.sum
|
||||
score_counter << points
|
||||
correct_cnt += answers.count{ |a| a[:correct] == true }
|
||||
incorrect_cnt += answers.count{ |a| a[:correct] == false }
|
||||
total_duration += ((sub.finished_at - sub.started_at).to_i rescue 30)
|
||||
sub.quiz_data.each do |question|
|
||||
questions_hash[question[:id]] ||= question
|
||||
end
|
||||
end
|
||||
stats[:submission_score_average] = score_counter.mean
|
||||
stats[:submission_score_high] = score_counter.max
|
||||
stats[:submission_score_low] = score_counter.min
|
||||
stats[:submission_score_stdev] = score_counter.standard_deviation
|
||||
if submissions.size > 0
|
||||
stats[:submission_correct_count_average] = correct_cnt.to_f / submissions.size
|
||||
stats[:submission_incorrect_count_average] = incorrect_cnt.to_f / submissions.size
|
||||
stats[:submission_duration_average] = total_duration.to_f / submissions.size
|
||||
else
|
||||
stats[:submission_correct_count_average] =
|
||||
stats[:submission_incorrect_count_average] =
|
||||
stats[:submission_duration_average] = 0
|
||||
end
|
||||
|
||||
assessment_questions = if questions_hash.any? { |_,q| q[:assessment_question_id] }
|
||||
Hash[
|
||||
AssessmentQuestion.where(:id => questions_hash.keys).
|
||||
map { |aq| [aq.id, aq] }
|
||||
]
|
||||
else
|
||||
{}
|
||||
end
|
||||
responses_for_question = {}
|
||||
submissions.each do |s|
|
||||
s.submission_data.each do |a|
|
||||
q_id = a[:question_id]
|
||||
a[:user_id] = s.user_id
|
||||
responses_for_question[q_id] ||= []
|
||||
responses_for_question[q_id] << a
|
||||
end
|
||||
end
|
||||
|
||||
questions_hash.keys.each do |id|
|
||||
obj = questions[id]
|
||||
unless obj
|
||||
obj = questions_hash[id]
|
||||
if obj[:assessment_question_id]
|
||||
aq_name = assessment_questions[obj[:assessment_question_id]].try(:name)
|
||||
obj[:name] = aq_name if aq_name
|
||||
end
|
||||
end
|
||||
if obj[:answers] && obj[:question_type] != 'text_only_question'
|
||||
stat = stats_for_question(obj, responses_for_question[obj[:id]])
|
||||
stats[:questions] << ['question', stat]
|
||||
end
|
||||
end
|
||||
|
||||
stats
|
||||
end
|
||||
|
||||
def to_csv
|
||||
columns = []
|
||||
columns << t('statistics.csv_columns.name', 'name') unless anonymous?
|
||||
columns << t('statistics.csv_columns.id', 'id') unless anonymous?
|
||||
columns << t('statistics.csv_columns.sis_id', 'sis_id') unless anonymous?
|
||||
columns << t('statistics.csv_columns.section', 'section')
|
||||
columns << t('statistics.csv_columns.section_id', 'section_id')
|
||||
columns << t('statistics.csv_columns.section_sis_id', 'section_sis_id')
|
||||
columns << t('statistics.csv_columns.submitted', 'submitted')
|
||||
columns << t('statistics.csv_columns.attempt', 'attempt') if includes_all_versions?
|
||||
first_question_index = columns.length
|
||||
submissions = submissions_for_statistics
|
||||
found_question_ids = {}
|
||||
quiz_datas = [quiz.quiz_data] + submissions.map(&:quiz_data)
|
||||
quiz_datas.each do |quiz_data|
|
||||
quiz_data.each do |question|
|
||||
next if question['entry_type'] == 'quiz_group'
|
||||
if !found_question_ids[question[:id]]
|
||||
columns << "#{question[:id]}: #{strip_tags(question[:question_text])}"
|
||||
columns << question[:points_possible]
|
||||
found_question_ids[question[:id]] = true
|
||||
end
|
||||
end
|
||||
end
|
||||
last_question_index = columns.length - 1
|
||||
columns << t('statistics.csv_columns.n_correct', 'n correct')
|
||||
columns << t('statistics.csv_columns.n_incorrect', 'n incorrect')
|
||||
columns << t('statistics.csv_columns.score', 'score')
|
||||
rows = []
|
||||
submissions.each do |submission|
|
||||
row = []
|
||||
row << submission.user.name unless anonymous?
|
||||
row << submission.user_id unless anonymous?
|
||||
row << submission.user.sis_pseudonym_for(quiz.context.account).try(:sis_user_id) unless anonymous?
|
||||
section_name = []
|
||||
section_id = []
|
||||
section_sis_id = []
|
||||
submission.quiz.context.student_enrollments.active.where(:user_id => submission.user_id).each do |enrollment|
|
||||
section_name << enrollment.course_section.name
|
||||
section_id << enrollment.course_section.id
|
||||
section_sis_id << enrollment.course_section.try(:sis_source_id)
|
||||
end
|
||||
row << section_name.join(", ")
|
||||
row << section_id.join(", ")
|
||||
row << section_sis_id.join(", ")
|
||||
row << submission.finished_at
|
||||
row << submission.attempt if includes_all_versions?
|
||||
columns[first_question_index..last_question_index].each do |id|
|
||||
next unless id.is_a?(String)
|
||||
id = id.to_i
|
||||
answer = submission.submission_data.detect{|a| a[:question_id] == id }
|
||||
question = submission.quiz_data.detect{|q| q[:id] == id}
|
||||
unless question
|
||||
# if this submission didn't answer this question, fill in with blanks
|
||||
row << ''
|
||||
row << ''
|
||||
next
|
||||
end
|
||||
strip_html_answers(question)
|
||||
answer_item = question && question[:answers].detect{|a| a[:id] == answer[:answer_id]}
|
||||
answer_item ||= answer
|
||||
if question[:question_type] == 'fill_in_multiple_blanks_question'
|
||||
blank_ids = question[:answers].map{|a| a[:blank_id] }.uniq
|
||||
row << blank_ids.map{|blank_id| answer["answer_for_#{blank_id}".to_sym].try(:gsub, /,/, '\,') }.compact.join(',')
|
||||
elsif question[:question_type] == 'multiple_answers_question'
|
||||
row << question[:answers].map{|a| answer["answer_#{a[:id]}".to_sym] == '1' ? a[:text].gsub(/,/, '\,') : nil }.compact.join(',')
|
||||
elsif question[:question_type] == 'multiple_dropdowns_question'
|
||||
blank_ids = question[:answers].map{|a| a[:blank_id] }.uniq
|
||||
answer_ids = blank_ids.map{|blank_id| answer["answer_for_#{blank_id}".to_sym] }
|
||||
row << answer_ids.map{|id| (question[:answers].detect{|a| a[:id] == id } || {})[:text].try(:gsub, /,/, '\,' ) }.compact.join(',')
|
||||
elsif question[:question_type] == 'calculated_question'
|
||||
list = question[:answers][0][:variables].map{|a| [a[:name],a[:value].to_s].map{|str| str.gsub(/=>/, '\=>') }.join('=>') }
|
||||
list << answer[:text]
|
||||
row << list.map{|str| (str || '').gsub(/,/, '\,') }.join(',')
|
||||
elsif question[:question_type] == 'matching_question'
|
||||
answer_ids = question[:answers].map{|a| a[:id] }
|
||||
answer_and_matches = answer_ids.map{|id| [id, answer["answer_#{id}".to_sym].to_i] }
|
||||
row << answer_and_matches.map{|id, match_id|
|
||||
res = []
|
||||
res << (question[:answers].detect{|a| a[:id] == id } || {})[:text]
|
||||
match = question[:matches].detect{|m| m[:match_id] == match_id } || question[:answers].detect{|m| m[:match_id] == match_id} || {}
|
||||
res << (match[:right] || match[:text])
|
||||
res.map{|s| (s || '').gsub(/=>/, '\=>')}.join('=>').gsub(/,/, '\,')
|
||||
}.join(',')
|
||||
elsif question[:question_type] == 'numerical_question'
|
||||
row << (answer && answer[:text])
|
||||
else
|
||||
row << ((answer_item && answer_item[:text]) || '')
|
||||
end
|
||||
row << (answer ? answer[:points] : "")
|
||||
end
|
||||
row << submission.submission_data.select{|a| a[:correct] }.length
|
||||
row << submission.submission_data.reject{|a| a[:correct] }.length
|
||||
row << submission.score
|
||||
rows << row
|
||||
end
|
||||
FasterCSV.generate do |csv|
|
||||
columns.each_with_index do |val, idx|
|
||||
r = []
|
||||
r << val
|
||||
r << ''
|
||||
rows.each do |row|
|
||||
r << row[idx]
|
||||
end
|
||||
csv << r
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def submissions_for_statistics
|
||||
ActiveRecord::Base::ConnectionSpecification.with_environment(:slave) do
|
||||
for_users = quiz.context.student_ids
|
||||
scope = quiz.quiz_submissions.where(:user_id => for_users)
|
||||
scope = scope.includes(:versions) if includes_all_versions?
|
||||
scope.map { |qs|
|
||||
includes_all_versions? ?
|
||||
qs.submitted_versions :
|
||||
[qs.latest_submitted_version].compact
|
||||
}.flatten.
|
||||
select{ |s| s && s.completed? && s.submission_data.is_a?(Array) }.
|
||||
sort { |a,b| b.updated_at <=> a.updated_at }
|
||||
end
|
||||
end
|
||||
|
||||
def strip_html_answers(question)
|
||||
return if !question || !question[:answers] || !(%w(multiple_choice_question multiple_answers_question).include? question[:question_type])
|
||||
for answer in question[:answers] do
|
||||
answer[:text] = strip_tags(answer[:html]) if !answer[:html].blank? && answer[:text].blank?
|
||||
end
|
||||
end
|
||||
|
||||
# takes a question hash from Quiz/Submission#quiz_data, and a set of
|
||||
# responses (from Submission#submission_data)
|
||||
#
|
||||
# returns:
|
||||
# ["question",
|
||||
# {"points_possible"=>1,
|
||||
# "question_type"=>"multiple_choice_question",
|
||||
# "question_name"=>"Some Question",
|
||||
# "name"=>"Some Question",
|
||||
# "question_text"=>"<p>Blah blah blah?</p>",
|
||||
# "answers"=>
|
||||
# [{"text"=>"blah",
|
||||
# "comments"=>"",
|
||||
# "weight"=>100,
|
||||
# "id"=>8379,
|
||||
# "responses"=>2,
|
||||
# "user_ids"=>[2, 3]},
|
||||
# {"text"=>"blarb",
|
||||
# "weight"=>0,
|
||||
# "id"=>8153,
|
||||
# "responses"=>1,
|
||||
# "user_ids"=>[1]}],
|
||||
# "assessment_question_id"=>1017,
|
||||
# "id"=>1017,
|
||||
# "responses"=>3,
|
||||
# "response_values"=>[...],
|
||||
# "unexpected_response_values"=>[],
|
||||
# "user_ids"=>[1,2,3],
|
||||
# "multiple_responses"=>false}],
|
||||
def stats_for_question(question, responses)
|
||||
question[:responses] = 0
|
||||
question[:response_values] = []
|
||||
question[:unexpected_response_values] = []
|
||||
question[:user_ids] = []
|
||||
question[:answers].each { |a|
|
||||
a[:responses] = 0
|
||||
a[:user_ids] = []
|
||||
}
|
||||
strip_html_answers(question)
|
||||
|
||||
question[:user_ids] = responses.map { |r| r[:user_id] }
|
||||
question[:response_values] = responses.map { |r| r[:text] }
|
||||
question[:responses] = responses.size
|
||||
|
||||
question = QuizQuestion::Base.from_question_data(question).stats(responses)
|
||||
none = {
|
||||
:responses => question[:responses] - question[:answers].map{|a| a[:responses] || 0}.sum,
|
||||
:id => "none",
|
||||
:weight => 0,
|
||||
:text => t('statistics.no_answer', "No Answer"),
|
||||
:user_ids => question[:user_ids] - question[:answers].map{|a| a[:user_ids] }.flatten
|
||||
} rescue nil
|
||||
question[:answers] << none if none && none[:responses] > 0
|
||||
question
|
||||
end
|
||||
end
|
|
@ -0,0 +1,17 @@
|
|||
class CreateQuizStatisticsTable < ActiveRecord::Migration
|
||||
tag :predeploy
|
||||
|
||||
def self.up
|
||||
create_table :quiz_statistics do |t|
|
||||
t.integer :quiz_id, :limit => 8
|
||||
t.boolean :includes_all_versions
|
||||
t.boolean :anonymous
|
||||
t.timestamps
|
||||
end
|
||||
add_index :quiz_statistics, :quiz_id
|
||||
end
|
||||
|
||||
def self.down
|
||||
drop_table :quiz_statistics
|
||||
end
|
||||
end
|
|
@ -542,240 +542,6 @@ describe Quiz do
|
|||
q.quiz_submissions.first.submission.assignment.should == q.assignment
|
||||
end
|
||||
|
||||
context 'statistics' do
|
||||
it 'should calculate mean/stddev as expected with no submissions' do
|
||||
stats = @course.quizzes.new.statistics
|
||||
stats[:submission_score_average].should be_nil
|
||||
stats[:submission_score_high].should be_nil
|
||||
stats[:submission_score_low].should be_nil
|
||||
stats[:submission_score_stdev].should be_nil
|
||||
end
|
||||
|
||||
it 'should calculate mean/stddev as expected with a few submissions' do
|
||||
q = @course.quizzes.new
|
||||
q.save!
|
||||
@user1 = User.create! :name => "some_user 1"
|
||||
@user2 = User.create! :name => "some_user 2"
|
||||
@user3 = User.create! :name => "some_user 2"
|
||||
student_in_course :course => @course, :user => @user1
|
||||
student_in_course :course => @course, :user => @user2
|
||||
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.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
|
||||
sub = q.generate_submission(@user2)
|
||||
sub.workflow_state = 'complete'
|
||||
sub.submission_data = [{ :points => 17, :text => "", :correct => "undefined", :question_id => -1 }]
|
||||
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
|
||||
sub = q.generate_submission(@user3)
|
||||
sub.workflow_state = 'complete'
|
||||
sub.submission_data = [{ :points => 20, :text => "", :correct => "undefined", :question_id => -1 }]
|
||||
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)
|
||||
end
|
||||
|
||||
it "should use the last completed submission, even if the current submission is in progress" do
|
||||
student_in_course(:active_all => true)
|
||||
q = @course.quizzes.create!
|
||||
q.quiz_questions.create!(:question_data => { :name => "test 1" })
|
||||
q.generate_quiz_data
|
||||
q.save!
|
||||
|
||||
# one complete submission
|
||||
qs = q.generate_submission(@student)
|
||||
qs.grade_submission
|
||||
|
||||
# and one in progress
|
||||
qs = q.generate_submission(@student)
|
||||
|
||||
stats = q.statistics(false)
|
||||
stats[:multiple_attempts_exist].should be_false
|
||||
end
|
||||
|
||||
context 'csv' do
|
||||
before(:each) do
|
||||
student_in_course(:active_all => true)
|
||||
@quiz = @course.quizzes.create!
|
||||
@quiz.quiz_questions.create!(:question_data => { :name => "test 1" })
|
||||
@quiz.generate_quiz_data
|
||||
@quiz.save!
|
||||
end
|
||||
|
||||
it 'should include previous versions even if the current version is incomplete' do
|
||||
# one complete submission
|
||||
qs = @quiz.generate_submission(@student)
|
||||
qs.grade_submission
|
||||
|
||||
# and one in progress
|
||||
@quiz.generate_submission(@student)
|
||||
|
||||
stats = FasterCSV.parse(@quiz.statistics_csv(:include_all_versions => true))
|
||||
# format for row is row_name, '', data1, data2, ...
|
||||
stats.first.length.should == 3
|
||||
end
|
||||
|
||||
it 'should not include user data for anonymous surveys' do
|
||||
# one complete submission
|
||||
qs = @quiz.generate_submission(@student)
|
||||
qs.grade_submission
|
||||
|
||||
# and one in progress
|
||||
@quiz.generate_submission(@student)
|
||||
|
||||
stats = FasterCSV.parse(@quiz.statistics_csv(:include_all_versions => true, :anonymous => true))
|
||||
# format for row is row_name, '', data1, data2, ...
|
||||
stats.first.length.should == 3
|
||||
stats[0][0].should == "section"
|
||||
end
|
||||
|
||||
it 'should have sections in quiz statistics_csv' do
|
||||
#enroll user in multiple sections
|
||||
pseudonym = pseudonym(@student)
|
||||
@student.pseudonym.sis_user_id = "user_sis_id_01"
|
||||
@student.pseudonym.save!
|
||||
section1 = @course.course_sections.first
|
||||
section1.sis_source_id = 'SISSection01'
|
||||
section1.save!
|
||||
section2 = CourseSection.new(:course => @course, :name => "section2")
|
||||
section2.sis_source_id = 'SISSection02'
|
||||
section2.save!
|
||||
@course.enroll_user(@student, "StudentEnrollment", :enrollment_state => 'active', :allow_multiple_enrollments => true, :section => section2)
|
||||
# one complete submission
|
||||
qs = @quiz.generate_submission(@student)
|
||||
qs.grade_submission
|
||||
|
||||
stats = FasterCSV.parse(@quiz.statistics_csv(:include_all_versions => true))
|
||||
# format for row is row_name, '', data1, data2, ...
|
||||
stats[0].should == ["name", "", "nobody@example.com"]
|
||||
stats[1].should == ["id", "", @student.id.to_s]
|
||||
stats[2].should == ["sis_id", "", "user_sis_id_01"]
|
||||
expect_multi_value_row(stats[3], "section", ["section2", "Unnamed Course"])
|
||||
expect_multi_value_row(stats[4], "section_id", [section1.id, section2.id])
|
||||
expect_multi_value_row(stats[5], "section_sis_id", ["SISSection02", "SISSection01"])
|
||||
stats.first.length.should == 3
|
||||
end
|
||||
|
||||
def expect_multi_value_row(row, expected_name, expected_values)
|
||||
row[0..1].should == [expected_name, ""]
|
||||
row[2].split(', ').sort.should == expected_values.map(&:to_s).sort
|
||||
end
|
||||
|
||||
it 'should not include previous versions by default' do
|
||||
# two complete submissions
|
||||
qs = @quiz.generate_submission(@student)
|
||||
qs.grade_submission
|
||||
qs = @quiz.generate_submission(@student)
|
||||
qs.grade_submission
|
||||
|
||||
stats = FasterCSV.parse(@quiz.statistics_csv)
|
||||
# format for row is row_name, '', data1, data2, ...
|
||||
stats.first.length.should == 3
|
||||
end
|
||||
|
||||
it 'should deal with incomplete fill-in-multiple-blanks questions' do
|
||||
@quiz.quiz_questions.create!(:question_data => { :name => "test 2",
|
||||
:question_type => 'fill_in_multiple_blanks_question',
|
||||
:question_text => "[ans0]",
|
||||
:answers =>
|
||||
{'answer_0' => {'answer_text' => 'foo', 'blank_id' => 'ans0', 'answer_weight' => '100'}}})
|
||||
@quiz.quiz_questions.create!(:question_data => { :name => "test 3",
|
||||
:question_type => 'fill_in_multiple_blanks_question',
|
||||
:question_text => "[ans0] [ans1]",
|
||||
:answers =>
|
||||
{'answer_0' => {'answer_text' => 'bar', 'blank_id' => 'ans0', 'answer_weight' => '100'},
|
||||
'answer_1' => {'answer_text' => 'baz', 'blank_id' => 'ans1', 'answer_weight' => '100'}}})
|
||||
@quiz.generate_quiz_data
|
||||
@quiz.save!
|
||||
@quiz.quiz_questions.size.should == 3
|
||||
qs = @quiz.generate_submission(@student)
|
||||
# submission will not answer question 2 and will partially answer question 3
|
||||
qs.submission_data = {
|
||||
"question_#{@quiz.quiz_questions[2].id}_#{AssessmentQuestion.variable_id('ans1')}" => 'baz'
|
||||
}
|
||||
qs.grade_submission
|
||||
stats = FasterCSV.parse(@quiz.statistics_csv)
|
||||
stats.size.should == 16 # 3 questions * 2 lines + ten more (name, id, sis_id, section, section_id, section_sis_id, submitted, correct, incorrect, score)
|
||||
stats[11].size.should == 3
|
||||
stats[11][2].should == ',baz'
|
||||
end
|
||||
|
||||
it 'should contain answers to numerical questions' do
|
||||
@quiz.quiz_questions.create!(:question_data => { :name => "numerical_question",
|
||||
:question_type => 'numerical_question',
|
||||
:question_text => "[num1]",
|
||||
:answers => {'answer_0' => {:numerical_answer_type => 'exact_answer'}}})
|
||||
|
||||
@quiz.quiz_questions.last.question_data[:answers].first[:exact] = 5
|
||||
|
||||
@quiz.generate_quiz_data
|
||||
@quiz.save!
|
||||
|
||||
qs = @quiz.generate_submission(@student)
|
||||
qs.submission_data = {
|
||||
"question_#{@quiz.quiz_questions[1].id}" => 5
|
||||
}
|
||||
qs.grade_submission
|
||||
|
||||
stats = FasterCSV.parse(@quiz.statistics_csv)
|
||||
stats[9][2].should == '5'
|
||||
end
|
||||
end
|
||||
|
||||
it 'should strip tags from html multiple-choice/multiple-answers' do
|
||||
student_in_course(:active_all => true)
|
||||
q = @course.quizzes.create!(:title => "new quiz")
|
||||
q.quiz_questions.create!(:question_data => {:name => 'q1', :points_possible => 1, 'question_type' => 'multiple_choice_question', 'answers' => {'answer_0' => {'answer_text' => '', 'answer_html' => '<em>zero</em>', 'answer_weight' => '100'}, 'answer_1' => {'answer_text' => "", 'answer_html' => "<p>one</p>", 'answer_weight' => '0'}}})
|
||||
q.quiz_questions.create!(:question_data => {:name => 'q2', :points_possible => 1, 'question_type' => 'multiple_answers_question', 'answers' => {'answer_0' => {'answer_text' => '', 'answer_html' => "<a href='http://example.com/caturday.gif'>lolcats</a>", 'answer_weight' => '100'}, 'answer_1' => {'answer_text' => 'lolrus', 'answer_weight' => '100'}}})
|
||||
q.generate_quiz_data
|
||||
q.save
|
||||
qs = q.generate_submission(@student)
|
||||
qs.submission_data = {
|
||||
"question_#{q.quiz_data[0][:id]}" => "#{q.quiz_data[0][:answers][0][:id]}",
|
||||
"question_#{q.quiz_data[1][:id]}_answer_#{q.quiz_data[1][:answers][0][:id]}" => "1",
|
||||
"question_#{q.quiz_data[1][:id]}_answer_#{q.quiz_data[1][:answers][1][:id]}" => "1"
|
||||
}
|
||||
qs.grade_submission
|
||||
|
||||
# visual statistics
|
||||
stats = q.statistics
|
||||
stats[:questions].length.should == 2
|
||||
stats[:questions][0].length.should == 2
|
||||
stats[:questions][0][0].should == "question"
|
||||
stats[:questions][0][1][:answers].length.should == 2
|
||||
stats[:questions][0][1][:answers][0][:responses].should == 1
|
||||
stats[:questions][0][1][:answers][0][:text].should == "zero"
|
||||
stats[:questions][0][1][:answers][1][:responses].should == 0
|
||||
stats[:questions][0][1][:answers][1][:text].should == "one"
|
||||
stats[:questions][1].length.should == 2
|
||||
stats[:questions][1][0].should == "question"
|
||||
stats[:questions][1][1][:answers].length.should == 2
|
||||
stats[:questions][1][1][:answers][0][:responses].should == 1
|
||||
stats[:questions][1][1][:answers][0][:text].should == "lolcats"
|
||||
stats[:questions][1][1][:answers][1][:responses].should == 1
|
||||
stats[:questions][1][1][:answers][1][:text].should == "lolrus"
|
||||
|
||||
# csv statistics
|
||||
stats = FasterCSV.parse(q.statistics_csv)
|
||||
stats[7][2].should == "zero"
|
||||
stats[9][2].should == "lolcats,lolrus"
|
||||
end
|
||||
end
|
||||
|
||||
context "clone_for" do
|
||||
it "should clone for other contexts" do
|
||||
u = User.create!(:name => "some user")
|
||||
|
|
|
@ -0,0 +1,256 @@
|
|||
#
|
||||
# Copyright (C) 2011 - 2012 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 File.expand_path(File.dirname(__FILE__) + '/../spec_helper.rb')
|
||||
|
||||
describe QuizStatistics do
|
||||
before { course }
|
||||
|
||||
it 'should calculate mean/stddev as expected with no submissions' do
|
||||
q = @course.quizzes.create!
|
||||
stats = q.statistics
|
||||
stats[:submission_score_average].should be_nil
|
||||
stats[:submission_score_high].should be_nil
|
||||
stats[:submission_score_low].should be_nil
|
||||
stats[:submission_score_stdev].should be_nil
|
||||
end
|
||||
|
||||
it 'should calculate mean/stddev as expected with a few submissions' do
|
||||
q = @course.quizzes.create!
|
||||
q.save!
|
||||
@user1 = User.create! :name => "some_user 1"
|
||||
@user2 = User.create! :name => "some_user 2"
|
||||
@user3 = User.create! :name => "some_user 2"
|
||||
student_in_course :course => @course, :user => @user1
|
||||
student_in_course :course => @course, :user => @user2
|
||||
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.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
|
||||
sub = q.generate_submission(@user2)
|
||||
sub.workflow_state = 'complete'
|
||||
sub.submission_data = [{ :points => 17, :text => "", :correct => "undefined", :question_id => -1 }]
|
||||
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
|
||||
sub = q.generate_submission(@user3)
|
||||
sub.workflow_state = 'complete'
|
||||
sub.submission_data = [{ :points => 20, :text => "", :correct => "undefined", :question_id => -1 }]
|
||||
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)
|
||||
end
|
||||
|
||||
it "should use the last completed submission, even if the current submission is in progress" do
|
||||
student_in_course(:active_all => true)
|
||||
q = @course.quizzes.create!
|
||||
q.quiz_questions.create!(:question_data => { :name => "test 1" })
|
||||
q.generate_quiz_data
|
||||
q.save!
|
||||
|
||||
# one complete submission
|
||||
qs = q.generate_submission(@student)
|
||||
qs.grade_submission
|
||||
|
||||
# and one in progress
|
||||
qs = q.generate_submission(@student)
|
||||
|
||||
stats = q.statistics(false)
|
||||
stats[:multiple_attempts_exist].should be_false
|
||||
end
|
||||
|
||||
context 'csv' do
|
||||
before(:each) do
|
||||
student_in_course(:active_all => true)
|
||||
@quiz = @course.quizzes.create!
|
||||
@quiz.quiz_questions.create!(:question_data => { :name => "test 1" })
|
||||
@quiz.generate_quiz_data
|
||||
@quiz.save!
|
||||
end
|
||||
|
||||
it 'should include previous versions even if the current version is incomplete' do
|
||||
# one complete submission
|
||||
qs = @quiz.generate_submission(@student)
|
||||
qs.grade_submission
|
||||
|
||||
# and one in progress
|
||||
@quiz.generate_submission(@student)
|
||||
|
||||
stats = FasterCSV.parse(@quiz.statistics_csv(:include_all_versions => true))
|
||||
# format for row is row_name, '', data1, data2, ...
|
||||
stats.first.length.should == 3
|
||||
end
|
||||
|
||||
it 'should not include user data for anonymous surveys' do
|
||||
# one complete submission
|
||||
qs = @quiz.generate_submission(@student)
|
||||
qs.grade_submission
|
||||
|
||||
# and one in progress
|
||||
@quiz.generate_submission(@student)
|
||||
|
||||
stats = FasterCSV.parse(@quiz.statistics_csv(:include_all_versions => true, :anonymous => true))
|
||||
# format for row is row_name, '', data1, data2, ...
|
||||
stats.first.length.should == 3
|
||||
stats[0][0].should == "section"
|
||||
end
|
||||
|
||||
it 'should have sections in quiz statistics_csv' do
|
||||
#enroll user in multiple sections
|
||||
pseudonym = pseudonym(@student)
|
||||
@student.pseudonym.sis_user_id = "user_sis_id_01"
|
||||
@student.pseudonym.save!
|
||||
section1 = @course.course_sections.first
|
||||
section1.sis_source_id = 'SISSection01'
|
||||
section1.save!
|
||||
section2 = CourseSection.new(:course => @course, :name => "section2")
|
||||
section2.sis_source_id = 'SISSection02'
|
||||
section2.save!
|
||||
@course.enroll_user(@student, "StudentEnrollment", :enrollment_state => 'active', :allow_multiple_enrollments => true, :section => section2)
|
||||
# one complete submission
|
||||
qs = @quiz.generate_submission(@student)
|
||||
qs.grade_submission
|
||||
|
||||
stats = FasterCSV.parse(@quiz.statistics_csv(:include_all_versions => true))
|
||||
# format for row is row_name, '', data1, data2, ...
|
||||
stats[0].should == ["name", "", "nobody@example.com"]
|
||||
stats[1].should == ["id", "", @student.id.to_s]
|
||||
stats[2].should == ["sis_id", "", "user_sis_id_01"]
|
||||
expect_multi_value_row(stats[3], "section", ["section2", "Unnamed Course"])
|
||||
expect_multi_value_row(stats[4], "section_id", [section1.id, section2.id])
|
||||
expect_multi_value_row(stats[5], "section_sis_id", ["SISSection02", "SISSection01"])
|
||||
stats.first.length.should == 3
|
||||
end
|
||||
|
||||
def expect_multi_value_row(row, expected_name, expected_values)
|
||||
row[0..1].should == [expected_name, ""]
|
||||
row[2].split(', ').sort.should == expected_values.map(&:to_s).sort
|
||||
end
|
||||
|
||||
it 'should not include previous versions by default' do
|
||||
# two complete submissions
|
||||
qs = @quiz.generate_submission(@student)
|
||||
qs.grade_submission
|
||||
qs = @quiz.generate_submission(@student)
|
||||
qs.grade_submission
|
||||
|
||||
stats = FasterCSV.parse(@quiz.statistics_csv)
|
||||
# format for row is row_name, '', data1, data2, ...
|
||||
stats.first.length.should == 3
|
||||
end
|
||||
|
||||
it 'should deal with incomplete fill-in-multiple-blanks questions' do
|
||||
@quiz.quiz_questions.create!(:question_data => { :name => "test 2",
|
||||
:question_type => 'fill_in_multiple_blanks_question',
|
||||
:question_text => "[ans0]",
|
||||
:answers =>
|
||||
{'answer_0' => {'answer_text' => 'foo', 'blank_id' => 'ans0', 'answer_weight' => '100'}}})
|
||||
@quiz.quiz_questions.create!(:question_data => { :name => "test 3",
|
||||
:question_type => 'fill_in_multiple_blanks_question',
|
||||
:question_text => "[ans0] [ans1]",
|
||||
:answers =>
|
||||
{'answer_0' => {'answer_text' => 'bar', 'blank_id' => 'ans0', 'answer_weight' => '100'},
|
||||
'answer_1' => {'answer_text' => 'baz', 'blank_id' => 'ans1', 'answer_weight' => '100'}}})
|
||||
@quiz.generate_quiz_data
|
||||
@quiz.save!
|
||||
@quiz.quiz_questions.size.should == 3
|
||||
qs = @quiz.generate_submission(@student)
|
||||
# submission will not answer question 2 and will partially answer question 3
|
||||
qs.submission_data = {
|
||||
"question_#{@quiz.quiz_questions[2].id}_#{AssessmentQuestion.variable_id('ans1')}" => 'baz'
|
||||
}
|
||||
qs.grade_submission
|
||||
stats = FasterCSV.parse(@quiz.statistics_csv)
|
||||
stats.size.should == 16 # 3 questions * 2 lines + ten more (name, id, sis_id, section, section_id, section_sis_id, submitted, correct, incorrect, score)
|
||||
stats[11].size.should == 3
|
||||
stats[11][2].should == ',baz'
|
||||
end
|
||||
|
||||
it 'should contain answers to numerical questions' do
|
||||
@quiz.quiz_questions.create!(:question_data => { :name => "numerical_question",
|
||||
:question_type => 'numerical_question',
|
||||
:question_text => "[num1]",
|
||||
:answers => {'answer_0' => {:numerical_answer_type => 'exact_answer'}}})
|
||||
|
||||
@quiz.quiz_questions.last.question_data[:answers].first[:exact] = 5
|
||||
|
||||
@quiz.generate_quiz_data
|
||||
@quiz.save!
|
||||
|
||||
qs = @quiz.generate_submission(@student)
|
||||
qs.submission_data = {
|
||||
"question_#{@quiz.quiz_questions[1].id}" => 5
|
||||
}
|
||||
qs.grade_submission
|
||||
|
||||
stats = FasterCSV.parse(@quiz.statistics_csv)
|
||||
stats[9][2].should == '5'
|
||||
end
|
||||
end
|
||||
|
||||
it 'should strip tags from html multiple-choice/multiple-answers' do
|
||||
student_in_course(:active_all => true)
|
||||
q = @course.quizzes.create!(:title => "new quiz")
|
||||
q.quiz_questions.create!(:question_data => {:name => 'q1', :points_possible => 1, 'question_type' => 'multiple_choice_question', 'answers' => {'answer_0' => {'answer_text' => '', 'answer_html' => '<em>zero</em>', 'answer_weight' => '100'}, 'answer_1' => {'answer_text' => "", 'answer_html' => "<p>one</p>", 'answer_weight' => '0'}}})
|
||||
q.quiz_questions.create!(:question_data => {:name => 'q2', :points_possible => 1, 'question_type' => 'multiple_answers_question', 'answers' => {'answer_0' => {'answer_text' => '', 'answer_html' => "<a href='http://example.com/caturday.gif'>lolcats</a>", 'answer_weight' => '100'}, 'answer_1' => {'answer_text' => 'lolrus', 'answer_weight' => '100'}}})
|
||||
q.generate_quiz_data
|
||||
q.save
|
||||
qs = q.generate_submission(@student)
|
||||
qs.submission_data = {
|
||||
"question_#{q.quiz_data[0][:id]}" => "#{q.quiz_data[0][:answers][0][:id]}",
|
||||
"question_#{q.quiz_data[1][:id]}_answer_#{q.quiz_data[1][:answers][0][:id]}" => "1",
|
||||
"question_#{q.quiz_data[1][:id]}_answer_#{q.quiz_data[1][:answers][1][:id]}" => "1"
|
||||
}
|
||||
qs.grade_submission
|
||||
|
||||
# visual statistics
|
||||
stats = q.statistics
|
||||
stats[:questions].length.should == 2
|
||||
stats[:questions][0].length.should == 2
|
||||
stats[:questions][0][0].should == "question"
|
||||
stats[:questions][0][1][:answers].length.should == 2
|
||||
stats[:questions][0][1][:answers][0][:responses].should == 1
|
||||
stats[:questions][0][1][:answers][0][:text].should == "zero"
|
||||
stats[:questions][0][1][:answers][1][:responses].should == 0
|
||||
stats[:questions][0][1][:answers][1][:text].should == "one"
|
||||
stats[:questions][1].length.should == 2
|
||||
stats[:questions][1][0].should == "question"
|
||||
stats[:questions][1][1][:answers].length.should == 2
|
||||
stats[:questions][1][1][:answers][0][:responses].should == 1
|
||||
stats[:questions][1][1][:answers][0][:text].should == "lolcats"
|
||||
stats[:questions][1][1][:answers][1][:responses].should == 1
|
||||
stats[:questions][1][1][:answers][1][:text].should == "lolrus"
|
||||
|
||||
# csv statistics
|
||||
stats = FasterCSV.parse(q.statistics_csv)
|
||||
stats[7][2].should == "zero"
|
||||
stats[9][2].should == "lolcats,lolrus"
|
||||
end
|
||||
end
|
Loading…
Reference in New Issue