Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Shuffle Question Order in Quiz Assessments/Homeworks #121

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
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: 6 additions & 1 deletion app/models.py
Original file line number Diff line number Diff line change
@@ -448,10 +448,11 @@ class Session(BaseModel):
id: PyObjectId = Field(default_factory=PyObjectId, alias="_id")
user_id: str
quiz_id: str
omr_mode: bool = False
created_at: datetime = Field(default_factory=datetime.utcnow)
events: List[Event] = []
has_quiz_ended: bool = False
omr_mode: bool = False
question_order: List[int] = []
metrics: Optional[SessionMetrics] = None # gets updated when quiz ends

class Config:
@@ -481,6 +482,9 @@ class SessionResponse(Session):

is_first: bool
session_answers: List[SessionAnswer]
question_order: List[
int
] # random order of questions for each quiz assesment/homework
time_remaining: Optional[int] = None # time in seconds

class Config:
@@ -506,6 +510,7 @@ class Config:
"time_spent": 30,
},
],
"question_order": [0, 1, 2, 3],
}
}

1 change: 0 additions & 1 deletion app/routers/quizzes.py
Original file line number Diff line number Diff line change
@@ -195,7 +195,6 @@ async def get_quiz(quiz_id: str, omr_mode: bool = Query(False)):
]
)
)

for question_set_index, question_set in enumerate(quiz["question_sets"]):
updated_subset_without_details = []
options_count_per_set = options_count_across_sets[question_set_index][
39 changes: 39 additions & 0 deletions app/routers/sessions.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
from fastapi import APIRouter, status, HTTPException
import random
from fastapi.responses import JSONResponse
from fastapi.encoders import jsonable_encoder
import pymongo
@@ -15,13 +16,46 @@
from datetime import datetime
from logger_config import get_logger
from typing import Dict
from settings import Settings


def str_to_datetime(datetime_str: str) -> datetime:
"""converts string to datetime format"""
return datetime.fromisoformat(datetime_str)


def shuffle_question_order(question_sets):
question_order = []
bucket_size = Settings().subset_size

global_index = 0 # Track global index across all questions
# Iterate over each question set
for question_set in question_sets:
total_questions = len(
question_set["questions"]
) # Get total number of questions in the current set
num_blocks = (
total_questions + bucket_size - 1
) // bucket_size # Equivalent to Math.ceil(total_questions / subset_size)

# For each block (subset of questions)
for block in range(num_blocks):
# Get the start and end index for the current block
start = block * bucket_size
end = min(start + bucket_size, total_questions)
block_indices = list(range(global_index, global_index + (end - start)))

# Shuffle the current block using Fisher-Yates algorithm
random.shuffle(block_indices)

# Append the shuffled indices to question_order
question_order.extend(block_indices)

# Update global index for the next set of questions
global_index += len(block_indices)
return question_order


router = APIRouter(prefix="/sessions", tags=["Sessions"])
logger = get_logger()

@@ -70,6 +104,10 @@ async def create_session(session: Session):
if last_session is None:
logger.info("No previous session exists for this user-quiz combo")
current_session["is_first"] = True
if not session.omr_mode:
current_session["question_order"] = shuffle_question_order(
quiz["question_sets"]
)
if quiz["time_limit"] is not None:
current_session["time_remaining"] = quiz["time_limit"][
"max"
@@ -136,6 +174,7 @@ async def create_session(session: Session):
current_session["time_remaining"] = last_session.get("time_remaining", None)
current_session["has_quiz_ended"] = last_session.get("has_quiz_ended", False)
current_session["metrics"] = last_session.get("metrics", None)
current_session["question_order"] = last_session["question_order"]

# restore the answers from the last (previous) sessions
session_answers_of_the_last_session = last_session["session_answers"]
62 changes: 62 additions & 0 deletions app/scripts/update_previous_sessions_with_random_ordering.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
from pymongo import MongoClient
import os
from dotenv import load_dotenv


def get_db_client():
"""Connect to MongoDB and return the client"""
if "MONGO_AUTH_CREDENTIALS" not in os.environ:
load_dotenv("../.env") # Adjust path if needed

client = MongoClient(os.getenv("MONGO_AUTH_CREDENTIALS"))
return client


# Function to update the question_order in sessions
def update_question_order_in_sessions():
"""Update question_order in all sessions where needed"""
client = get_db_client()

db_name = "quiz"
db = client[db_name]

# Get all sessions that don't have question_order field
sessions_to_update = db.sessions.find({"question_order": {"$exists": False}})

update_count = 0
for session in sessions_to_update:
# Get quiz ID for the respective session
quiz_id = session.get("quiz_id")
if not quiz_id:
continue

# Find the quiz document
quiz = db.quizzes.find_one({"_id": quiz_id})
if not quiz:
continue

# Get question sets from the quiz to calculate the totalt questions count
question_sets = quiz.get("question_sets", [])

# Calculate total number of questions across all sets
total_questions = sum(
len(question_set.get("questions", [])) for question_set in question_sets
)

# Create question_order array [0,1, 2, ..., total_questions-1]
question_order = list(range(0, total_questions))

# Update the session with the new question_order
result = db.sessions.update_one(
{"_id": session["_id"]}, {"$set": {"question_order": question_order}}
)

if result.modified_count:
update_count += 1

print(f"Updated question_order for {update_count} sessions")
client.close()


if __name__ == "__main__":
update_question_order_in_sessions()
6 changes: 5 additions & 1 deletion app/tests/base.py
Original file line number Diff line number Diff line change
@@ -126,7 +126,11 @@ def setUp(self):
# omr assessment with multiple question sets
response = self.client.post(
sessions.router.prefix + "/",
json={"quiz_id": self.multi_qset_omr["_id"], "user_id": 1},
json={
"quiz_id": self.multi_qset_omr["_id"],
"user_id": 1,
"omr_mode": True,
},
)
self.multi_qset_omr_session = json.loads(response.content)

71 changes: 71 additions & 0 deletions app/tests/test_sessions.py
Original file line number Diff line number Diff line change
@@ -4,6 +4,10 @@
from ..schemas import EventType
from datetime import datetime
import time
from settings import Settings


settings = Settings()


class SessionsTestCase(SessionsBaseTestCase):
@@ -288,3 +292,70 @@ def test_check_quiz_status_for_user(self):

# Assert that response is a dict
assert isinstance(response.json(), dict)

def test_check_question_order_first_session_and_omr_mode(self):
data = open("app/tests/dummy_data/multiple_question_set_omr_quiz.json")
quiz_data = json.load(data)
response = self.client.post(quizzes.router.prefix + "/", json=quiz_data)
quiz_id = json.loads(response.content)["id"]
quiz = self.client.get(quizzes.router.prefix + f"/{quiz_id}").json()
response = self.client.post(
sessions.router.prefix + "/",
json={"quiz_id": quiz["_id"], "user_id": 1, "omr_mode": True},
)
assert response.status_code == 201
session = json.loads(response.content)
assert session["is_first"] is True
assert len(session["question_order"]) == 0

def test_check_question_order_first_session_and_not_omr_mode(self):
data = open("app/tests/dummy_data/multiple_question_set_quiz.json")
quiz_data = json.load(data)
response = self.client.post(quizzes.router.prefix + "/", json=quiz_data)
quiz_id = json.loads(response.content)["id"]
quiz = self.client.get(quizzes.router.prefix + f"/{quiz_id}").json()
response = self.client.post(
sessions.router.prefix + "/", json={"quiz_id": quiz["_id"], "user_id": 1}
)
assert response.status_code == 201
session = json.loads(response.content)
assert session["is_first"] is True

question_order = session["question_order"]
check_limit = min(settings.subset_size, len(question_order))

for i in range(check_limit):
assert (
question_order[i] < check_limit
), f"Value {question_order[i]} exceeds {check_limit}"

def test_check_question_order_wit_previous_session_and_not_omr_mode(self):
self.session_id = self.multi_qset_quiz_session["_id"]
self.session_question_order = self.multi_qset_quiz_session["question_order"]
# better remove the homework_session_to_large session
response = self.client.post(
sessions.router.prefix + "/",
json={
"quiz_id": self.multi_qset_quiz_session["quiz_id"],
"user_id": self.multi_qset_quiz_session["user_id"],
},
)
assert response.status_code == 201
session = json.loads(response.content)
assert session["question_order"] != []
assert session["question_order"] == self.session_question_order

def test_check_question_order_with_previous_session_and_omr_mode(self):
self.session_id = self.multi_qset_omr_session["_id"]
self.session_question_order = self.multi_qset_omr_session["question_order"]
response = self.client.post(
sessions.router.prefix + "/",
json={
"quiz_id": self.multi_qset_omr_session["quiz_id"],
"user_id": self.multi_qset_omr_session["user_id"],
"omr_mode": True,
},
)
assert response.status_code == 201
session = json.loads(response.content)
assert session["question_order"] == self.session_question_order == []