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

Commit 2744e8a

Browse files
committed
Add: Dynamic problems for fitb questions.
1 parent baca85e commit 2744e8a

File tree

6 files changed

+112
-12
lines changed

6 files changed

+112
-12
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: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,9 @@
2222
import logging
2323
import datetime
2424
import importlib
25+
import random
26+
27+
import pytest
2528

2629

2730
logger = logging.getLogger(settings.logger)
@@ -258,6 +261,8 @@ def dummy():
258261
subchapter_list=_subchaptoc(base_course, chapter),
259262
questions=questions,
260263
motd=motd,
264+
get_random=_get_random,
265+
approx=pytest.approx,
261266
)
262267

263268

@@ -289,6 +294,40 @@ def _subchaptoc(course, chap):
289294
return toclist
290295

291296

297+
# Get a RNG for a problem.
298+
def _get_random(div_id):
299+
# See if this user has a stored seed; always get the most recent one. If no user is logged in, generate a new seed.
300+
row = (
301+
(
302+
db(
303+
(db.fitb_answers.div_id == div_id)
304+
& (db.fitb_answers.sid == auth.user.username)
305+
& (db.fitb_answers.course_name == auth.user.course_name)
306+
)
307+
.select(db.fitb_answers.dynamic_seed, orderby=~db.fitb_answers.id)
308+
.first()
309+
)
310+
if auth.user
311+
else None
312+
)
313+
# If so, return it. Allow a random seed of 0, hence the ``is not None`` test.
314+
if row and row.dynamic_seed is not None:
315+
seed = row.dynamic_seed
316+
else:
317+
# Otherwise, generate one and store it (if a user is logged in).
318+
seed = random.randint(-(2 ** 31), 2 ** 31 - 1)
319+
if auth.user:
320+
db.fitb_answers.insert(
321+
sid=auth.user.username,
322+
div_id=div_id,
323+
course_name=auth.user.course_name,
324+
dynamic_seed=seed,
325+
)
326+
327+
# Return a RNG using this seed.
328+
return random.Random(seed)
329+
330+
292331
def _exercises(basecourse, chapter):
293332
"""
294333
Given a base course and a chapter return the instructor generated questions

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: 52 additions & 8 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
# -------------------
2122
from gluon import current
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,11 @@ def fitb_feedback(answer_json, feedback):
7375
answer = answer_json.split(",")
7476
displayFeed = []
7577
isCorrectArray = []
78+
db = current.db
79+
# For dynamic problems.
80+
seed = None
81+
locals_ = {}
82+
globals_ = {"approx": approx}
7683
# The overall correctness of the entire problem.
7784
correct = True
7885
for blank, feedback_for_blank in zip(answer, feedback):
@@ -85,18 +92,55 @@ def fitb_feedback(answer_json, feedback):
8592
is_first_item = True
8693
# Check everything but the last answer, which always matches.
8794
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-
):
95+
solution_code = fb.get("solution_code")
96+
regex = fb.get("regex")
97+
number = fb.get("number")
98+
if solution_code:
99+
# Run the dynamic code to compute solution prereqs.
100+
dynamic_code = fb.get("dynamic_code")
101+
if dynamic_code:
102+
# Get the seed.
103+
seed = (
104+
db(
105+
(db.fitb_answers.div_id == div_id)
106+
& (db.fitb_answers.sid == current.auth.user.username)
107+
& (
108+
db.fitb_answers.course_name
109+
== current.auth.user.course_name
110+
)
111+
)
112+
.select(
113+
db.fitb_answers.dynamic_seed,
114+
orderby=~db.fitb_answers.id,
115+
)
116+
.first()
117+
.dynamic_seed
118+
)
119+
# Set it up.
120+
globals_["random"] = random.Random(seed)
121+
exec(dynamic_code, locals_, globals_)
122+
123+
# Compare this solution.
124+
globals_["ans"] = blank
125+
try:
126+
is_correct = eval(solution_code, locals_, globals_)
127+
except:
128+
is_correct = False
129+
if is_correct:
130+
isCorrectArray.append(is_first_item)
131+
if not is_first_item:
132+
correct = False
133+
displayFeed.append(fb["feedback"])
134+
break
135+
elif regex:
136+
if re.search(regex, blank, re.I if fb["regexFlags"] == "i" else 0):
92137
isCorrectArray.append(is_first_item)
93138
if not is_first_item:
94139
correct = False
95140
displayFeed.append(fb["feedback"])
96141
break
97142
else:
98-
assert "number" in fb
99-
min_, max_ = fb["number"]
143+
min_, max_ = number
100144
try:
101145
val = ast.literal_eval(blank)
102146
in_range = val >= min_ and val <= max_
@@ -118,7 +162,7 @@ def fitb_feedback(answer_json, feedback):
118162

119163
# Return grading results to the client for a non-test scenario.
120164
res = dict(correct=correct, displayFeed=displayFeed, isCorrectArray=isCorrectArray)
121-
return "T" if correct else "F", res
165+
return "T" if correct else "F", seed, res
122166

123167

124168
# 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 {{=a}} + {{=b}}?
110+
111+
- :int(ans) == a + b: Correct!
112+
:int(ans) == a - b: That's subtraction.
113+
:int(ans) == a * b: That's multiplication.
114+
:float(ans) == approx(a / b): That's division.
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)