feat: setup admin JSON generation and pdf viewer for free plan DPPs

This commit is contained in:
2026-07-27 14:56:25 +05:30
parent c214ca41b6
commit a5bb2d867c
74 changed files with 8546 additions and 915 deletions

View File

@@ -16,6 +16,22 @@ from billing import generate_mock_order, verify_webhook_signature
from llm import stream_tutor_response
from predictor import calculate_readiness_score
from growth import generate_referral_link, process_referral_reward
import boto3
import uuid
s3_client = boto3.client(
's3',
endpoint_url='http://localhost:9000',
aws_access_key_id='minioadmin',
aws_secret_access_key='minioadmin',
region_name='us-east-1'
)
BUCKET_NAME = "avatars"
try:
s3_client.create_bucket(Bucket=BUCKET_NAME)
except Exception:
pass # Bucket might already exist
@asynccontextmanager
async def lifespan(app: FastAPI):
@@ -575,6 +591,41 @@ def get_user_attempts(user_id: UUID, session: Session = Depends(get_session)):
# --- ADMIN ROUTES ---
@app.get("/api/admin/settings", tags=["Admin"])
def get_system_settings(session: Session = Depends(get_session)):
settings = session.get(models.SystemSettings, "global")
if not settings:
settings = models.SystemSettings()
session.add(settings)
session.commit()
session.refresh(settings)
return settings
class SettingsUpdate(BaseModel):
maintenance_mode: Optional[bool] = None
allow_new_registrations: Optional[bool] = None
enable_free_trial: Optional[bool] = None
trial_duration_days: Optional[int] = None
trial_tier: Optional[models.TierEnum] = None
@app.put("/api/admin/settings", tags=["Admin"])
def update_system_settings(payload: SettingsUpdate, session: Session = Depends(get_session)):
settings = session.get(models.SystemSettings, "global")
if not settings:
settings = models.SystemSettings()
if payload.maintenance_mode is not None: settings.maintenance_mode = payload.maintenance_mode
if payload.allow_new_registrations is not None: settings.allow_new_registrations = payload.allow_new_registrations
if payload.enable_free_trial is not None: settings.enable_free_trial = payload.enable_free_trial
if payload.trial_duration_days is not None: settings.trial_duration_days = payload.trial_duration_days
if payload.trial_tier is not None: settings.trial_tier = payload.trial_tier
settings.updated_at = datetime.utcnow()
session.add(settings)
session.commit()
session.refresh(settings)
return settings
@app.get("/api/admin/tests", tags=["Admin"])
def get_admin_tests(session: Session = Depends(get_session)):
tests = session.exec(select(models.Test)).all()
@@ -832,4 +883,192 @@ def delete_coupon(coupon_id: UUID, session: Session = Depends(get_session)):
session.delete(coupon)
session.commit()
return {"status": "success", "message": "Coupon deleted successfully"}
return {"status": "success", "message": "Coupon deleted successfully"}@app.post("/api/users/{user_id}/avatar", tags=["Users"])
async def upload_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")
file_extension = file.filename.split(".")[-1]
object_name = f"{user_id}_{uuid.uuid4().hex[:8]}.{file_extension}"
try:
s3_client.upload_fileobj(
file.file,
BUCKET_NAME,
object_name,
ExtraArgs={"ContentType": file.content_type}
)
url = f"http://localhost:9000/{BUCKET_NAME}/{object_name}"
user.profile_image_url = url
session.add(user)
session.commit()
return {"url": url}
except Exception as e:
print(f"S3 Upload failed: {e}")
raise HTTPException(status_code=500, detail="Failed to upload image")
# --- ADMIN STUDENT MANAGEMENT ---
class AdminUserPatch(BaseModel):
name: Optional[str] = None
tier: Optional[models.TierEnum] = None
streak_count: Optional[int] = None
target_exam: Optional[models.ExamEnum] = None
school: Optional[str] = None
city: Optional[str] = None
state: Optional[str] = None
@app.get("/api/admin/students", tags=["Admin"])
def list_all_students(session: Session = Depends(get_session)):
"""Return all users with basic stats for the admin student directory."""
users = session.exec(select(models.User)).all()
result = []
for u in users:
attempt_count = session.exec(
select(models.Attempt).where(models.Attempt.user_id == u.id)
).all()
mistake_count = session.exec(
select(models.Mistake).where(models.Mistake.user_id == u.id)
).all()
avg_accuracy = 0.0
if attempt_count:
submitted = [a for a in attempt_count if a.status == "Submitted"]
if submitted:
avg_accuracy = round(sum(a.accuracy for a in submitted) / len(submitted), 1)
result.append({
"id": str(u.id),
"name": u.name,
"email": u.email or "",
"phone": u.phone or "",
"tier": u.tier.value,
"target_exam": u.target_exam.value,
"city": u.city,
"state": u.state,
"streak_count": u.streak_count,
"longest_streak": u.longest_streak,
"last_active": u.last_active.isoformat(),
"created_at": u.created_at.isoformat(),
"total_tests": len(attempt_count),
"avg_accuracy": avg_accuracy,
"total_mistakes": len(mistake_count),
})
return result
@app.get("/api/admin/students/{user_id}", tags=["Admin"])
def get_student_detail(user_id: UUID, session: Session = Depends(get_session)):
"""Full student profile for admin detail view."""
user = session.get(models.User, user_id)
if not user:
raise HTTPException(status_code=404, detail="Student not found")
attempts = session.exec(
select(models.Attempt).where(models.Attempt.user_id == user_id)
.order_by(models.Attempt.submitted_at.desc()) # type: ignore
).all()
mistakes = session.exec(
select(models.Mistake).where(models.Mistake.user_id == user_id)
).all()
subscriptions = session.exec(
select(models.Subscription).where(models.Subscription.user_id == user_id)
).all()
transactions = session.exec(
select(models.Transaction).where(models.Transaction.user_id == user_id)
).all()
submitted = [a for a in attempts if a.status == "Submitted"]
avg_accuracy = round(sum(a.accuracy for a in submitted) / len(submitted), 1) if submitted else 0.0
mastered_count = len([m for m in mistakes if m.is_mastered])
weak_topics = list({m.topic for m in mistakes if not m.is_mastered})[:5]
recent_attempts = []
for a in attempts[:5]:
# Try to find the test name from Test table
test_name = "Practice Session"
if a.source_type.value == "Test":
test = session.get(models.Test, a.source_id)
if test:
test_name = test.title
recent_attempts.append({
"id": str(a.id),
"name": test_name,
"source_type": a.source_type.value,
"score": a.score,
"total_questions": a.total_questions,
"accuracy": a.accuracy,
"submitted_at": a.submitted_at.isoformat() if a.submitted_at else None,
})
return {
"id": str(user.id),
"name": user.name,
"email": user.email or "",
"phone": user.phone or "",
"tier": user.tier.value,
"target_exam": user.target_exam.value,
"student_class": user.student_class.value,
"school": user.school or "",
"city": user.city,
"state": user.state,
"streak_count": user.streak_count,
"longest_streak": user.longest_streak,
"created_at": user.created_at.isoformat(),
"last_active": user.last_active.isoformat(),
"subscription_end": user.subscription_end.isoformat() if user.subscription_end else None,
"profile_image_url": user.profile_image_url,
"stats": {
"total_tests": len(attempts),
"avg_accuracy": avg_accuracy,
"total_mistakes": len(mistakes),
"mastered_mistakes": mastered_count,
},
"recent_attempts": recent_attempts,
"weak_topics": weak_topics,
"subscriptions": [
{
"id": str(s.id),
"tier": s.tier.value,
"billing_cycle": s.billing_cycle,
"start_date": s.start_date.isoformat(),
"end_date": s.end_date.isoformat(),
"amount": s.amount,
"status": s.status,
}
for s in subscriptions
],
"transactions": [
{
"id": str(t.id),
"amount": t.amount,
"tier": t.tier,
"status": t.status,
"created_at": t.created_at.isoformat(),
}
for t in transactions
],
}
@app.patch("/api/admin/students/{user_id}", tags=["Admin"])
def patch_student(user_id: UUID, payload: AdminUserPatch, session: Session = Depends(get_session)):
"""Admin update: tier, name, streak, exam, location."""
user = session.get(models.User, user_id)
if not user:
raise HTTPException(status_code=404, detail="Student not found")
if payload.name is not None: user.name = payload.name
if payload.tier is not None: user.tier = payload.tier
if payload.streak_count is not None: user.streak_count = payload.streak_count
if payload.target_exam is not None: user.target_exam = payload.target_exam
if payload.school is not None: user.school = payload.school
if payload.city is not None: user.city = payload.city
if payload.state is not None: user.state = payload.state
session.add(user)
session.commit()
session.refresh(user)
return {"status": "ok", "user_id": str(user.id), "tier": user.tier.value}