Skip to content
This repository was archived by the owner on Jun 7, 2023. It is now read-only.

Commit b25264a

Browse files
committed
Add: Dynamic problems for fitb questions.
1 parent 11ffe8c commit b25264a

1 file changed

Lines changed: 66 additions & 45 deletions

File tree

runestone/fitb/fitb.py

Lines changed: 66 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -144,6 +144,7 @@ class FillInTheBlank(RunestoneIdDirective):
144144
option_spec = RunestoneIdDirective.option_spec.copy()
145145
option_spec.update(
146146
{
147+
"dynamic": directives.unchanged,
147148
"casei": directives.flag, # case insensitive matching
148149
}
149150
)
@@ -183,8 +184,17 @@ def run(self):
183184

184185
self.updateContent()
185186

186-
self.state.nested_parse(self.content, self.content_offset, fitbNode)
187+
# Process dynamic problem content.
187188
env = self.state.document.settings.env
189+
dynamic = self.options.get("dynamic")
190+
if dynamic:
191+
# Make sure we're server-side.
192+
if not env.config.runestone_server_side_grading:
193+
raise self.error("Dynamic problems require server-side grading.")
194+
# Add in a header to set up the RNG.
195+
fitbNode.template_start = "{{{{ random = get_random({})\n exec({}) }}}}\n".format(repr(self.options["divid"]), repr(dynamic)) + fitbNode.template_start
196+
197+
self.state.nested_parse(self.content, self.content_offset, fitbNode)
188198
self.options["divclass"] = env.config.fitb_div_class
189199

190200
# Expected _`structure`, with assigned variable names and transformations made:
@@ -214,17 +224,21 @@ def run(self):
214224
# self.feedbackArray = [
215225
# [ # blankArray
216226
# { # blankFeedbackDict: feedback 1
217-
# "regex" : feedback_field_name # (An answer, as a regex;
218-
# "regexFlags" : "x" # "i" if ``:casei:`` was specified, otherwise "".) OR
219-
# "number" : [min, max] # a range of correct numeric answers.
220-
# "feedback": feedback_field_body (after being rendered as HTML) # Provides feedback for this answer.
227+
# "regex" : feedback_field_name, # (An answer, as a regex;
228+
# "regexFlags" : "x", # "i" if ``:casei:`` was specified, otherwise "".) OR
229+
# "number" : [min, max], # a range of correct numeric answers OR
230+
# "solution_code" : source_code, # (For dynamic problems -- the dynamically-computed answer.
231+
# "dynamic_code" : source_code, # The first blank also contains setup code.)
232+
# "feedback": feedback_field_body, (after being rendered as HTML) # Provides feedback for this answer.
221233
# },
222234
# { # Feedback 2
223235
# Same as above.
224236
# }
225237
# ],
226238
# [ # Blank 2, same as above.
227-
# ]
239+
# ],
240+
# ...,
241+
# [dynamic_source] # For dynamic problems only.
228242
# ]
229243
#
230244
# ...and a transformed node structure:
@@ -266,47 +280,54 @@ def run(self):
266280
feedback_field_name = feedback_field[0]
267281
assert isinstance(feedback_field_name, nodes.field_name)
268282
feedback_field_name_raw = feedback_field_name.rawsource
269-
# See if this is a number, optinonally followed by a tolerance.
270-
try:
271-
# Parse the number. In Python 3 syntax, this would be ``str_num, *list_tol = feedback_field_name_raw.split()``.
272-
tmp = feedback_field_name_raw.split()
273-
str_num = tmp[0]
274-
list_tol = tmp[1:]
275-
num = ast.literal_eval(str_num)
276-
assert isinstance(num, Number)
277-
# If no tolerance is given, use a tolarance of 0.
278-
if len(list_tol) == 0:
279-
tol = 0
280-
else:
281-
assert len(list_tol) == 1
282-
tol = ast.literal_eval(list_tol[0])
283-
assert isinstance(tol, Number)
284-
# We have the number and a tolerance. Save that.
285-
blankFeedbackDict = {"number": [num - tol, num + tol]}
286-
except (SyntaxError, ValueError, AssertionError):
287-
# We can't parse this as a number, so assume it's a regex.
288-
regex = (
289-
# The given regex must match the entire string, from the beginning (which may be preceded by whitespaces) ...
290-
r"^\s*"
291-
+
292-
# ... to the contents (where a single space in the provided pattern is treated as one or more whitespaces in the student's answer) ...
293-
feedback_field_name.rawsource.replace(" ", r"\s+")
294-
# ... to the end (also with optional spaces).
295-
+ r"\s*$"
296-
)
297-
blankFeedbackDict = {
298-
"regex": regex,
299-
"regexFlags": "i" if "casei" in self.options else "",
300-
}
301-
# Test out the regex to make sure it compiles without an error.
283+
# Simply store the solution code for a dynamic problem.
284+
if dynamic:
285+
blankFeedbackDict = {"solution_code": feedback_field_name_raw}
286+
# For the first blank, also include the dynamic source code.
287+
if not blankArray:
288+
blankFeedbackDict["dynamic_code"] = dynamic
289+
else:
290+
# See if this is a number, optionally followed by a tolerance.
302291
try:
303-
re.compile(regex)
304-
except Exception as ex:
305-
raise self.error(
306-
'Error when compiling regex "{}": {}.'.format(
307-
regex, str(ex)
308-
)
292+
# Parse the number. In Python 3 syntax, this would be ``str_num, *list_tol = feedback_field_name_raw.split()``.
293+
tmp = feedback_field_name_raw.split()
294+
str_num = tmp[0]
295+
list_tol = tmp[1:]
296+
num = ast.literal_eval(str_num)
297+
assert isinstance(num, Number)
298+
# If no tolerance is given, use a tolerance of 0.
299+
if len(list_tol) == 0:
300+
tol = 0
301+
else:
302+
assert len(list_tol) == 1
303+
tol = ast.literal_eval(list_tol[0])
304+
assert isinstance(tol, Number)
305+
# We have the number and a tolerance. Save that.
306+
blankFeedbackDict = {"number": [num - tol, num + tol]}
307+
except (SyntaxError, ValueError, AssertionError):
308+
# We can't parse this as a number, so assume it's a regex.
309+
regex = (
310+
# The given regex must match the entire string, from the beginning (which may be preceded by whitespaces) ...
311+
r"^\s*"
312+
+
313+
# ... to the contents (where a single space in the provided pattern is treated as one or more whitespaces in the student's answer) ...
314+
feedback_field_name.rawsource.replace(" ", r"\s+")
315+
# ... to the end (also with optional spaces).
316+
+ r"\s*$"
309317
)
318+
blankFeedbackDict = {
319+
"regex": regex,
320+
"regexFlags": "i" if "casei" in self.options else "",
321+
}
322+
# Test out the regex to make sure it compiles without an error.
323+
try:
324+
re.compile(regex)
325+
except Exception as ex:
326+
raise self.error(
327+
'Error when compiling regex "{}": {}.'.format(
328+
regex, str(ex)
329+
)
330+
)
310331
blankArray.append(blankFeedbackDict)
311332

312333
feedback_field_body = feedback_field[1]

0 commit comments

Comments
 (0)