template tutoring-admin-dashboard v1

This commit is contained in:
DevVibeCoder
2026-09-14 23:53:29 +05:30 Unverified
commit c4d9755262
70 changed files with 7955 additions and 0 deletions
+5
View File
@@ -0,0 +1,5 @@
# Environment variables this project needs.
# Generated by DevVibeCoder from the project's .env — values are never copied.
# Fill these in locally, or add them under Settings → Secrets.
DATABASE_URL=
DATABASE_SCHEMA=
View File
+67
View File
@@ -0,0 +1,67 @@
"""Database engine + session.
Reads DATABASE_URL from the environment (provisioned Postgres). The engine is
created with a short connect timeout and a probe: if Postgres is unreachable at
startup, we fall back to a local SQLite file so the app still runs (Preview,
offline dev). The rest of the app never chooses a database — it imports
``SessionLocal`` and ``engine`` from here.
"""
from __future__ import annotations
import os
from collections.abc import Generator
from sqlalchemy import create_engine, text
from sqlalchemy.orm import DeclarativeBase, Session, sessionmaker
class Base(DeclarativeBase):
pass
def _normalize(url: str) -> str:
# SQLAlchemy + psycopg3 wants the postgresql+psycopg driver.
if url.startswith("postgresql://"):
return url.replace("postgresql://", "postgresql+psycopg://", 1)
if url.startswith("postgres://"):
return url.replace("postgres://", "postgresql+psycopg://", 1)
return url
def _make_engine():
url = os.getenv("DATABASE_URL", "").strip()
if url:
pg_url = _normalize(url)
try:
eng = create_engine(
pg_url,
pool_pre_ping=True,
connect_args={"connect_timeout": 5},
)
with eng.connect() as conn:
conn.execute(text("SELECT 1"))
print("[db] using Postgres")
return eng
except Exception as exc: # noqa: BLE001
print(f"[db] Postgres unreachable ({exc}); falling back to SQLite")
sqlite_path = os.path.join(os.path.dirname(__file__), "..", "app_fallback.db")
eng = create_engine(
f"sqlite:///{os.path.abspath(sqlite_path)}",
connect_args={"check_same_thread": False},
)
print("[db] using SQLite fallback")
return eng
engine = _make_engine()
SessionLocal = sessionmaker(bind=engine, autoflush=False, autocommit=False)
def get_db() -> Generator[Session, None, None]:
db = SessionLocal()
try:
yield db
finally:
db.close()
+33
View File
@@ -0,0 +1,33 @@
"""FastAPI entry point. Deploy imports ``app.main:app`` — keep both names."""
import os
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
from app.routes import api_router
from app.seed import init_db
app = FastAPI(title=os.getenv("APP_NAME", "SwatiYadav"))
@app.on_event("startup")
def _startup() -> None:
try:
init_db()
except Exception as exc: # noqa: BLE001
print(f"[startup] DB init skipped: {exc}")
# Allowed origins come from the environment so the deployed app and local dev
# use the same code path. "*" only when nothing is configured (dev default).
_origins = [o.strip() for o in os.getenv("CORS_ORIGINS", "*").split(",") if o.strip()]
app.add_middleware(
CORSMiddleware,
allow_origins=_origins,
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
# The one place the /api prefix is applied. Route modules must not repeat it.
app.include_router(api_router, prefix="/api")
+199
View File
@@ -0,0 +1,199 @@
"""SQLAlchemy ORM models for the SwatiYadav tuition website.
Every public section is a table so the teacher can edit it from admin. The
public site reads these; the admin panel writes them.
"""
from __future__ import annotations
from datetime import date, datetime, timezone
from sqlalchemy import (
Boolean,
Date,
DateTime,
ForeignKey,
Integer,
String,
Text,
UniqueConstraint,
)
from sqlalchemy.orm import Mapped, mapped_column, relationship
from app.db import Base
def _now() -> datetime:
return datetime.now(timezone.utc)
class AdminUser(Base):
__tablename__ = "admin_user"
id: Mapped[int] = mapped_column(Integer, primary_key=True)
username: Mapped[str] = mapped_column(String(80), unique=True, nullable=False)
password_hash: Mapped[str] = mapped_column(String(255), nullable=False)
class Profile(Base):
__tablename__ = "profile"
id: Mapped[int] = mapped_column(Integer, primary_key=True)
name: Mapped[str] = mapped_column(String(160), default="")
tagline: Mapped[str] = mapped_column(String(255), default="")
bio: Mapped[str] = mapped_column(Text, default="")
photo_url: Mapped[str] = mapped_column(Text, default="")
qualifications: Mapped[str] = mapped_column(Text, default="")
experience_years: Mapped[int] = mapped_column(Integer, default=0)
students_taught: Mapped[int] = mapped_column(Integer, default=0)
achievements: Mapped[str] = mapped_column(Text, default="")
hero_cta: Mapped[str] = mapped_column(String(120), default="Book a free demo")
class Subject(Base):
__tablename__ = "subject"
id: Mapped[int] = mapped_column(Integer, primary_key=True)
name: Mapped[str] = mapped_column(String(120), default="")
level: Mapped[str] = mapped_column(String(120), default="")
description: Mapped[str] = mapped_column(Text, default="")
mode: Mapped[str] = mapped_column(String(60), default="Online & Offline")
icon: Mapped[str] = mapped_column(String(20), default="📘")
sort_order: Mapped[int] = mapped_column(Integer, default=0)
class Course(Base):
__tablename__ = "course"
id: Mapped[int] = mapped_column(Integer, primary_key=True)
title: Mapped[str] = mapped_column(String(160), default="")
subject: Mapped[str] = mapped_column(String(120), default="")
description: Mapped[str] = mapped_column(Text, default="")
duration: Mapped[str] = mapped_column(String(120), default="")
mode: Mapped[str] = mapped_column(String(60), default="Online & Offline")
highlights: Mapped[str] = mapped_column(Text, default="") # newline-separated
sort_order: Mapped[int] = mapped_column(Integer, default=0)
class FeePlan(Base):
__tablename__ = "fee_plan"
id: Mapped[int] = mapped_column(Integer, primary_key=True)
name: Mapped[str] = mapped_column(String(120), default="")
price: Mapped[str] = mapped_column(String(60), default="")
period: Mapped[str] = mapped_column(String(60), default="per month")
features: Mapped[str] = mapped_column(Text, default="") # newline-separated
highlighted: Mapped[bool] = mapped_column(Boolean, default=False)
sort_order: Mapped[int] = mapped_column(Integer, default=0)
class Testimonial(Base):
__tablename__ = "testimonial"
id: Mapped[int] = mapped_column(Integer, primary_key=True)
author_name: Mapped[str] = mapped_column(String(120), default="")
relation: Mapped[str] = mapped_column(String(60), default="Student")
quote: Mapped[str] = mapped_column(Text, default="")
rating: Mapped[int] = mapped_column(Integer, default=5)
sort_order: Mapped[int] = mapped_column(Integer, default=0)
class SiteSettings(Base):
__tablename__ = "site_settings"
id: Mapped[int] = mapped_column(Integer, primary_key=True)
address: Mapped[str] = mapped_column(Text, default="")
phone: Mapped[str] = mapped_column(String(60), default="")
email: Mapped[str] = mapped_column(String(160), default="")
hours: Mapped[str] = mapped_column(String(255), default="")
whatsapp: Mapped[str] = mapped_column(String(160), default="")
instagram: Mapped[str] = mapped_column(String(160), default="")
youtube: Mapped[str] = mapped_column(String(160), default="")
facebook: Mapped[str] = mapped_column(String(160), default="")
footer_text: Mapped[str] = mapped_column(Text, default="")
class Review(Base):
__tablename__ = "review"
id: Mapped[int] = mapped_column(Integer, primary_key=True)
author_name: Mapped[str] = mapped_column(String(160), default="")
relation: Mapped[str] = mapped_column(String(60), default="Student")
rating: Mapped[int] = mapped_column(Integer, default=5)
message: Mapped[str] = mapped_column(Text, default="")
is_approved: Mapped[bool] = mapped_column(Boolean, default=False)
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=_now)
class Enquiry(Base):
__tablename__ = "enquiry"
id: Mapped[int] = mapped_column(Integer, primary_key=True)
name: Mapped[str] = mapped_column(String(160), default="")
email: Mapped[str] = mapped_column(String(160), default="")
phone: Mapped[str] = mapped_column(String(60), default="")
subject_interest: Mapped[str] = mapped_column(String(160), default="")
message: Mapped[str] = mapped_column(Text, default="")
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=_now)
is_read: Mapped[bool] = mapped_column(Boolean, default=False)
# ---- Student management (admin-only, private) ----
class Student(Base):
__tablename__ = "student"
id: Mapped[int] = mapped_column(Integer, primary_key=True)
name: Mapped[str] = mapped_column(String(160), default="")
phone: Mapped[str] = mapped_column(String(60), default="")
parent_name: Mapped[str] = mapped_column(String(160), default="")
parent_phone: Mapped[str] = mapped_column(String(60), default="")
address: Mapped[str] = mapped_column(Text, default="")
subject: Mapped[str] = mapped_column(String(160), default="")
monthly_fee: Mapped[int] = mapped_column(Integer, default=0)
start_date: Mapped[date] = mapped_column(Date, default=date.today)
is_active: Mapped[bool] = mapped_column(Boolean, default=True)
notes: Mapped[str] = mapped_column(Text, default="")
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=_now)
slots: Mapped[list["ScheduleSlot"]] = relationship(
back_populates="student",
cascade="all, delete-orphan",
)
class ScheduleSlot(Base):
__tablename__ = "schedule_slot"
id: Mapped[int] = mapped_column(Integer, primary_key=True)
student_id: Mapped[int] = mapped_column(
ForeignKey("student.id", ondelete="CASCADE"), nullable=False, index=True
)
weekday: Mapped[int] = mapped_column(Integer, default=0) # 0=Mon .. 6=Sun
start_time: Mapped[str] = mapped_column(String(5), default="") # "HH:MM"
end_time: Mapped[str] = mapped_column(String(5), default="")
student: Mapped["Student"] = relationship(back_populates="slots")
class Attendance(Base):
__tablename__ = "attendance"
__table_args__ = (UniqueConstraint("student_id", "date", name="uq_attendance_student_date"),)
id: Mapped[int] = mapped_column(Integer, primary_key=True)
student_id: Mapped[int] = mapped_column(
ForeignKey("student.id", ondelete="CASCADE"), nullable=False, index=True
)
date: Mapped[date] = mapped_column(Date, nullable=False, index=True)
status: Mapped[str] = mapped_column(String(10), default="present") # present|absent|off
note: Mapped[str] = mapped_column(Text, default="") # why absent / off
class DayMark(Base):
__tablename__ = "day_mark"
id: Mapped[int] = mapped_column(Integer, primary_key=True)
date: Mapped[date] = mapped_column(Date, unique=True, nullable=False, index=True)
kind: Mapped[str] = mapped_column(String(12), default="holiday") # holiday|class_off
note: Mapped[str] = mapped_column(String(255), default="")
+13
View File
@@ -0,0 +1,13 @@
"""Route barrel — every router the app serves is mounted here."""
from fastapi import APIRouter
from app.routes import admin, auth, content, health, students
api_router = APIRouter()
api_router.include_router(health.router, tags=["health"])
api_router.include_router(auth.router, tags=["auth"])
api_router.include_router(content.router, tags=["content"])
api_router.include_router(admin.router, tags=["admin"])
api_router.include_router(students.router, tags=["students"])
+241
View File
@@ -0,0 +1,241 @@
"""Admin (JWT-protected) endpoints: edit every section, manage enquiries.
All routes here depend on ``get_current_admin`` via the router dependency, so an
unauthenticated request gets 401 before reaching a handler.
"""
from __future__ import annotations
from fastapi import APIRouter, Depends, HTTPException
from sqlalchemy.orm import Session
from app.db import get_db
from app.models import (
Course,
Enquiry,
FeePlan,
Profile,
Review,
SiteSettings,
Subject,
Testimonial,
)
from app.schemas import (
CourseIn,
CourseOut,
EnquiryOut,
EnquiryPatch,
FeePlanIn,
FeePlanOut,
ProfileIn,
ProfileOut,
ReviewOut,
ReviewPatch,
SiteSettingsIn,
SiteSettingsOut,
SubjectIn,
SubjectOut,
TestimonialIn,
TestimonialOut,
)
from app.security import get_current_admin
router = APIRouter(prefix="/admin", dependencies=[Depends(get_current_admin)])
# ---- Profile (singleton) ----
@router.put("/profile", response_model=ProfileOut)
def update_profile(body: ProfileIn, db: Session = Depends(get_db)) -> Profile:
obj = db.query(Profile).first()
if not obj:
obj = Profile()
db.add(obj)
for k, v in body.model_dump().items():
setattr(obj, k, v)
db.commit()
db.refresh(obj)
return obj
# ---- Settings (singleton) ----
@router.put("/settings", response_model=SiteSettingsOut)
def update_settings(body: SiteSettingsIn, db: Session = Depends(get_db)) -> SiteSettings:
obj = db.query(SiteSettings).first()
if not obj:
obj = SiteSettings()
db.add(obj)
for k, v in body.model_dump().items():
setattr(obj, k, v)
db.commit()
db.refresh(obj)
return obj
# ---- Subjects CRUD ----
@router.post("/subjects", response_model=SubjectOut, status_code=201)
def create_subject(body: SubjectIn, db: Session = Depends(get_db)) -> Subject:
obj = Subject(**body.model_dump())
db.add(obj)
db.commit()
db.refresh(obj)
return obj
@router.put("/subjects/{item_id}", response_model=SubjectOut)
def update_subject(item_id: int, body: SubjectIn, db: Session = Depends(get_db)) -> Subject:
obj = db.get(Subject, item_id)
if not obj:
raise HTTPException(404, "Subject not found")
for k, v in body.model_dump().items():
setattr(obj, k, v)
db.commit()
db.refresh(obj)
return obj
@router.delete("/subjects/{item_id}", status_code=204)
def delete_subject(item_id: int, db: Session = Depends(get_db)) -> None:
obj = db.get(Subject, item_id)
if obj:
db.delete(obj)
db.commit()
# ---- Courses CRUD ----
@router.post("/courses", response_model=CourseOut, status_code=201)
def create_course(body: CourseIn, db: Session = Depends(get_db)) -> Course:
obj = Course(**body.model_dump())
db.add(obj)
db.commit()
db.refresh(obj)
return obj
@router.put("/courses/{item_id}", response_model=CourseOut)
def update_course(item_id: int, body: CourseIn, db: Session = Depends(get_db)) -> Course:
obj = db.get(Course, item_id)
if not obj:
raise HTTPException(404, "Course not found")
for k, v in body.model_dump().items():
setattr(obj, k, v)
db.commit()
db.refresh(obj)
return obj
@router.delete("/courses/{item_id}", status_code=204)
def delete_course(item_id: int, db: Session = Depends(get_db)) -> None:
obj = db.get(Course, item_id)
if obj:
db.delete(obj)
db.commit()
# ---- Fees CRUD ----
@router.post("/fees", response_model=FeePlanOut, status_code=201)
def create_fee(body: FeePlanIn, db: Session = Depends(get_db)) -> FeePlan:
obj = FeePlan(**body.model_dump())
db.add(obj)
db.commit()
db.refresh(obj)
return obj
@router.put("/fees/{item_id}", response_model=FeePlanOut)
def update_fee(item_id: int, body: FeePlanIn, db: Session = Depends(get_db)) -> FeePlan:
obj = db.get(FeePlan, item_id)
if not obj:
raise HTTPException(404, "Fee plan not found")
for k, v in body.model_dump().items():
setattr(obj, k, v)
db.commit()
db.refresh(obj)
return obj
@router.delete("/fees/{item_id}", status_code=204)
def delete_fee(item_id: int, db: Session = Depends(get_db)) -> None:
obj = db.get(FeePlan, item_id)
if obj:
db.delete(obj)
db.commit()
# ---- Testimonials CRUD ----
@router.post("/testimonials", response_model=TestimonialOut, status_code=201)
def create_testimonial(body: TestimonialIn, db: Session = Depends(get_db)) -> Testimonial:
obj = Testimonial(**body.model_dump())
db.add(obj)
db.commit()
db.refresh(obj)
return obj
@router.put("/testimonials/{item_id}", response_model=TestimonialOut)
def update_testimonial(item_id: int, body: TestimonialIn, db: Session = Depends(get_db)) -> Testimonial:
obj = db.get(Testimonial, item_id)
if not obj:
raise HTTPException(404, "Testimonial not found")
for k, v in body.model_dump().items():
setattr(obj, k, v)
db.commit()
db.refresh(obj)
return obj
@router.delete("/testimonials/{item_id}", status_code=204)
def delete_testimonial(item_id: int, db: Session = Depends(get_db)) -> None:
obj = db.get(Testimonial, item_id)
if obj:
db.delete(obj)
db.commit()
# ---- Reviews (moderation) ----
@router.get("/reviews", response_model=list[ReviewOut])
def list_reviews(db: Session = Depends(get_db)) -> list[Review]:
return db.query(Review).order_by(Review.created_at.desc(), Review.id.desc()).all()
@router.patch("/reviews/{item_id}", response_model=ReviewOut)
def patch_review(item_id: int, body: ReviewPatch, db: Session = Depends(get_db)) -> Review:
obj = db.get(Review, item_id)
if not obj:
raise HTTPException(404, "Review not found")
obj.is_approved = body.is_approved
db.commit()
db.refresh(obj)
return obj
@router.delete("/reviews/{item_id}", status_code=204)
def delete_review(item_id: int, db: Session = Depends(get_db)) -> None:
obj = db.get(Review, item_id)
if obj:
db.delete(obj)
db.commit()
# ---- Enquiries ----
@router.get("/enquiries", response_model=list[EnquiryOut])
def list_enquiries(db: Session = Depends(get_db)) -> list[Enquiry]:
return db.query(Enquiry).order_by(Enquiry.created_at.desc(), Enquiry.id.desc()).all()
@router.patch("/enquiries/{item_id}", response_model=EnquiryOut)
def patch_enquiry(item_id: int, body: EnquiryPatch, db: Session = Depends(get_db)) -> Enquiry:
obj = db.get(Enquiry, item_id)
if not obj:
raise HTTPException(404, "Enquiry not found")
obj.is_read = body.is_read
db.commit()
db.refresh(obj)
return obj
@router.delete("/enquiries/{item_id}", status_code=204)
def delete_enquiry(item_id: int, db: Session = Depends(get_db)) -> None:
obj = db.get(Enquiry, item_id)
if obj:
db.delete(obj)
db.commit()
+49
View File
@@ -0,0 +1,49 @@
"""Admin authentication: login, me, change password."""
from __future__ import annotations
from fastapi import APIRouter, Depends, HTTPException, status
from sqlalchemy.orm import Session
from app.db import get_db
from app.models import AdminUser
from app.schemas import ChangePasswordIn, LoginIn, MeOut, TokenOut
from app.security import (
create_token,
get_current_admin,
hash_password,
verify_password,
)
router = APIRouter(prefix="/auth")
@router.post("/login", response_model=TokenOut)
def login(body: LoginIn, db: Session = Depends(get_db)) -> TokenOut:
user = db.query(AdminUser).filter(AdminUser.username == body.username).first()
if not user or not verify_password(body.password, user.password_hash):
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Invalid username or password",
)
return TokenOut(access_token=create_token(user.username), username=user.username)
@router.get("/me", response_model=MeOut)
def me(admin: AdminUser = Depends(get_current_admin)) -> MeOut:
return MeOut(username=admin.username)
@router.post("/change-password", response_model=MeOut)
def change_password(
body: ChangePasswordIn,
admin: AdminUser = Depends(get_current_admin),
db: Session = Depends(get_db),
) -> MeOut:
if not verify_password(body.current_password, admin.password_hash):
raise HTTPException(status_code=400, detail="Current password is incorrect")
if len(body.new_password) < 6:
raise HTTPException(status_code=400, detail="New password must be at least 6 characters")
admin.password_hash = hash_password(body.new_password)
db.commit()
return MeOut(username=admin.username)
+128
View File
@@ -0,0 +1,128 @@
"""Public content endpoints — read the site, submit an enquiry.
These are all unauthenticated (the public site consumes them).
"""
from __future__ import annotations
from fastapi import APIRouter, Depends
from sqlalchemy.orm import Session
from app.db import get_db
from app.models import (
Course,
Enquiry,
FeePlan,
Profile,
Review,
SiteSettings,
Subject,
Testimonial,
)
from app.schemas import (
CourseOut,
EnquiryIn,
EnquiryOut,
FeePlanOut,
ProfileOut,
ReviewIn,
ReviewOut,
SiteSettingsOut,
SubjectOut,
TestimonialOut,
)
router = APIRouter()
def _get_or_create_profile(db: Session) -> Profile:
obj = db.query(Profile).first()
if not obj:
obj = Profile()
db.add(obj)
db.commit()
db.refresh(obj)
return obj
def _get_or_create_settings(db: Session) -> SiteSettings:
obj = db.query(SiteSettings).first()
if not obj:
obj = SiteSettings()
db.add(obj)
db.commit()
db.refresh(obj)
return obj
@router.get("/profile", response_model=ProfileOut)
def get_profile(db: Session = Depends(get_db)) -> Profile:
return _get_or_create_profile(db)
@router.get("/subjects", response_model=list[SubjectOut])
def list_subjects(db: Session = Depends(get_db)) -> list[Subject]:
return db.query(Subject).order_by(Subject.sort_order, Subject.id).all()
@router.get("/courses", response_model=list[CourseOut])
def list_courses(db: Session = Depends(get_db)) -> list[Course]:
return db.query(Course).order_by(Course.sort_order, Course.id).all()
@router.get("/fees", response_model=list[FeePlanOut])
def list_fees(db: Session = Depends(get_db)) -> list[FeePlan]:
return db.query(FeePlan).order_by(FeePlan.sort_order, FeePlan.id).all()
@router.get("/testimonials", response_model=list[TestimonialOut])
def list_testimonials(db: Session = Depends(get_db)) -> list[Testimonial]:
return db.query(Testimonial).order_by(Testimonial.sort_order, Testimonial.id).all()
@router.get("/settings", response_model=SiteSettingsOut)
def get_settings(db: Session = Depends(get_db)) -> SiteSettings:
return _get_or_create_settings(db)
@router.get("/reviews", response_model=list[ReviewOut])
def list_public_reviews(db: Session = Depends(get_db)) -> list[Review]:
"""Only approved reviews are shown on the public site."""
return (
db.query(Review)
.filter(Review.is_approved.is_(True))
.order_by(Review.created_at.desc(), Review.id.desc())
.all()
)
@router.post("/reviews", response_model=ReviewOut, status_code=201)
def create_review(body: ReviewIn, db: Session = Depends(get_db)) -> Review:
"""Public submission — stored unapproved until the teacher approves it."""
rating = max(1, min(5, body.rating))
obj = Review(
author_name=body.author_name.strip() or "Anonymous",
relation=body.relation,
rating=rating,
message=body.message,
is_approved=False,
)
db.add(obj)
db.commit()
db.refresh(obj)
return obj
@router.post("/enquiries", response_model=EnquiryOut, status_code=201)
def create_enquiry(body: EnquiryIn, db: Session = Depends(get_db)) -> Enquiry:
obj = Enquiry(
name=body.name,
email=body.email,
phone=body.phone,
subject_interest=body.subject_interest,
message=body.message,
)
db.add(obj)
db.commit()
db.refresh(obj)
return obj
+15
View File
@@ -0,0 +1,15 @@
"""Liveness endpoint. The shape every other route module copies."""
from fastapi import APIRouter
from pydantic import BaseModel
router = APIRouter()
class Health(BaseModel):
status: str
@router.get("/health", response_model=Health)
def health() -> Health:
return Health(status="ok")
+2
View File
@@ -0,0 +1,2 @@
"""Unused. Replaced by content.py / admin.py. Kept empty to avoid a dangling
import; not mounted in the router barrel."""
+261
View File
@@ -0,0 +1,261 @@
"""Admin (JWT-protected) student-management endpoints.
Private to the teacher: students & parent records, weekly schedule, monthly
attendance sheet with per-student offs and whole-day holidays/class-offs, a
"today" teaching list, and a monthly summary (present/absent counts + expected
income from fees). Nothing here is exposed on the public site.
"""
from __future__ import annotations
from calendar import monthrange
from datetime import date
from fastapi import APIRouter, Depends, HTTPException, Query
from sqlalchemy.orm import Session
from app.db import get_db
from app.models import Attendance, DayMark, ScheduleSlot, Student
from app.schemas import (
AttendanceClearIn,
AttendanceIn,
AttendanceOut,
DayMarkIn,
DayMarkOut,
MonthDataOut,
ScheduleSlotIn,
ScheduleSlotOut,
StudentIn,
StudentOut,
StudentSummaryOut,
SummaryOut,
TodayEntryOut,
)
from app.security import get_current_admin
router = APIRouter(prefix="/admin", dependencies=[Depends(get_current_admin)])
_STATUSES = {"present", "absent", "off"}
_KINDS = {"holiday", "class_off"}
def _month_bounds(month: str) -> tuple[date, date]:
"""Return (first_day, last_day) for a 'YYYY-MM' string."""
try:
year, mon = (int(p) for p in month.split("-"))
first = date(year, mon, 1)
last = date(year, mon, monthrange(year, mon)[1])
except (ValueError, TypeError):
raise HTTPException(400, "month must be YYYY-MM") from None
return first, last
# ---- Students CRUD ----
@router.get("/students", response_model=list[StudentOut])
def list_students(db: Session = Depends(get_db)) -> list[Student]:
return db.query(Student).order_by(Student.name.asc(), Student.id.asc()).all()
@router.post("/students", response_model=StudentOut, status_code=201)
def create_student(body: StudentIn, db: Session = Depends(get_db)) -> Student:
data = body.model_dump()
if data.get("start_date") is None:
data["start_date"] = date.today()
obj = Student(**data)
db.add(obj)
db.commit()
db.refresh(obj)
return obj
@router.put("/students/{item_id}", response_model=StudentOut)
def update_student(item_id: int, body: StudentIn, db: Session = Depends(get_db)) -> Student:
obj = db.get(Student, item_id)
if not obj:
raise HTTPException(404, "Student not found")
data = body.model_dump()
if data.get("start_date") is None:
data.pop("start_date")
for k, v in data.items():
setattr(obj, k, v)
db.commit()
db.refresh(obj)
return obj
@router.delete("/students/{item_id}", status_code=204)
def delete_student(item_id: int, db: Session = Depends(get_db)) -> None:
obj = db.get(Student, item_id)
if obj:
db.delete(obj)
db.commit()
# ---- Schedule ----
@router.get("/students/{item_id}/schedule", response_model=list[ScheduleSlotOut])
def get_schedule(item_id: int, db: Session = Depends(get_db)) -> list[ScheduleSlot]:
if not db.get(Student, item_id):
raise HTTPException(404, "Student not found")
return (
db.query(ScheduleSlot)
.filter(ScheduleSlot.student_id == item_id)
.order_by(ScheduleSlot.weekday.asc(), ScheduleSlot.start_time.asc())
.all()
)
@router.put("/students/{item_id}/schedule", response_model=list[ScheduleSlotOut])
def set_schedule(
item_id: int, body: list[ScheduleSlotIn], db: Session = Depends(get_db)
) -> list[ScheduleSlot]:
if not db.get(Student, item_id):
raise HTTPException(404, "Student not found")
db.query(ScheduleSlot).filter(ScheduleSlot.student_id == item_id).delete()
for slot in body:
db.add(ScheduleSlot(student_id=item_id, **slot.model_dump()))
db.commit()
return (
db.query(ScheduleSlot)
.filter(ScheduleSlot.student_id == item_id)
.order_by(ScheduleSlot.weekday.asc(), ScheduleSlot.start_time.asc())
.all()
)
@router.get("/schedule/today", response_model=list[TodayEntryOut])
def schedule_today(db: Session = Depends(get_db)) -> list[TodayEntryOut]:
today = date.today()
weekday = today.weekday() # Mon=0
marked = db.query(DayMark).filter(DayMark.date == today).first()
if marked is not None:
return [] # whole day off for everyone
rows = (
db.query(ScheduleSlot, Student)
.join(Student, Student.id == ScheduleSlot.student_id)
.filter(ScheduleSlot.weekday == weekday, Student.is_active.is_(True))
.order_by(ScheduleSlot.start_time.asc())
.all()
)
return [
TodayEntryOut(
student_id=s.id,
student_name=s.name,
subject=s.subject,
start_time=slot.start_time,
end_time=slot.end_time,
)
for slot, s in rows
]
# ---- Attendance ----
@router.get("/attendance", response_model=MonthDataOut)
def get_attendance(
month: str = Query(..., description="YYYY-MM"), db: Session = Depends(get_db)
) -> MonthDataOut:
first, last = _month_bounds(month)
attendance = (
db.query(Attendance)
.filter(Attendance.date >= first, Attendance.date <= last)
.all()
)
day_marks = (
db.query(DayMark).filter(DayMark.date >= first, DayMark.date <= last).all()
)
return MonthDataOut(attendance=attendance, day_marks=day_marks) # type: ignore[arg-type]
@router.put("/attendance", response_model=AttendanceOut)
def set_attendance(body: AttendanceIn, db: Session = Depends(get_db)) -> Attendance:
if body.status not in _STATUSES:
raise HTTPException(400, f"status must be one of {sorted(_STATUSES)}")
if not db.get(Student, body.student_id):
raise HTTPException(404, "Student not found")
obj = (
db.query(Attendance)
.filter(Attendance.student_id == body.student_id, Attendance.date == body.date)
.first()
)
if obj is None:
obj = Attendance(
student_id=body.student_id, date=body.date, status=body.status, note=body.note
)
db.add(obj)
else:
obj.status = body.status
obj.note = body.note
db.commit()
db.refresh(obj)
return obj
@router.post("/attendance/clear", status_code=204)
def clear_attendance(body: AttendanceClearIn, db: Session = Depends(get_db)) -> None:
db.query(Attendance).filter(
Attendance.student_id == body.student_id, Attendance.date == body.date
).delete()
db.commit()
# ---- Day marks (holiday / class off for everyone) ----
@router.post("/day-marks", response_model=DayMarkOut)
def set_day_mark(body: DayMarkIn, db: Session = Depends(get_db)) -> DayMark:
if body.kind not in _KINDS:
raise HTTPException(400, f"kind must be one of {sorted(_KINDS)}")
obj = db.query(DayMark).filter(DayMark.date == body.date).first()
if obj is None:
obj = DayMark(date=body.date, kind=body.kind, note=body.note)
db.add(obj)
else:
obj.kind = body.kind
obj.note = body.note
db.commit()
db.refresh(obj)
return obj
@router.delete("/day-marks/{on_date}", status_code=204)
def delete_day_mark(on_date: date, db: Session = Depends(get_db)) -> None:
db.query(DayMark).filter(DayMark.date == on_date).delete()
db.commit()
# ---- Summary ----
@router.get("/students/summary", response_model=SummaryOut)
def student_summary(
month: str = Query(..., description="YYYY-MM"), db: Session = Depends(get_db)
) -> SummaryOut:
first, last = _month_bounds(month)
students = db.query(Student).order_by(Student.name.asc(), Student.id.asc()).all()
attendance = (
db.query(Attendance)
.filter(Attendance.date >= first, Attendance.date <= last)
.all()
)
counts: dict[int, dict[str, int]] = {
s.id: {"present": 0, "absent": 0, "off": 0} for s in students
}
for a in attendance:
if a.student_id in counts and a.status in counts[a.student_id]:
counts[a.student_id][a.status] += 1
rows = [
StudentSummaryOut(
student_id=s.id,
student_name=s.name,
monthly_fee=s.monthly_fee,
present=counts[s.id]["present"],
absent=counts[s.id]["absent"],
off=counts[s.id]["off"],
)
for s in students
]
expected = sum(s.monthly_fee for s in students if s.is_active)
active = sum(1 for s in students if s.is_active)
return SummaryOut(
month=month,
active_students=active,
expected_income=expected,
students=rows,
)
+269
View File
@@ -0,0 +1,269 @@
"""Pydantic request/response schemas."""
from __future__ import annotations
from datetime import date, datetime
from pydantic import BaseModel, ConfigDict
class ORMModel(BaseModel):
model_config = ConfigDict(from_attributes=True)
# ---- Auth ----
class LoginIn(BaseModel):
username: str
password: str
class TokenOut(BaseModel):
access_token: str
token_type: str = "bearer"
username: str
class MeOut(BaseModel):
username: str
class ChangePasswordIn(BaseModel):
current_password: str
new_password: str
# ---- Profile ----
class ProfileIn(BaseModel):
name: str = ""
tagline: str = ""
bio: str = ""
photo_url: str = ""
qualifications: str = ""
experience_years: int = 0
students_taught: int = 0
achievements: str = ""
hero_cta: str = "Book a free demo"
class ProfileOut(ProfileIn, ORMModel):
id: int
# ---- Subject ----
class SubjectIn(BaseModel):
name: str = ""
level: str = ""
description: str = ""
mode: str = "Online & Offline"
icon: str = "📘"
sort_order: int = 0
class SubjectOut(SubjectIn, ORMModel):
id: int
# ---- Course ----
class CourseIn(BaseModel):
title: str = ""
subject: str = ""
description: str = ""
duration: str = ""
mode: str = "Online & Offline"
highlights: str = ""
sort_order: int = 0
class CourseOut(CourseIn, ORMModel):
id: int
# ---- Fee ----
class FeePlanIn(BaseModel):
name: str = ""
price: str = ""
period: str = "per month"
features: str = ""
highlighted: bool = False
sort_order: int = 0
class FeePlanOut(FeePlanIn, ORMModel):
id: int
# ---- Testimonial ----
class TestimonialIn(BaseModel):
author_name: str = ""
relation: str = "Student"
quote: str = ""
rating: int = 5
sort_order: int = 0
class TestimonialOut(TestimonialIn, ORMModel):
id: int
# ---- Settings ----
class SiteSettingsIn(BaseModel):
address: str = ""
phone: str = ""
email: str = ""
hours: str = ""
whatsapp: str = ""
instagram: str = ""
youtube: str = ""
facebook: str = ""
footer_text: str = ""
class SiteSettingsOut(SiteSettingsIn, ORMModel):
id: int
# ---- Review (public feedback, moderated) ----
class ReviewIn(BaseModel):
author_name: str
relation: str = "Student"
rating: int = 5
message: str
class ReviewOut(ORMModel):
id: int
author_name: str
relation: str
rating: int
message: str
is_approved: bool
created_at: datetime
class ReviewPatch(BaseModel):
is_approved: bool
# ---- Enquiry ----
class EnquiryIn(BaseModel):
name: str
email: str = ""
phone: str = ""
subject_interest: str = ""
message: str
class EnquiryOut(ORMModel):
id: int
name: str
email: str
phone: str
subject_interest: str
message: str
created_at: datetime
is_read: bool
class EnquiryPatch(BaseModel):
is_read: bool
# ---- Student management ----
class ScheduleSlotIn(BaseModel):
weekday: int = 0 # 0=Mon .. 6=Sun
start_time: str = ""
end_time: str = ""
class ScheduleSlotOut(ScheduleSlotIn, ORMModel):
id: int
student_id: int
class StudentIn(BaseModel):
name: str = ""
phone: str = ""
parent_name: str = ""
parent_phone: str = ""
address: str = ""
subject: str = ""
monthly_fee: int = 0
start_date: date | None = None
is_active: bool = True
notes: str = ""
class StudentOut(ORMModel):
id: int
name: str
phone: str
parent_name: str
parent_phone: str
address: str
subject: str
monthly_fee: int
start_date: date
is_active: bool
notes: str
class AttendanceIn(BaseModel):
student_id: int
date: date
status: str = "present" # present|absent|off
note: str = ""
class AttendanceOut(ORMModel):
id: int
student_id: int
date: date
status: str
note: str
class AttendanceClearIn(BaseModel):
student_id: int
date: date
class DayMarkIn(BaseModel):
date: date
kind: str = "holiday" # holiday|class_off
note: str = ""
class DayMarkOut(ORMModel):
id: int
date: date
kind: str
note: str
class MonthDataOut(BaseModel):
attendance: list[AttendanceOut]
day_marks: list[DayMarkOut]
class TodayEntryOut(BaseModel):
student_id: int
student_name: str
subject: str
start_time: str
end_time: str
class StudentSummaryOut(BaseModel):
student_id: int
student_name: str
monthly_fee: int
present: int
absent: int
off: int
class SummaryOut(BaseModel):
month: str
active_students: int
expected_income: int
students: list[StudentSummaryOut]
+66
View File
@@ -0,0 +1,66 @@
"""Password hashing + JWT tokens for the single admin account."""
from __future__ import annotations
import os
from datetime import datetime, timedelta, timezone
import jwt
from fastapi import Depends, HTTPException, status
from fastapi.security import OAuth2PasswordBearer
from passlib.context import CryptContext
from sqlalchemy.orm import Session
from app.db import get_db
from app.models import AdminUser
_pwd = CryptContext(schemes=["bcrypt"], deprecated="auto")
SECRET_KEY = os.getenv("JWT_SECRET", "swatiyadav-dev-secret-change-me")
ALGORITHM = "HS256"
TOKEN_TTL_HOURS = 24 * 7
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="api/auth/login", auto_error=False)
def hash_password(password: str) -> str:
return _pwd.hash(password)
def verify_password(password: str, hashed: str) -> bool:
try:
return _pwd.verify(password, hashed)
except Exception: # noqa: BLE001
return False
def create_token(username: str) -> str:
payload = {
"sub": username,
"exp": datetime.now(timezone.utc) + timedelta(hours=TOKEN_TTL_HOURS),
}
return jwt.encode(payload, SECRET_KEY, algorithm=ALGORITHM)
def get_current_admin(
token: str | None = Depends(oauth2_scheme),
db: Session = Depends(get_db),
) -> AdminUser:
creds_error = HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Not authenticated",
headers={"WWW-Authenticate": "Bearer"},
)
if not token:
raise creds_error
try:
payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
username = payload.get("sub")
except jwt.PyJWTError:
raise creds_error from None
if not username:
raise creds_error
user = db.query(AdminUser).filter(AdminUser.username == username).first()
if not user:
raise creds_error
return user
+253
View File
@@ -0,0 +1,253 @@
"""Create tables and seed the DEVTeacher placeholder content on first run.
Idempotent: if an admin already exists, nothing is seeded. The default admin is
username ``DEVTeacher`` / password ``DEVTeacher`` (the teacher changes it).
"""
from __future__ import annotations
from datetime import date
from sqlalchemy import inspect, text
from sqlalchemy.orm import Session
from app.db import Base, SessionLocal, engine
from app.models import (
AdminUser,
Attendance,
Course,
DayMark,
Enquiry,
FeePlan,
Profile,
Review,
ScheduleSlot,
SiteSettings,
Student,
Subject,
Testimonial,
)
from app.security import hash_password
DEFAULT_ADMIN_USER = "DEVTeacher"
DEFAULT_ADMIN_PASS = "DEVTeacher"
def _seed(db: Session) -> None:
db.add(AdminUser(username=DEFAULT_ADMIN_USER, password_hash=hash_password(DEFAULT_ADMIN_PASS)))
db.add(
Profile(
name="Swati Yadav",
tagline="Making learning joyful, clear and exam-ready",
bio=(
"Hello! I'm Swati Yadav, a passionate tuition teacher with years of experience "
"helping students build strong fundamentals and real confidence. I focus on "
"concept clarity, regular practice and personal attention so every student can "
"reach their full potential — whether the goal is topping the class, cracking "
"board exams, or simply falling in love with a subject."
),
photo_url="",
qualifications="M.Sc. Mathematics · B.Ed. · Certified Online Educator",
experience_years=10,
students_taught=500,
achievements=(
"95% of students improved by 2+ grades\n"
"Mentored 50+ board-exam toppers\n"
"Personalised study plans for every learner"
),
hero_cta="Book a free demo class",
)
)
subjects = [
("Mathematics", "Class 6–12", "Algebra, geometry, calculus and problem-solving made simple with step-by-step methods.", "📐", 1),
("Physics", "Class 9–12", "Concept-first physics with real-life examples, numericals and exam techniques.", "🔬", 2),
("Chemistry", "Class 9–12", "Organic, inorganic and physical chemistry taught with clarity and easy recall tricks.", "⚗️", 3),
("English", "Class 6–10", "Grammar, comprehension, writing skills and confident communication.", "📖", 4),
]
for name, level, desc, icon, order in subjects:
db.add(Subject(name=name, level=level, description=desc, mode="Online & Offline", icon=icon, sort_order=order))
courses = [
(
"Board Exam Crash Course",
"Mathematics & Science",
"An intensive revision programme covering the complete syllabus with mock tests, doubt-clearing and previous-year papers.",
"3 months",
"Practice tests every week\nPersonal doubt sessions\nComplete syllabus revision",
1,
),
(
"Foundation Builder",
"All Subjects",
"A steady long-term course for younger students that builds strong basics and study habits from the ground up.",
"Full academic year",
"Concept-first teaching\nRegular homework & feedback\nMonthly progress reports",
2,
),
(
"One-on-One Personal Tuition",
"Chosen Subject",
"Fully personalised private tuition tailored to a single student's pace, goals and weak areas.",
"Flexible",
"Completely customised plan\nFlexible timings\nDedicated attention",
3,
),
]
for title, subject, desc, duration, highlights, order in courses:
db.add(
Course(
title=title,
subject=subject,
description=desc,
duration=duration,
mode="Online & Offline",
highlights=highlights,
sort_order=order,
)
)
fees = [
("Group Batch", "₹1,500", "per month", "Small batches (max 8)\n3 classes a week\nWeekly tests\nStudy material included", False, 1),
("Premium Batch", "₹2,500", "per month", "Mini batches (max 4)\n5 classes a week\nWeekly tests + doubt sessions\nMonthly progress report", True, 2),
("1-on-1 Private", "₹4,000", "per month", "Fully personal tuition\nFlexible timings\nCustom study plan\nUnlimited doubt support", False, 3),
]
for name, price, period, features, highlighted, order in fees:
db.add(FeePlan(name=name, price=price, period=period, features=features, highlighted=highlighted, sort_order=order))
testimonials = [
("Aarav Sharma", "Student", "Swati ma'am made maths so easy to understand. My marks jumped from 62 to 91 in just one year!", 5, 1),
("Meena Gupta", "Parent", "My daughter actually looks forward to her classes now. The personal attention is wonderful.", 5, 2),
("Rohan Verma", "Student", "The best teacher I've ever had. She explains every concept until it truly clicks.", 5, 3),
("Kavita Nair", "Parent", "Very disciplined, caring and result-oriented. Highly recommend to every parent.", 5, 4),
]
for author, relation, quote, rating, order in testimonials:
db.add(Testimonial(author_name=author, relation=relation, quote=quote, rating=rating, sort_order=order))
db.add(
SiteSettings(
address="123 Vidya Nagar, Near City Library, Your City, 000000",
phone="+91 90000 00000",
email="hello@swatiyadav.com",
hours="Mon–Sat, 9:00 AM – 8:00 PM",
whatsapp="https://wa.me/919000000000",
instagram="https://instagram.com/",
youtube="https://youtube.com/",
facebook="https://facebook.com/",
footer_text="Empowering students with knowledge, confidence and care.",
)
)
db.add(
Enquiry(
name="Sample Parent",
email="parent@example.com",
phone="+91 98765 43210",
subject_interest="Mathematics",
message="Hi, I'd like to know the timings for the Class 10 maths batch. Thank you!",
is_read=False,
)
)
reviews = [
("Priya Deshmukh", "Parent", 5, "The weekly progress updates keep me informed and my son is far more confident now.", True),
("Ananya Rao", "Student", 5, "Doubts are always cleared patiently. Highly recommend her classes!", False),
]
for author, relation, rating, message, approved in reviews:
db.add(Review(author_name=author, relation=relation, rating=rating, message=message, is_approved=approved))
db.commit()
_seed_students(db)
def _seed_students(db: Session) -> None:
"""Sample students, schedules and a little attendance for this month."""
students_data = [
# name, phone, parent_name, parent_phone, address, subject, fee, slots
(
"Aarav Sharma", "+91 98111 22233", "Rakesh Sharma", "+91 98111 00011",
"12 Green Park, Your City", "Mathematics · Class 10", 1500,
[(0, "16:00", "17:00"), (2, "16:00", "17:00"), (4, "16:00", "17:00")],
),
(
"Isha Verma", "+91 98222 33344", "Sunita Verma", "+91 98222 00022",
"45 Lake View, Your City", "Physics · Class 12", 2500,
[(1, "17:30", "18:30"), (3, "17:30", "18:30")],
),
(
"Rohan Gupta", "+91 98333 44455", "Manoj Gupta", "+91 98333 00033",
"78 Rose Villa, Your City", "Chemistry · Class 11", 2500,
[(0, "18:00", "19:00"), (3, "18:00", "19:00"), (5, "10:00", "11:00")],
),
(
"Ananya Nair", "+91 98444 55566", "Kavita Nair", "+91 98444 00044",
"9 Sunrise Apartments, Your City", "English · Class 8", 1500,
[(1, "16:00", "17:00"), (4, "16:00", "17:00")],
),
]
student_objs: list[Student] = []
for name, phone, pname, pphone, addr, subj, fee, slots in students_data:
s = Student(
name=name, phone=phone, parent_name=pname, parent_phone=pphone,
address=addr, subject=subj, monthly_fee=fee, start_date=date.today(),
is_active=True, notes="",
)
db.add(s)
db.flush()
for weekday, start, end in slots:
db.add(ScheduleSlot(student_id=s.id, weekday=weekday, start_time=start, end_time=end))
student_objs.append(s)
# A little attendance for the first few days of the current month.
today = date.today()
statuses = ["present", "present", "absent", "present", "off"]
for day in range(1, 6):
try:
d = date(today.year, today.month, day)
except ValueError:
continue
if d > today:
break
for i, s in enumerate(student_objs):
db.add(Attendance(student_id=s.id, date=d, status=statuses[(day + i) % len(statuses)]))
# A sample holiday on the 1st (kept only if not in the future).
try:
first = date(today.year, today.month, 1)
db.add(DayMark(date=first, kind="holiday", note="Month start holiday"))
except ValueError:
pass
db.commit()
def _ensure_columns() -> None:
"""Add columns introduced after a table was first created.
``create_all`` never alters existing tables, so on databases that already
have the ``attendance`` table we add the newer ``note`` column by hand.
"""
inspector = inspect(engine)
if "attendance" not in inspector.get_table_names():
return
cols = {c["name"] for c in inspector.get_columns("attendance")}
if "note" not in cols:
default = "''" if engine.dialect.name != "sqlite" else "''"
with engine.begin() as conn:
conn.execute(
text(f"ALTER TABLE attendance ADD COLUMN note TEXT NOT NULL DEFAULT {default}")
)
def init_db() -> None:
Base.metadata.create_all(bind=engine)
_ensure_columns()
db = SessionLocal()
try:
if db.query(AdminUser).count() == 0:
_seed(db)
finally:
db.close()
+15
View File
@@ -0,0 +1,15 @@
[project]
name = "backend"
version = "0.1.0"
requires-python = ">=3.13"
dependencies = [
"fastapi[standard]==0.141.1",
"sqlalchemy==2.0.36",
"psycopg[binary]==3.2.3",
"passlib[bcrypt]==1.7.4",
"bcrypt==4.2.1",
"pyjwt==2.10.1",
]
[tool.uv]
package = false
+44
View File
@@ -0,0 +1,44 @@
"""Create the demo admin login the platform injects at deploy.
The platform generates DEMO_EMAIL / DEMO_PASSWORD, sets them in the environment
and runs this script. We create an admin account with that username/password so
the deployed app can be opened. Idempotent: re-running only updates the password.
The public admin login is username-based, so DEMO_EMAIL is used as the username.
The built-in ``DEVTeacher`` account still exists for the teacher.
"""
from __future__ import annotations
import os
from app.db import Base, SessionLocal, engine
from app.models import AdminUser
from app.security import hash_password
def main() -> None:
email = os.environ.get("DEMO_EMAIL")
password = os.environ.get("DEMO_PASSWORD")
if not email or not password:
print("[seed_demo] DEMO_EMAIL / DEMO_PASSWORD not set; skipping")
return
Base.metadata.create_all(bind=engine)
db = SessionLocal()
try:
user = db.query(AdminUser).filter(AdminUser.username == email).first()
if user is None:
user = AdminUser(username=email, password_hash=hash_password(password))
db.add(user)
print(f"[seed_demo] created demo admin {email}")
else:
user.password_hash = hash_password(password)
print(f"[seed_demo] updated demo admin {email}")
db.commit()
finally:
db.close()
if __name__ == "__main__":
main()
+1153
View File
File diff suppressed because it is too large Load Diff