diff --git a/app/models/quiz_question/base.rb b/app/models/quiz_question/base.rb
new file mode 100644
index 00000000000..015aad0f938
--- /dev/null
+++ b/app/models/quiz_question/base.rb
@@ -0,0 +1,128 @@
+#
+# Copyright (C) 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 .
+#
+
+class QuizQuestion::Base
+ # typically you'll call this factory method rather than instantiating a subclass directly
+ def self.from_question_data(data)
+ type_name = data[:question_type]
+ klass = question_types[type_name]
+ raise("unknown quiz question type: #{type_name}") unless klass
+ klass.new(data)
+ end
+
+ @@question_types = {}
+ mattr_reader :question_types
+
+ def self.inherited(klass)
+ type_name = klass.question_type
+ raise("question type #{type_name} already exists") if question_types.key?(type_name)
+ question_types[type_name] = klass
+ end
+
+ # override to change the name of the question type, defaults to the underscore-ized class name
+ def self.question_type
+ self.name.demodulize.underscore
+ end
+
+ def initialize(question_data)
+ # currently all the attributes are synthesized from @question_data
+ # since questions are stored in this format anyway, it prevents us from
+ # having to do a bunch of translation to some other format
+ @question_data = question_data
+ end
+
+ def question_id
+ @question_data[:id]
+ end
+
+ def points_possible
+ @question_data[:points_possible].to_f
+ end
+
+ def incorrect_dock
+ @question_data[:incorrect_dock].try(:to_f)
+ end
+
+ #
+ # Scoring Methods to override in the subclass
+ #
+
+ # the total number of parts to a question
+ # many questions have just one part (fill in the blank, etc)
+ # some questions have many parts and the score is (correct / total) * points_possible
+ # text-only questions have no parts
+ def total_answer_parts
+ 1
+ end
+
+ # where the scoring of the question happens
+ #
+ # a UserAnswer is passed in, and the # of parts correct in the answer is returned
+ #
+ # for questions types with just one answer part, return 1 for correct and 0 for incorrect
+ # for questions types with multiple answer parts, return the total # of parts that are correct
+ #
+ # you can also return true for full credit, or false for no credit
+ #
+ # if no answer is given at all, return nil
+ #
+ # (note this means nil != false in this return value)
+ def correct_answer_parts(user_answer)
+ nil
+ end
+
+ # Return the number of explicitly incorrect answer parts
+ #
+ # This will never be called before correct_answer_parts is called, so it can
+ # be calculated there if that's easier.
+ #
+ # If this is > 0, the user will be docked for each answer part that they got
+ # incorrect. Most question types leave this at 0, so the user isn't punished
+ # extra for wrong answers.
+ def incorrect_answer_parts(user_answer)
+ 0
+ end
+
+ # override and return true if the answer can't be auto-scored
+ def requires_manual_scoring?(user_answer)
+ false
+ end
+
+ def score_question(answer_data)
+ user_answer = UserAnswer.new(self.question_id, self.points_possible, answer_data)
+ user_answer.total_parts = total_answer_parts
+ correct_parts = correct_answer_parts(user_answer)
+
+ if !correct_parts.nil?
+ correct_parts = 0 if correct_parts == false
+ correct_parts = user_answer.total_parts if correct_parts == true
+
+ user_answer.incorrect_parts = incorrect_answer_parts(user_answer)
+ correct_parts = 0 if (correct_parts - user_answer.incorrect_parts) < user_answer.total_parts && @question_data[:allow_partial_credit] == false
+
+ user_answer.correct_parts = correct_parts
+ user_answer.incorrect_dock = incorrect_dock if user_answer.incorrect_parts > 0
+ elsif user_answer.undefined_if_blank? || requires_manual_scoring?(user_answer)
+ user_answer.undefined = true
+ end
+
+ user_answer
+ end
+end
+
+Dir[Rails.root + "app/models/quiz_question/*_question.rb"].each { |f| require_dependency f }
diff --git a/app/models/quiz_question/calculated_question.rb b/app/models/quiz_question/calculated_question.rb
new file mode 100644
index 00000000000..a850d331bcf
--- /dev/null
+++ b/app/models/quiz_question/calculated_question.rb
@@ -0,0 +1,25 @@
+#
+# Copyright (C) 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 .
+#
+
+class QuizQuestion::CalculatedQuestion < QuizQuestion::NumericalQuestion
+ def answers
+ answer = @question_data[:answers].first
+ return [] unless answer
+ return [{ :id => answer[:id], :numerical_answer_type => "exact_answer", :exact => answer[:answer], :margin => @question_data[:answer_tolerance] }]
+ end
+end
diff --git a/app/models/quiz_question/essay_question.rb b/app/models/quiz_question/essay_question.rb
new file mode 100644
index 00000000000..9f1efcb0c0d
--- /dev/null
+++ b/app/models/quiz_question/essay_question.rb
@@ -0,0 +1,29 @@
+#
+# Copyright (C) 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 .
+#
+
+class QuizQuestion::EssayQuestion < QuizQuestion::Base
+ def requires_manual_scoring?(user_answer)
+ true
+ end
+
+ def correct_answer_parts(user_answer)
+ config = Instructure::SanitizeField::SANITIZE
+ user_answer.answer_details[:text] = Sanitize.clean(user_answer.answer_text, config) || ""
+ nil
+ end
+end
diff --git a/app/models/quiz_question/fill_in_multiple_blanks_question.rb b/app/models/quiz_question/fill_in_multiple_blanks_question.rb
new file mode 100644
index 00000000000..b10adc9e5cc
--- /dev/null
+++ b/app/models/quiz_question/fill_in_multiple_blanks_question.rb
@@ -0,0 +1,60 @@
+#
+# Copyright (C) 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 .
+#
+
+class QuizQuestion::FillInMultipleBlanksQuestion < QuizQuestion::Base
+ def total_answer_parts
+ variables.length
+ end
+
+ def variables
+ @variables ||= @question_data[:answers].map{|a| a[:blank_id] }.uniq
+ end
+
+ def find_chosen_answer(variable, response)
+ response = (response || "").strip.downcase
+ @question_data[:answers].detect{|answer| answer[:blank_id] == variable && (answer[:text] || "").strip.downcase == response } || { :text => response, :id => nil, :weight => 0 }
+ end
+
+ def answer_text(answer)
+ answer[:text]
+ end
+
+ def correct_answer_parts(user_answer)
+ chosen_answers = {}
+ total_answers = 0
+
+ variables.each do |variable|
+ variable_id = AssessmentQuestion.variable_id(variable)
+ response = user_answer[variable_id]
+ if response.present?
+ total_answers += 1
+ end
+ chosen_answer = find_chosen_answer(variable, response)
+ chosen_answers[variable] = chosen_answer
+ end
+
+ return nil if total_answers == 0
+
+ return chosen_answers.count do |variable, answer|
+ answer ||= { :id => nil, :text => nil, :weight => 0 }
+ user_answer.answer_details["answer_for_#{variable}".to_sym] = answer_text(answer)
+ user_answer.answer_details["answer_id_for_#{variable}".to_sym] = answer[:id]
+ answer && answer[:weight] == 100 && !variables.empty?
+ end
+ end
+end
diff --git a/app/models/quiz_question/matching_question.rb b/app/models/quiz_question/matching_question.rb
new file mode 100644
index 00000000000..03917bd4c5c
--- /dev/null
+++ b/app/models/quiz_question/matching_question.rb
@@ -0,0 +1,47 @@
+#
+# Copyright (C) 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 .
+#
+
+class QuizQuestion::MatchingQuestion < QuizQuestion::Base
+ def total_answer_parts
+ @question_data[:answers].length
+ end
+
+ def correct_answer_parts(user_answer)
+ total_answers = 0
+ correct_answers = 0
+
+ @question_data[:answers].each do |answer|
+ answer_match = user_answer["answer_#{answer[:id]}"].to_s
+
+ if answer_match.present?
+ total_answers += 1
+ found_matched = @question_data[:answers].find {|a| a[:match_id].to_i == answer_match.to_i }
+ if found_matched == answer || (found_matched && found_matched[:right] && found_matched[:right] == answer[:right])
+ correct_answers += 1
+ answer_match = answer[:match_id].to_s
+ end
+ end
+
+ user_answer.answer_details["answer_#{answer[:id]}".to_sym] = answer_match
+ end
+
+ return nil if total_answers == 0
+
+ correct_answers
+ end
+end
diff --git a/app/models/quiz_question/multiple_answers_question.rb b/app/models/quiz_question/multiple_answers_question.rb
new file mode 100644
index 00000000000..dbda6a5e1e8
--- /dev/null
+++ b/app/models/quiz_question/multiple_answers_question.rb
@@ -0,0 +1,56 @@
+#
+# Copyright (C) 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 .
+#
+
+class QuizQuestion::MultipleAnswersQuestion < QuizQuestion::Base
+ def total_answer_parts
+ len = @question_data[:answers].select{|a| a[:weight] == 100}.length
+ len = 1 if len == 0
+ len
+ end
+
+ def incorrect_answer_parts(user_answer)
+ @incorrect_answers
+ end
+
+ def correct_answer_parts(user_answer)
+ total_answers = 0
+ correct_answers = 0
+ @incorrect_answers = 0
+
+ @question_data[:answers].each do |answer|
+ response = user_answer["answer_#{answer[:id]}"]
+ next unless response
+ total_answers += 1
+ user_answer.answer_details["answer_#{answer[:id]}".to_sym] = response
+
+ # Total possible is divided by the number of correct answers.
+ # For every correct answer they correctly select, they get partial
+ # points. For every correct answer they don't select, do nothing.
+ # For every incorrect answer that they select, dock them partial
+ # points.
+ if answer[:weight] == 100 && response == "1"
+ correct_answers += 1
+ elsif answer[:weight] != 100 && response == "1"
+ @incorrect_answers += 1
+ end
+ end
+
+ return nil if total_answers == 0
+ return correct_answers
+ end
+end
diff --git a/app/models/quiz_question/multiple_choice_question.rb b/app/models/quiz_question/multiple_choice_question.rb
new file mode 100644
index 00000000000..6dc84d182ac
--- /dev/null
+++ b/app/models/quiz_question/multiple_choice_question.rb
@@ -0,0 +1,36 @@
+#
+# Copyright (C) 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 .
+#
+
+class QuizQuestion::MultipleChoiceQuestion < QuizQuestion::Base
+ def correct_answer_parts(user_answer)
+ answer_text = user_answer.answer_text
+ return nil if answer_text.nil?
+ answer_id = answer_text.to_i
+ answer = @question_data[:answers].find { |a| a[:id] == answer_id }
+ return 0 unless answer
+ user_answer.answer_id = answer[:id]
+ return 0 if answer[:weight] != 100
+ return 1
+ end
+end
+
+class QuizQuestion::TrueFalseQuestion < QuizQuestion::MultipleChoiceQuestion
+end
+
+class QuizQuestion::MissingWordQuestion < QuizQuestion::MultipleChoiceQuestion
+end
diff --git a/app/models/quiz_question/multiple_dropdowns_question.rb b/app/models/quiz_question/multiple_dropdowns_question.rb
new file mode 100644
index 00000000000..7ad16b90485
--- /dev/null
+++ b/app/models/quiz_question/multiple_dropdowns_question.rb
@@ -0,0 +1,27 @@
+#
+# Copyright (C) 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 .
+#
+
+class QuizQuestion::MultipleDropdownsQuestion < QuizQuestion::FillInMultipleBlanksQuestion
+ def find_chosen_answer(variable, response)
+ @question_data[:answers].detect{ |answer| answer[:blank_id] == variable && answer[:id] == response.to_i } || { :text => nil, :id => nil, :weight => 0 }
+ end
+
+ def answer_text(answer)
+ answer[:id]
+ end
+end
diff --git a/app/models/quiz_question/numerical_question.rb b/app/models/quiz_question/numerical_question.rb
new file mode 100644
index 00000000000..9652991e3c3
--- /dev/null
+++ b/app/models/quiz_question/numerical_question.rb
@@ -0,0 +1,53 @@
+#
+# Copyright (C) 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 .
+#
+
+require 'bigdecimal'
+
+class QuizQuestion::NumericalQuestion < QuizQuestion::Base
+ def answers
+ @question_data[:answers].sort_by{|a| a[:weight] || 0}
+ end
+
+ def correct_answer_parts(user_answer)
+ answer_text = user_answer.answer_text
+ return nil if answer_text.nil?
+ return false if answer_text.blank?
+
+ # we use BigDecimal here to avoid rounding errors at the edge of the tolerance
+ # e.g. in floating point, -11.7 with margin of 0.02 isn't inclusive of the answer -11.72
+ answer_number = BigDecimal.new(answer_text.to_s)
+
+ match = answers.find do |answer|
+ if answer[:numerical_answer_type] == "exact_answer"
+ val = BigDecimal.new(answer[:exact].to_s)
+ margin = BigDecimal.new(answer[:margin].to_s)
+ min = val - margin
+ max = val + margin
+ answer_number >= min && answer_number <= max
+ else
+ answer_number >= BigDecimal.new(answer[:start].to_s) && answer_number <= BigDecimal.new(answer[:end].to_s)
+ end
+ end
+
+ if match
+ user_answer.answer_id = match[:id]
+ end
+
+ !!match
+ end
+end
diff --git a/app/models/quiz_question/short_answer_question.rb b/app/models/quiz_question/short_answer_question.rb
new file mode 100644
index 00000000000..3e1dc3fdb50
--- /dev/null
+++ b/app/models/quiz_question/short_answer_question.rb
@@ -0,0 +1,36 @@
+#
+# Copyright (C) 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 .
+#
+
+class QuizQuestion::ShortAnswerQuestion < QuizQuestion::Base
+ def correct_answer_parts(user_answer)
+ answer_text = user_answer.answer_text
+ return nil if answer_text.nil?
+
+ answer_text = CGI::escapeHTML(answer_text).strip.downcase
+
+ answer = @question_data[:answers].sort_by{|a| a[:weight] || 0}.find do |answer|
+ (answer[:text] || "").strip.downcase == answer_text
+ end
+
+ if answer
+ user_answer.answer_id = answer[:id]
+ end
+
+ !!answer
+ end
+end
diff --git a/app/models/quiz_question/text_only_question.rb b/app/models/quiz_question/text_only_question.rb
new file mode 100644
index 00000000000..6fa28bc2af7
--- /dev/null
+++ b/app/models/quiz_question/text_only_question.rb
@@ -0,0 +1,23 @@
+#
+# Copyright (C) 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 .
+#
+
+class QuizQuestion::TextOnlyQuestion < QuizQuestion::Base
+ def total_answer_parts
+ 0
+ end
+end
diff --git a/app/models/quiz_question/user_answer.rb b/app/models/quiz_question/user_answer.rb
new file mode 100644
index 00000000000..88d4ec6c27d
--- /dev/null
+++ b/app/models/quiz_question/user_answer.rb
@@ -0,0 +1,77 @@
+#
+# Copyright (C) 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 .
+#
+
+# Stores the scoring information for a user's answer to a quiz question
+class UserAnswer < Struct.new(:question_id, :points_possible, :total_parts, :correct_parts, :incorrect_parts, :answer_id, :undefined, :answer_details, :incorrect_dock)
+ def initialize(question_id, points_possible, answer_data)
+ super(question_id, points_possible, 1, 0, 0)
+ @points = 0.0
+ @answer_data = answer_data
+ self.answer_details = { :text => answer_text || "" }
+ end
+
+ def [](k)
+ @answer_data["question_#{question_id}_#{k}".to_sym]
+ end
+
+ def answer_text
+ @answer_data["question_#{question_id}".to_sym]
+ end
+
+ def score
+ if total_parts == 0
+ return 0
+ end
+ score = (correct_parts.to_f / total_parts) * points_possible
+ if incorrect_parts > 0
+ if incorrect_dock
+ score -= incorrect_dock * incorrect_parts
+ else
+ score -= (incorrect_parts.to_f / total_parts) * points_possible
+ end
+ score = 0.0 if score < 0
+ end
+ score
+ end
+
+ # this seems like it should be part of the question data, not the user answer data
+ def undefined_if_blank?
+ @answer_data[:undefined_if_blank]
+ end
+
+ # returns whether the answer is correct, in a bit of an odd way --
+ # returns boolean true or false if the answer is completely correct or incorrect
+ # returns the string "partial" if the answer is partially correct
+ # returns the string "no_score" for text-only questions
+ # returns the string "undefined" if no answer was given and
+ # undefined_if_blank is specified, or if the question can't be scored
+ # automatically (like an essay question)
+ def correctness
+ if total_parts == 0
+ "no_score"
+ elsif undefined
+ "undefined"
+ elsif correct_parts == total_parts && incorrect_parts == 0
+ true
+ elsif (correct_parts - incorrect_parts) > 0
+ "partial"
+ else
+ false
+ end
+ end
+end
diff --git a/app/models/quiz_submission.rb b/app/models/quiz_submission.rb
index 4b3b68d1651..51f9feecb13 100644
--- a/app/models/quiz_submission.rb
+++ b/app/models/quiz_submission.rb
@@ -474,179 +474,18 @@ class QuizSubmission < ActiveRecord::Base
# added or the question answer they selected goes away, then the
# the teacher gets the added burden of going back and manually assigning
# scores for these questions per student.
- undefined_if_blank = params[:undefined_if_blank]
- answer_text = params["question_#{q[:id]}"] rescue ""
- user_answer = {}
- user_answer[:text] = answer_text || ""
- user_answer[:question_id] = q[:id]
- user_answer[:points] = 0
- user_answer[:correct] = false
- question_type = q[:question_type]
- q[:points_possible] = q[:points_possible].to_f
- if question_type == "multiple_choice_question" || question_type == "true_false_question" || question_type == "missing_word_question"
- q[:answers].each do |answer|
- if answer[:id] == answer_text.to_i
- user_answer[:answer_id] = answer[:id]
- user_answer[:correct] = answer[:weight] == 100
- user_answer[:points] = q[:points_possible]
- end
- end
- user_answer[:correct] = "undefined" if answer_text == nil && undefined_if_blank
- elsif question_type == "short_answer_question"
- answers = q[:answers].sort_by{|a| a[:weight] || 0}
- match = false
- answers.each do |answer|
- if (answer[:text] || "").strip.downcase == CGI::escapeHTML(answer_text || "").strip.downcase && !match
- match = true
- user_answer[:answer_id] = answer[:id]
- user_answer[:correct] = true
- user_answer[:points] = q[:points_possible]
- end
- end
- user_answer[:correct] = "undefined" if answer_text == nil && undefined_if_blank
- elsif question_type == "essay_question"
- config = Instructure::SanitizeField::SANITIZE
- user_answer[:text] = Sanitize.clean(user_answer[:text] || "", config)
- user_answer[:correct] = "undefined"
- elsif question_type == "text_only_question"
- user_answer[:correct] = "no_score"
- elsif question_type == "matching_question"
- user_answer[:points] = 0
- found_match = false
- q[:answers].each do |answer|
- answer_match = params["question_#{q[:id]}_answer_#{answer[:id]}"].to_s rescue ""
- found_match = true if answer_match != nil
- found_matched = q[:answers].find{|a| a[:match_id].to_i == answer_match.to_i}
- if found_matched == answer || (found_matched && found_matched[:right] && found_matched[:right] == answer[:right])
- user_answer[:points] += (q[:points_possible].to_f / q[:answers].length.to_f) rescue 0
- answer_match = answer[:match_id].to_s
- end
- user_answer["answer_#{answer[:id]}".to_sym] = answer_match
- end
- if q[:allow_partial_credit] == false && user_answer[:points] < q[:points_possible].to_f
- user_answer[:points] = 0
- end
- user_answer[:correct] = "partial"
- user_answer[:correct] = false if user_answer[:points] == 0
- user_answer[:correct] = true if user_answer[:points] == q[:points_possible]
- user_answer[:correct] = "undefined" if !found_match && undefined_if_blank
- elsif question_type == "numerical_question"
- answer_number = answer_text.to_f
- answers = q[:answers].sort_by{|a| a[:weight] || 0}
- match = false
- answers.each do |answer|
- if !match
- if answer[:numerical_answer_type] == "exact_answer"
- match = true if answer_number >= answer[:exact] - answer[:margin] && answer_number <= answer[:exact] + answer[:margin]
- else
- match = true if answer_number >= answer[:start] && answer_number <= answer[:end]
- end
- if match
- user_answer[:answer_id] = answer[:id]
- user_answer[:correct] = true
- user_answer[:points] = q[:points_possible]
- end
- end
- end
- user_answer[:correct] = "undefined" if answer_text == nil && undefined_if_blank
- elsif question_type == "calculated_question"
- answer_number = answer_text.to_f
- val = q[:answers].first[:answer].to_f rescue 0
- margin = q[:answer_tolerance].to_f
- min = val - margin
- max = val + margin
- user_answer[:answer_id] = q[:answers].first[:id]
- if answer_number >= min && answer_number <= max
- user_answer[:correct] = true
- user_answer[:points] = q[:points_possible]
- end
- elsif question_type == "multiple_answers_question"
- correct_sequence = ""
- user_sequence = ""
- found_any = false
- user_answer[:points] = 0
- n_correct = q[:answers].select{|a| a[:weight] == 100}.length
- n_correct = 1 if n_correct == 0
- q[:answers].each do |answer|
- response = params["question_#{q[:id]}_answer_#{answer[:id]}"] rescue ""
- response ||= ""
- found_any = true if response != nil
- user_answer["answer_#{answer[:id]}".to_sym] = response
- correct = nil
- # Total possible is divided by the number of correct answers.
- # For every correct answer they correctly select, they get partial
- # points. For every correct answer they don't select, do nothing.
- # For every incorrect answer that they select, dock them partial
- # points.
- correct = true if answer[:weight] == 100 && response == "1"
- correct = false if answer[:weight] != 100 && response == "1"
- if correct == true
- user_answer[:points] += (q[:points_possible].to_f / n_correct.to_f) rescue 0
- elsif correct == false
- dock = (q[:incorrect_dock] || (q[:points_possible].to_f / n_correct.to_f)).to_f rescue 0.0
- user_answer[:points] -= dock
- end
- correct_sequence = correct_sequence + (answer[:weight] == 100 ? "1" : "0")
- user_sequence = user_sequence + (response == "1" ? "1" : "0")
- end
- # even if they only selected wrong answers, they can't score less than 0
- user_answer[:points] = [user_answer[:points], 0].max
- if correct_sequence == user_sequence
- user_answer[:points] = q[:points_possible]
- end
- if q[:allow_partial_credit] == false && user_answer[:points] < q[:points_possible].to_f
- user_answer[:points] = 0
- end
- user_answer[:correct] = "partial"
- user_answer[:correct] = false if user_answer[:points] == 0
- user_answer[:correct] = true if user_answer[:points] == q[:points_possible]
- user_answer[:correct] = "undefined" if !found_any && undefined_if_blank
- elsif question_type == "multiple_dropdowns_question"
- chosen_answers = {}
- variables = q[:answers].map{|a| a[:blank_id] }.uniq
- variables.each do |variable|
- variable_id = AssessmentQuestion.variable_id(variable)
- response = (params["question_#{q[:id]}_#{variable_id}"] rescue nil).to_i
- chosen_answer = q[:answers].detect{|answer| answer[:blank_id] == variable && answer[:id] == response.to_i }
- chosen_answers[variable] = chosen_answer
- end
- answer_tally = 0
- chosen_answers.each do |variable, answer|
- answer_tally += q[:points_possible].to_f / variables.length.to_f if answer && answer[:weight] == 100 && !variables.empty?
- user_answer["answer_for_#{variable}".to_sym] = answer[:id] rescue nil
- user_answer["answer_id_for_#{variable}".to_sym] = answer[:id] rescue nil
- end
- user_answer[:points] = answer_tally
- user_answer[:correct] = true if answer_tally == q[:points_possible]
- user_answer[:correct] = "partial" if answer_tally > 0 && answer_tally < q[:points_possible]
- elsif question_type == "fill_in_multiple_blanks_question"
- chosen_answers = {}
- variables = q[:answers].map{|a| a[:blank_id] }.uniq
- variables.each do |variable|
- variable_id = AssessmentQuestion.variable_id(variable)
- response = params["question_#{q[:id]}_#{variable_id}"]
- response ||= ""
- chosen_answer = q[:answers].detect{|answer| answer[:blank_id] == variable && (answer[:text] || "").strip.downcase == response.strip.downcase }
- chosen_answers[variable] = chosen_answer || {:text => response, :weight => 0}
- end
- answer_tally = 0
- chosen_answers.each do |variable, answer|
- answer_tally += 1 if answer[:weight] == 100 && !variables.empty?
- user_answer["answer_for_#{variable}".to_sym] = answer[:text] rescue nil
- user_answer["answer_id_for_#{variable}".to_sym] = answer[:id] rescue nil
- end
- if !variables.empty?
- user_answer[:points] = (answer_tally / variables.length.to_f) * q[:points_possible].to_f
- end
- user_answer[:correct] = true if answer_tally == variables.length.to_i
- user_answer[:correct] = "partial" if answer_tally > 0 && answer_tally < variables.length.to_i
- else
- end
- user_answer[:points] = 0.0 unless user_answer[:correct]
- user_answer[:points] = (user_answer[:points] * 100.0).round.to_f / 100.0
- user_answer
+ qq = QuizQuestion::Base.from_question_data(q)
+ user_answer = qq.score_question(params)
+ result = {
+ :correct => user_answer.correctness,
+ :points => user_answer.score,
+ :question_id => user_answer.question_id,
+ }
+ result[:answer_id] = user_answer.answer_id if user_answer.answer_id
+ result.merge!(user_answer.answer_details)
+ return result
end
-
+
named_scope :before, lambda{|date|
{:conditions => ['quiz_submissions.created_at < ?', date]}
}
diff --git a/spec/models/quiz_submission_spec.rb b/spec/models/quiz_submission_spec.rb
index 9c82985db21..f1526b7f760 100644
--- a/spec/models/quiz_submission_spec.rb
+++ b/spec/models/quiz_submission_spec.rb
@@ -541,9 +541,10 @@ describe QuizSubmission do
it "should score an essay_question" do
qd = essay_question_data
- text = "that's too dang hard!"
+ text = "that's too dang hard! "
+ sanitized = "that's too dang hard! alert(1)"
QuizSubmission.score_question(qd, { "question_1" => text }).should ==
- { :question_id => 1, :correct => "undefined", :points => 0, :text => text }
+ { :question_id => 1, :correct => "undefined", :points => 0, :text => sanitized }
QuizSubmission.score_question(qd, {}).should ==
{ :question_id => 1, :correct => "undefined", :points => 0, :text => "" }
@@ -620,10 +621,8 @@ describe QuizSubmission do
"question_1_answer_7398" => "6068",
"question_1_answer_7399" => "6069",
})
- correct = user_answer.delete(:correct)
- points = user_answer.delete(:points)
user_answer.should == {
- :question_id => 1, :text => "",
+ :question_id => 1, :correct => true, :points => 50, :text => "",
:answer_7396 => "6061",
:answer_6081 => "3855",
:answer_4224 => "1397",
@@ -631,10 +630,6 @@ describe QuizSubmission do
:answer_7398 => "6068",
:answer_7399 => "6069",
}
- pending("floating point error") do
- correct.should == true
- points.should == 50.0
- end
# selected a different answer but the text of that answer was the same
user_answer = QuizSubmission.score_question(q, {
@@ -645,10 +640,8 @@ describe QuizSubmission do
"question_1_answer_7398" => "6068",
"question_1_answer_7399" => "6069",
})
- correct = user_answer.delete(:correct)
- points = user_answer.delete(:points)
user_answer.should == {
- :question_id => 1, :text => "",
+ :question_id => 1, :correct => true, :points => 50, :text => "",
:answer_7396 => "6061",
:answer_6081 => "3855",
:answer_4224 => "1397",
@@ -656,23 +649,17 @@ describe QuizSubmission do
:answer_7398 => "6068",
:answer_7399 => "6069",
}
- pending("floating point error") do
- correct.should == true
- points.should == 50.0
- end
- # undefined
- pending("don't treat no answer as the blank string, breaking undefined_if_blank") do
- QuizSubmission.score_question(q, { "undefined_if_blank" => "1" }).should == {
- :question_id => 1, :correct => "undefined", :points => 0, :text => "",
- :answer_7396 => "",
- :answer_6081 => "",
- :answer_4224 => "",
- :answer_7397 => "",
- :answer_7398 => "",
- :answer_7399 => "",
- }
- end
+ # no answer shouldn't be treated as a blank string, breaking undefined_if_blank
+ QuizSubmission.score_question(q, { "undefined_if_blank" => "1" }).should == {
+ :question_id => 1, :correct => "undefined", :points => 0, :text => "",
+ :answer_7396 => "",
+ :answer_6081 => "",
+ :answer_4224 => "",
+ :answer_7397 => "",
+ :answer_7398 => "",
+ :answer_7399 => "",
+ }
end
it "should score a numerical_question" do
@@ -707,12 +694,11 @@ describe QuizSubmission do
QuizSubmission.score_question(qd, { :undefined_if_blank => "1" }).should == {
:question_id => 1, :correct => "undefined", :points => 0, :text => "" }
- pending("don't treat a blank answer as 0.0") do
- qd2 = qd.dup
- qd2["answers"] << { "exact" => 0, "numerical_answer_type" => "exact_answer", "margin" => 0, "weight" => 100, "id" => 1234 }
- QuizSubmission.score_question(qd2, {}).should == {
- :question_id => 1, :correct => false, :points => 0, :text => "" }
- end
+ # blank answer should not be treated as 0.0
+ qd2 = qd.dup
+ qd2["answers"] << { "exact" => 0, "numerical_answer_type" => "exact_answer", "margin" => 0, "weight" => 100, "id" => 1234 }
+ QuizSubmission.score_question(qd2, { "question_1" => "" }).should == {
+ :question_id => 1, :correct => false, :points => 0, :text => "" }
end
it "should score a calculated_question" do
@@ -725,15 +711,13 @@ describe QuizSubmission do
:question_id => 1, :correct => true, :points => 26.2, :text => "-11.68", :answer_id => 6396 }
QuizSubmission.score_question(qd, { "question_1" => "-11.675" }).should == {
- :question_id => 1, :correct => false, :points => 0, :text => "-11.675", :answer_id => 6396 }
+ :question_id => 1, :correct => false, :points => 0, :text => "-11.675" }
QuizSubmission.score_question(qd, {}).should == {
- :question_id => 1, :correct => false, :points => 0, :text => "", :answer_id => 6396 }
+ :question_id => 1, :correct => false, :points => 0, :text => "" }
- pending("floating point error") do
- QuizSubmission.score_question(qd, { "question_1" => "-11.72" }).should == {
- :question_id => 1, :correct => true, :points => 26.2, :text => "-11.72", :answer_id => 6396 }
- end
+ QuizSubmission.score_question(qd, { "question_1" => "-11.72" }).should == {
+ :question_id => 1, :correct => true, :points => 26.2, :text => "-11.72", :answer_id => 6396 }
end
it "should score a multiple_answers_question" do
@@ -850,9 +834,8 @@ describe QuizSubmission do
"question_1_answer_9701" => "0",
"question_1_answer_7381" => "1",
})
- user_answer.delete(:points).should == 0
user_answer.should == {
- :question_id => 1, :correct => false, :text => "",
+ :question_id => 1, :correct => false, :points => 0, :text => "",
:answer_9761 => "0",
:answer_3079 => "1",
:answer_5194 => "0",
@@ -891,10 +874,8 @@ describe QuizSubmission do
:answer_7381 => "0",
}
- pending("don't treat no answer as the blank string, breaking undefined_if_blank") do
- QuizSubmission.score_question(qd, { "undefined_if_blank" => "1" }).should ==
- { :question_id => 1, :correct => "undefined", :points => 0, :text => "" }
- end
+ QuizSubmission.score_question(qd, { "undefined_if_blank" => "1" }).should ==
+ { :question_id => 1, :correct => "undefined", :points => 0, :text => "" }
end
it "should score a multiple_dropdowns_question" do
@@ -948,10 +929,8 @@ describe QuizSubmission do
}
user_answer = QuizSubmission.score_question(q, { "question_1630873_4e6185159bea49c4d29047379b400ad5"=>"6994", "question_1630873_3f507e80e33ef092a02948a064433ec5"=>"7676", "question_1630873_78635a3709b540a59678c806b102d038"=>"9908", "question_1630873_657b11f1c17376f178c4d80c4c25d0ab"=>"1121", "question_1630873_02c8346333761ffe9bbddee7b1c5a537"=>"4390", "question_1630873_1865cbc77c83d7571ed8b3a108d11d3d"=>"7604", "question_1630873_94239fc44b4f8aaf36bd3596768f4816"=>"6955", "question_1630873_cd073d17d0d9558fb2be7d7bf9a1c840"=>"3353", "question_1630873_69d0969351d989767d7096f28daf7461"=>"3390"})
- correct = user_answer.delete(:correct)
- points = user_answer.delete(:points)
user_answer.should == {
- :question_id => 1630873, :text => "",
+ :question_id => 1630873, :correct => true, :points => 0.5, :text => "",
:answer_for_structure1 => 4390,
:answer_id_for_structure1 => 4390,
:answer_for_event1 => 3390,
@@ -971,11 +950,6 @@ describe QuizSubmission do
:answer_for_structure7 => 1121,
:answer_id_for_structure7 => 1121,
}
-
- pending("floating point error") do
- correct.should == true
- points.should == 0.5
- end
end
it "should score a fill_in_multiple_blanks_question" do