From c04d285be616178f3549d0d601a0fa9d05a4a840 Mon Sep 17 00:00:00 2001 From: sachin <26170834+sachinsshetty@users.noreply.github.com> Date: Wed, 3 Dec 2025 19:53:21 +0100 Subject: [PATCH 01/14] add-dashboard --- docs/user_management.md | 6 ++++++ src/main.py | 36 +++++++++++++++++++++++++++++++++++- 2 files changed, 41 insertions(+), 1 deletion(-) diff --git a/docs/user_management.md b/docs/user_management.md index 0c6ecc0..583edd2 100644 --- a/docs/user_management.md +++ b/docs/user_management.md @@ -5,6 +5,12 @@ curl -X POST http://localhost:8000/register/ \ -H "Content-Type: application/json" \ -d '{"username": "gymbro", "email": "gym@example.com", "password": "secret123"}' + +curl -X POST https://flex-fitness.club/register/ \ + -H "Content-Type: application/json" \ + -d '{"username": "gymbro", "email": "gym@example.com", "password": "secret123"}' + + Login curl -X POST http://localhost:8000/login \ diff --git a/src/main.py b/src/main.py index a621bfd..c43c57a 100644 --- a/src/main.py +++ b/src/main.py @@ -172,4 +172,38 @@ def get_leaderboard(db: Session = Depends(get_db)): xp=u.xp, streak_count=u.streak_count ) for u in users - ] \ No newline at end of file + ] + +# === DASHBOARD ENDPOINT === +@app.get("/dashboard/", response_model=schemas.UserPublic) +def get_dashboard( + current_user: models.User = Depends(get_current_user), + db: Session = Depends(get_db) +): + """ + Returns complete user profile for the dashboard: + - username, level, xp, streak, total_workouts + - plus any earned badges (bonus!) + """ + # Fetch earned badges with details + earned_badges = [ + schemas.BadgeResponse( + name=b.badge.name, + description=b.badge.description, + icon_url=b.badge.icon_url, + earned_at=b.earned_at + ) + for b in current_user.user_badges + ] + + return schemas.UserPublic( + id=current_user.id, + username=current_user.username, + email=current_user.email, + level=current_user.level, + xp=current_user.xp, + streak_count=current_user.streak_count, + total_workouts=current_user.total_workouts, + # Add badges if you want (optional extra) + # badges=earned_badges + ) \ No newline at end of file From 402e703cc3d8eab61e50196abec0a0ff8a26de36 Mon Sep 17 00:00:00 2001 From: sachin <26170834+sachinsshetty@users.noreply.github.com> Date: Wed, 3 Dec 2025 20:44:40 +0100 Subject: [PATCH 02/14] add-work --- src/main.py | 63 ++++++++++++++++++++++++++++++++++------------------- 1 file changed, 40 insertions(+), 23 deletions(-) diff --git a/src/main.py b/src/main.py index c43c57a..81e4ebf 100644 --- a/src/main.py +++ b/src/main.py @@ -70,38 +70,36 @@ async def get_current_user(token: str = Depends(oauth2_scheme), db: Session = De return user -# === Routes === - -@app.post("/register/", response_model=schemas.UserPublic, status_code=201) -def register_user(user_in: schemas.UserCreate, db: Session = Depends(get_db)): - # Check if user exists - if db.query(models.User).filter(models.User.email == user_in.email).first(): - raise HTTPException(400, "Email already registered") - if db.query(models.User).filter(models.User.username == user_in.username).first(): - raise HTTPException(400, "Username already taken") - - hashed_password = get_password_hash(user_in.password) - user = models.User( - email=user_in.email, - username=user_in.username, - hashed_password=hashed_password, - ) - db.add(user) +# === Registration === +@app.post("/register/", response_model=schemas.UserPublic) +def register(user: schemas.UserCreate, db: Session = Depends(get_db)): + db_user = db.query(models.User).filter(models.User.email == user.email).first() + if db_user: + raise HTTPException(status_code=400, detail="Email already registered") + hashed_password = get_password_hash(user.password) + db_user = models.User(email=user.email, username=user.username, hashed_password=hashed_password) + db.add(db_user) db.commit() - db.refresh(user) - return user + db.refresh(db_user) + return db_user +# === Login === @app.post("/login") def login(form_data: OAuth2PasswordRequestForm = Depends(), db: Session = Depends(get_db)): user = db.query(models.User).filter(models.User.email == form_data.username).first() if not user or not verify_password(form_data.password, user.hashed_password): - raise HTTPException(401, "Incorrect email or password") - - access_token = create_access_token(data={"sub": str(user.id)}) + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="Incorrect email or password", + headers={"WWW-Authenticate": "Bearer"}, + ) + access_token_expires = timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES) + access_token = create_access_token(data={"sub": user.id}, expires_delta=access_token_expires) return {"access_token": access_token, "token_type": "bearer"} +# === Workout Logging === @app.post("/workouts/", response_model=schemas.WorkoutResponse) def log_workout( workout: schemas.WorkoutCreate, @@ -206,4 +204,23 @@ def get_dashboard( total_workouts=current_user.total_workouts, # Add badges if you want (optional extra) # badges=earned_badges - ) \ No newline at end of file + ) + +# === WORKOUT HISTORY ENDPOINT === +@app.get("/workouts/history/", response_model=List[schemas.WorkoutResponse]) +def get_workout_history( + current_user: models.User = Depends(get_current_user), + db: Session = Depends(get_db) +): + workouts = db.query(models.Workout).filter(models.Workout.user_id == current_user.id).order_by(models.Workout.created_at.desc()).all() + return [ + schemas.WorkoutResponse( + id=w.id, + workout_type=w.workout_type, + duration_min=w.duration_min, + calories=w.calories, + created_at=w.created_at, + # Add gamification if needed, but since it's history, keep basic or calculate on fly + # For simplicity, omit gamification or set empty + ) for w in workouts + ] \ No newline at end of file From 3e722f4846170bb4c844ebe929317c6d02266c7f Mon Sep 17 00:00:00 2001 From: sachin <26170834+sachinsshetty@users.noreply.github.com> Date: Sun, 7 Dec 2025 00:05:00 +0100 Subject: [PATCH 03/14] init-attendance --- attendance/README.md | 6 ++ attendance/main.py | 133 +++++++++++++++++++++++++++++ attendance/requirements.txt | 6 ++ attendance/templates/home.html | 24 ++++++ attendance/templates/today_qr.html | 19 +++++ 5 files changed, 188 insertions(+) create mode 100644 attendance/README.md create mode 100644 attendance/main.py create mode 100644 attendance/requirements.txt create mode 100644 attendance/templates/home.html create mode 100644 attendance/templates/today_qr.html diff --git a/attendance/README.md b/attendance/README.md new file mode 100644 index 0000000..b9a8c14 --- /dev/null +++ b/attendance/README.md @@ -0,0 +1,6 @@ +QR code - attendance management + + +pip install -r requirements.txt + +uvicorn main:app --host 0.0.0.0 --port 8000 \ No newline at end of file diff --git a/attendance/main.py b/attendance/main.py new file mode 100644 index 0000000..06245f8 --- /dev/null +++ b/attendance/main.py @@ -0,0 +1,133 @@ +from fastapi import FastAPI, Depends, HTTPException, Request +from fastapi.responses import HTMLResponse, StreamingResponse +from fastapi.middleware.cors import CORSMiddleware +from fastapi.templating import Jinja2Templates +from sqlalchemy.orm import Session +from datetime import datetime, date, timedelta +import qrcode +from io import BytesIO +import hashlib + +import models +from database import engine, SessionLocal +from schemas import AttendanceCreate, AttendanceResponse + +templates = Jinja2Templates(directory="templates") +Base.metadata.create_all(bind=engine) + +app = FastAPI(title="Daily QR Attendance System") + +app.add_middleware( + CORSMiddleware, + allow_origins=["*"], + allow_credentials=True, + allow_methods=["*"], + allow_headers=["*"], +) + +def get_db(): + db = SessionLocal() + try: + yield db + finally: + db.close() + +# Generate deterministic daily code: "ATTEND-YYYY-MM-DD" +def get_today_code() -> str: + today = date.today().isoformat() + return f"ATTEND-{today}" + +# Optional: Add secret salt for extra security +# def get_today_code() -> str: +# today = date.today().isoformat() +# secret = "my-school-secret-2025" +# return hashlib.sha256(f"{today}{secret}".encode()).hexdigest()[:12].upper() + +@app.get("/", response_class=HTMLResponse) +def home(request: Request): + today_code = get_today_code() + return templates.TemplateResponse("home.html", { + "request": request, + "today_code": today_code, + "date": date.today().strftime("%A, %B %d, %Y") + }) + +# Today's QR Code (big & beautiful for projector) +@app.get("/today-qr", response_class=HTMLResponse) +def today_qr_page(request: Request): + code = get_today_code() + return templates.TemplateResponse("today_qr.html", { + "request": request, + "code": code, + "date": date.today().strftime("%d %B %Y"), + "day": date.today().strftime("%A") + }) + +# API: Get today's code as JSON (for Android app if needed) +@app.get("/api/today-code") +def api_today_code(): + return {"code": get_today_code(), "date": date.today().isoformat()} + +# Raw QR Image +@app.get("/qr-image") +def get_qr_image(): + code = get_today_code() + qr = qrcode.QRCode(version=1, box_size=20, border=4) + qr.add_data(code) + qr.make(fit=True) + img = qr.make_image(fill_color="black", back_color="white") + + buffer = BytesIO() + img.save(buffer, format="PNG") + buffer.seek(0) + + return StreamingResponse(buffer, media_type="image/png") + +# MARK ATTENDANCE – Now checks daily code + one per day per student +@app.post("/attendance", response_model=AttendanceResponse) +def mark_attendance(payload: AttendanceCreate, db: Session = Depends(get_db)): + scanned_code = payload.student_id.strip() # We reuse student_id field for QR content + today_code = get_today_code() + + if scanned_code != today_code: + raise HTTPException(status_code=400, detail="Invalid or expired QR code") + + # Find student by actual student_id (you can pass it separately or embed in app) + # For now, we'll assume you send real student_id in a new field + # Let's fix the schema – we'll accept both + + raise HTTPException(status_code=400, detail="Use new /attendance2 endpoint") + +# NEW BETTER ENDPOINT +class AttendanceDaily(BaseModel): + student_id: str # real student ID like STD001 + qr_code: str # the scanned daily code + +@app.post("/attendance2", response_model=AttendanceResponse) +def mark_attendance_daily(payload: AttendanceDaily, db: Session = Depends(get_db)): + if payload.qr_code != get_today_code(): + raise HTTPException(status_code=400, detail="QR code is expired or invalid") + + student = db.query(models.Student).filter(models.Student.student_id == payload.student_id).first() + if not student: + raise HTTPException(status_code=404, detail="Student not found") + + # Check if already attended today + today_start = datetime.combine(date.today(), datetime.min.time()) + already = db.query(models.Attendance).filter( + models.Attendance.student_id == payload.student_id, + models.Attendance.timestamp >= today_start + ).first() + + if already: + raise HTTPException(status_code=400, detail="Already marked attendance today") + + attendance = models.Attendance(student_id=payload.student_id) + db.add(attendance) + db.commit() + + return AttendanceResponse( + message="Attendance marked successfully!", + student_name=student.name, + timestamp=attendance.timestamp + ) \ No newline at end of file diff --git a/attendance/requirements.txt b/attendance/requirements.txt new file mode 100644 index 0000000..0bc3621 --- /dev/null +++ b/attendance/requirements.txt @@ -0,0 +1,6 @@ +fastapi==0.115.2 +uvicorn==0.30.6 +sqlalchemy==2.0.36 +pydantic==2.9.2 +qrcode[pil]==7.4.2 +Jinja2==3.1.4 \ No newline at end of file diff --git a/attendance/templates/home.html b/attendance/templates/home.html new file mode 100644 index 0000000..a626518 --- /dev/null +++ b/attendance/templates/home.html @@ -0,0 +1,24 @@ + + +
+{{ date }}
+Code: {{ today_code }}
Show this on projector or TV
+Scan to mark attendance
+ + \ No newline at end of file From 2e52ec998ed137f1808cb51a4d2286352282c552 Mon Sep 17 00:00:00 2001 From: sachin <26170834+sachinsshetty@users.noreply.github.com> Date: Sun, 7 Dec 2025 00:12:30 +0100 Subject: [PATCH 04/14] daily-qr-code --- attendance/README.md | 17 +++++++++++++++- attendance/__init__.py | 0 attendance/database.py | 10 +++++++++ attendance/main.py | 46 +++++++++++++++++++++++------------------- attendance/models.py | 18 +++++++++++++++++ attendance/schemas.py | 17 ++++++++++++++++ 6 files changed, 86 insertions(+), 22 deletions(-) create mode 100644 attendance/__init__.py create mode 100644 attendance/database.py create mode 100644 attendance/models.py create mode 100644 attendance/schemas.py diff --git a/attendance/README.md b/attendance/README.md index b9a8c14..7fbfff7 100644 --- a/attendance/README.md +++ b/attendance/README.md @@ -3,4 +3,19 @@ QR code - attendance management pip install -r requirements.txt -uvicorn main:app --host 0.0.0.0 --port 8000 \ No newline at end of file +uvicorn main:app --host 0.0.0.0 --port 8000 + + +http://your-ip:8000/api/today-code + + + +http://your-ip:8000/qr-image + +http://your-ip:8000/ + +http://your-ip:8000/today-qr + + +http://localhost:8000/add-test-students + diff --git a/attendance/__init__.py b/attendance/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/attendance/database.py b/attendance/database.py new file mode 100644 index 0000000..46f5ef5 --- /dev/null +++ b/attendance/database.py @@ -0,0 +1,10 @@ +from sqlalchemy import create_engine +from sqlalchemy.ext.declarative import declarative_base +from sqlalchemy.orm import sessionmaker + +SQLALCHEMY_DATABASE_URL = "sqlite:///./attendance.db" + +engine = create_engine(SQLALCHEMY_DATABASE_URL, connect_args={"check_same_thread": False}) +SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine) + +Base = declarative_base() \ No newline at end of file diff --git a/attendance/main.py b/attendance/main.py index 06245f8..14d5e1b 100644 --- a/attendance/main.py +++ b/attendance/main.py @@ -8,12 +8,14 @@ from io import BytesIO import hashlib +from fastapi import FastAPI, Depends, HTTPException, Request import models from database import engine, SessionLocal -from schemas import AttendanceCreate, AttendanceResponse +from schemas import AttendanceDaily, AttendanceResponse, AttendanceCreate + templates = Jinja2Templates(directory="templates") -Base.metadata.create_all(bind=engine) +models.Base.metadata.create_all(bind=engine) app = FastAPI(title="Daily QR Attendance System") @@ -37,7 +39,7 @@ def get_today_code() -> str: today = date.today().isoformat() return f"ATTEND-{today}" -# Optional: Add secret salt for extra security +# Optional: Add secret salt for extra security (uncomment if needed) # def get_today_code() -> str: # today = date.today().isoformat() # secret = "my-school-secret-2025" @@ -83,26 +85,12 @@ def get_qr_image(): return StreamingResponse(buffer, media_type="image/png") -# MARK ATTENDANCE – Now checks daily code + one per day per student +# Deprecated: Old attendance endpoint @app.post("/attendance", response_model=AttendanceResponse) def mark_attendance(payload: AttendanceCreate, db: Session = Depends(get_db)): - scanned_code = payload.student_id.strip() # We reuse student_id field for QR content - today_code = get_today_code() - - if scanned_code != today_code: - raise HTTPException(status_code=400, detail="Invalid or expired QR code") - - # Find student by actual student_id (you can pass it separately or embed in app) - # For now, we'll assume you send real student_id in a new field - # Let's fix the schema – we'll accept both - - raise HTTPException(status_code=400, detail="Use new /attendance2 endpoint") - -# NEW BETTER ENDPOINT -class AttendanceDaily(BaseModel): - student_id: str # real student ID like STD001 - qr_code: str # the scanned daily code + raise HTTPException(status_code=400, detail="Use /attendance2 endpoint") +# MARK ATTENDANCE – Checks daily code + one per day per student @app.post("/attendance2", response_model=AttendanceResponse) def mark_attendance_daily(payload: AttendanceDaily, db: Session = Depends(get_db)): if payload.qr_code != get_today_code(): @@ -130,4 +118,20 @@ def mark_attendance_daily(payload: AttendanceDaily, db: Session = Depends(get_db message="Attendance marked successfully!", student_name=student.name, timestamp=attendance.timestamp - ) \ No newline at end of file + ) + +# Optional: Add test students +@app.post("/add-test-students") +def add_test_students(db: Session = Depends(get_db)): + test_students = [ + {"student_id": "STD001", "name": "Alice Johnson"}, + {"student_id": "STD002", "name": "Bob Smith"}, + {"student_id": "STD003", "name": "Carol Lee"}, + {"student_id": "STD004", "name": "David Kim"}, + {"student_id": "STD005", "name": "Emma Wilson"}, + ] + for s in test_students: + if not db.query(models.Student).filter(models.Student.student_id == s["student_id"]).first(): + db.add(models.Student(**s)) + db.commit() + return {"message": "Test students added"} \ No newline at end of file diff --git a/attendance/models.py b/attendance/models.py new file mode 100644 index 0000000..b95b219 --- /dev/null +++ b/attendance/models.py @@ -0,0 +1,18 @@ +from sqlalchemy import Column, Integer, String, DateTime, ForeignKey +from sqlalchemy.orm import relationship +from datetime import datetime +from database import Base # ← absolute import (fixed) + +class Student(Base): + __tablename__ = "students" + id = Column(Integer, primary_key=True, index=True) + student_id = Column(String, unique=True, index=True) + name = Column(String) + attendances = relationship("Attendance", back_populates="student") + +class Attendance(Base): + __tablename__ = "attendances" + id = Column(Integer, primary_key=True, index=True) + student_id = Column(String, ForeignKey("students.student_id")) + timestamp = Column(DateTime, default=datetime.utcnow) + student = relationship("Student", back_populates="attendances") \ No newline at end of file diff --git a/attendance/schemas.py b/attendance/schemas.py new file mode 100644 index 0000000..bb29360 --- /dev/null +++ b/attendance/schemas.py @@ -0,0 +1,17 @@ +from pydantic import BaseModel +from datetime import datetime + +class AttendanceCreate(BaseModel): + student_id: str + +class AttendanceDaily(BaseModel): + student_id: str # real student ID like STD001 + qr_code: str # the scanned daily code + +class AttendanceResponse(BaseModel): + message: str + student_name: str | None = None + timestamp: datetime + + class Config: + from_attributes = True \ No newline at end of file From 7a9b397984bdc2af732c27787f1b2b59ea6b0802 Mon Sep 17 00:00:00 2001 From: sachin <26170834+sachinsshetty@users.noreply.github.com> Date: Sun, 7 Dec 2025 01:36:51 +0100 Subject: [PATCH 05/14] dcoekr --- attendance/Dockerfile | 22 ++++++++++++++++++++++ attendance/compose.yml | 20 ++++++++++++++++++++ attendance/requirements.txt | 2 +- 3 files changed, 43 insertions(+), 1 deletion(-) create mode 100644 attendance/Dockerfile create mode 100644 attendance/compose.yml diff --git a/attendance/Dockerfile b/attendance/Dockerfile new file mode 100644 index 0000000..d68fb83 --- /dev/null +++ b/attendance/Dockerfile @@ -0,0 +1,22 @@ +# Use official lightweight Python image +FROM python:3.12-slim + +# Set working directory +WORKDIR /app + +# Install only what we need +COPY requirements.txt . +RUN pip install --no-cache-dir -r requirements.txt + +# Copy entire project +COPY . . + +# Create non-root user (security) +RUN adduser --disabled-password --gecos '' appuser +USER appuser + +# Expose port +EXPOSE 8000 + +# Run the server +CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"] \ No newline at end of file diff --git a/attendance/compose.yml b/attendance/compose.yml new file mode 100644 index 0000000..4aa5e0a --- /dev/null +++ b/attendance/compose.yml @@ -0,0 +1,20 @@ +version: '3.9' + +services: + attendance-api: + build: . + container_name: daily-qr-attendance + restart: unless-stopped + ports: + - "8000:8000" # Access from phone/PC: http://your-ip:8000 + volumes: + - ./attendance.db:/app/attendance.db # Persist database + - ./templates:/app/templates # Hot reload templates (optional) + environment: + - PYTHONUNBUFFERED=1 + networks: + - attendance-net + +networks: + attendance-net: + driver: bridge \ No newline at end of file diff --git a/attendance/requirements.txt b/attendance/requirements.txt index 0bc3621..f1af01f 100644 --- a/attendance/requirements.txt +++ b/attendance/requirements.txt @@ -1,5 +1,5 @@ fastapi==0.115.2 -uvicorn==0.30.6 +uvicorn[standard]==0.30.6 sqlalchemy==2.0.36 pydantic==2.9.2 qrcode[pil]==7.4.2 From 97d60d6175af8e9379796bd0d47a79fe0735e89e Mon Sep 17 00:00:00 2001 From: sachin <26170834+sachinsshetty@users.noreply.github.com> Date: Sun, 7 Dec 2025 01:37:15 +0100 Subject: [PATCH 06/14] stats --- attendance/compose.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/attendance/compose.yml b/attendance/compose.yml index 4aa5e0a..c979e10 100644 --- a/attendance/compose.yml +++ b/attendance/compose.yml @@ -6,7 +6,7 @@ services: container_name: daily-qr-attendance restart: unless-stopped ports: - - "8000:8000" # Access from phone/PC: http://your-ip:8000 + - "80:8000" # Access from phone/PC: http://your-ip:8000 volumes: - ./attendance.db:/app/attendance.db # Persist database - ./templates:/app/templates # Hot reload templates (optional) From 90245f554a9d973b79c9590b9ce774fec921d9cc Mon Sep 17 00:00:00 2001 From: sachin <26170834+sachinsshetty@users.noreply.github.com> Date: Sun, 7 Dec 2025 01:38:04 +0100 Subject: [PATCH 07/14] add --- attendance/.dockerignore | 91 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 91 insertions(+) create mode 100644 attendance/.dockerignore diff --git a/attendance/.dockerignore b/attendance/.dockerignore new file mode 100644 index 0000000..393663e --- /dev/null +++ b/attendance/.dockerignore @@ -0,0 +1,91 @@ +samples +*.wav +*.log +venv +*.db +*.nemo +*.json +.github +.devcontainer +docs +misc +samples +node_modules +# Ignore all Python files except those explicitly copied +*.pyc +*.pyo +*.pyd +models/ +# Ignore all virtual environments +venv/ +env/ +.env/ +.venv/ +__pycache__/ + +# Ignore build artifacts +build/ +dist/ +*.egg-info/ + +# Ignore local version control files +.git/ +.gitignore + +# Ignore local environment files +.env + +# Ignore local log files +*.log + +# Ignore all node_modules +node_modules/ + +# Ignore all Docker-related files +Dockerfile +docker-compose.yml + +# Ignore all local development files +.vscode/ +.idea/ +.pytest_cache/ + +# Ignore all test files +*.test.* +*.spec.* +*_test.* +*_spec.* + +# Ignore all backup files +*.bak +*.swp +*.tmp +*.orig + +# Ignore all documentation files +*.md +*.txt +*.rst + +# Ignore all temporary files +*.tmp +*.temp +*.cache + +# Ignore all user-specific files +*.user +*.prefs +*.rc + +# Ignore all unnecessary directories and files +__pycache__ +__pypackages__ + + +!requirements.txt +!docker-requirements.txt +!client-requirements.txt +!server-requirements.txt + +#!model_requirements.txt +#!server_requirements.txt \ No newline at end of file From 7790ef64b8be2f75238fda2d8768063f04a715e0 Mon Sep 17 00:00:00 2001 From: sachin <26170834+sachinsshetty@users.noreply.github.com> Date: Sun, 7 Dec 2025 01:41:41 +0100 Subject: [PATCH 08/14] fix --- attendance/compose.yml | 9 ++++++--- attendance/database.py | 16 +++++++++++++--- 2 files changed, 19 insertions(+), 6 deletions(-) diff --git a/attendance/compose.yml b/attendance/compose.yml index c979e10..95c0ff2 100644 --- a/attendance/compose.yml +++ b/attendance/compose.yml @@ -1,3 +1,4 @@ +# docker-compose.yml version: '3.9' services: @@ -6,15 +7,17 @@ services: container_name: daily-qr-attendance restart: unless-stopped ports: - - "80:8000" # Access from phone/PC: http://your-ip:8000 + - "80:8000" volumes: - - ./attendance.db:/app/attendance.db # Persist database - - ./templates:/app/templates # Hot reload templates (optional) + - db-data:/app # This mounts the whole folder with correct permissions environment: - PYTHONUNBUFFERED=1 networks: - attendance-net +volumes: + db-data: # Named volume = persistent + correct permissions + networks: attendance-net: driver: bridge \ No newline at end of file diff --git a/attendance/database.py b/attendance/database.py index 46f5ef5..9d0687f 100644 --- a/attendance/database.py +++ b/attendance/database.py @@ -1,10 +1,20 @@ +# database.py +import os from sqlalchemy import create_engine from sqlalchemy.ext.declarative import declarative_base from sqlalchemy.orm import sessionmaker -SQLALCHEMY_DATABASE_URL = "sqlite:///./attendance.db" +BASE_DIR = os.path.dirname(os.path.abspath(__file__)) +DB_PATH = os.path.join(BASE_DIR, "attendance.db") -engine = create_engine(SQLALCHEMY_DATABASE_URL, connect_args={"check_same_thread": False}) -SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine) +SQLALCHEMY_DATABASE_URL = f"sqlite:///{DB_PATH}" + +engine = create_engine( + SQLALCHEMY_DATABASE_URL, + connect_args={"check_same_thread": False}, + # This is the magic line for Docker + non-root user + connect_args={"mode": 0o666} if os.path.exists(DB_PATH) else {} +) +SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine) Base = declarative_base() \ No newline at end of file From 7d5cf19d90281cba6e7c587f75ea92c8d46eaf62 Mon Sep 17 00:00:00 2001 From: sachin <26170834+sachinsshetty@users.noreply.github.com> Date: Sun, 7 Dec 2025 01:44:41 +0100 Subject: [PATCH 09/14] difx --- attendance/README.md | 10 ++++++++++ attendance/compose.yml | 11 ++--------- attendance/database.py | 11 +++++++---- 3 files changed, 19 insertions(+), 13 deletions(-) diff --git a/attendance/README.md b/attendance/README.md index 7fbfff7..391ec2c 100644 --- a/attendance/README.md +++ b/attendance/README.md @@ -19,3 +19,13 @@ http://your-ip:8000/today-qr http://localhost:8000/add-test-students + + +# 1. Clone / go into your project folder +cd attendance-server + +# 2. Build and start (first time) +docker compose up --build -d + +# 3. Subsequent runs (just start) +docker compose up -d \ No newline at end of file diff --git a/attendance/compose.yml b/attendance/compose.yml index 95c0ff2..c16c026 100644 --- a/attendance/compose.yml +++ b/attendance/compose.yml @@ -1,4 +1,3 @@ -# docker-compose.yml version: '3.9' services: @@ -9,15 +8,9 @@ services: ports: - "80:8000" volumes: - - db-data:/app # This mounts the whole folder with correct permissions + - ./data:/app # All files (including DB) saved on host environment: - PYTHONUNBUFFERED=1 - networks: - - attendance-net volumes: - db-data: # Named volume = persistent + correct permissions - -networks: - attendance-net: - driver: bridge \ No newline at end of file + data: \ No newline at end of file diff --git a/attendance/database.py b/attendance/database.py index 9d0687f..f6f4cba 100644 --- a/attendance/database.py +++ b/attendance/database.py @@ -1,19 +1,22 @@ -# database.py +# database.py import os from sqlalchemy import create_engine from sqlalchemy.ext.declarative import declarative_base from sqlalchemy.orm import sessionmaker +# Ensure the database file is in the writable /app directory BASE_DIR = os.path.dirname(os.path.abspath(__file__)) DB_PATH = os.path.join(BASE_DIR, "attendance.db") SQLALCHEMY_DATABASE_URL = f"sqlite:///{DB_PATH}" +# This is the correct way — connect_args appears only once engine = create_engine( SQLALCHEMY_DATABASE_URL, - connect_args={"check_same_thread": False}, - # This is the magic line for Docker + non-root user - connect_args={"mode": 0o666} if os.path.exists(DB_PATH) else {} + connect_args={"check_same_thread": False}, # Required for SQLite in FastAPI + # Optional: make file writable by all (only needed if permissions are strict) + # Remove this line if you use the volume method below + # connect_args={"check_same_thread": False, "timeout": 30}, ) SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine) From 0649f217e6b783de95333c2027e771a5ee4a61ce Mon Sep 17 00:00:00 2001 From: sachin <26170834+sachinsshetty@users.noreply.github.com> Date: Sun, 7 Dec 2025 01:48:17 +0100 Subject: [PATCH 10/14] test --- attendance/Dockerfile | 15 +-------------- 1 file changed, 1 insertion(+), 14 deletions(-) diff --git a/attendance/Dockerfile b/attendance/Dockerfile index d68fb83..15a7c6a 100644 --- a/attendance/Dockerfile +++ b/attendance/Dockerfile @@ -1,22 +1,9 @@ -# Use official lightweight Python image -FROM python:3.12-slim +FROM python:3.10-alpine -# Set working directory WORKDIR /app - -# Install only what we need COPY requirements.txt . RUN pip install --no-cache-dir -r requirements.txt -# Copy entire project COPY . . -# Create non-root user (security) -RUN adduser --disabled-password --gecos '' appuser -USER appuser - -# Expose port -EXPOSE 8000 - -# Run the server CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"] \ No newline at end of file From fd39367246b3c21dbaf2b8aa49cbe854cad37471 Mon Sep 17 00:00:00 2001 From: sachin <26170834+sachinsshetty@users.noreply.github.com> Date: Sun, 7 Dec 2025 02:31:34 +0100 Subject: [PATCH 11/14] fix --- attendance/Dockerfile | 20 +++++++++++++++++++- attendance/__init__.py | 0 attendance/compose.yml | 17 +++++++---------- attendance/requirements.txt | 2 +- 4 files changed, 27 insertions(+), 12 deletions(-) delete mode 100644 attendance/__init__.py diff --git a/attendance/Dockerfile b/attendance/Dockerfile index 15a7c6a..8c47bca 100644 --- a/attendance/Dockerfile +++ b/attendance/Dockerfile @@ -1,9 +1,27 @@ -FROM python:3.10-alpine +# Dockerfile +FROM python:3.12-slim +# Set working directory WORKDIR /app + +# Install system dependencies (needed for PIL/qrcode) +RUN apt-get update && apt-get install -y --no-install-recommends \ + gcc \ + libsqlite3-dev \ + && rm -rf /var/lib/apt/lists/* + +# Copy requirements first (better caching) COPY requirements.txt . RUN pip install --no-cache-dir -r requirements.txt +# Copy project files COPY . . +# Create directory for templates if not exists +RUN mkdir -p templates + +# Expose port +EXPOSE 8000 + +# Command to run the app CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"] \ No newline at end of file diff --git a/attendance/__init__.py b/attendance/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/attendance/compose.yml b/attendance/compose.yml index c16c026..127d81f 100644 --- a/attendance/compose.yml +++ b/attendance/compose.yml @@ -1,16 +1,13 @@ -version: '3.9' - +# docker-compose.yml services: - attendance-api: + attendance-app: build: . - container_name: daily-qr-attendance + container_name: qr-attendance-system restart: unless-stopped ports: - "80:8000" - volumes: - - ./data:/app # All files (including DB) saved on host - environment: - - PYTHONUNBUFFERED=1 + # ← NO VOLUMES AT ALL → fresh DB on every restart -volumes: - data: \ No newline at end of file +networks: + default: + driver: bridge \ No newline at end of file diff --git a/attendance/requirements.txt b/attendance/requirements.txt index f1af01f..0bc3621 100644 --- a/attendance/requirements.txt +++ b/attendance/requirements.txt @@ -1,5 +1,5 @@ fastapi==0.115.2 -uvicorn[standard]==0.30.6 +uvicorn==0.30.6 sqlalchemy==2.0.36 pydantic==2.9.2 qrcode[pil]==7.4.2 From 3ffadeb767a8151517a6a61a1ce5ebcf80a16e13 Mon Sep 17 00:00:00 2001 From: sachin <26170834+sachinsshetty@users.noreply.github.com> Date: Sun, 7 Dec 2025 02:43:21 +0100 Subject: [PATCH 12/14] curl-add-stiden --- attendance/README.md | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/attendance/README.md b/attendance/README.md index 391ec2c..a903316 100644 --- a/attendance/README.md +++ b/attendance/README.md @@ -28,4 +28,11 @@ cd attendance-server docker compose up --build -d # 3. Subsequent runs (just start) -docker compose up -d \ No newline at end of file +docker compose up -d + + +curl -X POST "http://192.168.1.50:8000/add-test-students" -H "Content-Type: application/json" + +curl -X POST "http://your-pc-ip:8000/add-student" \ + -H "Content-Type: application/json" \ + -d '{"student_id": "STD999", "name": "Sachin Kumar"}' \ No newline at end of file From c03098acd7c63108b049238ed3398c40f630eefe Mon Sep 17 00:00:00 2001 From: sachin <26170834+sachinsshetty@users.noreply.github.com> Date: Sun, 7 Dec 2025 18:57:13 +0100 Subject: [PATCH 13/14] add-attendance --- src/main.py | 110 +++++++++++++++++++++++++++++++++++- src/models.py | 17 +++++- src/requirements.txt | 4 +- src/schemas.py | 16 ++++++ src/templates/home.html | 24 ++++++++ src/templates/today_qr.html | 19 +++++++ 6 files changed, 187 insertions(+), 3 deletions(-) create mode 100644 src/templates/home.html create mode 100644 src/templates/today_qr.html diff --git a/src/main.py b/src/main.py index 81e4ebf..b32a882 100644 --- a/src/main.py +++ b/src/main.py @@ -14,6 +14,20 @@ from jose import JWTError, jwt from pydantic import BaseModel +from io import BytesIO +from fastapi import FastAPI, Depends, HTTPException, Request +from fastapi.responses import HTMLResponse, StreamingResponse +from fastapi.middleware.cors import CORSMiddleware +from fastapi.templating import Jinja2Templates +from sqlalchemy.orm import Session +from datetime import datetime, date, timedelta +import qrcode + +from schemas import AttendanceDaily, AttendanceResponse, AttendanceCreate + +templates = Jinja2Templates(directory="templates") + + # === Security Setup === SECRET_KEY = "your-super-secret-jwt-key-change-in-production!!!" # Use env var! ALGORITHM = "HS256" @@ -223,4 +237,98 @@ def get_workout_history( # Add gamification if needed, but since it's history, keep basic or calculate on fly # For simplicity, omit gamification or set empty ) for w in workouts - ] \ No newline at end of file + ] + +def get_today_code() -> str: + today = date.today().isoformat() + return f"ATTEND-{today}" + +@app.get("/attend", response_class=HTMLResponse) +def home(request: Request): + today_code = get_today_code() + return templates.TemplateResponse("home.html", { + "request": request, + "today_code": today_code, + "date": date.today().strftime("%A, %B %d, %Y") + }) + +@app.get("/today-qr", response_class=HTMLResponse) +def today_qr_page(request: Request): + code = get_today_code() + return templates.TemplateResponse("today_qr.html", { + "request": request, + "code": code, + "date": date.today().strftime("%d %B %Y"), + "day": date.today().strftime("%A") + }) + +# API: Get today's code as JSON (for Android app if needed) +@app.get("/api/today-code") +def api_today_code(): + return {"code": get_today_code(), "date": date.today().isoformat()} + +# Raw QR Image +@app.get("/qr-image") +def get_qr_image(): + code = get_today_code() + qr = qrcode.QRCode(version=1, box_size=20, border=4) + qr.add_data(code) + qr.make(fit=True) + img = qr.make_image(fill_color="black", back_color="white") + + buffer = BytesIO() + img.save(buffer, format="PNG") + buffer.seek(0) + + return StreamingResponse(buffer, media_type="image/png") + +# Deprecated: Old attendance endpoint +@app.post("/attendance", response_model=AttendanceResponse) +def mark_attendance(payload: AttendanceCreate, db: Session = Depends(get_db)): + raise HTTPException(status_code=400, detail="Use /attendance2 endpoint") + +# MARK ATTENDANCE – Checks daily code + one per day per student +@app.post("/attendance2", response_model=AttendanceResponse) +def mark_attendance_daily(payload: AttendanceDaily, db: Session = Depends(get_db)): + if payload.qr_code != get_today_code(): + raise HTTPException(status_code=400, detail="QR code is expired or invalid") + + student = db.query(models.Student).filter(models.Student.student_id == payload.student_id).first() + if not student: + raise HTTPException(status_code=404, detail="Student not found") + + # Check if already attended today + today_start = datetime.combine(date.today(), datetime.min.time()) + already = db.query(models.Attendance).filter( + models.Attendance.student_id == payload.student_id, + models.Attendance.timestamp >= today_start + ).first() + + if already: + raise HTTPException(status_code=400, detail="Already marked attendance today") + + attendance = models.Attendance(student_id=payload.student_id) + db.add(attendance) + db.commit() + + return AttendanceResponse( + message="Attendance marked successfully!", + student_name=student.name, + timestamp=attendance.timestamp + ) + +# Optional: Add test students +@app.post("/add-test-students") +def add_test_students(db: Session = Depends(get_db)): + test_students = [ + {"student_id": "STD001", "name": "Alice Johnson"}, + {"student_id": "STD002", "name": "Bob Smith"}, + {"student_id": "STD003", "name": "Carol Lee"}, + {"student_id": "STD004", "name": "David Kim"}, + {"student_id": "STD005", "name": "Emma Wilson"}, + ] + for s in test_students: + if not db.query(models.Student).filter(models.Student.student_id == s["student_id"]).first(): + db.add(models.Student(**s)) + db.commit() + return {"message": "Test students added"} \ No newline at end of file diff --git a/src/models.py b/src/models.py index cea4772..28dbb08 100644 --- a/src/models.py +++ b/src/models.py @@ -48,4 +48,19 @@ class UserBadge(Base): earned_at = Column(DateTime, default=datetime.utcnow) user = relationship("User", back_populates="user_badges") - badge = relationship("Badge") \ No newline at end of file + badge = relationship("Badge") + + +class Student(Base): + __tablename__ = "students" + id = Column(Integer, primary_key=True, index=True) + student_id = Column(String, unique=True, index=True) + name = Column(String) + attendances = relationship("Attendance", back_populates="student") + +class Attendance(Base): + __tablename__ = "attendances" + id = Column(Integer, primary_key=True, index=True) + student_id = Column(String, ForeignKey("students.student_id")) + timestamp = Column(DateTime, default=datetime.utcnow) + student = relationship("Student", back_populates="attendances") \ No newline at end of file diff --git a/src/requirements.txt b/src/requirements.txt index baa281d..1ff2311 100644 --- a/src/requirements.txt +++ b/src/requirements.txt @@ -6,4 +6,6 @@ pydantic[email] bcrypt==4.1.3 passlib[bcrypt]==1.7.4 python-jose[cryptography] -python-multipart \ No newline at end of file +python-multipart +qrcode[pil]==7.4.2 +Jinja2==3.1.4 \ No newline at end of file diff --git a/src/schemas.py b/src/schemas.py index 334a27b..c52b00d 100644 --- a/src/schemas.py +++ b/src/schemas.py @@ -55,5 +55,21 @@ class BadgeResponse(BaseModel): icon_url: str earned_at: datetime + class Config: + from_attributes = True + + +class AttendanceCreate(BaseModel): + student_id: str + +class AttendanceDaily(BaseModel): + student_id: str # real student ID like STD001 + qr_code: str # the scanned daily code + +class AttendanceResponse(BaseModel): + message: str + student_name: str | None = None + timestamp: datetime + class Config: from_attributes = True \ No newline at end of file diff --git a/src/templates/home.html b/src/templates/home.html new file mode 100644 index 0000000..a626518 --- /dev/null +++ b/src/templates/home.html @@ -0,0 +1,24 @@ + + + +{{ date }}
+Code: {{ today_code }}
Show this on projector or TV
+Scan to mark attendance
+ + \ No newline at end of file From 40da90bde2ea3e130fbff8789cb0d05b8ea52d88 Mon Sep 17 00:00:00 2001 From: sachin <26170834+sachinsshetty@users.noreply.github.com> Date: Sun, 7 Dec 2025 19:14:05 +0100 Subject: [PATCH 14/14] complete-migration --- README.md | 13 ++- attendance/.dockerignore | 91 ------------------- attendance/Dockerfile | 27 ------ attendance/README.md | 38 -------- attendance/compose.yml | 13 --- attendance/database.py | 23 ----- attendance/main.py | 137 ----------------------------- attendance/models.py | 18 ---- attendance/requirements.txt | 6 -- attendance/schemas.py | 17 ---- attendance/templates/home.html | 24 ----- attendance/templates/today_qr.html | 19 ---- 12 files changed, 12 insertions(+), 414 deletions(-) delete mode 100644 attendance/.dockerignore delete mode 100644 attendance/Dockerfile delete mode 100644 attendance/README.md delete mode 100644 attendance/compose.yml delete mode 100644 attendance/database.py delete mode 100644 attendance/main.py delete mode 100644 attendance/models.py delete mode 100644 attendance/requirements.txt delete mode 100644 attendance/schemas.py delete mode 100644 attendance/templates/home.html delete mode 100644 attendance/templates/today_qr.html diff --git a/README.md b/README.md index 1c08ed1..6a30d0d 100644 --- a/README.md +++ b/README.md @@ -8,4 +8,15 @@ cd src uvicorn main:app --reload -docker build -t dwani/flex-fit-api -f Dockerfile . \ No newline at end of file +docker build -t dwani/flex-fit-api -f Dockerfile . + + +-- + + + +curl -X POST "http://192.168.1.50:8000/add-test-students" -H "Content-Type: application/json" + +curl -X POST "http://your-pc-ip:8000/add-student" \ + -H "Content-Type: application/json" \ + -d '{"student_id": "STD999", "name": "Sachin Kumar"}' \ No newline at end of file diff --git a/attendance/.dockerignore b/attendance/.dockerignore deleted file mode 100644 index 393663e..0000000 --- a/attendance/.dockerignore +++ /dev/null @@ -1,91 +0,0 @@ -samples -*.wav -*.log -venv -*.db -*.nemo -*.json -.github -.devcontainer -docs -misc -samples -node_modules -# Ignore all Python files except those explicitly copied -*.pyc -*.pyo -*.pyd -models/ -# Ignore all virtual environments -venv/ -env/ -.env/ -.venv/ -__pycache__/ - -# Ignore build artifacts -build/ -dist/ -*.egg-info/ - -# Ignore local version control files -.git/ -.gitignore - -# Ignore local environment files -.env - -# Ignore local log files -*.log - -# Ignore all node_modules -node_modules/ - -# Ignore all Docker-related files -Dockerfile -docker-compose.yml - -# Ignore all local development files -.vscode/ -.idea/ -.pytest_cache/ - -# Ignore all test files -*.test.* -*.spec.* -*_test.* -*_spec.* - -# Ignore all backup files -*.bak -*.swp -*.tmp -*.orig - -# Ignore all documentation files -*.md -*.txt -*.rst - -# Ignore all temporary files -*.tmp -*.temp -*.cache - -# Ignore all user-specific files -*.user -*.prefs -*.rc - -# Ignore all unnecessary directories and files -__pycache__ -__pypackages__ - - -!requirements.txt -!docker-requirements.txt -!client-requirements.txt -!server-requirements.txt - -#!model_requirements.txt -#!server_requirements.txt \ No newline at end of file diff --git a/attendance/Dockerfile b/attendance/Dockerfile deleted file mode 100644 index 8c47bca..0000000 --- a/attendance/Dockerfile +++ /dev/null @@ -1,27 +0,0 @@ -# Dockerfile -FROM python:3.12-slim - -# Set working directory -WORKDIR /app - -# Install system dependencies (needed for PIL/qrcode) -RUN apt-get update && apt-get install -y --no-install-recommends \ - gcc \ - libsqlite3-dev \ - && rm -rf /var/lib/apt/lists/* - -# Copy requirements first (better caching) -COPY requirements.txt . -RUN pip install --no-cache-dir -r requirements.txt - -# Copy project files -COPY . . - -# Create directory for templates if not exists -RUN mkdir -p templates - -# Expose port -EXPOSE 8000 - -# Command to run the app -CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"] \ No newline at end of file diff --git a/attendance/README.md b/attendance/README.md deleted file mode 100644 index a903316..0000000 --- a/attendance/README.md +++ /dev/null @@ -1,38 +0,0 @@ -QR code - attendance management - - -pip install -r requirements.txt - -uvicorn main:app --host 0.0.0.0 --port 8000 - - -http://your-ip:8000/api/today-code - - - -http://your-ip:8000/qr-image - -http://your-ip:8000/ - -http://your-ip:8000/today-qr - - -http://localhost:8000/add-test-students - - - -# 1. Clone / go into your project folder -cd attendance-server - -# 2. Build and start (first time) -docker compose up --build -d - -# 3. Subsequent runs (just start) -docker compose up -d - - -curl -X POST "http://192.168.1.50:8000/add-test-students" -H "Content-Type: application/json" - -curl -X POST "http://your-pc-ip:8000/add-student" \ - -H "Content-Type: application/json" \ - -d '{"student_id": "STD999", "name": "Sachin Kumar"}' \ No newline at end of file diff --git a/attendance/compose.yml b/attendance/compose.yml deleted file mode 100644 index 127d81f..0000000 --- a/attendance/compose.yml +++ /dev/null @@ -1,13 +0,0 @@ -# docker-compose.yml -services: - attendance-app: - build: . - container_name: qr-attendance-system - restart: unless-stopped - ports: - - "80:8000" - # ← NO VOLUMES AT ALL → fresh DB on every restart - -networks: - default: - driver: bridge \ No newline at end of file diff --git a/attendance/database.py b/attendance/database.py deleted file mode 100644 index f6f4cba..0000000 --- a/attendance/database.py +++ /dev/null @@ -1,23 +0,0 @@ -# database.py -import os -from sqlalchemy import create_engine -from sqlalchemy.ext.declarative import declarative_base -from sqlalchemy.orm import sessionmaker - -# Ensure the database file is in the writable /app directory -BASE_DIR = os.path.dirname(os.path.abspath(__file__)) -DB_PATH = os.path.join(BASE_DIR, "attendance.db") - -SQLALCHEMY_DATABASE_URL = f"sqlite:///{DB_PATH}" - -# This is the correct way — connect_args appears only once -engine = create_engine( - SQLALCHEMY_DATABASE_URL, - connect_args={"check_same_thread": False}, # Required for SQLite in FastAPI - # Optional: make file writable by all (only needed if permissions are strict) - # Remove this line if you use the volume method below - # connect_args={"check_same_thread": False, "timeout": 30}, -) - -SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine) -Base = declarative_base() \ No newline at end of file diff --git a/attendance/main.py b/attendance/main.py deleted file mode 100644 index 14d5e1b..0000000 --- a/attendance/main.py +++ /dev/null @@ -1,137 +0,0 @@ -from fastapi import FastAPI, Depends, HTTPException, Request -from fastapi.responses import HTMLResponse, StreamingResponse -from fastapi.middleware.cors import CORSMiddleware -from fastapi.templating import Jinja2Templates -from sqlalchemy.orm import Session -from datetime import datetime, date, timedelta -import qrcode -from io import BytesIO -import hashlib - -from fastapi import FastAPI, Depends, HTTPException, Request -import models -from database import engine, SessionLocal -from schemas import AttendanceDaily, AttendanceResponse, AttendanceCreate - - -templates = Jinja2Templates(directory="templates") -models.Base.metadata.create_all(bind=engine) - -app = FastAPI(title="Daily QR Attendance System") - -app.add_middleware( - CORSMiddleware, - allow_origins=["*"], - allow_credentials=True, - allow_methods=["*"], - allow_headers=["*"], -) - -def get_db(): - db = SessionLocal() - try: - yield db - finally: - db.close() - -# Generate deterministic daily code: "ATTEND-YYYY-MM-DD" -def get_today_code() -> str: - today = date.today().isoformat() - return f"ATTEND-{today}" - -# Optional: Add secret salt for extra security (uncomment if needed) -# def get_today_code() -> str: -# today = date.today().isoformat() -# secret = "my-school-secret-2025" -# return hashlib.sha256(f"{today}{secret}".encode()).hexdigest()[:12].upper() - -@app.get("/", response_class=HTMLResponse) -def home(request: Request): - today_code = get_today_code() - return templates.TemplateResponse("home.html", { - "request": request, - "today_code": today_code, - "date": date.today().strftime("%A, %B %d, %Y") - }) - -# Today's QR Code (big & beautiful for projector) -@app.get("/today-qr", response_class=HTMLResponse) -def today_qr_page(request: Request): - code = get_today_code() - return templates.TemplateResponse("today_qr.html", { - "request": request, - "code": code, - "date": date.today().strftime("%d %B %Y"), - "day": date.today().strftime("%A") - }) - -# API: Get today's code as JSON (for Android app if needed) -@app.get("/api/today-code") -def api_today_code(): - return {"code": get_today_code(), "date": date.today().isoformat()} - -# Raw QR Image -@app.get("/qr-image") -def get_qr_image(): - code = get_today_code() - qr = qrcode.QRCode(version=1, box_size=20, border=4) - qr.add_data(code) - qr.make(fit=True) - img = qr.make_image(fill_color="black", back_color="white") - - buffer = BytesIO() - img.save(buffer, format="PNG") - buffer.seek(0) - - return StreamingResponse(buffer, media_type="image/png") - -# Deprecated: Old attendance endpoint -@app.post("/attendance", response_model=AttendanceResponse) -def mark_attendance(payload: AttendanceCreate, db: Session = Depends(get_db)): - raise HTTPException(status_code=400, detail="Use /attendance2 endpoint") - -# MARK ATTENDANCE – Checks daily code + one per day per student -@app.post("/attendance2", response_model=AttendanceResponse) -def mark_attendance_daily(payload: AttendanceDaily, db: Session = Depends(get_db)): - if payload.qr_code != get_today_code(): - raise HTTPException(status_code=400, detail="QR code is expired or invalid") - - student = db.query(models.Student).filter(models.Student.student_id == payload.student_id).first() - if not student: - raise HTTPException(status_code=404, detail="Student not found") - - # Check if already attended today - today_start = datetime.combine(date.today(), datetime.min.time()) - already = db.query(models.Attendance).filter( - models.Attendance.student_id == payload.student_id, - models.Attendance.timestamp >= today_start - ).first() - - if already: - raise HTTPException(status_code=400, detail="Already marked attendance today") - - attendance = models.Attendance(student_id=payload.student_id) - db.add(attendance) - db.commit() - - return AttendanceResponse( - message="Attendance marked successfully!", - student_name=student.name, - timestamp=attendance.timestamp - ) - -# Optional: Add test students -@app.post("/add-test-students") -def add_test_students(db: Session = Depends(get_db)): - test_students = [ - {"student_id": "STD001", "name": "Alice Johnson"}, - {"student_id": "STD002", "name": "Bob Smith"}, - {"student_id": "STD003", "name": "Carol Lee"}, - {"student_id": "STD004", "name": "David Kim"}, - {"student_id": "STD005", "name": "Emma Wilson"}, - ] - for s in test_students: - if not db.query(models.Student).filter(models.Student.student_id == s["student_id"]).first(): - db.add(models.Student(**s)) - db.commit() - return {"message": "Test students added"} \ No newline at end of file diff --git a/attendance/models.py b/attendance/models.py deleted file mode 100644 index b95b219..0000000 --- a/attendance/models.py +++ /dev/null @@ -1,18 +0,0 @@ -from sqlalchemy import Column, Integer, String, DateTime, ForeignKey -from sqlalchemy.orm import relationship -from datetime import datetime -from database import Base # ← absolute import (fixed) - -class Student(Base): - __tablename__ = "students" - id = Column(Integer, primary_key=True, index=True) - student_id = Column(String, unique=True, index=True) - name = Column(String) - attendances = relationship("Attendance", back_populates="student") - -class Attendance(Base): - __tablename__ = "attendances" - id = Column(Integer, primary_key=True, index=True) - student_id = Column(String, ForeignKey("students.student_id")) - timestamp = Column(DateTime, default=datetime.utcnow) - student = relationship("Student", back_populates="attendances") \ No newline at end of file diff --git a/attendance/requirements.txt b/attendance/requirements.txt deleted file mode 100644 index 0bc3621..0000000 --- a/attendance/requirements.txt +++ /dev/null @@ -1,6 +0,0 @@ -fastapi==0.115.2 -uvicorn==0.30.6 -sqlalchemy==2.0.36 -pydantic==2.9.2 -qrcode[pil]==7.4.2 -Jinja2==3.1.4 \ No newline at end of file diff --git a/attendance/schemas.py b/attendance/schemas.py deleted file mode 100644 index bb29360..0000000 --- a/attendance/schemas.py +++ /dev/null @@ -1,17 +0,0 @@ -from pydantic import BaseModel -from datetime import datetime - -class AttendanceCreate(BaseModel): - student_id: str - -class AttendanceDaily(BaseModel): - student_id: str # real student ID like STD001 - qr_code: str # the scanned daily code - -class AttendanceResponse(BaseModel): - message: str - student_name: str | None = None - timestamp: datetime - - class Config: - from_attributes = True \ No newline at end of file diff --git a/attendance/templates/home.html b/attendance/templates/home.html deleted file mode 100644 index a626518..0000000 --- a/attendance/templates/home.html +++ /dev/null @@ -1,24 +0,0 @@ - - - -{{ date }}
-Code: {{ today_code }}
Show this on projector or TV
-Scan to mark attendance
- - \ No newline at end of file