diff --git a/app/controllers/quizzes/quizzes_controller.rb b/app/controllers/quizzes/quizzes_controller.rb
index 52f486fbb24..46d4ca5f8f6 100644
--- a/app/controllers/quizzes/quizzes_controller.rb
+++ b/app/controllers/quizzes/quizzes_controller.rb
@@ -23,6 +23,7 @@ class Quizzes::QuizzesController < ApplicationController
include KalturaHelper
include ::Filters::Quizzes
include SubmittablesGradingPeriodProtection
+ include QuizMathDataFixup
# If Quiz#one_time_results is on, this flag must be set whenever we've
# rendered the submission results to the student so that the results can be
@@ -294,6 +295,12 @@ class Quizzes::QuizzesController < ApplicationController
def edit
if authorized_action(@quiz, @current_user, :update)
+
+ if params[:fixup_quiz_math_questions] == "1"
+ InstStatsd::Statsd.increment("fixingup_quiz_math_question")
+ @quiz = fixup_quiz_questions_with_bad_math(@quiz)
+ end
+
add_crumb(@quiz.title, named_context_url(@context, :context_quiz_url, @quiz))
@assignment = @quiz.assignment
@quiz.title = params[:title] if params[:title]
@@ -927,6 +934,11 @@ class Quizzes::QuizzesController < ApplicationController
redirect_to course_quiz_url(@context, @quiz) and return
end
+ if params[:fixup_quiz_math_questions] == "1"
+ InstStatsd::Statsd.increment("fixingup_quiz_math_submission")
+ fixup_submission_questions_with_bad_math(@submission)
+ end
+
if !@submission.preview? && (!@js_env || !@js_env[:QUIZ_SUBMISSION_EVENTS_URL])
events_url = api_v1_course_quiz_submission_events_url(@context, @quiz, @submission)
js_env QUIZ_SUBMISSION_EVENTS_URL: events_url
diff --git a/lib/quiz_math_data_fixup.rb b/lib/quiz_math_data_fixup.rb
new file mode 100644
index 00000000000..b21220b9783
--- /dev/null
+++ b/lib/quiz_math_data_fixup.rb
@@ -0,0 +1,189 @@
+# frozen_string_literal: true
+#
+# Copyright (C) 2020 - present 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 .
+#
+
+module QuizMathDataFixup
+ def fixup_quiz_questions_with_bad_math(quiz_or_bank, check_date: nil, question_bank: false)
+ changed = false
+ if question_bank
+ questions = quiz_or_bank.assessment_questions
+ else
+ questions = quiz_or_bank.quiz_questions
+ end
+ questions = questions.where("updated_at>?", check_date) if check_date
+ questions.find_each do |quiz_question|
+ begin
+ new_data = fixup_question_data(quiz_question.question_data)
+ quiz_question.question_data = new_data
+ if quiz_question.changed?
+ stat = question_bank ? 'updated_math_qb_question' : 'updated_math_question'
+ InstStatsd::Statsd.increment(stat)
+ changed = true
+ quiz_question.save!
+ end
+ rescue => e
+ Canvas::Errors.capture(e)
+ end
+ end
+ qstat = question_bank ? 'updated_math_question_bank' : 'updated_math_quiz'
+ InstStatsd::Statsd.increment(qstat) if changed
+ quiz_or_bank
+ end
+
+ def fixup_submission_questions_with_bad_math(submission)
+ submission.questions&.each_with_index do |question, index|
+ begin
+ data = fixup_question_data(question)
+ submission.questions[index] = data
+ rescue => e
+ Canvas::Errors.capture(e)
+ end
+ end
+ begin
+ submission.save! if submission.changed?
+ rescue => e
+ Canvas::Errors.capture(e)
+ end
+ end
+
+ def fixup_question_data(question)
+ question[:answers]&.each_with_index do |answer, index|
+ old_answer = answer.dup
+ new_answer = fixup_answer(answer)
+ question[:answers][index] = new_answer if new_answer != old_answer
+ end
+ question
+ end
+
+ def fixup_answer(answer)
+ answer_changed = false
+ [%i[html text], %i[comments_html comments]].each do |shtml, stext|
+ max_len = shtml == :html ? 16_384 : 5_120 # max allowable length for the data field
+ # inline LaTeX is contained w/in ,
+ # which is probably there because Canvas replaced an equation image
+ # while the new_math_equation_handling flag was on
+ # deal with MathJax generated children that weren't children of .math_equation_latex
+
+ if (answer[shtml] && answer[shtml].length > 0)
+ html = answer[shtml]
+ html = Nokogiri::HTML::DocumentFragment.parse(html)
+ if html.children.length == 1 && html.children[0].node_type == Nokogiri::XML::Node::TEXT_NODE
+ m = %r{equation_images\/([^\s]+)}.match(html.content)
+ if m && m[1]
+ code = URI.unescape(URI.unescape(m[1]))
+ answer[shtml] =
+ ""
+ answer[stext] = ''
+ return answer
+ else
+ answer[shtml] = ''
+ if html.content.length > max_len
+ answer[stext] =
+ "#{I18n.t('LENGTHY TEXT TRUNCATED: ')}#{html.content[0, max_len - 100]}"
+ else
+ answer[stext] = html.content[0, 16_384]
+ end
+ return answer
+ end
+ end
+ html.search('.math_equation_latex').each do |latex|
+ # find MathJax generated children, extract the eq's mathml
+ # incase we need it later, then remove them
+ mjnodes =
+ html.search('[class^="MathJax"]')
+
+ if mjnodes.length > 0
+ n = mjnodes.filter('[data-mathml]')[0]
+ mml = n.attribute('data-mathml') if n
+ mjnodes.each(&:remove)
+ answer_changed = true
+ end
+ if (latex.content.length > 0)
+ code = latex.content.gsub(/(^\\\(|\\\)$)/, '')
+ escaped = URI.escape(URI.escape(code))
+ latex.replace(
+ "#{
+ mml
+ }"
+ )
+ answer_changed = true
+ end
+ end
+ mjnodes = html.search('[class^="MathJax"]')
+
+ if mjnodes.length > 0
+ if mjnodes.length == html.elements.length
+ n = mjnodes.filter('[data-mathml]')[0]
+ mml = n.attribute('data-mathml') if n
+ end
+ mjnodes.each(&:remove)
+ latex = html.search('.math_equation_latex')[0]
+ img = html.search('img.equation_image')
+
+ if latex && latex.content.length > 0
+ latex.content = "\\(#{latex.content}\\)" if latex.content !~ /\\\(.*\\\)/
+ elsif img.length == 0
+ html.inner_html = "#{mml}"
+ end
+ answer_changed = true
+ end
+ hrnodes = html.search('span.hidden-readable')
+ if hrnodes.length > 0
+ hrnodes.each(&:remove)
+ answer_changed = true
+ end
+
+ if answer_changed
+ answer[shtml] = html.to_s if answer_changed
+ answer[stext] = ''
+ end
+ elsif answer[stext] && answer[stext].length > 0
+ m = %r{equation_images\/([^\s]+)}.match(answer[stext])
+ if m && m[1]
+ code = URI.unescape(URI.unescape(m[1]))
+ answer[shtml] =
+ " max_len
+ answer[stext] = "#{I18n.t('LENGTHY TEXT TRUNCATED: ')}#{html.content[0, max_len - 100]}"
+ end
+ end
+ end
+ answer
+ end
+
+ def check_or_fix_quizzes(batch_of_ids)
+ Quizzes::Quiz.where(id: batch_of_ids).find_each { |q| fixup_quiz_questions_with_bad_math(q) }
+ end
+
+ def check_or_fix_question_banks(batch_of_ids)
+ AssessmentQuestionBank.where(id: batch_of_ids).find_each { |q| fixup_quiz_questions_with_bad_math(q, question_bank: true) }
+ end
+end
diff --git a/public/javascripts/mathml.js b/public/javascripts/mathml.js
index d18a00126ab..126f5ebafb9 100644
--- a/public/javascripts/mathml.js
+++ b/public/javascripts/mathml.js
@@ -97,7 +97,7 @@ const mathml = {
isMathInElement(elem) {
if (ENV?.FEATURES?.new_math_equation_handling) {
// handle the change from image + hidden mathml to mathjax formatted latex
- if (elem.querySelector('.math_equation_latex')) {
+ if (elem.querySelector('.math_equation_latex,.math_equation_mml')) {
return true
}