Commit 8211600b by Peter Baratta

Merge pull request #726 from edx/peterb/numerical/evaluate-staff

Use calc's evaluator for staff answers in `Numercial` type
parents f14e9302 e0f0ab09
...@@ -7,6 +7,10 @@ the top. Include a label indicating the component affected. ...@@ -7,6 +7,10 @@ the top. Include a label indicating the component affected.
Blades: Took videoalpha out of alpha, replacing the old video player Blades: Took videoalpha out of alpha, replacing the old video player
Common: Allow instructors to input complicated expressions as answers to
`NumericalResponse`s. Prior to the change only numbers were allowed, now any
answer from '1/3' to 'sqrt(12)*(1-1/3^2+1/5/3^2)' are valid.
LMS: Enable beta instructor dashboard. The beta dashboard is a rearchitecture LMS: Enable beta instructor dashboard. The beta dashboard is a rearchitecture
of the existing instructor dashboard and is available by clicking a link at of the existing instructor dashboard and is available by clicking a link at
the top right of the existing dashboard. the top right of the existing dashboard.
......
...@@ -17,6 +17,7 @@ import logging ...@@ -17,6 +17,7 @@ import logging
import numbers import numbers
import numpy import numpy
import os import os
from pyparsing import ParseException
import sys import sys
import random import random
import re import re
...@@ -826,45 +827,89 @@ class NumericalResponse(LoncapaResponse): ...@@ -826,45 +827,89 @@ class NumericalResponse(LoncapaResponse):
required_attributes = ['answer'] required_attributes = ['answer']
max_inputfields = 1 max_inputfields = 1
def __init__(self, *args, **kwargs):
self.correct_answer = ''
self.tolerance = '0' # Default value
super(NumericalResponse, self).__init__(*args, **kwargs)
def setup_response(self): def setup_response(self):
xml = self.xml xml = self.xml
context = self.context context = self.context
self.correct_answer = contextualize_text(xml.get('answer'), context) self.correct_answer = contextualize_text(xml.get('answer'), context)
# Find the tolerance
tolerance_xml = xml.xpath(
'//*[@id=$id]//responseparam[@type="tolerance"]/@default',
id=xml.get('id')
)
if tolerance_xml: # If it isn't an empty list...
self.tolerance = contextualize_text(tolerance_xml[0], context)
def get_staff_ans(self):
"""
Given the staff answer as a string, find its float value.
Use `evaluator` for this, but for backward compatability, try the
built-in method `complex` (which used to be the standard).
"""
try: try:
self.tolerance_xml = xml.xpath( correct_ans = complex(self.correct_answer)
'//*[@id=$id]//responseparam[@type="tolerance"]/@default', except ValueError:
id=xml.get('id'))[0] # When `correct_answer` is not of the form X+Yj, it raises a
self.tolerance = contextualize_text(self.tolerance_xml, context) # `ValueError`. Then test if instead it is a math expression.
except IndexError: # xpath found an empty list, so (...)[0] is the error # `complex` seems to only generate `ValueErrors`, only catch these.
self.tolerance = '0' try:
correct_ans = evaluator({}, {}, self.correct_answer)
except Exception:
log.debug("Content error--answer '%s' is not a valid number", self.correct_answer)
raise StudentInputError(
"There was a problem with the staff answer to this problem"
)
return correct_ans
def get_score(self, student_answers): def get_score(self, student_answers):
'''Grade a numeric response ''' '''Grade a numeric response '''
student_answer = student_answers[self.answer_id] student_answer = student_answers[self.answer_id]
try: correct_float = self.get_staff_ans()
correct_ans = complex(self.correct_answer)
except ValueError:
log.debug("Content error--answer '{0}' is not a valid complex number".format(
self.correct_answer))
raise StudentInputError(
"There was a problem with the staff answer to this problem")
try: general_exception = StudentInputError(
correct = compare_with_tolerance( u"Could not interpret '{0}' as a number".format(cgi.escape(student_answer))
evaluator(dict(), dict(), student_answer), )
correct_ans, self.tolerance)
# We should catch this explicitly.
# I think this is just pyparsing.ParseException, calc.UndefinedVariable:
# But we'd need to confirm
except:
# Use the traceback-preserving version of re-raising with a
# different type
type, value, traceback = sys.exc_info()
raise StudentInputError, ("Could not interpret '%s' as a number" % # Begin `evaluator` block
cgi.escape(student_answer)), traceback # Catch a bunch of exceptions and give nicer messages to the student.
try:
student_float = evaluator({}, {}, student_answer)
except UndefinedVariable as undef_var:
raise StudentInputError(
u"You may not use variables ({0}) in numerical problems".format(undef_var.message)
)
except ValueError as val_err:
if 'factorial' in val_err.message:
# This is thrown when fact() or factorial() is used in an answer
# that evaluates on negative and/or non-integer inputs
# ve.message will be: `factorial() only accepts integral values` or
# `factorial() not defined for negative values`
raise StudentInputError(
("factorial function evaluated outside its domain:"
"'{0}'").format(cgi.escape(student_answer))
)
else:
raise general_exception
except ParseException:
raise StudentInputError(
u"Invalid math syntax: '{0}'".format(cgi.escape(student_answer))
)
except Exception:
raise general_exception
# End `evaluator` block -- we figured out the student's answer!
correct = compare_with_tolerance(
student_float, correct_float, self.tolerance
)
if correct: if correct:
return CorrectMap(self.answer_id, 'correct') return CorrectMap(self.answer_id, 'correct')
else: else:
...@@ -1691,18 +1736,26 @@ class FormulaResponse(LoncapaResponse): ...@@ -1691,18 +1736,26 @@ class FormulaResponse(LoncapaResponse):
required_attributes = ['answer', 'samples'] required_attributes = ['answer', 'samples']
max_inputfields = 1 max_inputfields = 1
def __init__(self, *args, **kwargs):
self.correct_answer = ''
self.samples = ''
self.tolerance = '1e-5' # Default value
self.case_sensitive = False
super(FormulaResponse, self).__init__(*args, **kwargs)
def setup_response(self): def setup_response(self):
xml = self.xml xml = self.xml
context = self.context context = self.context
self.correct_answer = contextualize_text(xml.get('answer'), context) self.correct_answer = contextualize_text(xml.get('answer'), context)
self.samples = contextualize_text(xml.get('samples'), context) self.samples = contextualize_text(xml.get('samples'), context)
try:
self.tolerance_xml = xml.xpath( # Find the tolerance
'//*[@id=$id]//responseparam[@type="tolerance"]/@default', tolerance_xml = xml.xpath(
id=xml.get('id'))[0] '//*[@id=$id]//responseparam[@type="tolerance"]/@default',
self.tolerance = contextualize_text(self.tolerance_xml, context) id=xml.get('id')
except Exception: )
self.tolerance = '0.00001' if tolerance_xml: # If it isn't an empty list...
self.tolerance = contextualize_text(tolerance_xml[0], context)
ts = xml.get('type') ts = xml.get('type')
if ts is None: if ts is None:
...@@ -1734,7 +1787,7 @@ class FormulaResponse(LoncapaResponse): ...@@ -1734,7 +1787,7 @@ class FormulaResponse(LoncapaResponse):
ranges = dict(zip(variables, sranges)) ranges = dict(zip(variables, sranges))
for _ in range(numsamples): for _ in range(numsamples):
instructor_variables = self.strip_dict(dict(self.context)) instructor_variables = self.strip_dict(dict(self.context))
student_variables = dict() student_variables = {}
# ranges give numerical ranges for testing # ranges give numerical ranges for testing
for var in ranges: for var in ranges:
# TODO: allow specified ranges (i.e. integers and complex numbers) for random variables # TODO: allow specified ranges (i.e. integers and complex numbers) for random variables
...@@ -1746,7 +1799,7 @@ class FormulaResponse(LoncapaResponse): ...@@ -1746,7 +1799,7 @@ class FormulaResponse(LoncapaResponse):
# Call `evaluator` on the instructor's answer and get a number # Call `evaluator` on the instructor's answer and get a number
instructor_result = evaluator( instructor_result = evaluator(
instructor_variables, dict(), instructor_variables, {},
expected, case_sensitive=self.case_sensitive expected, case_sensitive=self.case_sensitive
) )
try: try:
...@@ -1756,7 +1809,7 @@ class FormulaResponse(LoncapaResponse): ...@@ -1756,7 +1809,7 @@ class FormulaResponse(LoncapaResponse):
# Call `evaluator` on the student's answer; look for exceptions # Call `evaluator` on the student's answer; look for exceptions
student_result = evaluator( student_result = evaluator(
student_variables, student_variables,
dict(), {},
given, given,
case_sensitive=self.case_sensitive case_sensitive=self.case_sensitive
) )
...@@ -2422,7 +2475,7 @@ class ChoiceTextResponse(LoncapaResponse): ...@@ -2422,7 +2475,7 @@ class ChoiceTextResponse(LoncapaResponse):
# if all that is important is verifying numericality # if all that is important is verifying numericality
try: try:
partial_correct = compare_with_tolerance( partial_correct = compare_with_tolerance(
evaluator(dict(), dict(), answer_value), evaluator({}, {}, answer_value),
correct_ans, correct_ans,
tolerance tolerance
) )
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment