Implement active user tiers, class-based Tailwind theme switcher, 3-tiled static dashboard layout, Minio object storage integration, school & state settings database sync, and streak wishes onboarding modal

This commit is contained in:
2026-07-07 13:43:30 +05:30
parent 426b98ddab
commit 04db17ea0e
24 changed files with 913 additions and 236 deletions

1
backend/.gitignore vendored
View File

@@ -10,3 +10,4 @@ venv/
ENV/
env.bak/
venv.bak/
celerybeat-schedule*

Binary file not shown.

View File

@@ -456,6 +456,8 @@ class UserUpdate(BaseModel):
name: Optional[str] = None
target_exam: Optional[models.ExamEnum] = None
student_class: Optional[models.ClassEnum] = None
school: Optional[str] = None
state: Optional[str] = None
@app.put("/api/users/{user_id}", response_model=models.User, tags=["Users"])
def update_user_profile(user_id: UUID, payload: UserUpdate, session: Session = Depends(get_session)):
@@ -468,14 +470,36 @@ def update_user_profile(user_id: UUID, payload: UserUpdate, session: Session = D
if payload.target_exam is not None:
user.target_exam = payload.target_exam
if payload.student_class is not None:
# Note: mapping alias 'class' in User class to 'student_class'
user.student_class = payload.student_class
if payload.school is not None:
user.school = payload.school
if payload.state is not None:
user.state = payload.state
session.add(user)
session.commit()
session.refresh(user)
return user
from fastapi import UploadFile, File
from storage import upload_file
@app.post("/api/users/{user_id}/upload-avatar", tags=["Users"])
async def upload_user_avatar(user_id: UUID, file: UploadFile = File(...), session: Session = Depends(get_session)):
user = session.get(models.User, user_id)
if not user:
raise HTTPException(status_code=404, detail="User not found")
contents = await file.read()
file_extension = file.filename.split(".")[-1] if "." in file.filename else "jpg"
object_name = f"avatar_{user_id}.{file_extension}"
public_url = upload_file(contents, object_name, content_type=file.content_type)
if not public_url:
raise HTTPException(status_code=500, detail="Failed to upload image to Minio.")
return {"url": public_url}
@app.get("/api/tests", response_model=List[models.Test], tags=["CBT Engine"])
def list_tests(exam_pattern: Optional[models.ExamEnum] = None, session: Session = Depends(get_session)):
query = select(models.Test)

View File

@@ -10,3 +10,4 @@ pyjwt==2.8.0
celery==5.3.6
openai==1.14.0
httpx==0.27.2
boto3==1.34.0

View File

@@ -10,24 +10,65 @@ def seed_local_database():
# Ensure tables are created
init_db()
has_data = False
with Session(engine) as session:
# Prevent double seeding in local testing environment
if session.exec(select(models.Question)).first() is not None:
print("Local environment database already contains data components. Dropping all and reseeding...")
import sqlmodel
sqlmodel.SQLModel.metadata.drop_all(engine)
init_db()
has_data = True
if has_data:
print("Local environment database already contains data components. Dropping all and reseeding...")
import sqlmodel
sqlmodel.SQLModel.metadata.drop_all(engine)
init_db()
print("Seeding baseline content entities...")
with Session(engine) as session:
# 0. Create the baseline student users (Free, Pro, AI Coach)
free_student_id = UUID("11111111-1111-1111-1111-111111111111")
free_student = models.User(
id=free_student_id,
phone="8888888888",
email="free@pragativeer.com",
password_hash="mock_hash",
name="Amit Kumar (Free Tier)",
student_class=models.ClassEnum.TWELFTH,
medium=models.MediumEnum.ENGLISH,
target_exam=models.ExamEnum.JEE_MAIN,
city="Patna",
state="Bihar",
tier=models.TierEnum.FREE,
streak_count=0,
last_active=datetime.utcnow()
)
session.add(free_student)
pro_student_id = UUID("22222222-2222-2222-2222-222222222222")
pro_student = models.User(
id=pro_student_id,
phone="7777777777",
email="pro@pragativeer.com",
password_hash="mock_hash",
name="Priya Singh (Pro Tier)",
student_class=models.ClassEnum.TWELFTH,
medium=models.MediumEnum.ENGLISH,
target_exam=models.ExamEnum.NEET,
city="Kota",
state="Rajasthan",
tier=models.TierEnum.PRO,
streak_count=3,
last_active=datetime.utcnow()
)
session.add(pro_student)
print("Seeding baseline content entities...")
# 0. Create the baseline student user (seeded target: JEE_MAIN)
student_id = UUID("b0e17378-864d-439e-8806-708dc48a891b")
student_user = models.User(
id=student_id,
phone="9999999999",
email="student@pragativeer.com",
password_hash="mock_hash",
name="Rahul Sharma",
name="Rahul Sharma (AI Coach Tier)",
student_class=models.ClassEnum.TWELFTH,
medium=models.MediumEnum.ENGLISH,
target_exam=models.ExamEnum.JEE_MAIN,

91
backend/storage.py Normal file
View File

@@ -0,0 +1,91 @@
import os
import logging
import boto3
from botocore.client import Config
from botocore.exceptions import ClientError
import json
logger = logging.getLogger("storage")
# Read settings from environment
MINIO_ENDPOINT = os.getenv("MINIO_ENDPOINT", "minio:9000")
MINIO_ACCESS_KEY = os.getenv("MINIO_ACCESS_KEY", "minio_admin")
MINIO_SECRET_KEY = os.getenv("MINIO_SECRET_KEY", "minio_secure_password_2026")
MINIO_BUCKET = os.getenv("MINIO_BUCKET", "pragativeer-uploads")
# Initialize S3/Minio client
# Since backend runs inside Docker, endpoint_url must point to 'http://minio:9000'
# However, from the client browser, it will be reached via 'http://localhost:9000'
s3_client = boto3.client(
"s3",
endpoint_url=f"http://{MINIO_ENDPOINT}",
aws_access_key_id=MINIO_ACCESS_KEY,
aws_secret_access_key=MINIO_SECRET_KEY,
config=Config(signature_version="s3v4"),
region_name="us-east-1"
)
def ensure_bucket_exists():
try:
# Check if bucket exists
s3_client.head_bucket(Bucket=MINIO_BUCKET)
logger.info(f"Minio bucket '{MINIO_BUCKET}' already exists.")
except ClientError as e:
error_code = e.response["Error"]["Code"]
if error_code == "404":
try:
# Create bucket
s3_client.create_bucket(Bucket=MINIO_BUCKET)
logger.info(f"Successfully created Minio bucket '{MINIO_BUCKET}'.")
# Set public read policy for direct browser access
bucket_policy = {
"Version": "2012-10-17",
"Statement": [
{
"Sid": "PublicReadGetObject",
"Effect": "Allow",
"Principal": "*",
"Action": ["s3:GetObject"],
"Resource": [f"arn:aws:s3:::{MINIO_BUCKET}/*"]
}
]
}
s3_client.put_bucket_policy(
Bucket=MINIO_BUCKET,
Policy=json.dumps(bucket_policy)
)
logger.info(f"Public read policy applied to Minio bucket '{MINIO_BUCKET}'.")
except Exception as ex:
logger.error(f"Failed to create Minio bucket: {ex}")
else:
logger.error(f"Error checking Minio bucket: {e}")
def upload_file(file_content: bytes, object_name: str, content_type: str = "application/octet-stream") -> str:
"""Uploads file content to Minio and returns the direct public URL."""
try:
ensure_bucket_exists()
s3_client.put_object(
Bucket=MINIO_BUCKET,
Key=object_name,
Body=file_content,
ContentType=content_type
)
# Construct public URL (from browser's point of view, it connects to localhost:9000)
# We can resolve this dynamically by returning a path like /api/uploads/ or direct localhost:9000 link
public_url = f"http://localhost:9000/{MINIO_BUCKET}/{object_name}"
logger.info(f"Successfully uploaded {object_name} to Minio. URL: {public_url}")
return public_url
except Exception as e:
logger.error(f"Failed to upload file to Minio: {e}")
return ""
def get_file(object_name: str) -> bytes:
"""Downloads file content from Minio."""
try:
response = s3_client.get_object(Bucket=MINIO_BUCKET, Key=object_name)
return response["Body"].read()
except Exception as e:
logger.error(f"Failed to get file from Minio: {e}")
return b""