Fixup bad math in quiz questions

closes LS-1678
flag=none

test plan:
  - find a quiz with bad math equations in the answers
  - request /courses/:id/quizzes/:id/edit?fixup_quiz_math_questions=1
  > expect the quiz answers to look better

Change-Id: I2fc0d0beaadab3343df4b10a5f7413bd1b907e7d
Reviewed-on: https://gerrit.instructure.com/c/canvas-lms/+/254429
Reviewed-by: Rob Orton <rob@instructure.com>
QA-Review: Rob Orton <rob@instructure.com>
Product-Review: Rob Orton <rob@instructure.com>
Tested-by: Service Cloud Jenkins <svc.cloudjenkins@instructure.com>
This commit is contained in:
Ed Schiebel 2020-12-06 12:57:34 -05:00
parent ac294386b0
commit f3b7e8ded6
3 changed files with 202 additions and 1 deletions

View File

@ -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

189
lib/quiz_math_data_fixup.rb Normal file
View File

@ -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 <http://www.gnu.org/licenses/>.
#
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 <span class="math_equation_latex">,
# 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] =
"<img class='equation_image' src='/equation_images/#{m[1]}' alt='LaTeX: #{
code
}' title='#{code} data-equation-content='#{code}>"
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(
"<img class='equation_image' src='/equation_images/#{escaped}' alt='LaTeX: #{
code
}' title='#{code}>"
)
answer_changed = true
elsif mml
latex.replace(
"<math xmlns='http://www.w3.org/1998/Math/MathML' class='math_equation_mml'>#{
mml
}</math>"
)
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 = "<span class='math_equation_mml'>#{mml}</span>"
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] =
"<img class='equation_image' src='/equation_images/#{m[1]}' alt='LaTeX: #{
code
}' title='#{code}>"
answer[stext] = ''
return answer
elsif answer[stext].length > 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

View File

@ -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
}