1515import tempfile
1616from io import open
1717import json
18+ import random
1819
1920# Third-party imports
2021# -------------------
21- from gluon import current
22+ from gluon import current , template
23+ from pytest import approx
2224from runestone .lp .lp_common_lib import (
2325 STUDENT_SOURCE_PATH ,
2426 code_here_comment ,
@@ -61,7 +63,7 @@ def is_server_feedback(div_id, course):
6163
6264# Provide feedback for a fill-in-the-blank problem. This should produce
6365# identical results to the code in ``evaluateAnswers`` in ``fitb.js``.
64- def fitb_feedback (answer_json , feedback ):
66+ def fitb_feedback (div_id , answer_json , feedback ):
6567 # Grade based on this feedback. The new format is JSON; the old is
6668 # comma-separated.
6769 try :
@@ -73,6 +75,10 @@ def fitb_feedback(answer_json, feedback):
7375 answer = answer_json .split ("," )
7476 displayFeed = []
7577 isCorrectArray = []
78+ # For dynamic problems.
79+ seed = None
80+ locals_ = {}
81+ globals_ = {"approx" : approx }
7682 # The overall correctness of the entire problem.
7783 correct = True
7884 for blank , feedback_for_blank in zip (answer , feedback ):
@@ -85,18 +91,38 @@ def fitb_feedback(answer_json, feedback):
8591 is_first_item = True
8692 # Check everything but the last answer, which always matches.
8793 for fb in feedback_for_blank [:- 1 ]:
88- if "regex" in fb :
89- if re .search (
90- fb ["regex" ], blank , re .I if fb ["regexFlags" ] == "i" else 0
91- ):
94+ solution_code = fb .get ("solution_code" )
95+ regex = fb .get ("regex" )
96+ number = fb .get ("number" )
97+ if solution_code :
98+ # Run the dynamic code to compute solution prereqs.
99+ dynamic_code = fb .get ("dynamic_code" )
100+ if dynamic_code :
101+ seed = get_seed (div_id )
102+ globals_ ["random" ] = random .Random (seed )
103+ exec (dynamic_code , locals_ , globals_ )
104+
105+ # Compare this solution.
106+ globals_ ["ans" ] = blank
107+ try :
108+ is_correct = eval (solution_code , locals_ , globals_ )
109+ except :
110+ is_correct = False
111+ if is_correct :
112+ isCorrectArray .append (is_first_item )
113+ if not is_first_item :
114+ correct = False
115+ displayFeed .append (template .render (fb ["feedback" ], context = globals_ ))
116+ break
117+ elif regex :
118+ if re .search (regex , blank , re .I if fb ["regexFlags" ] == "i" else 0 ):
92119 isCorrectArray .append (is_first_item )
93120 if not is_first_item :
94121 correct = False
95122 displayFeed .append (fb ["feedback" ])
96123 break
97124 else :
98- assert "number" in fb
99- min_ , max_ = fb ["number" ]
125+ min_ , max_ = number
100126 try :
101127 val = ast .literal_eval (blank )
102128 in_range = val >= min_ and val <= max_
@@ -118,7 +144,55 @@ def fitb_feedback(answer_json, feedback):
118144
119145 # Return grading results to the client for a non-test scenario.
120146 res = dict (correct = correct , displayFeed = displayFeed , isCorrectArray = isCorrectArray )
121- return "T" if correct else "F" , res
147+ return "T" if correct else "F" , seed , res
148+
149+
150+ # Get a random seed from the database, or create and save the seed if it wasn't present.
151+ def get_seed (div_id ):
152+ # See if this user has a stored seed; always get the most recent one. If no user is logged in or there's no stored seed, generate a new seed. Return a RNG based on this seed.
153+ db = current .db
154+ auth = current .auth
155+ row = (
156+ (
157+ db (
158+ (db .fitb_answers .div_id == div_id )
159+ & (db .fitb_answers .sid == auth .user .username )
160+ & (db .fitb_answers .course_name == auth .user .course_name )
161+ )
162+ .select (db .fitb_answers .dynamic_seed , orderby = ~ db .fitb_answers .id )
163+ .first ()
164+ )
165+ if auth .user
166+ else None
167+ )
168+ # If so, return it. Allow a random seed of 0, hence the ``is not None`` test.
169+ if row and row .dynamic_seed is not None :
170+ return row .dynamic_seed
171+ else :
172+ # Otherwise, generate one and store it (if a user is logged in).
173+ return set_seed (div_id )
174+
175+
176+ # Get a RNG based on a stored seed.
177+ def get_random (div_id ):
178+ # Return a RNG using this seed.
179+ return random .Random (get_seed (div_id ))
180+
181+
182+ # Create a new random seed then store it if possible. TODO: provide an "entire class" option to set the same seed for the current class.
183+ def set_seed (div_id ):
184+ seed = random .randint (- (2 ** 31 ), 2 ** 31 - 1 )
185+
186+ auth = current .auth
187+ if auth .user :
188+ current .db .fitb_answers .insert (
189+ sid = auth .user .username ,
190+ div_id = div_id ,
191+ course_name = auth .user .course_name ,
192+ dynamic_seed = seed ,
193+ )
194+
195+ return seed
122196
123197
124198# lp feedback
0 commit comments