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

Commit c895d19

Browse files
committed
Add: Dynamic problems for fitb questions.
1 parent fd5a2d7 commit c895d19

File tree

1 file changed

+66
-45
lines changed

1 file changed

+66
-45
lines changed

runestone/fitb/fitb.py

Lines changed: 66 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,7 @@ class FillInTheBlank(RunestoneIdDirective):
136136
option_spec.update(
137137
{
138138
"blankid": directives.unchanged,
139+
"dynamic": directives.unchanged,
139140
"casei": directives.flag, # case insensitive matching
140141
}
141142
)
@@ -175,8 +176,17 @@ def run(self):
175176

176177
self.updateContent()
177178

178-
self.state.nested_parse(self.content, self.content_offset, fitbNode)
179+
# Process dynamic problem content.
179180
env = self.state.document.settings.env
181+
dynamic = self.options.get("dynamic")
182+
if dynamic:
183+
# Make sure we're server-side.
184+
if not env.config.runestone_server_side_grading:
185+
raise self.error("Dynamic problems require server-side grading.")
186+
# Add in a header to set up the RNG.
187+
fitbNode.template_start = "{{{{ random = get_random({})\n exec({}) }}}}\n".format(repr(self.options["divid"]), repr(dynamic)) + fitbNode.template_start
188+
189+
self.state.nested_parse(self.content, self.content_offset, fitbNode)
180190
self.options["divclass"] = env.config.fitb_div_class
181191

182192
# Expected _`structure`, with assigned variable names and transformations made:
@@ -206,17 +216,21 @@ def run(self):
206216
# self.feedbackArray = [
207217
# [ # blankArray
208218
# { # blankFeedbackDict: feedback 1
209-
# "regex" : feedback_field_name # (An answer, as a regex;
210-
# "regexFlags" : "x" # "i" if ``:casei:`` was specified, otherwise "".) OR
211-
# "number" : [min, max] # a range of correct numeric answers.
212-
# "feedback": feedback_field_body (after being rendered as HTML) # Provides feedback for this answer.
219+
# "regex" : feedback_field_name, # (An answer, as a regex;
220+
# "regexFlags" : "x", # "i" if ``:casei:`` was specified, otherwise "".) OR
221+
# "number" : [min, max], # a range of correct numeric answers OR
222+
# "solution_code" : source_code, # for dynamic problems. For these problems, the first
223+
# "dynamic_code" : source_code, # blank also contains this entry.
224+
# "feedback": feedback_field_body, (after being rendered as HTML) # Provides feedback for this answer.
213225
# },
214226
# { # Feedback 2
215227
# Same as above.
216228
# }
217229
# ],
218230
# [ # Blank 2, same as above.
219-
# ]
231+
# ],
232+
# ...,
233+
# [dynamic_source] # For dynamic problems only.
220234
# ]
221235
#
222236
# ...and a transformed node structure:
@@ -258,47 +272,54 @@ def run(self):
258272
feedback_field_name = feedback_field[0]
259273
assert isinstance(feedback_field_name, nodes.field_name)
260274
feedback_field_name_raw = feedback_field_name.rawsource
261-
# See if this is a number, optinonally followed by a tolerance.
262-
try:
263-
# Parse the number. In Python 3 syntax, this would be ``str_num, *list_tol = feedback_field_name_raw.split()``.
264-
tmp = feedback_field_name_raw.split()
265-
str_num = tmp[0]
266-
list_tol = tmp[1:]
267-
num = ast.literal_eval(str_num)
268-
assert isinstance(num, Number)
269-
# If no tolerance is given, use a tolarance of 0.
270-
if len(list_tol) == 0:
271-
tol = 0
272-
else:
273-
assert len(list_tol) == 1
274-
tol = ast.literal_eval(list_tol[0])
275-
assert isinstance(tol, Number)
276-
# We have the number and a tolerance. Save that.
277-
blankFeedbackDict = {"number": [num - tol, num + tol]}
278-
except (SyntaxError, ValueError, AssertionError):
279-
# We can't parse this as a number, so assume it's a regex.
280-
regex = (
281-
# The given regex must match the entire string, from the beginning (which may be preceded by whitespaces) ...
282-
r"^\s*"
283-
+
284-
# ... to the contents (where a single space in the provided pattern is treated as one or more whitespaces in the student's answer) ...
285-
feedback_field_name.rawsource.replace(" ", r"\s+")
286-
# ... to the end (also with optional spaces).
287-
+ r"\s*$"
288-
)
289-
blankFeedbackDict = {
290-
"regex": regex,
291-
"regexFlags": "i" if "casei" in self.options else "",
292-
}
293-
# Test out the regex to make sure it compiles without an error.
275+
# Simply store the solution code for a dynamic problem.
276+
if dynamic:
277+
blankFeedbackDict = {"solution_code": feedback_field_name_raw}
278+
# For the first blank, also include the dynamic source code.
279+
if not blankArray:
280+
blankFeedbackDict["dynamic_code"] = dynamic
281+
else:
282+
# See if this is a number, optionally followed by a tolerance.
294283
try:
295-
re.compile(regex)
296-
except Exception as ex:
297-
raise self.error(
298-
'Error when compiling regex "{}": {}.'.format(
299-
regex, str(ex)
300-
)
284+
# Parse the number. In Python 3 syntax, this would be ``str_num, *list_tol = feedback_field_name_raw.split()``.
285+
tmp = feedback_field_name_raw.split()
286+
str_num = tmp[0]
287+
list_tol = tmp[1:]
288+
num = ast.literal_eval(str_num)
289+
assert isinstance(num, Number)
290+
# If no tolerance is given, use a tolerance of 0.
291+
if len(list_tol) == 0:
292+
tol = 0
293+
else:
294+
assert len(list_tol) == 1
295+
tol = ast.literal_eval(list_tol[0])
296+
assert isinstance(tol, Number)
297+
# We have the number and a tolerance. Save that.
298+
blankFeedbackDict = {"number": [num - tol, num + tol]}
299+
except (SyntaxError, ValueError, AssertionError):
300+
# We can't parse this as a number, so assume it's a regex.
301+
regex = (
302+
# The given regex must match the entire string, from the beginning (which may be preceded by whitespaces) ...
303+
r"^\s*"
304+
+
305+
# ... to the contents (where a single space in the provided pattern is treated as one or more whitespaces in the student's answer) ...
306+
feedback_field_name.rawsource.replace(" ", r"\s+")
307+
# ... to the end (also with optional spaces).
308+
+ r"\s*$"
301309
)
310+
blankFeedbackDict = {
311+
"regex": regex,
312+
"regexFlags": "i" if "casei" in self.options else "",
313+
}
314+
# Test out the regex to make sure it compiles without an error.
315+
try:
316+
re.compile(regex)
317+
except Exception as ex:
318+
raise self.error(
319+
'Error when compiling regex "{}": {}.'.format(
320+
regex, str(ex)
321+
)
322+
)
302323
blankArray.append(blankFeedbackDict)
303324

304325
feedback_field_body = feedback_field[1]

0 commit comments

Comments
 (0)