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

Adds Review Quiz models and routes #114

Open
wants to merge 16 commits into
base: main
Choose a base branch
from
Open
35 changes: 35 additions & 0 deletions app/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
QuizType,
EventType,
TestFormat,
ReviewQuizType,
)
from datetime import datetime

Expand Down Expand Up @@ -239,6 +240,7 @@ class Quiz(BaseModel):
instructions: Optional[str] = None
language: QuizLanguage = "en"
metadata: QuizMetadata = None
is_review_quiz_requested: Optional[bool] = False

class Config:
allow_population_by_field_name = True
Expand Down Expand Up @@ -450,6 +452,10 @@ class Session(BaseModel):
created_at: datetime = Field(default_factory=datetime.utcnow)
events: List[Event] = []
has_quiz_ended: bool = False
is_review_quiz_requested: Optional[
bool
] = False # but this creates extra session when changed to true
omr_mode: bool = False
metrics: Optional[SessionMetrics] = None # gets updated when quiz ends

class Config:
Expand Down Expand Up @@ -515,3 +521,32 @@ class UpdateSessionResponse(BaseModel):

class Config:
schema_extra = {"example": {"time_remaining": 300}}


class GenerateReviewQuiz(BaseModel):
"""Input model for generating review quiz for a quiz"""

quiz_id: str

class Config:
schema_extra = {"example": {"quiz_id": "671"}}


class GenerateReviewQuizForSession(BaseModel):
"""Input model for generating review quiz for a user+quiz combination"""

user_id: str
quiz_id: str

class Config:
schema_extra = {"example": {"user_id": "abc", "quiz_id": "672"}}


class ReviewQuiz(Quiz):
user_id: Optional[str] = "" # need not always be there
review_type: ReviewQuizType
parent_quiz_id: str
# should i create new collection for questions too?

class Config:
fields = {"is_review_quiz_requested": {"exclude": True}}
114 changes: 112 additions & 2 deletions app/routers/quizzes.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,26 @@
from fastapi.responses import JSONResponse
from fastapi.encoders import jsonable_encoder
from database import client
from models import Quiz, GetQuizResponse, CreateQuizResponse
from models import (
Quiz,
GetQuizResponse,
CreateQuizResponse,
GenerateReviewQuiz,
ReviewQuiz,
)
from settings import Settings
from schemas import QuizType
from schemas import QuizType, ReviewQuizType
from logger_config import get_logger
import json
import boto3
import os

sns_client = boto3.client(
"sns",
region_name="ap-south-1",
aws_access_key_id=os.getenv("AWS_ACCESS_KEY_ID"),
aws_secret_access_key=os.getenv("AWS_SECRET_ACCESS_KEY"),
)

router = APIRouter(prefix="/quiz", tags=["Quiz"])
settings = Settings()
Expand Down Expand Up @@ -217,3 +233,97 @@ async def get_quiz(quiz_id: str):

logger.info(f"Finished getting quiz: {quiz_id}")
return quiz


@router.get("/generate-review")
async def generate_review_quiz(review_params: GenerateReviewQuiz):
review_params = jsonable_encoder(review_params)
quiz_id = review_params["quiz_id"]

quiz = client.quiz.quizzes.find_one({"_id": quiz_id})

if quiz is None:
print("No quiz exists for given id")
return None

if (
"is_review_quiz_requested" not in quiz
or quiz["is_review_quiz_requested"] is False
):
# trigger sns
message = {
"action": "review_quiz",
"review_type": ReviewQuizType.review_quiz.value,
"quiz_id": quiz_id,
"environment": "staging",
}
response = sns_client.publish(
TargetArn="arn:aws:sns:ap-south-1:111766607077:sessionCreator-staging",
Message=json.dumps(message),
MessageStructure="string",
)
if response["ResponseMetadata"]["HTTPStatusCode"] == 200:
quiz["is_review_quiz_requested"] = True
client.quiz.quizzes.update_one({"_id": quiz_id}, {"$set": quiz})
return (
"Requested For Review Quiz Generation. Please wait for a few minutes."
)
else:
print("Request failed.")
elif (
"is_review_quiz_requested" in quiz and quiz["is_review_quiz_requested"] is True
):
review_quiz = client.quiz.review_quizzes.find_one(
{
"review_type": ReviewQuizType.review_session.value,
"parent_quiz_id": quiz_id,
}
)

if review_quiz is not None:
return review_quiz["_id"]
else:
return f"Review quiz for {quiz_id} is still being generated. Please wait."


@router.post("/review", response_model=CreateQuizResponse)
async def create_review_quiz(review_quiz: ReviewQuiz):
review_quiz = jsonable_encoder(review_quiz)

for question_set_index, question_set in enumerate(review_quiz["question_sets"]):
questions = question_set["questions"]
for question_index, _ in enumerate(questions):
questions[question_index]["question_set_id"] = question_set["_id"]

result = client.quiz.questions.insert_many(questions)
if not result.acknowledged:
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Review Questions Insertion Error",
)

review_quiz["question_sets"][question_set_index]["questions"] = questions

new_quiz_result = client.quiz.review_quizzes.insert_one(review_quiz)
if not new_quiz_result.acknowledged:
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Review Quiz Insertion Error",
)

return JSONResponse(
status_code=status.HTTP_201_CREATED, content={"id": new_quiz_result.inserted_id}
)


@router.get("/review/{quiz_id}", response_model=GetQuizResponse)
async def get_review_quiz(quiz_id: str):
review_quiz_collection = client.quiz.review_quizzes

if (quiz := review_quiz_collection.find_one({"_id": quiz_id})) is None:
logger.warning(f"Requested quiz {quiz_id} not found")
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND, detail=f"quiz {quiz_id} not found"
)

return quiz
101 changes: 88 additions & 13 deletions app/routers/sessions.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,19 +3,31 @@
from fastapi.encoders import jsonable_encoder
import pymongo
from database import client
from schemas import EventType
from schemas import EventType, ReviewQuizType
from models import (
Event,
Session,
SessionAnswer,
SessionResponse,
UpdateSession,
UpdateSessionResponse,
GenerateReviewQuizForSession,
)
from datetime import datetime
from logger_config import get_logger
from typing import Dict

import os
import boto3
import json

sns_client = boto3.client(
"sns",
region_name="ap-south-1",
aws_access_key_id=os.getenv("AWS_ACCESS_KEY_ID"),
aws_secret_access_key=os.getenv("AWS_SECRET_ACCESS_KEY"),
)


def str_to_datetime(datetime_str: str) -> datetime:
"""converts string to datetime format"""
Expand All @@ -32,18 +44,18 @@ async def create_session(session: Session):
f"Creating new session for user: {session.user_id} and quiz: {session.quiz_id}"
)
current_session = jsonable_encoder(session)

quiz = client.quiz.quizzes.find_one({"_id": current_session["quiz_id"]})

if quiz is None:
error_message = (
f"Quiz {current_session['quiz_id']} not found while creating the session"
)
logger.error(error_message)
raise HTTPException(
status_code=404,
detail=error_message,
)
if (
quiz := client.quiz.quizzes.find_one({"_id": current_session["quiz_id"]})
) is None:
if (
quiz := client.quiz.review_quizzes.find_one(
{"_id": current_session["quiz_id"]}
)
) is None:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=f"Quiz/Review {current_session['quiz_id']} not found while creating the session",
)

# try to get the previous two sessions of a user+quiz pair if they exist
previous_two_sessions = list(
Expand Down Expand Up @@ -318,6 +330,69 @@ async def get_session(session_id: str):
)


@router.post("/generate-review")
async def generate_review_quiz_for_session(review_params: GenerateReviewQuizForSession):
review_params = jsonable_encoder(review_params)
user_id = review_params["user_id"]
quiz_id = review_params["quiz_id"]

# find corresponding session
session = client.quiz.sessions.find_one({"user_id": user_id, "quiz_id": quiz_id})

if session is None:
return "No session exists for given user+quiz combo"

if session["has_quiz_ended"] is True and (
"is_review_quiz_requested" not in session
or session["is_review_quiz_requested"] is False
):
# trigger sns
message = {
"action": "review_quiz",
"review_type": ReviewQuizType.review_session.value,
"session_id": session["_id"],
"environment": "staging",
}
response = sns_client.publish(
TargetArn="arn:aws:sns:ap-south-1:111766607077:sessionCreator-staging",
Message=json.dumps(message),
MessageStructure="string",
)

if response["ResponseMetadata"]["HTTPStatusCode"] == 200:
session["is_review_quiz_requested"] = True
client.quiz.sessions.update_one({"_id": session["_id"]}, {"$set": session})
# this could lead to creation of new session when quiz UI opened again. check it
return (
"Requested For Review Quiz Generation. Please wait for a few minutes."
)
else:
return "Request failed."

elif (
session["has_quiz_ended"] is True
and session["is_review_quiz_requested"] is True
):
# check if review quiz id has been generated
review_quiz = client.quiz.review_quizzes.find_one(
{
"review_type": ReviewQuizType.review_session.value,
"parent_quiz_id": quiz_id,
"user_id": user_id,
}
)
if review_quiz is not None:
review_quiz_id = review_quiz["_id"]
return review_quiz_id
else:
return (
f"Review Quiz for {user_id}+{quiz_id} is being generated. Please wait."
)

elif session["has_quiz_ended"] is False:
return "Please complete the quiz before requesting for review!"


@router.get("/user/{user_id}/quiz-attempts", response_model=Dict[str, bool])
async def check_all_quiz_status(user_id: str) -> Dict[str, bool]:
"""
Expand Down
5 changes: 5 additions & 0 deletions app/schemas.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,3 +59,8 @@ class EventType(str, Enum):
resume_quiz = "resume-quiz"
end_quiz = "end-quiz"
dummy_event = "dummy-event"


class ReviewQuizType(str, Enum):
review_session = "review-session"
review_quiz = "review-quiz"
Loading
Loading