Skip to content
This repository was archived by the owner on Jun 30, 2024. It is now read-only.
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 4 additions & 3 deletions controllers/ajax.py
Original file line number Diff line number Diff line change
Expand Up @@ -182,7 +182,7 @@ def hsblog():
# Grade on the server if needed.
do_server_feedback, feedback = is_server_feedback(div_id, course)
if do_server_feedback:
correct, res_update = fitb_feedback(answer_json, feedback)
correct, seed, res_update = fitb_feedback(div_id, answer_json, feedback)
res.update(res_update)

# Save this data.
Expand All @@ -193,6 +193,7 @@ def hsblog():
answer=answer_json,
correct=correct,
course_name=course,
dynamic_seed=seed,
)

elif event == "dragNdrop" and auth.user:
Expand Down Expand Up @@ -1202,13 +1203,13 @@ def getAssessResults():
)
.first()
)
if not rows:
if not rows or rows.answer is None:
return "" # server doesn't have it so we load from local storage instead
#
res = {"answer": rows.answer, "timestamp": str(rows.timestamp)}
do_server_feedback, feedback = is_server_feedback(div_id, course)
if do_server_feedback:
correct, res_update = fitb_feedback(rows.answer, feedback)
correct, seed, res_update = fitb_feedback(div_id, rows.answer, feedback)
res.update(res_update)
return json.dumps(res)
elif event == "mChoice":
Expand Down
7 changes: 7 additions & 0 deletions controllers/books.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,11 @@
import importlib
import random

import pytest

from feedback import get_random


logger = logging.getLogger(settings.logger)
logger.setLevel(settings.log_level)

Expand Down Expand Up @@ -291,6 +296,8 @@ def dummy():
questions=questions,
motd=motd,
banner_num=banner_num,
get_random=get_random,
approx=pytest.approx,
)


Expand Down
2 changes: 2 additions & 0 deletions models/db_ebook.py
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,8 @@
Field("course_name", "string"),
Field("answer", "string"),
Field("correct", "boolean"),
# The seed used to generate this dynamic problem.
Field("dynamic_seed", "integer"),
migrate=table_migrate_prefix + "fitb_answers.table",
)
# dragndrop_answers
Expand Down
92 changes: 83 additions & 9 deletions modules/feedback.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,12 @@
import tempfile
from io import open
import json
import random

# Third-party imports
# -------------------
from gluon import current
from gluon import current, template
from pytest import approx
from runestone.lp.lp_common_lib import (
STUDENT_SOURCE_PATH,
code_here_comment,
Expand Down Expand Up @@ -61,7 +63,7 @@ def is_server_feedback(div_id, course):

# Provide feedback for a fill-in-the-blank problem. This should produce
# identical results to the code in ``evaluateAnswers`` in ``fitb.js``.
def fitb_feedback(answer_json, feedback):
def fitb_feedback(div_id, answer_json, feedback):
# Grade based on this feedback. The new format is JSON; the old is
# comma-separated.
try:
Expand All @@ -73,6 +75,10 @@ def fitb_feedback(answer_json, feedback):
answer = answer_json.split(",")
displayFeed = []
isCorrectArray = []
# For dynamic problems.
seed = None
locals_ = {}
globals_ = {"approx": approx}
# The overall correctness of the entire problem.
correct = True
for blank, feedback_for_blank in zip(answer, feedback):
Expand All @@ -85,18 +91,38 @@ def fitb_feedback(answer_json, feedback):
is_first_item = True
# Check everything but the last answer, which always matches.
for fb in feedback_for_blank[:-1]:
if "regex" in fb:
if re.search(
fb["regex"], blank, re.I if fb["regexFlags"] == "i" else 0
):
solution_code = fb.get("solution_code")
regex = fb.get("regex")
number = fb.get("number")
if solution_code:
# Run the dynamic code to compute solution prereqs.
dynamic_code = fb.get("dynamic_code")
if dynamic_code:
seed = get_seed(div_id)
globals_["random"] = random.Random(seed)
exec(dynamic_code, locals_, globals_)

# Compare this solution.
globals_["ans"] = blank
try:
is_correct = eval(solution_code, locals_, globals_)
except:
is_correct = False
if is_correct:
isCorrectArray.append(is_first_item)
if not is_first_item:
correct = False
displayFeed.append(template.render(fb["feedback"], context=globals_))
break
elif regex:
if re.search(regex, blank, re.I if fb["regexFlags"] == "i" else 0):
isCorrectArray.append(is_first_item)
if not is_first_item:
correct = False
displayFeed.append(fb["feedback"])
break
else:
assert "number" in fb
min_, max_ = fb["number"]
min_, max_ = number
try:
val = ast.literal_eval(blank)
in_range = val >= min_ and val <= max_
Expand All @@ -118,7 +144,55 @@ def fitb_feedback(answer_json, feedback):

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


# Get a random seed from the database, or create and save the seed if it wasn't present.
def get_seed(div_id):
# 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.
db = current.db
auth = current.auth
row = (
(
db(
(db.fitb_answers.div_id == div_id)
& (db.fitb_answers.sid == auth.user.username)
& (db.fitb_answers.course_name == auth.user.course_name)
)
.select(db.fitb_answers.dynamic_seed, orderby=~db.fitb_answers.id)
.first()
)
if auth.user
else None
)
# If so, return it. Allow a random seed of 0, hence the ``is not None`` test.
if row and row.dynamic_seed is not None:
return row.dynamic_seed
else:
# Otherwise, generate one and store it (if a user is logged in).
return set_seed(div_id)


# Get a RNG based on a stored seed.
def get_random(div_id):
# Return a RNG using this seed.
return random.Random(get_seed(div_id))


# 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.
def set_seed(div_id):
seed = random.randint(-(2 ** 31), 2 ** 31 - 1)

auth = current.auth
if auth.user:
current.db.fitb_answers.insert(
sid=auth.user.username,
div_id=div_id,
course_name=auth.user.course_name,
dynamic_seed=seed,
)

return seed


# lp feedback
Expand Down
14 changes: 14 additions & 0 deletions tests/test_course_1/_sources/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,20 @@ Fill in the Blank
:x: Nope.


.. fillintheblank:: fitb_dynamic
:dynamic:
a = random.randrange(0, 10)
b = random.randrange(0, 10)

What is :math:`{{=a}} + {{=b}}`?

- :int(ans) == a + b: Correct!
:int(ans) == a - b: That's :math:`{{=a}} - {{=b}}`.
:int(ans) == a * b: That's :math:`{{=a}}\cdot{{=b}}`.
:float(ans) == approx(a / b): That's :math:`{{=a}}/{{=b}}`.
:x: I don't know what you're doing.


Short answers
-------------
.. shortanswer:: test_short_answer_1
Expand Down