@@ -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,6 +176,15 @@ def run(self):
175176
176177 self .updateContent ()
177178
179+ # Process dynamic problem content.
180+ dynamic = self .options .get ("dynamic" )
181+ if dynamic :
182+ # Make sure we're server-side.
183+ if not env .config .runestone_server_side_grading :
184+ raise self .error ("Dynamic problems require server-side grading." )
185+ # Add in a header to set up the RNG.
186+ fitbNode .template_start = "{{{{ random = get_random({})\n exec({}) }}}}\n " .format (repr (self .options ["divid" ]), repr (dynamic )) + fitbNode .template_start
187+
178188 self .state .nested_parse (self .content , self .content_offset , fitbNode )
179189 env = self .state .document .settings .env
180190 self .options ["divclass" ] = env .config .fitb_div_class
@@ -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