Spaces:
Sleeping
Sleeping
Commit ·
a2d9a28
1
Parent(s): e58085c
Upload code backend lan dau
Browse files- .env +12 -0
- Dockerfile +23 -0
- README.md +66 -11
- app/__init__.py +1 -0
- app/__pycache__/__init__.cpython-312.pyc +0 -0
- app/__pycache__/config.cpython-312.pyc +0 -0
- app/__pycache__/database.cpython-312.pyc +0 -0
- app/__pycache__/main.cpython-312.pyc +0 -0
- app/config.py +39 -0
- app/database.py +83 -0
- app/main.py +68 -0
- app/models/__init__.py +2 -0
- app/models/__pycache__/__init__.cpython-312.pyc +0 -0
- app/models/__pycache__/schemas.cpython-312.pyc +0 -0
- app/models/schemas.py +152 -0
- app/routers/__init__.py +2 -0
- app/routers/__pycache__/__init__.cpython-312.pyc +0 -0
- app/routers/__pycache__/auth.cpython-312.pyc +0 -0
- app/routers/__pycache__/classes.cpython-312.pyc +0 -0
- app/routers/__pycache__/solver.cpython-312.pyc +0 -0
- app/routers/__pycache__/subjects.cpython-312.pyc +0 -0
- app/routers/__pycache__/teachers.cpython-312.pyc +0 -0
- app/routers/__pycache__/timetable.cpython-312.pyc +0 -0
- app/routers/__pycache__/units.cpython-312.pyc +0 -0
- app/routers/auth.py +152 -0
- app/routers/classes.py +97 -0
- app/routers/solver.py +38 -0
- app/routers/subjects.py +94 -0
- app/routers/teachers.py +152 -0
- app/routers/timetable.py +131 -0
- app/routers/units.py +74 -0
- app/services/__init__.py +2 -0
- app/services/__pycache__/__init__.cpython-312.pyc +0 -0
- app/services/__pycache__/solver_service.cpython-312.pyc +0 -0
- app/services/solver_service.py +124 -0
- requirements.txt +15 -0
.env
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Supabase Configuration
|
| 2 |
+
SUPABASE_URL=https://gtwatjmkyfweohhvpute.supabase.co
|
| 3 |
+
SUPABASE_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Imd0d2F0am1reWZ3ZW9oaHZwdXRlIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NjU4ODEyNzIsImV4cCI6MjA4MTQ1NzI3Mn0.vZjoP5dcnYJJGS23rFgg9KMZwpevJDf2Fk76J35RkZM
|
| 4 |
+
SUPABASE_SERVICE_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Imd0d2F0am1reWZ3ZW9oaHZwdXRlIiwicm9sZSI6InNlcnZpY2Vfcm9sZSIsImlhdCI6MTc2NTg4MTI3MiwiZXhwIjoyMDgxNDU3MjcyfQ.7_aMq3PHNJr_HeiWV6n8Lom2hEcOBBI2zsa7h_nzfRo
|
| 5 |
+
|
| 6 |
+
# JWT Configuration
|
| 7 |
+
SECRET_KEY=hoangtanthiendeptrai1209@
|
| 8 |
+
ALGORITHM=HS256
|
| 9 |
+
ACCESS_TOKEN_EXPIRE_MINUTES=30
|
| 10 |
+
|
| 11 |
+
# CORS Configuration
|
| 12 |
+
CORS_ORIGINS=["http://localhost:3000"]
|
Dockerfile
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
FROM python:3.11-slim
|
| 2 |
+
|
| 3 |
+
WORKDIR /app
|
| 4 |
+
|
| 5 |
+
# Install system dependencies
|
| 6 |
+
RUN apt-get update && apt-get install -y \
|
| 7 |
+
gcc \
|
| 8 |
+
g++ \
|
| 9 |
+
&& rm -rf /var/lib/apt/lists/*
|
| 10 |
+
|
| 11 |
+
# Copy requirements and install Python dependencies
|
| 12 |
+
COPY requirements.txt .
|
| 13 |
+
RUN pip install --no-cache-dir -r requirements.txt
|
| 14 |
+
|
| 15 |
+
# Copy application code
|
| 16 |
+
COPY . .
|
| 17 |
+
|
| 18 |
+
# Expose port (Hugging Face Spaces uses port 7860)
|
| 19 |
+
EXPOSE 7860
|
| 20 |
+
|
| 21 |
+
# Run the application
|
| 22 |
+
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "7860"]
|
| 23 |
+
|
README.md
CHANGED
|
@@ -1,11 +1,66 @@
|
|
| 1 |
-
-
|
| 2 |
-
|
| 3 |
-
|
| 4 |
-
|
| 5 |
-
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# TKB Web Backend - Hugging Face Spaces
|
| 2 |
+
|
| 3 |
+
Backend API cho hệ thống quản lý thời khóa biểu, được deploy trên Hugging Face Spaces.
|
| 4 |
+
|
| 5 |
+
## Cấu trúc
|
| 6 |
+
|
| 7 |
+
```
|
| 8 |
+
web-backend/
|
| 9 |
+
├── app/
|
| 10 |
+
│ ├── __init__.py
|
| 11 |
+
│ ├── main.py # FastAPI application
|
| 12 |
+
│ ├── config.py # Configuration
|
| 13 |
+
│ ├── database.py # Supabase connection
|
| 14 |
+
│ ├── models/ # Pydantic models
|
| 15 |
+
│ ├── routers/ # API routes
|
| 16 |
+
│ │ ├── auth.py
|
| 17 |
+
│ │ ├── units.py
|
| 18 |
+
│ │ ├── teachers.py
|
| 19 |
+
│ │ ├── subjects.py
|
| 20 |
+
│ │ ├── classes.py
|
| 21 |
+
│ │ ├── timetable.py
|
| 22 |
+
│ │ └── solver.py
|
| 23 |
+
│ ├── services/ # Business logic
|
| 24 |
+
│ │ ├── solver_service.py
|
| 25 |
+
│ │ ├── timetable_service.py
|
| 26 |
+
│ │ └── ai_service.py
|
| 27 |
+
│ └── utils/ # Utilities
|
| 28 |
+
├── requirements.txt
|
| 29 |
+
├── Dockerfile
|
| 30 |
+
└── README.md
|
| 31 |
+
```
|
| 32 |
+
|
| 33 |
+
## Setup
|
| 34 |
+
|
| 35 |
+
1. Cài đặt dependencies:
|
| 36 |
+
```bash
|
| 37 |
+
pip install -r requirements.txt
|
| 38 |
+
```
|
| 39 |
+
|
| 40 |
+
2. Cấu hình environment variables:
|
| 41 |
+
```bash
|
| 42 |
+
export SUPABASE_URL=your_supabase_url
|
| 43 |
+
export SUPABASE_KEY=your_supabase_key
|
| 44 |
+
export GEMINI_API_KEY=your_gemini_key
|
| 45 |
+
```
|
| 46 |
+
|
| 47 |
+
3. Chạy server:
|
| 48 |
+
```bash
|
| 49 |
+
uvicorn app.main:app --host 0.0.0.0 --port 7860
|
| 50 |
+
```
|
| 51 |
+
|
| 52 |
+
## Deploy lên Hugging Face Spaces
|
| 53 |
+
|
| 54 |
+
1. Tạo Space mới trên Hugging Face
|
| 55 |
+
2. Chọn SDK: Docker
|
| 56 |
+
3. Push code lên repository
|
| 57 |
+
4. Hugging Face sẽ tự động build và deploy
|
| 58 |
+
|
| 59 |
+
## API Endpoints
|
| 60 |
+
|
| 61 |
+
- `POST /api/auth/login` - Đăng nhập
|
| 62 |
+
- `GET /api/units` - Lấy danh sách đơn vị
|
| 63 |
+
- `GET /api/teachers` - Lấy danh sách giáo viên
|
| 64 |
+
- `POST /api/timetable/solve` - Xếp thời khóa biểu
|
| 65 |
+
- `GET /api/timetable/{session_id}` - Lấy thời khóa biểu
|
| 66 |
+
|
app/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
# TKB Web Backend Application
|
app/__pycache__/__init__.cpython-312.pyc
ADDED
|
Binary file (143 Bytes). View file
|
|
|
app/__pycache__/config.cpython-312.pyc
ADDED
|
Binary file (1.38 kB). View file
|
|
|
app/__pycache__/database.cpython-312.pyc
ADDED
|
Binary file (3.98 kB). View file
|
|
|
app/__pycache__/main.cpython-312.pyc
ADDED
|
Binary file (3.19 kB). View file
|
|
|
app/config.py
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Configuration settings for the application
|
| 3 |
+
"""
|
| 4 |
+
from pydantic_settings import BaseSettings
|
| 5 |
+
from typing import Optional
|
| 6 |
+
|
| 7 |
+
|
| 8 |
+
class Settings(BaseSettings):
|
| 9 |
+
"""Application settings loaded from environment variables"""
|
| 10 |
+
|
| 11 |
+
# Supabase Configuration
|
| 12 |
+
supabase_url: str
|
| 13 |
+
supabase_key: str
|
| 14 |
+
supabase_service_key: Optional[str] = None
|
| 15 |
+
|
| 16 |
+
# Gemini AI Configuration
|
| 17 |
+
gemini_api_key: Optional[str] = None
|
| 18 |
+
|
| 19 |
+
# JWT Configuration
|
| 20 |
+
secret_key: str = "your-secret-key-change-in-production"
|
| 21 |
+
algorithm: str = "HS256"
|
| 22 |
+
access_token_expire_minutes: int = 30
|
| 23 |
+
|
| 24 |
+
# CORS Configuration
|
| 25 |
+
cors_origins: list[str] = [
|
| 26 |
+
"http://localhost:3000",
|
| 27 |
+
"https://your-vercel-app.vercel.app"
|
| 28 |
+
]
|
| 29 |
+
|
| 30 |
+
# Hugging Face Spaces
|
| 31 |
+
space_id: Optional[str] = None
|
| 32 |
+
|
| 33 |
+
class Config:
|
| 34 |
+
env_file = ".env"
|
| 35 |
+
case_sensitive = False
|
| 36 |
+
|
| 37 |
+
|
| 38 |
+
settings = Settings()
|
| 39 |
+
|
app/database.py
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Supabase database connection and utilities
|
| 3 |
+
"""
|
| 4 |
+
from supabase import create_client, Client
|
| 5 |
+
from app.config import settings
|
| 6 |
+
from typing import Optional
|
| 7 |
+
import logging
|
| 8 |
+
|
| 9 |
+
logger = logging.getLogger(__name__)
|
| 10 |
+
|
| 11 |
+
class DatabaseManager:
|
| 12 |
+
"""Manages Supabase database connections"""
|
| 13 |
+
|
| 14 |
+
def __init__(self):
|
| 15 |
+
self.supabase: Optional[Client] = None
|
| 16 |
+
self._initialize_client()
|
| 17 |
+
|
| 18 |
+
def _initialize_client(self):
|
| 19 |
+
"""Initialize Supabase client"""
|
| 20 |
+
try:
|
| 21 |
+
self.supabase = create_client(
|
| 22 |
+
settings.supabase_url,
|
| 23 |
+
settings.supabase_key
|
| 24 |
+
)
|
| 25 |
+
logger.info("Supabase client initialized successfully")
|
| 26 |
+
except Exception as e:
|
| 27 |
+
logger.error(f"Failed to initialize Supabase client: {e}")
|
| 28 |
+
raise
|
| 29 |
+
|
| 30 |
+
def get_client(self) -> Client:
|
| 31 |
+
"""Get Supabase client instance"""
|
| 32 |
+
if self.supabase is None:
|
| 33 |
+
self._initialize_client()
|
| 34 |
+
return self.supabase
|
| 35 |
+
|
| 36 |
+
# Helper methods for common operations
|
| 37 |
+
def execute_query(self, table: str, operation: str = "select", **kwargs):
|
| 38 |
+
"""Execute a query on a table"""
|
| 39 |
+
client = self.get_client()
|
| 40 |
+
|
| 41 |
+
try:
|
| 42 |
+
if operation == "select":
|
| 43 |
+
query = client.table(table).select("*")
|
| 44 |
+
if "filters" in kwargs:
|
| 45 |
+
for filter_item in kwargs["filters"]:
|
| 46 |
+
query = query.eq(filter_item["column"], filter_item["value"])
|
| 47 |
+
if "limit" in kwargs:
|
| 48 |
+
query = query.limit(kwargs["limit"])
|
| 49 |
+
if "order" in kwargs:
|
| 50 |
+
order_col = kwargs["order"]["column"]
|
| 51 |
+
order_desc = kwargs["order"].get("desc", False)
|
| 52 |
+
query = query.order(order_col, desc=order_desc)
|
| 53 |
+
result = query.execute()
|
| 54 |
+
return result
|
| 55 |
+
|
| 56 |
+
elif operation == "insert":
|
| 57 |
+
data = kwargs.get("data", {})
|
| 58 |
+
result = client.table(table).insert(data).execute()
|
| 59 |
+
return result
|
| 60 |
+
|
| 61 |
+
elif operation == "update":
|
| 62 |
+
query = client.table(table).update(kwargs.get("data", {}))
|
| 63 |
+
if "filters" in kwargs:
|
| 64 |
+
for filter_item in kwargs["filters"]:
|
| 65 |
+
query = query.eq(filter_item["column"], filter_item["value"])
|
| 66 |
+
result = query.execute()
|
| 67 |
+
return result
|
| 68 |
+
|
| 69 |
+
elif operation == "delete":
|
| 70 |
+
query = client.table(table).delete()
|
| 71 |
+
if "filters" in kwargs:
|
| 72 |
+
for filter_item in kwargs["filters"]:
|
| 73 |
+
query = query.eq(filter_item["column"], filter_item["value"])
|
| 74 |
+
result = query.execute()
|
| 75 |
+
return result
|
| 76 |
+
except Exception as e:
|
| 77 |
+
logger.error(f"Error executing query on {table}: {e}")
|
| 78 |
+
raise
|
| 79 |
+
|
| 80 |
+
|
| 81 |
+
# Global database manager instance
|
| 82 |
+
db_manager = DatabaseManager()
|
| 83 |
+
|
app/main.py
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
FastAPI main application
|
| 3 |
+
"""
|
| 4 |
+
from fastapi import FastAPI, Request
|
| 5 |
+
from fastapi.middleware.cors import CORSMiddleware
|
| 6 |
+
from fastapi.responses import JSONResponse
|
| 7 |
+
from app.config import settings
|
| 8 |
+
from app.routers import auth, units, teachers, subjects, classes, timetable, solver
|
| 9 |
+
import logging
|
| 10 |
+
|
| 11 |
+
logging.basicConfig(level=logging.INFO)
|
| 12 |
+
logger = logging.getLogger(__name__)
|
| 13 |
+
|
| 14 |
+
app = FastAPI(
|
| 15 |
+
title="TKB Web API",
|
| 16 |
+
description="API cho hệ thống quản lý thời khóa biểu",
|
| 17 |
+
version="2.0.0"
|
| 18 |
+
)
|
| 19 |
+
|
| 20 |
+
# CORS Middleware
|
| 21 |
+
app.add_middleware(
|
| 22 |
+
CORSMiddleware,
|
| 23 |
+
allow_origins=settings.cors_origins,
|
| 24 |
+
allow_credentials=True,
|
| 25 |
+
allow_methods=["*"],
|
| 26 |
+
allow_headers=["*"],
|
| 27 |
+
)
|
| 28 |
+
|
| 29 |
+
# Include routers
|
| 30 |
+
app.include_router(auth.router, prefix="/api/auth", tags=["Authentication"])
|
| 31 |
+
app.include_router(units.router, prefix="/api/units", tags=["Units"])
|
| 32 |
+
app.include_router(teachers.router, prefix="/api/teachers", tags=["Teachers"])
|
| 33 |
+
app.include_router(subjects.router, prefix="/api/subjects", tags=["Subjects"])
|
| 34 |
+
app.include_router(classes.router, prefix="/api/classes", tags=["Classes"])
|
| 35 |
+
app.include_router(timetable.router, prefix="/api/timetable", tags=["Timetable"])
|
| 36 |
+
app.include_router(solver.router, prefix="/api/solver", tags=["Solver"])
|
| 37 |
+
|
| 38 |
+
|
| 39 |
+
@app.get("/")
|
| 40 |
+
async def root():
|
| 41 |
+
"""Root endpoint"""
|
| 42 |
+
return {
|
| 43 |
+
"message": "TKB Web API",
|
| 44 |
+
"version": "2.0.0",
|
| 45 |
+
"status": "running"
|
| 46 |
+
}
|
| 47 |
+
|
| 48 |
+
|
| 49 |
+
@app.get("/health")
|
| 50 |
+
async def health_check():
|
| 51 |
+
"""Health check endpoint"""
|
| 52 |
+
return {"status": "healthy"}
|
| 53 |
+
|
| 54 |
+
|
| 55 |
+
@app.exception_handler(Exception)
|
| 56 |
+
async def global_exception_handler(request: Request, exc: Exception):
|
| 57 |
+
"""Global exception handler"""
|
| 58 |
+
logger.error(f"Unhandled exception: {exc}", exc_info=True)
|
| 59 |
+
return JSONResponse(
|
| 60 |
+
status_code=500,
|
| 61 |
+
content={"detail": "Internal server error"}
|
| 62 |
+
)
|
| 63 |
+
|
| 64 |
+
|
| 65 |
+
if __name__ == "__main__":
|
| 66 |
+
import uvicorn
|
| 67 |
+
uvicorn.run(app, host="0.0.0.0", port=7860)
|
| 68 |
+
|
app/models/__init__.py
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Models package
|
| 2 |
+
|
app/models/__pycache__/__init__.cpython-312.pyc
ADDED
|
Binary file (150 Bytes). View file
|
|
|
app/models/__pycache__/schemas.cpython-312.pyc
ADDED
|
Binary file (6.86 kB). View file
|
|
|
app/models/schemas.py
ADDED
|
@@ -0,0 +1,152 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Pydantic models for request/response schemas
|
| 3 |
+
"""
|
| 4 |
+
from pydantic import BaseModel, EmailStr
|
| 5 |
+
from typing import Optional, List, Dict, Any
|
| 6 |
+
from datetime import datetime
|
| 7 |
+
|
| 8 |
+
|
| 9 |
+
# Authentication Models
|
| 10 |
+
class LoginRequest(BaseModel):
|
| 11 |
+
username: str
|
| 12 |
+
password: str
|
| 13 |
+
|
| 14 |
+
|
| 15 |
+
class TokenResponse(BaseModel):
|
| 16 |
+
access_token: str
|
| 17 |
+
token_type: str = "bearer"
|
| 18 |
+
|
| 19 |
+
|
| 20 |
+
# Unit Models
|
| 21 |
+
class UnitCreate(BaseModel):
|
| 22 |
+
name: str
|
| 23 |
+
|
| 24 |
+
|
| 25 |
+
class UnitResponse(BaseModel):
|
| 26 |
+
id: int
|
| 27 |
+
name: str
|
| 28 |
+
|
| 29 |
+
class Config:
|
| 30 |
+
from_attributes = True
|
| 31 |
+
|
| 32 |
+
|
| 33 |
+
# Teacher Models
|
| 34 |
+
class TeacherCreate(BaseModel):
|
| 35 |
+
id: str
|
| 36 |
+
name: str
|
| 37 |
+
subjects: List[str] = []
|
| 38 |
+
secondary_subjects: List[str] = []
|
| 39 |
+
busy_slots: List[List[int]] = [] # [[day, period], ...]
|
| 40 |
+
is_locked: bool = False
|
| 41 |
+
work_days_preference: int = 0
|
| 42 |
+
role: Optional[str] = None
|
| 43 |
+
target_periods: Optional[int] = None
|
| 44 |
+
email: Optional[EmailStr] = None
|
| 45 |
+
professional_group_name: Optional[str] = None
|
| 46 |
+
|
| 47 |
+
|
| 48 |
+
class TeacherResponse(BaseModel):
|
| 49 |
+
unit_id: int
|
| 50 |
+
school_year: str
|
| 51 |
+
id: str
|
| 52 |
+
name: str
|
| 53 |
+
subjects: List[str]
|
| 54 |
+
secondary_subjects: List[str]
|
| 55 |
+
busy_slots: List[List[int]]
|
| 56 |
+
is_locked: bool
|
| 57 |
+
work_days_preference: int
|
| 58 |
+
role: Optional[str]
|
| 59 |
+
target_periods: Optional[int]
|
| 60 |
+
email: Optional[str]
|
| 61 |
+
professional_group_name: Optional[str]
|
| 62 |
+
|
| 63 |
+
class Config:
|
| 64 |
+
from_attributes = True
|
| 65 |
+
|
| 66 |
+
|
| 67 |
+
# Subject Models
|
| 68 |
+
class SubjectCreate(BaseModel):
|
| 69 |
+
name: str
|
| 70 |
+
periods_per_week: Dict[str, int] = {} # {grade: periods}
|
| 71 |
+
is_double_period_only: bool = False
|
| 72 |
+
|
| 73 |
+
|
| 74 |
+
class SubjectResponse(BaseModel):
|
| 75 |
+
unit_id: int
|
| 76 |
+
school_year: str
|
| 77 |
+
name: str
|
| 78 |
+
periods_per_week: Dict[str, int]
|
| 79 |
+
is_double_period_only: bool
|
| 80 |
+
|
| 81 |
+
class Config:
|
| 82 |
+
from_attributes = True
|
| 83 |
+
|
| 84 |
+
|
| 85 |
+
# Class Models
|
| 86 |
+
class ClassCreate(BaseModel):
|
| 87 |
+
name: str
|
| 88 |
+
grade: Optional[int] = None
|
| 89 |
+
session: Optional[str] = None
|
| 90 |
+
homeroom_teacher_id: Optional[str] = None
|
| 91 |
+
fixed_off_slots: List[List[int]] = []
|
| 92 |
+
is_locked: bool = False
|
| 93 |
+
|
| 94 |
+
|
| 95 |
+
class ClassResponse(BaseModel):
|
| 96 |
+
unit_id: int
|
| 97 |
+
school_year: str
|
| 98 |
+
name: str
|
| 99 |
+
grade: Optional[int]
|
| 100 |
+
session: Optional[str]
|
| 101 |
+
homeroom_teacher_id: Optional[str]
|
| 102 |
+
fixed_off_slots: List[List[int]]
|
| 103 |
+
is_locked: bool
|
| 104 |
+
|
| 105 |
+
class Config:
|
| 106 |
+
from_attributes = True
|
| 107 |
+
|
| 108 |
+
|
| 109 |
+
# Timetable Models
|
| 110 |
+
class TimetableSessionCreate(BaseModel):
|
| 111 |
+
session_name: str
|
| 112 |
+
effective_date: Optional[str] = None
|
| 113 |
+
|
| 114 |
+
|
| 115 |
+
class TimetableSessionResponse(BaseModel):
|
| 116 |
+
id: int
|
| 117 |
+
unit_id: int
|
| 118 |
+
school_year: str
|
| 119 |
+
session_name: str
|
| 120 |
+
is_locked: bool
|
| 121 |
+
created_at: str
|
| 122 |
+
effective_date: Optional[str]
|
| 123 |
+
|
| 124 |
+
class Config:
|
| 125 |
+
from_attributes = True
|
| 126 |
+
|
| 127 |
+
|
| 128 |
+
class TimetableData(BaseModel):
|
| 129 |
+
timetable: Dict[str, Any] # Flexible structure for timetable data
|
| 130 |
+
unplaced_lessons: List[Dict[str, Any]] = []
|
| 131 |
+
|
| 132 |
+
|
| 133 |
+
# Solver Models
|
| 134 |
+
class SolverRequest(BaseModel):
|
| 135 |
+
unit_id: int
|
| 136 |
+
school_year: str
|
| 137 |
+
mode: str = "ortools_balanced" # ortools_balanced, ortools_days_off
|
| 138 |
+
num_workers: int = 8
|
| 139 |
+
time_limit: float = 300.0
|
| 140 |
+
weights: Optional[Dict[str, float]] = None
|
| 141 |
+
priority_teacher_ids: Optional[List[str]] = None
|
| 142 |
+
phase: Optional[str] = None # phase1, phase2, phase3
|
| 143 |
+
|
| 144 |
+
|
| 145 |
+
class SolverResponse(BaseModel):
|
| 146 |
+
success: bool
|
| 147 |
+
timetable: Optional[Dict[str, Any]] = None
|
| 148 |
+
objective_value: Optional[float] = None
|
| 149 |
+
unplaced_lessons: List[Dict[str, Any]] = []
|
| 150 |
+
message: str
|
| 151 |
+
execution_time: Optional[float] = None
|
| 152 |
+
|
app/routers/__init__.py
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Routers package
|
| 2 |
+
|
app/routers/__pycache__/__init__.cpython-312.pyc
ADDED
|
Binary file (151 Bytes). View file
|
|
|
app/routers/__pycache__/auth.cpython-312.pyc
ADDED
|
Binary file (7.42 kB). View file
|
|
|
app/routers/__pycache__/classes.cpython-312.pyc
ADDED
|
Binary file (4.08 kB). View file
|
|
|
app/routers/__pycache__/solver.cpython-312.pyc
ADDED
|
Binary file (1.93 kB). View file
|
|
|
app/routers/__pycache__/subjects.cpython-312.pyc
ADDED
|
Binary file (3.98 kB). View file
|
|
|
app/routers/__pycache__/teachers.cpython-312.pyc
ADDED
|
Binary file (6.54 kB). View file
|
|
|
app/routers/__pycache__/timetable.cpython-312.pyc
ADDED
|
Binary file (5.95 kB). View file
|
|
|
app/routers/__pycache__/units.cpython-312.pyc
ADDED
|
Binary file (3.93 kB). View file
|
|
|
app/routers/auth.py
ADDED
|
@@ -0,0 +1,152 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Authentication routes - Debug Version (Using Logger)
|
| 3 |
+
"""
|
| 4 |
+
from fastapi import APIRouter, Depends, HTTPException, status
|
| 5 |
+
from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm
|
| 6 |
+
from datetime import datetime, timedelta
|
| 7 |
+
from typing import Optional
|
| 8 |
+
from jose import JWTError, jwt
|
| 9 |
+
from app.config import settings
|
| 10 |
+
from app.models.schemas import LoginRequest, TokenResponse
|
| 11 |
+
from app.database import db_manager
|
| 12 |
+
import hashlib
|
| 13 |
+
import logging
|
| 14 |
+
|
| 15 |
+
# Cấu hình logger để đảm bảo hiện ra terminal
|
| 16 |
+
logging.basicConfig(level=logging.INFO)
|
| 17 |
+
logger = logging.getLogger("uvicorn.error") # Sử dụng logger của uvicorn để chắc chắn hiện
|
| 18 |
+
|
| 19 |
+
router = APIRouter()
|
| 20 |
+
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/api/auth/login")
|
| 21 |
+
|
| 22 |
+
|
| 23 |
+
def hash_password(password: str) -> str:
|
| 24 |
+
"""Hash password using SHA-256 (compatible with original code)"""
|
| 25 |
+
return hashlib.sha256(password.encode('utf-8')).hexdigest()
|
| 26 |
+
|
| 27 |
+
|
| 28 |
+
def verify_password(stored_hash: str, provided_password: str) -> bool:
|
| 29 |
+
"""Verify password against stored hash (SHA-256)"""
|
| 30 |
+
calculated_hash = hash_password(provided_password)
|
| 31 |
+
# Log hash để so sánh
|
| 32 |
+
logger.warning(f"CHECK PASS: Hash trong DB = {stored_hash}")
|
| 33 |
+
logger.warning(f"CHECK PASS: Hash nhập vào = {calculated_hash}")
|
| 34 |
+
return stored_hash == calculated_hash
|
| 35 |
+
|
| 36 |
+
|
| 37 |
+
def create_access_token(data: dict, expires_delta: Optional[timedelta] = None):
|
| 38 |
+
"""Create JWT access token"""
|
| 39 |
+
to_encode = data.copy()
|
| 40 |
+
if expires_delta:
|
| 41 |
+
expire = datetime.utcnow() + expires_delta
|
| 42 |
+
else:
|
| 43 |
+
expire = datetime.utcnow() + timedelta(minutes=settings.access_token_expire_minutes)
|
| 44 |
+
to_encode.update({"exp": expire})
|
| 45 |
+
encoded_jwt = jwt.encode(to_encode, settings.secret_key, algorithm=settings.algorithm)
|
| 46 |
+
return encoded_jwt
|
| 47 |
+
|
| 48 |
+
|
| 49 |
+
async def get_current_user(token: str = Depends(oauth2_scheme)):
|
| 50 |
+
"""Get current authenticated user"""
|
| 51 |
+
credentials_exception = HTTPException(
|
| 52 |
+
status_code=status.HTTP_401_UNAUTHORIZED,
|
| 53 |
+
detail="Could not validate credentials",
|
| 54 |
+
headers={"WWW-Authenticate": "Bearer"},
|
| 55 |
+
)
|
| 56 |
+
try:
|
| 57 |
+
payload = jwt.decode(token, settings.secret_key, algorithms=[settings.algorithm])
|
| 58 |
+
username: str = payload.get("sub")
|
| 59 |
+
if username is None:
|
| 60 |
+
raise credentials_exception
|
| 61 |
+
except JWTError:
|
| 62 |
+
raise credentials_exception
|
| 63 |
+
|
| 64 |
+
# Verify user exists in database
|
| 65 |
+
try:
|
| 66 |
+
result = db_manager.execute_query(
|
| 67 |
+
"users",
|
| 68 |
+
"select",
|
| 69 |
+
filters=[{"column": "username", "value": username}]
|
| 70 |
+
)
|
| 71 |
+
if not result.data:
|
| 72 |
+
raise credentials_exception
|
| 73 |
+
return result.data[0]
|
| 74 |
+
except Exception as e:
|
| 75 |
+
logger.error(f"Error fetching user: {e}")
|
| 76 |
+
raise credentials_exception
|
| 77 |
+
|
| 78 |
+
|
| 79 |
+
@router.post("/login", response_model=TokenResponse)
|
| 80 |
+
async def login(form_data: OAuth2PasswordRequestForm = Depends()):
|
| 81 |
+
"""Login endpoint with LOGGER WARNING"""
|
| 82 |
+
try:
|
| 83 |
+
logger.warning("\n" + "="*50)
|
| 84 |
+
logger.warning(f"DEBUG: Bắt đầu đăng nhập cho user: '{form_data.username}'")
|
| 85 |
+
|
| 86 |
+
# 1. Get user from database
|
| 87 |
+
result = db_manager.execute_query(
|
| 88 |
+
"users",
|
| 89 |
+
"select",
|
| 90 |
+
filters=[{"column": "username", "value": form_data.username}]
|
| 91 |
+
)
|
| 92 |
+
|
| 93 |
+
logger.warning(f"DEBUG: Kết quả tìm trong DB (Raw Data): {result.data}")
|
| 94 |
+
|
| 95 |
+
if not result.data:
|
| 96 |
+
logger.warning("DEBUG: LỖI -> Không tìm thấy user hoặc bị RLS chặn. Danh sách trả về rỗng []")
|
| 97 |
+
logger.warning("="*50 + "\n")
|
| 98 |
+
raise HTTPException(
|
| 99 |
+
status_code=status.HTTP_401_UNAUTHORIZED,
|
| 100 |
+
detail="Incorrect username or password"
|
| 101 |
+
)
|
| 102 |
+
|
| 103 |
+
user = result.data[0]
|
| 104 |
+
|
| 105 |
+
# 2. Verify password
|
| 106 |
+
stored_hash = user.get("password_hash", "")
|
| 107 |
+
# Password verify sẽ tự log hash bên trong hàm verify_password
|
| 108 |
+
|
| 109 |
+
is_valid = False
|
| 110 |
+
if not stored_hash:
|
| 111 |
+
logger.warning("DEBUG: LỖI -> User trong DB không có cột password_hash")
|
| 112 |
+
else:
|
| 113 |
+
is_valid = verify_password(stored_hash, form_data.password)
|
| 114 |
+
|
| 115 |
+
if not is_valid:
|
| 116 |
+
logger.warning("DEBUG: KẾT QUẢ -> Mật khẩu KHÔNG khớp!")
|
| 117 |
+
logger.warning("="*50 + "\n")
|
| 118 |
+
raise HTTPException(
|
| 119 |
+
status_code=status.HTTP_401_UNAUTHORIZED,
|
| 120 |
+
detail="Incorrect username or password"
|
| 121 |
+
)
|
| 122 |
+
|
| 123 |
+
logger.warning("DEBUG: KẾT QUẢ -> Đăng nhập THÀNH CÔNG!")
|
| 124 |
+
logger.warning("="*50 + "\n")
|
| 125 |
+
|
| 126 |
+
# 3. Create access token
|
| 127 |
+
access_token_expires = timedelta(minutes=settings.access_token_expire_minutes)
|
| 128 |
+
access_token = create_access_token(
|
| 129 |
+
data={"sub": user["username"], "role": user.get("role", "user")},
|
| 130 |
+
expires_delta=access_token_expires
|
| 131 |
+
)
|
| 132 |
+
|
| 133 |
+
return {"access_token": access_token, "token_type": "bearer"}
|
| 134 |
+
|
| 135 |
+
except HTTPException:
|
| 136 |
+
raise
|
| 137 |
+
except Exception as e:
|
| 138 |
+
logger.error(f"Login error: {e}")
|
| 139 |
+
logger.warning(f"DEBUG: LỖI HỆ THỐNG -> {str(e)}")
|
| 140 |
+
raise HTTPException(
|
| 141 |
+
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
| 142 |
+
detail="Internal server error"
|
| 143 |
+
)
|
| 144 |
+
|
| 145 |
+
|
| 146 |
+
@router.get("/me")
|
| 147 |
+
async def get_current_user_info(current_user: dict = Depends(get_current_user)):
|
| 148 |
+
"""Get current user information"""
|
| 149 |
+
return {
|
| 150 |
+
"username": current_user.get("username"),
|
| 151 |
+
"role": current_user.get("role", "user")
|
| 152 |
+
}
|
app/routers/classes.py
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Classes management routes
|
| 3 |
+
"""
|
| 4 |
+
from fastapi import APIRouter, Depends, HTTPException, Query
|
| 5 |
+
from app.models.schemas import ClassCreate, ClassResponse
|
| 6 |
+
from app.routers.auth import get_current_user
|
| 7 |
+
from app.database import db_manager
|
| 8 |
+
import json
|
| 9 |
+
import logging
|
| 10 |
+
|
| 11 |
+
logger = logging.getLogger(__name__)
|
| 12 |
+
|
| 13 |
+
router = APIRouter()
|
| 14 |
+
|
| 15 |
+
|
| 16 |
+
@router.get("", response_model=list[ClassResponse])
|
| 17 |
+
async def get_classes(
|
| 18 |
+
unit_id: int = Query(...),
|
| 19 |
+
school_year: str = Query(...),
|
| 20 |
+
current_user: dict = Depends(get_current_user)
|
| 21 |
+
):
|
| 22 |
+
"""Get all classes for a unit and school year"""
|
| 23 |
+
try:
|
| 24 |
+
result = db_manager.execute_query(
|
| 25 |
+
"classes",
|
| 26 |
+
"select",
|
| 27 |
+
filters=[
|
| 28 |
+
{"column": "unit_id", "value": unit_id},
|
| 29 |
+
{"column": "school_year", "value": school_year}
|
| 30 |
+
]
|
| 31 |
+
)
|
| 32 |
+
# Parse JSON fields
|
| 33 |
+
classes = []
|
| 34 |
+
for cls in result.data:
|
| 35 |
+
cls["fixed_off_slots"] = json.loads(cls.get("fixed_off_slots") or "[]")
|
| 36 |
+
classes.append(cls)
|
| 37 |
+
return classes
|
| 38 |
+
except Exception as e:
|
| 39 |
+
logger.error(f"Error fetching classes: {e}")
|
| 40 |
+
raise HTTPException(status_code=500, detail="Error fetching classes")
|
| 41 |
+
|
| 42 |
+
|
| 43 |
+
@router.post("", response_model=ClassResponse)
|
| 44 |
+
async def create_class(
|
| 45 |
+
unit_id: int,
|
| 46 |
+
school_year: str,
|
| 47 |
+
class_data: ClassCreate,
|
| 48 |
+
current_user: dict = Depends(get_current_user)
|
| 49 |
+
):
|
| 50 |
+
"""Create a new class"""
|
| 51 |
+
try:
|
| 52 |
+
data = {
|
| 53 |
+
"unit_id": unit_id,
|
| 54 |
+
"school_year": school_year,
|
| 55 |
+
"name": class_data.name,
|
| 56 |
+
"grade": class_data.grade,
|
| 57 |
+
"session": class_data.session,
|
| 58 |
+
"homeroom_teacher_id": class_data.homeroom_teacher_id,
|
| 59 |
+
"fixed_off_slots": json.dumps(class_data.fixed_off_slots),
|
| 60 |
+
"is_locked": class_data.is_locked
|
| 61 |
+
}
|
| 62 |
+
result = db_manager.execute_query("classes", "insert", data=data)
|
| 63 |
+
if result.data:
|
| 64 |
+
class_result = result.data[0]
|
| 65 |
+
class_result["fixed_off_slots"] = class_data.fixed_off_slots
|
| 66 |
+
return class_result
|
| 67 |
+
raise HTTPException(status_code=500, detail="Error creating class")
|
| 68 |
+
except HTTPException:
|
| 69 |
+
raise
|
| 70 |
+
except Exception as e:
|
| 71 |
+
logger.error(f"Error creating class: {e}")
|
| 72 |
+
raise HTTPException(status_code=500, detail="Error creating class")
|
| 73 |
+
|
| 74 |
+
|
| 75 |
+
@router.delete("/{class_name}")
|
| 76 |
+
async def delete_class(
|
| 77 |
+
unit_id: int,
|
| 78 |
+
school_year: str,
|
| 79 |
+
class_name: str,
|
| 80 |
+
current_user: dict = Depends(get_current_user)
|
| 81 |
+
):
|
| 82 |
+
"""Delete a class"""
|
| 83 |
+
try:
|
| 84 |
+
db_manager.execute_query(
|
| 85 |
+
"classes",
|
| 86 |
+
"delete",
|
| 87 |
+
filters=[
|
| 88 |
+
{"column": "unit_id", "value": unit_id},
|
| 89 |
+
{"column": "school_year", "value": school_year},
|
| 90 |
+
{"column": "name", "value": class_name}
|
| 91 |
+
]
|
| 92 |
+
)
|
| 93 |
+
return {"message": "Class deleted successfully"}
|
| 94 |
+
except Exception as e:
|
| 95 |
+
logger.error(f"Error deleting class: {e}")
|
| 96 |
+
raise HTTPException(status_code=500, detail="Error deleting class")
|
| 97 |
+
|
app/routers/solver.py
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Timetable solver routes
|
| 3 |
+
"""
|
| 4 |
+
from fastapi import APIRouter, Depends, HTTPException, BackgroundTasks
|
| 5 |
+
from app.models.schemas import SolverRequest, SolverResponse
|
| 6 |
+
from app.routers.auth import get_current_user
|
| 7 |
+
from app.services.solver_service import SolverService
|
| 8 |
+
import logging
|
| 9 |
+
|
| 10 |
+
logger = logging.getLogger(__name__)
|
| 11 |
+
|
| 12 |
+
router = APIRouter()
|
| 13 |
+
|
| 14 |
+
|
| 15 |
+
@router.post("/solve", response_model=SolverResponse)
|
| 16 |
+
async def solve_timetable(
|
| 17 |
+
request: SolverRequest,
|
| 18 |
+
background_tasks: BackgroundTasks,
|
| 19 |
+
current_user: dict = Depends(get_current_user)
|
| 20 |
+
):
|
| 21 |
+
"""Solve timetable scheduling problem"""
|
| 22 |
+
try:
|
| 23 |
+
solver_service = SolverService()
|
| 24 |
+
result = await solver_service.solve(
|
| 25 |
+
unit_id=request.unit_id,
|
| 26 |
+
school_year=request.school_year,
|
| 27 |
+
mode=request.mode,
|
| 28 |
+
num_workers=request.num_workers,
|
| 29 |
+
time_limit=request.time_limit,
|
| 30 |
+
weights=request.weights,
|
| 31 |
+
priority_teacher_ids=request.priority_teacher_ids,
|
| 32 |
+
phase=request.phase
|
| 33 |
+
)
|
| 34 |
+
return result
|
| 35 |
+
except Exception as e:
|
| 36 |
+
logger.error(f"Error solving timetable: {e}")
|
| 37 |
+
raise HTTPException(status_code=500, detail=f"Error solving timetable: {str(e)}")
|
| 38 |
+
|
app/routers/subjects.py
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Subjects management routes
|
| 3 |
+
"""
|
| 4 |
+
from fastapi import APIRouter, Depends, HTTPException, Query
|
| 5 |
+
from app.models.schemas import SubjectCreate, SubjectResponse
|
| 6 |
+
from app.routers.auth import get_current_user
|
| 7 |
+
from app.database import db_manager
|
| 8 |
+
import json
|
| 9 |
+
import logging
|
| 10 |
+
|
| 11 |
+
logger = logging.getLogger(__name__)
|
| 12 |
+
|
| 13 |
+
router = APIRouter()
|
| 14 |
+
|
| 15 |
+
|
| 16 |
+
@router.get("", response_model=list[SubjectResponse])
|
| 17 |
+
async def get_subjects(
|
| 18 |
+
unit_id: int = Query(...),
|
| 19 |
+
school_year: str = Query(...),
|
| 20 |
+
current_user: dict = Depends(get_current_user)
|
| 21 |
+
):
|
| 22 |
+
"""Get all subjects for a unit and school year"""
|
| 23 |
+
try:
|
| 24 |
+
result = db_manager.execute_query(
|
| 25 |
+
"subjects",
|
| 26 |
+
"select",
|
| 27 |
+
filters=[
|
| 28 |
+
{"column": "unit_id", "value": unit_id},
|
| 29 |
+
{"column": "school_year", "value": school_year}
|
| 30 |
+
]
|
| 31 |
+
)
|
| 32 |
+
# Parse JSON fields
|
| 33 |
+
subjects = []
|
| 34 |
+
for subject in result.data:
|
| 35 |
+
subject["periods_per_week"] = json.loads(subject.get("periods_per_week") or "{}")
|
| 36 |
+
subjects.append(subject)
|
| 37 |
+
return subjects
|
| 38 |
+
except Exception as e:
|
| 39 |
+
logger.error(f"Error fetching subjects: {e}")
|
| 40 |
+
raise HTTPException(status_code=500, detail="Error fetching subjects")
|
| 41 |
+
|
| 42 |
+
|
| 43 |
+
@router.post("", response_model=SubjectResponse)
|
| 44 |
+
async def create_subject(
|
| 45 |
+
unit_id: int,
|
| 46 |
+
school_year: str,
|
| 47 |
+
subject: SubjectCreate,
|
| 48 |
+
current_user: dict = Depends(get_current_user)
|
| 49 |
+
):
|
| 50 |
+
"""Create a new subject"""
|
| 51 |
+
try:
|
| 52 |
+
data = {
|
| 53 |
+
"unit_id": unit_id,
|
| 54 |
+
"school_year": school_year,
|
| 55 |
+
"name": subject.name,
|
| 56 |
+
"periods_per_week": json.dumps(subject.periods_per_week),
|
| 57 |
+
"is_double_period_only": subject.is_double_period_only
|
| 58 |
+
}
|
| 59 |
+
result = db_manager.execute_query("subjects", "insert", data=data)
|
| 60 |
+
if result.data:
|
| 61 |
+
subject_data = result.data[0]
|
| 62 |
+
subject_data["periods_per_week"] = subject.periods_per_week
|
| 63 |
+
return subject_data
|
| 64 |
+
raise HTTPException(status_code=500, detail="Error creating subject")
|
| 65 |
+
except HTTPException:
|
| 66 |
+
raise
|
| 67 |
+
except Exception as e:
|
| 68 |
+
logger.error(f"Error creating subject: {e}")
|
| 69 |
+
raise HTTPException(status_code=500, detail="Error creating subject")
|
| 70 |
+
|
| 71 |
+
|
| 72 |
+
@router.delete("/{subject_name}")
|
| 73 |
+
async def delete_subject(
|
| 74 |
+
unit_id: int,
|
| 75 |
+
school_year: str,
|
| 76 |
+
subject_name: str,
|
| 77 |
+
current_user: dict = Depends(get_current_user)
|
| 78 |
+
):
|
| 79 |
+
"""Delete a subject"""
|
| 80 |
+
try:
|
| 81 |
+
db_manager.execute_query(
|
| 82 |
+
"subjects",
|
| 83 |
+
"delete",
|
| 84 |
+
filters=[
|
| 85 |
+
{"column": "unit_id", "value": unit_id},
|
| 86 |
+
{"column": "school_year", "value": school_year},
|
| 87 |
+
{"column": "name", "value": subject_name}
|
| 88 |
+
]
|
| 89 |
+
)
|
| 90 |
+
return {"message": "Subject deleted successfully"}
|
| 91 |
+
except Exception as e:
|
| 92 |
+
logger.error(f"Error deleting subject: {e}")
|
| 93 |
+
raise HTTPException(status_code=500, detail="Error deleting subject")
|
| 94 |
+
|
app/routers/teachers.py
ADDED
|
@@ -0,0 +1,152 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Teachers management routes
|
| 3 |
+
"""
|
| 4 |
+
from fastapi import APIRouter, Depends, HTTPException, Query
|
| 5 |
+
from app.models.schemas import TeacherCreate, TeacherResponse
|
| 6 |
+
from app.routers.auth import get_current_user
|
| 7 |
+
from app.database import db_manager
|
| 8 |
+
import json
|
| 9 |
+
import logging
|
| 10 |
+
|
| 11 |
+
logger = logging.getLogger(__name__)
|
| 12 |
+
|
| 13 |
+
router = APIRouter()
|
| 14 |
+
|
| 15 |
+
|
| 16 |
+
@router.get("", response_model=list[TeacherResponse])
|
| 17 |
+
async def get_teachers(
|
| 18 |
+
unit_id: int = Query(...),
|
| 19 |
+
school_year: str = Query(...),
|
| 20 |
+
current_user: dict = Depends(get_current_user)
|
| 21 |
+
):
|
| 22 |
+
"""Get all teachers for a unit and school year"""
|
| 23 |
+
try:
|
| 24 |
+
result = db_manager.execute_query(
|
| 25 |
+
"teachers",
|
| 26 |
+
"select",
|
| 27 |
+
filters=[
|
| 28 |
+
{"column": "unit_id", "value": unit_id},
|
| 29 |
+
{"column": "school_year", "value": school_year}
|
| 30 |
+
]
|
| 31 |
+
)
|
| 32 |
+
# Parse JSON fields
|
| 33 |
+
teachers = []
|
| 34 |
+
for teacher in result.data:
|
| 35 |
+
teacher["subjects"] = json.loads(teacher.get("subjects") or "[]")
|
| 36 |
+
teacher["secondary_subjects"] = json.loads(teacher.get("secondary_subjects") or "[]")
|
| 37 |
+
teacher["busy_slots"] = json.loads(teacher.get("busy_slots") or "[]")
|
| 38 |
+
teachers.append(teacher)
|
| 39 |
+
return teachers
|
| 40 |
+
except Exception as e:
|
| 41 |
+
logger.error(f"Error fetching teachers: {e}")
|
| 42 |
+
raise HTTPException(status_code=500, detail="Error fetching teachers")
|
| 43 |
+
|
| 44 |
+
|
| 45 |
+
@router.post("", response_model=TeacherResponse)
|
| 46 |
+
async def create_teacher(
|
| 47 |
+
unit_id: int,
|
| 48 |
+
school_year: str,
|
| 49 |
+
teacher: TeacherCreate,
|
| 50 |
+
current_user: dict = Depends(get_current_user)
|
| 51 |
+
):
|
| 52 |
+
"""Create a new teacher"""
|
| 53 |
+
try:
|
| 54 |
+
data = {
|
| 55 |
+
"unit_id": unit_id,
|
| 56 |
+
"school_year": school_year,
|
| 57 |
+
"id": teacher.id,
|
| 58 |
+
"name": teacher.name,
|
| 59 |
+
"subjects": json.dumps(teacher.subjects),
|
| 60 |
+
"secondary_subjects": json.dumps(teacher.secondary_subjects),
|
| 61 |
+
"busy_slots": json.dumps(teacher.busy_slots),
|
| 62 |
+
"is_locked": teacher.is_locked,
|
| 63 |
+
"work_days_preference": teacher.work_days_preference,
|
| 64 |
+
"role": teacher.role,
|
| 65 |
+
"target_periods": teacher.target_periods,
|
| 66 |
+
"email": teacher.email,
|
| 67 |
+
"professional_group_name": teacher.professional_group_name
|
| 68 |
+
}
|
| 69 |
+
result = db_manager.execute_query("teachers", "insert", data=data)
|
| 70 |
+
if result.data:
|
| 71 |
+
teacher_data = result.data[0]
|
| 72 |
+
teacher_data["subjects"] = teacher.subjects
|
| 73 |
+
teacher_data["secondary_subjects"] = teacher.secondary_subjects
|
| 74 |
+
teacher_data["busy_slots"] = teacher.busy_slots
|
| 75 |
+
return teacher_data
|
| 76 |
+
raise HTTPException(status_code=500, detail="Error creating teacher")
|
| 77 |
+
except HTTPException:
|
| 78 |
+
raise
|
| 79 |
+
except Exception as e:
|
| 80 |
+
logger.error(f"Error creating teacher: {e}")
|
| 81 |
+
raise HTTPException(status_code=500, detail="Error creating teacher")
|
| 82 |
+
|
| 83 |
+
|
| 84 |
+
@router.put("/{teacher_id}", response_model=TeacherResponse)
|
| 85 |
+
async def update_teacher(
|
| 86 |
+
unit_id: int,
|
| 87 |
+
school_year: str,
|
| 88 |
+
teacher_id: str,
|
| 89 |
+
teacher: TeacherCreate,
|
| 90 |
+
current_user: dict = Depends(get_current_user)
|
| 91 |
+
):
|
| 92 |
+
"""Update a teacher"""
|
| 93 |
+
try:
|
| 94 |
+
data = {
|
| 95 |
+
"name": teacher.name,
|
| 96 |
+
"subjects": json.dumps(teacher.subjects),
|
| 97 |
+
"secondary_subjects": json.dumps(teacher.secondary_subjects),
|
| 98 |
+
"busy_slots": json.dumps(teacher.busy_slots),
|
| 99 |
+
"is_locked": teacher.is_locked,
|
| 100 |
+
"work_days_preference": teacher.work_days_preference,
|
| 101 |
+
"role": teacher.role,
|
| 102 |
+
"target_periods": teacher.target_periods,
|
| 103 |
+
"email": teacher.email,
|
| 104 |
+
"professional_group_name": teacher.professional_group_name
|
| 105 |
+
}
|
| 106 |
+
result = db_manager.execute_query(
|
| 107 |
+
"teachers",
|
| 108 |
+
"update",
|
| 109 |
+
data=data,
|
| 110 |
+
filters=[
|
| 111 |
+
{"column": "unit_id", "value": unit_id},
|
| 112 |
+
{"column": "school_year", "value": school_year},
|
| 113 |
+
{"column": "id", "value": teacher_id}
|
| 114 |
+
]
|
| 115 |
+
)
|
| 116 |
+
if not result.data:
|
| 117 |
+
raise HTTPException(status_code=404, detail="Teacher not found")
|
| 118 |
+
teacher_data = result.data[0]
|
| 119 |
+
teacher_data["subjects"] = teacher.subjects
|
| 120 |
+
teacher_data["secondary_subjects"] = teacher.secondary_subjects
|
| 121 |
+
teacher_data["busy_slots"] = teacher.busy_slots
|
| 122 |
+
return teacher_data
|
| 123 |
+
except HTTPException:
|
| 124 |
+
raise
|
| 125 |
+
except Exception as e:
|
| 126 |
+
logger.error(f"Error updating teacher: {e}")
|
| 127 |
+
raise HTTPException(status_code=500, detail="Error updating teacher")
|
| 128 |
+
|
| 129 |
+
|
| 130 |
+
@router.delete("/{teacher_id}")
|
| 131 |
+
async def delete_teacher(
|
| 132 |
+
unit_id: int,
|
| 133 |
+
school_year: str,
|
| 134 |
+
teacher_id: str,
|
| 135 |
+
current_user: dict = Depends(get_current_user)
|
| 136 |
+
):
|
| 137 |
+
"""Delete a teacher"""
|
| 138 |
+
try:
|
| 139 |
+
db_manager.execute_query(
|
| 140 |
+
"teachers",
|
| 141 |
+
"delete",
|
| 142 |
+
filters=[
|
| 143 |
+
{"column": "unit_id", "value": unit_id},
|
| 144 |
+
{"column": "school_year", "value": school_year},
|
| 145 |
+
{"column": "id", "value": teacher_id}
|
| 146 |
+
]
|
| 147 |
+
)
|
| 148 |
+
return {"message": "Teacher deleted successfully"}
|
| 149 |
+
except Exception as e:
|
| 150 |
+
logger.error(f"Error deleting teacher: {e}")
|
| 151 |
+
raise HTTPException(status_code=500, detail="Error deleting teacher")
|
| 152 |
+
|
app/routers/timetable.py
ADDED
|
@@ -0,0 +1,131 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Timetable management routes
|
| 3 |
+
"""
|
| 4 |
+
from fastapi import APIRouter, Depends, HTTPException, Query
|
| 5 |
+
from app.models.schemas import TimetableSessionCreate, TimetableSessionResponse, TimetableData
|
| 6 |
+
from app.routers.auth import get_current_user
|
| 7 |
+
from app.database import db_manager
|
| 8 |
+
import json
|
| 9 |
+
import logging
|
| 10 |
+
from datetime import datetime
|
| 11 |
+
|
| 12 |
+
logger = logging.getLogger(__name__)
|
| 13 |
+
|
| 14 |
+
router = APIRouter()
|
| 15 |
+
|
| 16 |
+
|
| 17 |
+
@router.get("/sessions", response_model=list[TimetableSessionResponse])
|
| 18 |
+
async def get_timetable_sessions(
|
| 19 |
+
unit_id: int = Query(...),
|
| 20 |
+
school_year: str = Query(...),
|
| 21 |
+
current_user: dict = Depends(get_current_user)
|
| 22 |
+
):
|
| 23 |
+
"""Get all timetable sessions for a unit and school year"""
|
| 24 |
+
try:
|
| 25 |
+
result = db_manager.execute_query(
|
| 26 |
+
"timetable_sessions",
|
| 27 |
+
"select",
|
| 28 |
+
filters=[
|
| 29 |
+
{"column": "unit_id", "value": unit_id},
|
| 30 |
+
{"column": "school_year", "value": school_year}
|
| 31 |
+
],
|
| 32 |
+
order={"column": "created_at", "desc": True}
|
| 33 |
+
)
|
| 34 |
+
return result.data
|
| 35 |
+
except Exception as e:
|
| 36 |
+
logger.error(f"Error fetching timetable sessions: {e}")
|
| 37 |
+
raise HTTPException(status_code=500, detail="Error fetching timetable sessions")
|
| 38 |
+
|
| 39 |
+
|
| 40 |
+
@router.post("/sessions", response_model=TimetableSessionResponse)
|
| 41 |
+
async def create_timetable_session(
|
| 42 |
+
unit_id: int,
|
| 43 |
+
school_year: str,
|
| 44 |
+
session: TimetableSessionCreate,
|
| 45 |
+
timetable_data: TimetableData,
|
| 46 |
+
current_user: dict = Depends(get_current_user)
|
| 47 |
+
):
|
| 48 |
+
"""Create a new timetable session"""
|
| 49 |
+
try:
|
| 50 |
+
data = {
|
| 51 |
+
"unit_id": unit_id,
|
| 52 |
+
"school_year": school_year,
|
| 53 |
+
"session_name": session.session_name,
|
| 54 |
+
"timetable_data": json.dumps(timetable_data.timetable),
|
| 55 |
+
"is_locked": False,
|
| 56 |
+
"created_at": datetime.now().isoformat(),
|
| 57 |
+
"effective_date": session.effective_date
|
| 58 |
+
}
|
| 59 |
+
result = db_manager.execute_query("timetable_sessions", "insert", data=data)
|
| 60 |
+
if result.data:
|
| 61 |
+
return result.data[0]
|
| 62 |
+
raise HTTPException(status_code=500, detail="Error creating timetable session")
|
| 63 |
+
except HTTPException:
|
| 64 |
+
raise
|
| 65 |
+
except Exception as e:
|
| 66 |
+
logger.error(f"Error creating timetable session: {e}")
|
| 67 |
+
raise HTTPException(status_code=500, detail="Error creating timetable session")
|
| 68 |
+
|
| 69 |
+
|
| 70 |
+
@router.get("/sessions/{session_id}", response_model=TimetableData)
|
| 71 |
+
async def get_timetable_data(
|
| 72 |
+
session_id: int,
|
| 73 |
+
current_user: dict = Depends(get_current_user)
|
| 74 |
+
):
|
| 75 |
+
"""Get timetable data for a session"""
|
| 76 |
+
try:
|
| 77 |
+
result = db_manager.execute_query(
|
| 78 |
+
"timetable_sessions",
|
| 79 |
+
"select",
|
| 80 |
+
filters=[{"column": "id", "value": session_id}]
|
| 81 |
+
)
|
| 82 |
+
if not result.data:
|
| 83 |
+
raise HTTPException(status_code=404, detail="Timetable session not found")
|
| 84 |
+
|
| 85 |
+
session = result.data[0]
|
| 86 |
+
timetable_data = json.loads(session.get("timetable_data", "{}"))
|
| 87 |
+
return TimetableData(timetable=timetable_data)
|
| 88 |
+
except HTTPException:
|
| 89 |
+
raise
|
| 90 |
+
except Exception as e:
|
| 91 |
+
logger.error(f"Error fetching timetable data: {e}")
|
| 92 |
+
raise HTTPException(status_code=500, detail="Error fetching timetable data")
|
| 93 |
+
|
| 94 |
+
|
| 95 |
+
@router.put("/sessions/{session_id}/lock")
|
| 96 |
+
async def toggle_session_lock(
|
| 97 |
+
session_id: int,
|
| 98 |
+
is_locked: bool,
|
| 99 |
+
current_user: dict = Depends(get_current_user)
|
| 100 |
+
):
|
| 101 |
+
"""Toggle lock status of a timetable session"""
|
| 102 |
+
try:
|
| 103 |
+
db_manager.execute_query(
|
| 104 |
+
"timetable_sessions",
|
| 105 |
+
"update",
|
| 106 |
+
data={"is_locked": is_locked},
|
| 107 |
+
filters=[{"column": "id", "value": session_id}]
|
| 108 |
+
)
|
| 109 |
+
return {"message": "Session lock status updated"}
|
| 110 |
+
except Exception as e:
|
| 111 |
+
logger.error(f"Error updating session lock: {e}")
|
| 112 |
+
raise HTTPException(status_code=500, detail="Error updating session lock")
|
| 113 |
+
|
| 114 |
+
|
| 115 |
+
@router.delete("/sessions/{session_id}")
|
| 116 |
+
async def delete_timetable_session(
|
| 117 |
+
session_id: int,
|
| 118 |
+
current_user: dict = Depends(get_current_user)
|
| 119 |
+
):
|
| 120 |
+
"""Delete a timetable session"""
|
| 121 |
+
try:
|
| 122 |
+
db_manager.execute_query(
|
| 123 |
+
"timetable_sessions",
|
| 124 |
+
"delete",
|
| 125 |
+
filters=[{"column": "id", "value": session_id}]
|
| 126 |
+
)
|
| 127 |
+
return {"message": "Timetable session deleted successfully"}
|
| 128 |
+
except Exception as e:
|
| 129 |
+
logger.error(f"Error deleting timetable session: {e}")
|
| 130 |
+
raise HTTPException(status_code=500, detail="Error deleting timetable session")
|
| 131 |
+
|
app/routers/units.py
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Units management routes
|
| 3 |
+
"""
|
| 4 |
+
from fastapi import APIRouter, Depends, HTTPException
|
| 5 |
+
from app.models.schemas import UnitCreate, UnitResponse
|
| 6 |
+
from app.routers.auth import get_current_user
|
| 7 |
+
from app.database import db_manager
|
| 8 |
+
import logging
|
| 9 |
+
|
| 10 |
+
logger = logging.getLogger(__name__)
|
| 11 |
+
|
| 12 |
+
router = APIRouter()
|
| 13 |
+
|
| 14 |
+
|
| 15 |
+
@router.get("", response_model=list[UnitResponse])
|
| 16 |
+
async def get_all_units(current_user: dict = Depends(get_current_user)):
|
| 17 |
+
"""Get all units"""
|
| 18 |
+
try:
|
| 19 |
+
result = db_manager.execute_query("units", "select", order={"column": "name", "desc": False})
|
| 20 |
+
return result.data
|
| 21 |
+
except Exception as e:
|
| 22 |
+
logger.error(f"Error fetching units: {e}")
|
| 23 |
+
raise HTTPException(status_code=500, detail="Error fetching units")
|
| 24 |
+
|
| 25 |
+
|
| 26 |
+
@router.post("", response_model=UnitResponse)
|
| 27 |
+
async def create_unit(unit: UnitCreate, current_user: dict = Depends(get_current_user)):
|
| 28 |
+
"""Create a new unit"""
|
| 29 |
+
try:
|
| 30 |
+
result = db_manager.execute_query(
|
| 31 |
+
"units",
|
| 32 |
+
"insert",
|
| 33 |
+
data={"name": unit.name}
|
| 34 |
+
)
|
| 35 |
+
return result.data[0] if result.data else None
|
| 36 |
+
except Exception as e:
|
| 37 |
+
logger.error(f"Error creating unit: {e}")
|
| 38 |
+
raise HTTPException(status_code=500, detail="Error creating unit")
|
| 39 |
+
|
| 40 |
+
|
| 41 |
+
@router.put("/{unit_id}", response_model=UnitResponse)
|
| 42 |
+
async def update_unit(unit_id: int, unit: UnitCreate, current_user: dict = Depends(get_current_user)):
|
| 43 |
+
"""Update a unit"""
|
| 44 |
+
try:
|
| 45 |
+
result = db_manager.execute_query(
|
| 46 |
+
"units",
|
| 47 |
+
"update",
|
| 48 |
+
data={"name": unit.name},
|
| 49 |
+
filters=[{"column": "id", "value": unit_id}]
|
| 50 |
+
)
|
| 51 |
+
if not result.data:
|
| 52 |
+
raise HTTPException(status_code=404, detail="Unit not found")
|
| 53 |
+
return result.data[0]
|
| 54 |
+
except HTTPException:
|
| 55 |
+
raise
|
| 56 |
+
except Exception as e:
|
| 57 |
+
logger.error(f"Error updating unit: {e}")
|
| 58 |
+
raise HTTPException(status_code=500, detail="Error updating unit")
|
| 59 |
+
|
| 60 |
+
|
| 61 |
+
@router.delete("/{unit_id}")
|
| 62 |
+
async def delete_unit(unit_id: int, current_user: dict = Depends(get_current_user)):
|
| 63 |
+
"""Delete a unit"""
|
| 64 |
+
try:
|
| 65 |
+
db_manager.execute_query(
|
| 66 |
+
"units",
|
| 67 |
+
"delete",
|
| 68 |
+
filters=[{"column": "id", "value": unit_id}]
|
| 69 |
+
)
|
| 70 |
+
return {"message": "Unit deleted successfully"}
|
| 71 |
+
except Exception as e:
|
| 72 |
+
logger.error(f"Error deleting unit: {e}")
|
| 73 |
+
raise HTTPException(status_code=500, detail="Error deleting unit")
|
| 74 |
+
|
app/services/__init__.py
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Services package
|
| 2 |
+
|
app/services/__pycache__/__init__.cpython-312.pyc
ADDED
|
Binary file (152 Bytes). View file
|
|
|
app/services/__pycache__/solver_service.cpython-312.pyc
ADDED
|
Binary file (4.53 kB). View file
|
|
|
app/services/solver_service.py
ADDED
|
@@ -0,0 +1,124 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Timetable solver service - adapts the existing solver logic for web API
|
| 3 |
+
"""
|
| 4 |
+
import time
|
| 5 |
+
from typing import Optional, Dict, Any, List
|
| 6 |
+
from app.models.schemas import SolverResponse
|
| 7 |
+
from app.database import db_manager
|
| 8 |
+
import json
|
| 9 |
+
import logging
|
| 10 |
+
|
| 11 |
+
logger = logging.getLogger(__name__)
|
| 12 |
+
|
| 13 |
+
# Note: This is a simplified version. You'll need to adapt the actual solver
|
| 14 |
+
# from TKB_V1.2/core/solver_ortools.py to work in this async context
|
| 15 |
+
class SolverService:
|
| 16 |
+
"""Service for solving timetable scheduling problems"""
|
| 17 |
+
|
| 18 |
+
async def solve(
|
| 19 |
+
self,
|
| 20 |
+
unit_id: int,
|
| 21 |
+
school_year: str,
|
| 22 |
+
mode: str = "ortools_balanced",
|
| 23 |
+
num_workers: int = 8,
|
| 24 |
+
time_limit: float = 300.0,
|
| 25 |
+
weights: Optional[Dict[str, float]] = None,
|
| 26 |
+
priority_teacher_ids: Optional[List[str]] = None,
|
| 27 |
+
phase: Optional[str] = None
|
| 28 |
+
) -> SolverResponse:
|
| 29 |
+
"""
|
| 30 |
+
Solve timetable scheduling problem
|
| 31 |
+
|
| 32 |
+
This is a placeholder that needs to be implemented with the actual
|
| 33 |
+
solver logic from TKB_V1.2/core/solver_ortools.py
|
| 34 |
+
"""
|
| 35 |
+
start_time = time.time()
|
| 36 |
+
|
| 37 |
+
try:
|
| 38 |
+
# Load school data
|
| 39 |
+
school_data = await self._load_school_data(unit_id, school_year)
|
| 40 |
+
|
| 41 |
+
# TODO: Implement actual solver logic here
|
| 42 |
+
# This should call the OrToolsSolverWorker logic but adapted for async
|
| 43 |
+
|
| 44 |
+
# Placeholder response
|
| 45 |
+
execution_time = time.time() - start_time
|
| 46 |
+
|
| 47 |
+
return SolverResponse(
|
| 48 |
+
success=True,
|
| 49 |
+
timetable={},
|
| 50 |
+
objective_value=0.0,
|
| 51 |
+
unplaced_lessons=[],
|
| 52 |
+
message="Solver not yet fully implemented",
|
| 53 |
+
execution_time=execution_time
|
| 54 |
+
)
|
| 55 |
+
|
| 56 |
+
except Exception as e:
|
| 57 |
+
logger.error(f"Solver error: {e}")
|
| 58 |
+
execution_time = time.time() - start_time
|
| 59 |
+
return SolverResponse(
|
| 60 |
+
success=False,
|
| 61 |
+
message=f"Error: {str(e)}",
|
| 62 |
+
execution_time=execution_time
|
| 63 |
+
)
|
| 64 |
+
|
| 65 |
+
async def _load_school_data(self, unit_id: int, school_year: str) -> Dict[str, Any]:
|
| 66 |
+
"""Load school data from database"""
|
| 67 |
+
try:
|
| 68 |
+
# Load teachers
|
| 69 |
+
teachers_result = db_manager.execute_query(
|
| 70 |
+
"teachers",
|
| 71 |
+
"select",
|
| 72 |
+
filters=[
|
| 73 |
+
{"column": "unit_id", "value": unit_id},
|
| 74 |
+
{"column": "school_year", "value": school_year}
|
| 75 |
+
]
|
| 76 |
+
)
|
| 77 |
+
|
| 78 |
+
# Load subjects
|
| 79 |
+
subjects_result = db_manager.execute_query(
|
| 80 |
+
"subjects",
|
| 81 |
+
"select",
|
| 82 |
+
filters=[
|
| 83 |
+
{"column": "unit_id", "value": unit_id},
|
| 84 |
+
{"column": "school_year", "value": school_year}
|
| 85 |
+
]
|
| 86 |
+
)
|
| 87 |
+
|
| 88 |
+
# Load classes
|
| 89 |
+
classes_result = db_manager.execute_query(
|
| 90 |
+
"classes",
|
| 91 |
+
"select",
|
| 92 |
+
filters=[
|
| 93 |
+
{"column": "unit_id", "value": unit_id},
|
| 94 |
+
{"column": "school_year", "value": school_year}
|
| 95 |
+
]
|
| 96 |
+
)
|
| 97 |
+
|
| 98 |
+
# Parse JSON fields
|
| 99 |
+
teachers = []
|
| 100 |
+
for teacher in teachers_result.data:
|
| 101 |
+
teacher["subjects"] = json.loads(teacher.get("subjects") or "[]")
|
| 102 |
+
teacher["secondary_subjects"] = json.loads(teacher.get("secondary_subjects") or "[]")
|
| 103 |
+
teacher["busy_slots"] = json.loads(teacher.get("busy_slots") or "[]")
|
| 104 |
+
teachers.append(teacher)
|
| 105 |
+
|
| 106 |
+
subjects = []
|
| 107 |
+
for subject in subjects_result.data:
|
| 108 |
+
subject["periods_per_week"] = json.loads(subject.get("periods_per_week") or "{}")
|
| 109 |
+
subjects.append(subject)
|
| 110 |
+
|
| 111 |
+
classes = []
|
| 112 |
+
for cls in classes_result.data:
|
| 113 |
+
cls["fixed_off_slots"] = json.loads(cls.get("fixed_off_slots") or "[]")
|
| 114 |
+
classes.append(cls)
|
| 115 |
+
|
| 116 |
+
return {
|
| 117 |
+
"teachers": teachers,
|
| 118 |
+
"subjects": subjects,
|
| 119 |
+
"classes": classes
|
| 120 |
+
}
|
| 121 |
+
except Exception as e:
|
| 122 |
+
logger.error(f"Error loading school data: {e}")
|
| 123 |
+
raise
|
| 124 |
+
|
requirements.txt
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
fastapi==0.104.1
|
| 2 |
+
uvicorn[standard]==0.24.0
|
| 3 |
+
python-multipart==0.0.6
|
| 4 |
+
pydantic==2.5.0
|
| 5 |
+
pydantic-settings==2.1.0
|
| 6 |
+
supabase==2.0.3
|
| 7 |
+
postgrest==0.13.0
|
| 8 |
+
python-jose[cryptography]==3.3.0
|
| 9 |
+
passlib[bcrypt]==1.7.4
|
| 10 |
+
python-dotenv==1.0.0
|
| 11 |
+
ortools==9.9.3963
|
| 12 |
+
google-generativeai==0.3.2
|
| 13 |
+
httpx==0.25.2
|
| 14 |
+
python-dateutil==2.8.2
|
| 15 |
+
|