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

Commit 615ca3b

Browse files
committed
Add: Dynamic problems for fitb questions.
1 parent baca85e commit 615ca3b

File tree

6 files changed

+110
-13
lines changed

6 files changed

+110
-13
lines changed

controllers/ajax.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -162,7 +162,7 @@ def hsblog():
162162
# Grade on the server if needed.
163163
do_server_feedback, feedback = is_server_feedback(div_id, course)
164164
if do_server_feedback:
165-
correct, res_update = fitb_feedback(answer_json, feedback)
165+
correct, seed, res_update = fitb_feedback(div_id, answer_json, feedback)
166166
res.update(res_update)
167167

168168
# Save this data.
@@ -173,6 +173,7 @@ def hsblog():
173173
answer=answer_json,
174174
correct=correct,
175175
course_name=course,
176+
dynamic_seed=seed,
176177
)
177178

178179
elif event == "dragNdrop" and auth.user:
@@ -1191,13 +1192,13 @@ def getAssessResults():
11911192
)
11921193
.first()
11931194
)
1194-
if not rows:
1195+
if not rows or rows.answer is None:
11951196
return "" # server doesn't have it so we load from local storage instead
11961197
#
11971198
res = {"answer": rows.answer, "timestamp": str(rows.timestamp)}
11981199
do_server_feedback, feedback = is_server_feedback(div_id, course)
11991200
if do_server_feedback:
1200-
correct, res_update = fitb_feedback(rows.answer, feedback)
1201+
correct, seed, res_update = fitb_feedback(div_id, rows.answer, feedback)
12011202
res.update(res_update)
12021203
return json.dumps(res)
12031204
elif event == "mChoice":

controllers/books.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,10 @@
2323
import datetime
2424
import importlib
2525

26+
import pytest
27+
28+
from feedback import get_random
29+
2630

2731
logger = logging.getLogger(settings.logger)
2832
logger.setLevel(settings.log_level)
@@ -258,6 +262,8 @@ def dummy():
258262
subchapter_list=_subchaptoc(base_course, chapter),
259263
questions=questions,
260264
motd=motd,
265+
get_random=get_random,
266+
approx=pytest.approx,
261267
)
262268

263269

models/db_ebook.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,8 @@
113113
Field("course_name", "string"),
114114
Field("answer", "string"),
115115
Field("correct", "boolean"),
116+
# The seed used to generate this dynamic problem.
117+
Field("dynamic_seed", "integer"),
116118
migrate=table_migrate_prefix + "fitb_answers.table",
117119
)
118120
db.define_table(

modules/feedback.py

Lines changed: 83 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,12 @@
1515
import tempfile
1616
from io import open
1717
import 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
2224
from 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

tests/test_course_1/_sources/index.rst

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,20 @@ Fill in the Blank
101101
:x: Nope.
102102

103103

104+
.. fillintheblank:: fitb_dynamic
105+
:dynamic:
106+
a = random.randrange(0, 10)
107+
b = random.randrange(0, 10)
108+
109+
What is :math:`{{=a}} + {{=b}}`?
110+
111+
- :int(ans) == a + b: Correct!
112+
:int(ans) == a - b: That's :math:`{{=a}} - {{=b}}`.
113+
:int(ans) == a * b: That's :math:`{{=a}}\cdot{{=b}}`.
114+
:float(ans) == approx(a / b): That's :math:`{{=a}}/{{=b}}`.
115+
:x: I don't know what you're doing.
116+
117+
104118
Short answers
105119
-------------
106120
.. shortanswer:: test_short_answer_1

tests/test_server.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -986,7 +986,7 @@ def __eq__(self, other):
986986
"Given name",
987987
"e-mail",
988988
"avg grade (%)",
989-
"Q-5",
989+
"Q-6",
990990
"Q-2",
991991
"Q-1",
992992
"Q-1",

0 commit comments

Comments
 (0)