Spaces:
Sleeping
Sleeping
upload
Browse files- .dockerignore +26 -0
- Dockerfile +31 -0
- requirements.txt +29 -0
- services/.DS_Store +0 -0
- services/__pycache__/__init__.cpython-312.pyc +0 -0
- services/api/.DS_Store +0 -0
- services/api/__pycache__/__init__.cpython-312.pyc +0 -0
- services/api/__pycache__/api_endpoints.cpython-312.pyc +0 -0
- services/api/__pycache__/chat_endpoints.cpython-312.pyc +0 -0
- services/api/__pycache__/video_upload.cpython-312.pyc +0 -0
- services/api/api_endpoints.py +1853 -0
- services/api/chat_endpoints.py +102 -0
- services/api/chatbot/__pycache__/config.cpython-312.pyc +0 -0
- services/api/chatbot/__pycache__/config.cpython-313.pyc +0 -0
- services/api/chatbot/__pycache__/core.cpython-312.pyc +0 -0
- services/api/chatbot/__pycache__/core.cpython-313.pyc +0 -0
- services/api/chatbot/__pycache__/llm.cpython-312.pyc +0 -0
- services/api/chatbot/__pycache__/llm.cpython-313.pyc +0 -0
- services/api/chatbot/__pycache__/prompts.cpython-312.pyc +0 -0
- services/api/chatbot/__pycache__/prompts.cpython-313.pyc +0 -0
- services/api/chatbot/__pycache__/retrieval.cpython-312.pyc +0 -0
- services/api/chatbot/__pycache__/retrieval.cpython-313.pyc +0 -0
- services/api/chatbot/config.py +18 -0
- services/api/chatbot/core.py +117 -0
- services/api/chatbot/llm.py +29 -0
- services/api/chatbot/memory.py +0 -0
- services/api/chatbot/prompts.py +73 -0
- services/api/chatbot/retrieval.py +531 -0
- services/api/chatbot/self_query.py +0 -0
- services/api/db/__pycache__/__init__.cpython-312.pyc +0 -0
- services/api/db/__pycache__/auth.cpython-312.pyc +0 -0
- services/api/db/__pycache__/db_connection.cpython-312.pyc +0 -0
- services/api/db/__pycache__/token_utils.cpython-312.pyc +0 -0
- services/api/db/auth.py +437 -0
- services/api/db/token_utils.py +50 -0
- services/api/skills.csv +21 -0
.dockerignore
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Version control
|
| 2 |
+
.git
|
| 3 |
+
.gitignore
|
| 4 |
+
|
| 5 |
+
# Python
|
| 6 |
+
__pycache__
|
| 7 |
+
*.pyc
|
| 8 |
+
*.pyo
|
| 9 |
+
*.pyd
|
| 10 |
+
.Python
|
| 11 |
+
venv
|
| 12 |
+
.env
|
| 13 |
+
|
| 14 |
+
# IDE
|
| 15 |
+
.vscode
|
| 16 |
+
.idea
|
| 17 |
+
|
| 18 |
+
# React frontend
|
| 19 |
+
react-frontend/node_modules
|
| 20 |
+
react-frontend/build
|
| 21 |
+
react-frontend/.env*
|
| 22 |
+
|
| 23 |
+
# Misc
|
| 24 |
+
.DS_Store
|
| 25 |
+
README.md
|
| 26 |
+
*.log
|
Dockerfile
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Use Python 3.9 slim image as base
|
| 2 |
+
FROM python:3.9-slim
|
| 3 |
+
|
| 4 |
+
# Set working directory
|
| 5 |
+
WORKDIR /app
|
| 6 |
+
|
| 7 |
+
# Set environment variables
|
| 8 |
+
ENV PYTHONUNBUFFERED=1 \
|
| 9 |
+
PYTHONDONTWRITEBYTECODE=1
|
| 10 |
+
|
| 11 |
+
# Install system dependencies
|
| 12 |
+
RUN apt-get update && \
|
| 13 |
+
apt-get install -y --no-install-recommends \
|
| 14 |
+
build-essential \
|
| 15 |
+
curl \
|
| 16 |
+
&& rm -rf /var/lib/apt/lists/*
|
| 17 |
+
|
| 18 |
+
# Copy requirements file
|
| 19 |
+
COPY requirements.txt .
|
| 20 |
+
|
| 21 |
+
# Install Python dependencies
|
| 22 |
+
RUN pip install --no-cache-dir -r requirements.txt
|
| 23 |
+
|
| 24 |
+
# Copy the rest of the application
|
| 25 |
+
COPY . .
|
| 26 |
+
|
| 27 |
+
# Expose the port
|
| 28 |
+
EXPOSE 7860
|
| 29 |
+
|
| 30 |
+
# Command to run the application
|
| 31 |
+
CMD ["uvicorn", "services.api.db.auth:app", "--host", "0.0.0.0", "--port", "7860"]
|
requirements.txt
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Core dependencies
|
| 2 |
+
fastapi==0.109.2
|
| 3 |
+
uvicorn==0.27.1
|
| 4 |
+
python-multipart==0.0.7
|
| 5 |
+
python-dotenv==1.0.1
|
| 6 |
+
|
| 7 |
+
# Database
|
| 8 |
+
pymysql==1.1.0
|
| 9 |
+
bcrypt==4.1.2
|
| 10 |
+
SQLAlchemy==2.0.27
|
| 11 |
+
|
| 12 |
+
# Cloud Services
|
| 13 |
+
boto3==1.34.34
|
| 14 |
+
awscli==1.32.34
|
| 15 |
+
|
| 16 |
+
# Auth
|
| 17 |
+
python-jose==3.3.0
|
| 18 |
+
pyasn1==0.4.8
|
| 19 |
+
pyasn1-modules==0.2.8
|
| 20 |
+
|
| 21 |
+
# AI/ML
|
| 22 |
+
google-generativeai==0.3.2
|
| 23 |
+
langchain==0.1.4
|
| 24 |
+
langchain-community==0.0.16
|
| 25 |
+
qdrant-client==1.7.0
|
| 26 |
+
sentence-transformers==2.5.1
|
| 27 |
+
|
| 28 |
+
# Data Processing
|
| 29 |
+
pandas==2.2.0
|
services/.DS_Store
ADDED
|
Binary file (6.15 kB). View file
|
|
|
services/__pycache__/__init__.cpython-312.pyc
ADDED
|
Binary file (224 Bytes). View file
|
|
|
services/api/.DS_Store
ADDED
|
Binary file (6.15 kB). View file
|
|
|
services/api/__pycache__/__init__.cpython-312.pyc
ADDED
|
Binary file (228 Bytes). View file
|
|
|
services/api/__pycache__/api_endpoints.cpython-312.pyc
ADDED
|
Binary file (73.7 kB). View file
|
|
|
services/api/__pycache__/chat_endpoints.cpython-312.pyc
ADDED
|
Binary file (4.99 kB). View file
|
|
|
services/api/__pycache__/video_upload.cpython-312.pyc
ADDED
|
Binary file (4.11 kB). View file
|
|
|
services/api/api_endpoints.py
ADDED
|
@@ -0,0 +1,1853 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from fastapi import APIRouter, Depends, HTTPException, Request, Cookie, UploadFile, File, Form
|
| 2 |
+
from typing import List, Optional, Dict
|
| 3 |
+
from pydantic import BaseModel
|
| 4 |
+
from services.api.db.token_utils import decode_token
|
| 5 |
+
from dotenv import load_dotenv
|
| 6 |
+
import os
|
| 7 |
+
import pymysql
|
| 8 |
+
import pandas as pd
|
| 9 |
+
import boto3
|
| 10 |
+
import json
|
| 11 |
+
from botocore.exceptions import ClientError
|
| 12 |
+
from datetime import datetime
|
| 13 |
+
from fastapi.middleware.cors import CORSMiddleware
|
| 14 |
+
|
| 15 |
+
# Load environment variables
|
| 16 |
+
load_dotenv()
|
| 17 |
+
MYSQL_USER = os.getenv("MYSQL_USER")
|
| 18 |
+
MYSQL_PASSWORD = os.getenv("MYSQL_PASSWORD")
|
| 19 |
+
MYSQL_HOST = os.getenv("MYSQL_HOST", "localhost")
|
| 20 |
+
MYSQL_DB = os.getenv("MYSQL_DB")
|
| 21 |
+
MYSQL_PORT = int(os.getenv("MYSQL_PORT", "3306"))
|
| 22 |
+
|
| 23 |
+
# AWS Configuration
|
| 24 |
+
AWS_ACCESS_KEY = os.getenv("AWS_ACCESS_KEY_ID")
|
| 25 |
+
AWS_SECRET_KEY = os.getenv("AWS_SECRET_ACCESS_KEY")
|
| 26 |
+
REGION = os.getenv("REGION_NAME", "ap-southeast-1")
|
| 27 |
+
|
| 28 |
+
# Initialize S3 client
|
| 29 |
+
s3 = boto3.client('s3',
|
| 30 |
+
aws_access_key_id=AWS_ACCESS_KEY,
|
| 31 |
+
aws_secret_access_key=AWS_SECRET_KEY,
|
| 32 |
+
region_name=REGION
|
| 33 |
+
)
|
| 34 |
+
|
| 35 |
+
router = APIRouter(
|
| 36 |
+
prefix="/api",
|
| 37 |
+
tags=["courses"],
|
| 38 |
+
responses={404: {"description": "Not found"}},
|
| 39 |
+
)
|
| 40 |
+
|
| 41 |
+
# Simple test endpoint to verify routing
|
| 42 |
+
@router.post("/test")
|
| 43 |
+
async def test_post():
|
| 44 |
+
return {"message": "POST test endpoint works"}
|
| 45 |
+
|
| 46 |
+
def connect_db():
|
| 47 |
+
try:
|
| 48 |
+
print(f"Connecting to database {MYSQL_DB} on {MYSQL_HOST}:{MYSQL_PORT}")
|
| 49 |
+
connection = pymysql.connect(
|
| 50 |
+
host=MYSQL_HOST,
|
| 51 |
+
user=MYSQL_USER,
|
| 52 |
+
password=MYSQL_PASSWORD,
|
| 53 |
+
database=MYSQL_DB,
|
| 54 |
+
port=MYSQL_PORT
|
| 55 |
+
)
|
| 56 |
+
print("Database connection successful")
|
| 57 |
+
return connection
|
| 58 |
+
except Exception as e:
|
| 59 |
+
print(f"Database connection error: {str(e)}")
|
| 60 |
+
raise HTTPException(status_code=500, detail=f"Database connection error: {str(e)}")
|
| 61 |
+
|
| 62 |
+
# Models
|
| 63 |
+
class Course(BaseModel):
|
| 64 |
+
id: int
|
| 65 |
+
name: str
|
| 66 |
+
instructor: str
|
| 67 |
+
description: Optional[str]
|
| 68 |
+
rating: Optional[float] = None
|
| 69 |
+
enrolled: Optional[int] = 0
|
| 70 |
+
|
| 71 |
+
class QuestionOption(BaseModel):
|
| 72 |
+
text: str
|
| 73 |
+
isCorrect: bool = False
|
| 74 |
+
|
| 75 |
+
class QuizQuestion(BaseModel):
|
| 76 |
+
question: str
|
| 77 |
+
options: list[str]
|
| 78 |
+
correctAnswer: int
|
| 79 |
+
|
| 80 |
+
class Quiz(BaseModel):
|
| 81 |
+
title: str
|
| 82 |
+
description: Optional[str] = None
|
| 83 |
+
questions: Dict[str, QuizQuestion]
|
| 84 |
+
|
| 85 |
+
class LectureListItem(BaseModel):
|
| 86 |
+
id: int
|
| 87 |
+
title: str
|
| 88 |
+
courseId: int
|
| 89 |
+
description: Optional[str] = None
|
| 90 |
+
|
| 91 |
+
class Lecture(BaseModel):
|
| 92 |
+
id: int
|
| 93 |
+
courseId: int
|
| 94 |
+
title: str
|
| 95 |
+
description: Optional[str] = None
|
| 96 |
+
content: Optional[str] = None
|
| 97 |
+
videoUrl: Optional[str] = None
|
| 98 |
+
quiz: Optional[Quiz] = None
|
| 99 |
+
|
| 100 |
+
class LectureDetails(Lecture):
|
| 101 |
+
courseName: Optional[str] = None
|
| 102 |
+
courseDescription: Optional[str] = None
|
| 103 |
+
courseLectures: Optional[List[LectureListItem]] = None
|
| 104 |
+
|
| 105 |
+
# Get all courses
|
| 106 |
+
@router.get("/courses", response_model=List[Course])
|
| 107 |
+
async def get_courses(request: Request, auth_token: str = Cookie(None)):
|
| 108 |
+
try:
|
| 109 |
+
# Try to get token from Authorization header if cookie is not present
|
| 110 |
+
if not auth_token:
|
| 111 |
+
auth_header = request.headers.get('Authorization')
|
| 112 |
+
if auth_header and auth_header.startswith('Bearer '):
|
| 113 |
+
auth_token = auth_header.split(' ')[1]
|
| 114 |
+
|
| 115 |
+
if not auth_token:
|
| 116 |
+
raise HTTPException(status_code=401, detail="No authentication token provided")
|
| 117 |
+
|
| 118 |
+
# Verify user is authenticated
|
| 119 |
+
try:
|
| 120 |
+
user_data = decode_token(auth_token)
|
| 121 |
+
except Exception as e:
|
| 122 |
+
print(f"Token decode error: {str(e)}")
|
| 123 |
+
raise HTTPException(status_code=401, detail="Invalid authentication token")
|
| 124 |
+
|
| 125 |
+
conn = connect_db()
|
| 126 |
+
courses = []
|
| 127 |
+
|
| 128 |
+
try:
|
| 129 |
+
with conn.cursor(pymysql.cursors.DictCursor) as cursor:
|
| 130 |
+
query = """
|
| 131 |
+
SELECT
|
| 132 |
+
c.CourseID as id,
|
| 133 |
+
c.CourseName as name,
|
| 134 |
+
CONCAT(i.InstructorName, ' (', i.AccountName, ')') as instructor,
|
| 135 |
+
c.Descriptions as description
|
| 136 |
+
FROM Courses c
|
| 137 |
+
JOIN Instructors i ON c.InstructorID = i.InstructorID
|
| 138 |
+
"""
|
| 139 |
+
cursor.execute(query)
|
| 140 |
+
courses = cursor.fetchall()
|
| 141 |
+
|
| 142 |
+
# Get ratings for each course
|
| 143 |
+
for course in courses:
|
| 144 |
+
cursor.execute("""
|
| 145 |
+
SELECT AVG(Rating) as avg_rating, COUNT(*) as count
|
| 146 |
+
FROM Enrollments
|
| 147 |
+
WHERE CourseID = %s AND Rating IS NOT NULL
|
| 148 |
+
""", (course['id'],))
|
| 149 |
+
|
| 150 |
+
rating_data = cursor.fetchone()
|
| 151 |
+
if rating_data and rating_data['avg_rating']:
|
| 152 |
+
course['rating'] = float(rating_data['avg_rating'])
|
| 153 |
+
else:
|
| 154 |
+
course['rating'] = None
|
| 155 |
+
|
| 156 |
+
# Get enrollment count
|
| 157 |
+
cursor.execute("""
|
| 158 |
+
SELECT COUNT(*) as enrolled
|
| 159 |
+
FROM Enrollments
|
| 160 |
+
WHERE CourseID = %s
|
| 161 |
+
""", (course['id'],))
|
| 162 |
+
|
| 163 |
+
enrolled_data = cursor.fetchone()
|
| 164 |
+
if enrolled_data:
|
| 165 |
+
course['enrolled'] = enrolled_data['enrolled']
|
| 166 |
+
else:
|
| 167 |
+
course['enrolled'] = 0
|
| 168 |
+
finally:
|
| 169 |
+
conn.close()
|
| 170 |
+
|
| 171 |
+
return courses
|
| 172 |
+
except Exception as e:
|
| 173 |
+
raise HTTPException(status_code=500, detail=str(e))
|
| 174 |
+
|
| 175 |
+
# Get course details
|
| 176 |
+
@router.get("/courses/{course_id}", response_model=Course)
|
| 177 |
+
async def get_course_details(request: Request, course_id: int, auth_token: str = Cookie(None)):
|
| 178 |
+
try:
|
| 179 |
+
# Try to get token from Authorization header if cookie is not present
|
| 180 |
+
if not auth_token:
|
| 181 |
+
auth_header = request.headers.get('Authorization')
|
| 182 |
+
if auth_header and auth_header.startswith('Bearer '):
|
| 183 |
+
auth_token = auth_header.split(' ')[1]
|
| 184 |
+
|
| 185 |
+
if not auth_token:
|
| 186 |
+
raise HTTPException(status_code=401, detail="No authentication token provided")
|
| 187 |
+
|
| 188 |
+
# Verify user is authenticated
|
| 189 |
+
try:
|
| 190 |
+
user_data = decode_token(auth_token)
|
| 191 |
+
except Exception as e:
|
| 192 |
+
print(f"Token decode error: {str(e)}")
|
| 193 |
+
raise HTTPException(status_code=401, detail="Invalid authentication token")
|
| 194 |
+
|
| 195 |
+
conn = connect_db()
|
| 196 |
+
course = None
|
| 197 |
+
|
| 198 |
+
try:
|
| 199 |
+
with conn.cursor(pymysql.cursors.DictCursor) as cursor:
|
| 200 |
+
# First check if course exists
|
| 201 |
+
query = """
|
| 202 |
+
SELECT
|
| 203 |
+
c.CourseID as id,
|
| 204 |
+
c.CourseName as name,
|
| 205 |
+
CONCAT(i.InstructorName, ' (', i.AccountName, ')') as instructor,
|
| 206 |
+
c.Descriptions as description
|
| 207 |
+
FROM Courses c
|
| 208 |
+
JOIN Instructors i ON c.InstructorID = i.InstructorID
|
| 209 |
+
WHERE c.CourseID = %s
|
| 210 |
+
"""
|
| 211 |
+
cursor.execute(query, (course_id,))
|
| 212 |
+
course = cursor.fetchone()
|
| 213 |
+
|
| 214 |
+
if not course:
|
| 215 |
+
raise HTTPException(status_code=404, detail=f"Course with ID {course_id} not found")
|
| 216 |
+
|
| 217 |
+
# Get ratings and enrollment count in one query
|
| 218 |
+
cursor.execute("""
|
| 219 |
+
SELECT
|
| 220 |
+
COUNT(*) as enrolled,
|
| 221 |
+
COALESCE(AVG(Rating), 0) as avg_rating,
|
| 222 |
+
COUNT(CASE WHEN Rating IS NOT NULL THEN 1 END) as rating_count
|
| 223 |
+
FROM Enrollments
|
| 224 |
+
WHERE CourseID = %s
|
| 225 |
+
""", (course_id,))
|
| 226 |
+
|
| 227 |
+
stats = cursor.fetchone()
|
| 228 |
+
if stats:
|
| 229 |
+
course['enrolled'] = stats['enrolled']
|
| 230 |
+
course['rating'] = float(stats['avg_rating']) if stats['rating_count'] > 0 else None
|
| 231 |
+
else:
|
| 232 |
+
course['enrolled'] = 0
|
| 233 |
+
course['rating'] = None
|
| 234 |
+
|
| 235 |
+
finally:
|
| 236 |
+
conn.close()
|
| 237 |
+
|
| 238 |
+
return course
|
| 239 |
+
except HTTPException:
|
| 240 |
+
raise
|
| 241 |
+
except Exception as e:
|
| 242 |
+
print(f"Error in get_course_details: {str(e)}")
|
| 243 |
+
raise HTTPException(status_code=500, detail=str(e))
|
| 244 |
+
|
| 245 |
+
# Get lectures for a course
|
| 246 |
+
@router.get("/courses/{course_id}/lectures", response_model=List[Lecture])
|
| 247 |
+
async def get_course_lectures(request: Request, course_id: int, auth_token: str = Cookie(None)):
|
| 248 |
+
try:
|
| 249 |
+
# Try to get token from Authorization header if cookie is not present
|
| 250 |
+
if not auth_token:
|
| 251 |
+
auth_header = request.headers.get('Authorization')
|
| 252 |
+
if auth_header and auth_header.startswith('Bearer '):
|
| 253 |
+
auth_token = auth_header.split(' ')[1]
|
| 254 |
+
|
| 255 |
+
if not auth_token:
|
| 256 |
+
raise HTTPException(status_code=401, detail="No authentication token provided")
|
| 257 |
+
|
| 258 |
+
try:
|
| 259 |
+
user_data = decode_token(auth_token)
|
| 260 |
+
except Exception as e:
|
| 261 |
+
print(f"Token decode error: {str(e)}")
|
| 262 |
+
raise HTTPException(status_code=401, detail="Invalid authentication token")
|
| 263 |
+
|
| 264 |
+
conn = connect_db()
|
| 265 |
+
lectures = []
|
| 266 |
+
|
| 267 |
+
try:
|
| 268 |
+
with conn.cursor(pymysql.cursors.DictCursor) as cursor:
|
| 269 |
+
# First verify the course exists
|
| 270 |
+
cursor.execute("SELECT CourseID FROM Courses WHERE CourseID = %s", (course_id,))
|
| 271 |
+
if not cursor.fetchone():
|
| 272 |
+
raise HTTPException(status_code=404, detail="Course not found")
|
| 273 |
+
|
| 274 |
+
query = """
|
| 275 |
+
SELECT
|
| 276 |
+
LectureID as id,
|
| 277 |
+
CourseID as courseId,
|
| 278 |
+
Title as title,
|
| 279 |
+
Description as description
|
| 280 |
+
FROM Lectures
|
| 281 |
+
WHERE CourseID = %s
|
| 282 |
+
ORDER BY LectureID
|
| 283 |
+
"""
|
| 284 |
+
cursor.execute(query, (course_id,))
|
| 285 |
+
lectures = cursor.fetchall()
|
| 286 |
+
|
| 287 |
+
# Ensure all fields match the Lecture model
|
| 288 |
+
for lecture in lectures:
|
| 289 |
+
# Ensure the fields exist and have appropriate null values if missing
|
| 290 |
+
if lecture.get('description') is None:
|
| 291 |
+
lecture['description'] = None
|
| 292 |
+
finally:
|
| 293 |
+
conn.close()
|
| 294 |
+
|
| 295 |
+
return lectures
|
| 296 |
+
except HTTPException:
|
| 297 |
+
raise
|
| 298 |
+
except Exception as e:
|
| 299 |
+
print(f"Error in get_course_lectures: {str(e)}")
|
| 300 |
+
raise HTTPException(status_code=500, detail=str(e))
|
| 301 |
+
|
| 302 |
+
# Get lecture details
|
| 303 |
+
@router.get("/lectures/{lecture_id}", response_model=LectureDetails)
|
| 304 |
+
async def get_lecture_details(request: Request, lecture_id: int, auth_token: str = Cookie(None)):
|
| 305 |
+
try:
|
| 306 |
+
# Try to get token from Authorization header if cookie is not present
|
| 307 |
+
if not auth_token:
|
| 308 |
+
auth_header = request.headers.get('Authorization')
|
| 309 |
+
if auth_header and auth_header.startswith('Bearer '):
|
| 310 |
+
auth_token = auth_header.split(' ')[1]
|
| 311 |
+
|
| 312 |
+
if not auth_token:
|
| 313 |
+
raise HTTPException(status_code=401, detail="No authentication token provided")
|
| 314 |
+
|
| 315 |
+
# Verify user is authenticated
|
| 316 |
+
try:
|
| 317 |
+
user_data = decode_token(auth_token)
|
| 318 |
+
except Exception as e:
|
| 319 |
+
print(f"Token decode error: {str(e)}")
|
| 320 |
+
raise HTTPException(status_code=401, detail="Invalid authentication token")
|
| 321 |
+
|
| 322 |
+
conn = connect_db()
|
| 323 |
+
try:
|
| 324 |
+
with conn.cursor(pymysql.cursors.DictCursor) as cursor:
|
| 325 |
+
# Get lecture details with course data
|
| 326 |
+
query = """
|
| 327 |
+
SELECT
|
| 328 |
+
l.LectureID as id,
|
| 329 |
+
l.CourseID as courseId,
|
| 330 |
+
l.Title as title,
|
| 331 |
+
l.Description as description,
|
| 332 |
+
l.Content as content,
|
| 333 |
+
c.CourseName as courseName,
|
| 334 |
+
c.CourseID,
|
| 335 |
+
c.Descriptions as courseDescription
|
| 336 |
+
FROM Lectures l
|
| 337 |
+
JOIN Courses c ON l.CourseID = c.CourseID
|
| 338 |
+
WHERE l.LectureID = %s
|
| 339 |
+
"""
|
| 340 |
+
cursor.execute(query, (lecture_id,))
|
| 341 |
+
lecture = cursor.fetchone()
|
| 342 |
+
|
| 343 |
+
if not lecture:
|
| 344 |
+
raise HTTPException(status_code=404, detail="Lecture not found")
|
| 345 |
+
|
| 346 |
+
# Get course lectures
|
| 347 |
+
cursor.execute("""
|
| 348 |
+
SELECT
|
| 349 |
+
LectureID as id,
|
| 350 |
+
CourseID as courseId,
|
| 351 |
+
Title as title,
|
| 352 |
+
Description as description
|
| 353 |
+
FROM Lectures
|
| 354 |
+
WHERE CourseID = %s
|
| 355 |
+
ORDER BY LectureID
|
| 356 |
+
""", (lecture['courseId'],))
|
| 357 |
+
|
| 358 |
+
course_lectures = cursor.fetchall()
|
| 359 |
+
if not course_lectures:
|
| 360 |
+
course_lectures = []
|
| 361 |
+
|
| 362 |
+
response_data = {
|
| 363 |
+
"id": lecture['id'],
|
| 364 |
+
"courseId": lecture['courseId'],
|
| 365 |
+
"title": lecture['title'],
|
| 366 |
+
"description": lecture['description'],
|
| 367 |
+
"content": lecture['content'],
|
| 368 |
+
"courseName": lecture['courseName'],
|
| 369 |
+
"courseDescription": lecture['courseDescription'],
|
| 370 |
+
"courseLectures": course_lectures,
|
| 371 |
+
"videoUrl": None,
|
| 372 |
+
"quiz": None # Initialize quiz as None
|
| 373 |
+
}
|
| 374 |
+
|
| 375 |
+
# Get video URL if exists
|
| 376 |
+
video_path = f"videos/cid{lecture['courseId']}/lid{lecture['id']}/vid_lecture.mp4"
|
| 377 |
+
try:
|
| 378 |
+
s3.head_object(Bucket="tlhmaterials", Key=video_path)
|
| 379 |
+
response_data['videoUrl'] = f"https://tlhmaterials.s3-{REGION}.amazonaws.com/{video_path}"
|
| 380 |
+
except:
|
| 381 |
+
pass # Keep videoUrl as None if no video exists
|
| 382 |
+
|
| 383 |
+
# Get quiz if exists - fixing this part
|
| 384 |
+
cursor.execute("""
|
| 385 |
+
SELECT q.QuizID, q.Title, q.Description
|
| 386 |
+
FROM Quizzes q
|
| 387 |
+
WHERE q.LectureID = %s
|
| 388 |
+
""", (lecture_id,))
|
| 389 |
+
|
| 390 |
+
quiz_data = cursor.fetchone()
|
| 391 |
+
if quiz_data:
|
| 392 |
+
quiz = {
|
| 393 |
+
"id": quiz_data['QuizID'],
|
| 394 |
+
"title": quiz_data['Title'],
|
| 395 |
+
"description": quiz_data['Description'],
|
| 396 |
+
"questions": {}
|
| 397 |
+
}
|
| 398 |
+
|
| 399 |
+
# Get quiz questions
|
| 400 |
+
cursor.execute("""
|
| 401 |
+
SELECT
|
| 402 |
+
q.QuestionID,
|
| 403 |
+
q.QuestionText,
|
| 404 |
+
o.OptionID,
|
| 405 |
+
o.OptionText,
|
| 406 |
+
o.IsCorrect
|
| 407 |
+
FROM Questions q
|
| 408 |
+
JOIN Options o ON q.QuestionID = o.QuestionID
|
| 409 |
+
WHERE q.QuizID = %s
|
| 410 |
+
ORDER BY q.QuestionID, o.OptionID
|
| 411 |
+
""", (quiz_data['QuizID'],))
|
| 412 |
+
|
| 413 |
+
questions_data = cursor.fetchall()
|
| 414 |
+
current_question_id = None
|
| 415 |
+
current_options = []
|
| 416 |
+
correct_option_index = 0
|
| 417 |
+
|
| 418 |
+
for row in questions_data:
|
| 419 |
+
if current_question_id != row['QuestionID']:
|
| 420 |
+
# Save previous question data
|
| 421 |
+
if current_question_id is not None:
|
| 422 |
+
quiz['questions'][str(current_question_id)] = {
|
| 423 |
+
'question': question_text,
|
| 424 |
+
'options': current_options,
|
| 425 |
+
'correctAnswer': correct_option_index
|
| 426 |
+
}
|
| 427 |
+
|
| 428 |
+
# Start new question
|
| 429 |
+
current_question_id = row['QuestionID']
|
| 430 |
+
question_text = row['QuestionText']
|
| 431 |
+
current_options = []
|
| 432 |
+
correct_option_index = 0
|
| 433 |
+
|
| 434 |
+
current_options.append(row['OptionText'])
|
| 435 |
+
if row['IsCorrect']:
|
| 436 |
+
correct_option_index = len(current_options) - 1
|
| 437 |
+
|
| 438 |
+
# Save the last question
|
| 439 |
+
if current_question_id is not None:
|
| 440 |
+
quiz['questions'][str(current_question_id)] = {
|
| 441 |
+
'question': question_text,
|
| 442 |
+
'options': current_options,
|
| 443 |
+
'correctAnswer': correct_option_index
|
| 444 |
+
}
|
| 445 |
+
|
| 446 |
+
response_data['quiz'] = quiz
|
| 447 |
+
|
| 448 |
+
return response_data
|
| 449 |
+
|
| 450 |
+
finally:
|
| 451 |
+
conn.close()
|
| 452 |
+
|
| 453 |
+
except HTTPException:
|
| 454 |
+
raise
|
| 455 |
+
except Exception as e:
|
| 456 |
+
print(f"Error in get_lecture_details: {str(e)}")
|
| 457 |
+
raise HTTPException(status_code=500, detail=str(e))
|
| 458 |
+
|
| 459 |
+
# Get instructor's courses
|
| 460 |
+
@router.get("/instructor/courses", response_model=List[Course])
|
| 461 |
+
async def get_instructor_courses(
|
| 462 |
+
request: Request,
|
| 463 |
+
auth_token: str = Cookie(None)
|
| 464 |
+
):
|
| 465 |
+
try:
|
| 466 |
+
# Get token from header if not in cookie
|
| 467 |
+
if not auth_token:
|
| 468 |
+
auth_header = request.headers.get('Authorization')
|
| 469 |
+
if auth_header and auth_header.startswith('Bearer '):
|
| 470 |
+
auth_token = auth_header.split(' ')[1]
|
| 471 |
+
else:
|
| 472 |
+
raise HTTPException(status_code=401, detail="No authentication token provided")
|
| 473 |
+
|
| 474 |
+
# Verify token and get user data
|
| 475 |
+
try:
|
| 476 |
+
user_data = decode_token(auth_token)
|
| 477 |
+
username = user_data['username']
|
| 478 |
+
role = user_data['role']
|
| 479 |
+
|
| 480 |
+
# Verify user is an instructor
|
| 481 |
+
if role != "Instructor":
|
| 482 |
+
raise HTTPException(status_code=403, detail="Only instructors can access this endpoint")
|
| 483 |
+
|
| 484 |
+
# Connect to database
|
| 485 |
+
conn = connect_db()
|
| 486 |
+
|
| 487 |
+
with conn.cursor(pymysql.cursors.DictCursor) as cursor:
|
| 488 |
+
# Get instructor ID
|
| 489 |
+
cursor.execute("""
|
| 490 |
+
SELECT InstructorID
|
| 491 |
+
FROM Instructors
|
| 492 |
+
WHERE AccountName = %s
|
| 493 |
+
""", (username,))
|
| 494 |
+
|
| 495 |
+
instructor = cursor.fetchone()
|
| 496 |
+
if not instructor:
|
| 497 |
+
raise HTTPException(status_code=404, detail="Instructor not found")
|
| 498 |
+
|
| 499 |
+
instructor_id = instructor['InstructorID']
|
| 500 |
+
|
| 501 |
+
# Get courses by this instructor
|
| 502 |
+
query = """
|
| 503 |
+
SELECT
|
| 504 |
+
c.CourseID as id,
|
| 505 |
+
c.CourseName as name,
|
| 506 |
+
CONCAT(i.InstructorName, ' (', i.AccountName, ')') as instructor,
|
| 507 |
+
c.Descriptions as description,
|
| 508 |
+
(SELECT COUNT(*) FROM Enrollments WHERE CourseID = c.CourseID) as enrolled,
|
| 509 |
+
COALESCE(
|
| 510 |
+
(SELECT AVG(Rating)
|
| 511 |
+
FROM Enrollments
|
| 512 |
+
WHERE CourseID = c.CourseID AND Rating IS NOT NULL),
|
| 513 |
+
0
|
| 514 |
+
) as rating
|
| 515 |
+
FROM Courses c
|
| 516 |
+
JOIN Instructors i ON c.InstructorID = i.InstructorID
|
| 517 |
+
WHERE c.InstructorID = %s
|
| 518 |
+
ORDER BY c.CourseID DESC
|
| 519 |
+
"""
|
| 520 |
+
|
| 521 |
+
cursor.execute(query, (instructor_id,))
|
| 522 |
+
courses = cursor.fetchall()
|
| 523 |
+
|
| 524 |
+
# Format the courses data
|
| 525 |
+
formatted_courses = []
|
| 526 |
+
for course in courses:
|
| 527 |
+
formatted_course = {
|
| 528 |
+
'id': course['id'],
|
| 529 |
+
'name': course['name'],
|
| 530 |
+
'instructor': course['instructor'],
|
| 531 |
+
'description': course['description'],
|
| 532 |
+
'enrolled': course['enrolled'],
|
| 533 |
+
'rating': float(course['rating']) if course['rating'] else None
|
| 534 |
+
}
|
| 535 |
+
formatted_courses.append(formatted_course)
|
| 536 |
+
|
| 537 |
+
return formatted_courses
|
| 538 |
+
|
| 539 |
+
except Exception as e:
|
| 540 |
+
print(f"Database error: {str(e)}")
|
| 541 |
+
raise HTTPException(status_code=500, detail=f"Database error: {str(e)}")
|
| 542 |
+
|
| 543 |
+
except HTTPException as he:
|
| 544 |
+
raise he
|
| 545 |
+
except Exception as e:
|
| 546 |
+
print(f"Error fetching instructor courses: {str(e)}")
|
| 547 |
+
raise HTTPException(status_code=500, detail=str(e))
|
| 548 |
+
finally:
|
| 549 |
+
if 'conn' in locals():
|
| 550 |
+
conn.close()
|
| 551 |
+
|
| 552 |
+
# Create a new course
|
| 553 |
+
class CourseCreate(BaseModel):
|
| 554 |
+
name: str
|
| 555 |
+
description: str
|
| 556 |
+
skills: List[str]
|
| 557 |
+
difficulty: str
|
| 558 |
+
duration: Optional[int] = None
|
| 559 |
+
|
| 560 |
+
@router.post("/instructor/courses", response_model=Course)
|
| 561 |
+
async def create_course(
|
| 562 |
+
course_data: CourseCreate,
|
| 563 |
+
request: Request,
|
| 564 |
+
auth_token: str = Cookie(None)
|
| 565 |
+
):
|
| 566 |
+
try:
|
| 567 |
+
# Get token from header if not in cookie
|
| 568 |
+
if not auth_token:
|
| 569 |
+
auth_header = request.headers.get('Authorization')
|
| 570 |
+
if auth_header and auth_header.startswith('Bearer '):
|
| 571 |
+
auth_token = auth_header.split(' ')[1]
|
| 572 |
+
else:
|
| 573 |
+
raise HTTPException(status_code=401, detail="No authentication token provided")
|
| 574 |
+
|
| 575 |
+
# Verify token and get user data
|
| 576 |
+
try:
|
| 577 |
+
user_data = decode_token(auth_token)
|
| 578 |
+
username = user_data['username']
|
| 579 |
+
role = user_data['role']
|
| 580 |
+
|
| 581 |
+
# Log debugging information
|
| 582 |
+
print(f"POST /instructor/courses - User: {username}, Role: {role}")
|
| 583 |
+
print(f"Course data: {course_data}")
|
| 584 |
+
|
| 585 |
+
# Verify user is an instructor
|
| 586 |
+
if role != "Instructor":
|
| 587 |
+
raise HTTPException(status_code=403, detail="Only instructors can create courses")
|
| 588 |
+
|
| 589 |
+
# Connect to database
|
| 590 |
+
conn = connect_db()
|
| 591 |
+
|
| 592 |
+
with conn.cursor(pymysql.cursors.DictCursor) as cursor:
|
| 593 |
+
# Get instructor ID
|
| 594 |
+
cursor.execute("""
|
| 595 |
+
SELECT InstructorID
|
| 596 |
+
FROM Instructors
|
| 597 |
+
WHERE AccountName = %s
|
| 598 |
+
""", (username,))
|
| 599 |
+
|
| 600 |
+
instructor = cursor.fetchone()
|
| 601 |
+
if not instructor:
|
| 602 |
+
raise HTTPException(status_code=404, detail="Instructor not found")
|
| 603 |
+
|
| 604 |
+
instructor_id = instructor['InstructorID']
|
| 605 |
+
|
| 606 |
+
# Insert new course
|
| 607 |
+
cursor.execute("""
|
| 608 |
+
INSERT INTO Courses
|
| 609 |
+
(CourseName, Descriptions, Skills, Difficulty, EstimatedDuration, InstructorID)
|
| 610 |
+
VALUES (%s, %s, %s, %s, %s, %s)
|
| 611 |
+
""", (
|
| 612 |
+
course_data.name,
|
| 613 |
+
course_data.description,
|
| 614 |
+
json.dumps(course_data.skills),
|
| 615 |
+
course_data.difficulty,
|
| 616 |
+
course_data.duration or "Self-paced",
|
| 617 |
+
instructor_id
|
| 618 |
+
))
|
| 619 |
+
|
| 620 |
+
# Get the created course ID
|
| 621 |
+
course_id = cursor.lastrowid
|
| 622 |
+
conn.commit()
|
| 623 |
+
|
| 624 |
+
# Return the created course
|
| 625 |
+
cursor.execute("""
|
| 626 |
+
SELECT
|
| 627 |
+
c.CourseID as id,
|
| 628 |
+
c.CourseName as name,
|
| 629 |
+
CONCAT(i.InstructorName, ' (', i.AccountName, ')') as instructor,
|
| 630 |
+
c.Descriptions as description,
|
| 631 |
+
0 as enrolled,
|
| 632 |
+
NULL as rating
|
| 633 |
+
FROM Courses c
|
| 634 |
+
JOIN Instructors i ON c.InstructorID = i.InstructorID
|
| 635 |
+
WHERE c.CourseID = %s
|
| 636 |
+
""", (course_id,))
|
| 637 |
+
|
| 638 |
+
new_course = cursor.fetchone()
|
| 639 |
+
if not new_course:
|
| 640 |
+
raise HTTPException(status_code=500, detail="Course was created but couldn't be retrieved")
|
| 641 |
+
|
| 642 |
+
return new_course
|
| 643 |
+
|
| 644 |
+
except Exception as e:
|
| 645 |
+
if 'conn' in locals():
|
| 646 |
+
conn.rollback()
|
| 647 |
+
print(f"Database error: {str(e)}")
|
| 648 |
+
raise HTTPException(status_code=500, detail=f"Database error: {str(e)}")
|
| 649 |
+
|
| 650 |
+
except HTTPException as he:
|
| 651 |
+
raise he
|
| 652 |
+
except Exception as e:
|
| 653 |
+
print(f"Error creating course: {str(e)}")
|
| 654 |
+
raise HTTPException(status_code=500, detail=str(e))
|
| 655 |
+
finally:
|
| 656 |
+
if 'conn' in locals():
|
| 657 |
+
conn.close()
|
| 658 |
+
|
| 659 |
+
# Enroll in a course
|
| 660 |
+
@router.post("/courses/{course_id}/enroll")
|
| 661 |
+
async def enroll_in_course(
|
| 662 |
+
course_id: int,
|
| 663 |
+
request: Request,
|
| 664 |
+
auth_token: str = Cookie(None)
|
| 665 |
+
):
|
| 666 |
+
try:
|
| 667 |
+
# Get token from header if not in cookie
|
| 668 |
+
if not auth_token:
|
| 669 |
+
auth_header = request.headers.get('Authorization')
|
| 670 |
+
if auth_header and auth_header.startswith('Bearer '):
|
| 671 |
+
auth_token = auth_header.split(' ')[1]
|
| 672 |
+
else:
|
| 673 |
+
raise HTTPException(status_code=401, detail="No authentication token provided")
|
| 674 |
+
|
| 675 |
+
# Verify token and get user data
|
| 676 |
+
try:
|
| 677 |
+
user_data = decode_token(auth_token)
|
| 678 |
+
|
| 679 |
+
# Get LearnerID from Learners table using the username
|
| 680 |
+
conn = connect_db()
|
| 681 |
+
with conn.cursor(pymysql.cursors.DictCursor) as cursor:
|
| 682 |
+
cursor.execute("""
|
| 683 |
+
SELECT LearnerID
|
| 684 |
+
FROM Learners
|
| 685 |
+
WHERE AccountName = %s
|
| 686 |
+
""", (user_data['username'],))
|
| 687 |
+
learner = cursor.fetchone()
|
| 688 |
+
if not learner:
|
| 689 |
+
raise HTTPException(status_code=404, detail="Learner not found")
|
| 690 |
+
learner_id = learner['LearnerID']
|
| 691 |
+
except Exception as e:
|
| 692 |
+
print(f"Token/user verification error: {str(e)}")
|
| 693 |
+
raise HTTPException(status_code=401, detail="Invalid authentication token or user not found")
|
| 694 |
+
|
| 695 |
+
# Check if the course exists
|
| 696 |
+
with conn.cursor(pymysql.cursors.DictCursor) as cursor:
|
| 697 |
+
cursor.execute("""
|
| 698 |
+
SELECT CourseID
|
| 699 |
+
FROM Courses
|
| 700 |
+
WHERE CourseID = %s
|
| 701 |
+
""", (course_id,))
|
| 702 |
+
course = cursor.fetchone()
|
| 703 |
+
if not course:
|
| 704 |
+
raise HTTPException(status_code=404, detail="Course not found")
|
| 705 |
+
|
| 706 |
+
# Check if already enrolled
|
| 707 |
+
cursor.execute("""
|
| 708 |
+
SELECT EnrollmentID
|
| 709 |
+
FROM Enrollments
|
| 710 |
+
WHERE LearnerID = %s AND CourseID = %s
|
| 711 |
+
""", (learner_id, course_id))
|
| 712 |
+
existing_enrollment = cursor.fetchone()
|
| 713 |
+
if existing_enrollment:
|
| 714 |
+
return {"message": "Already enrolled in this course"}
|
| 715 |
+
|
| 716 |
+
# Get the actual column names from the Enrollments table
|
| 717 |
+
cursor.execute("DESCRIBE Enrollments")
|
| 718 |
+
columns = cursor.fetchall()
|
| 719 |
+
column_names = [col['Field'] for col in columns]
|
| 720 |
+
print(f"Available columns in Enrollments table: {column_names}")
|
| 721 |
+
|
| 722 |
+
# Enroll the learner in the course with the correct date column
|
| 723 |
+
try:
|
| 724 |
+
# Try different common column names for the enrollment date
|
| 725 |
+
enroll_date = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
|
| 726 |
+
cursor.execute(
|
| 727 |
+
"CALL sp_EnrollLearner(%s, %s, %s)",
|
| 728 |
+
(learner_id, course_id, enroll_date)
|
| 729 |
+
)
|
| 730 |
+
|
| 731 |
+
conn.commit()
|
| 732 |
+
return {"message": "Successfully enrolled in the course"}
|
| 733 |
+
except Exception as e:
|
| 734 |
+
conn.rollback()
|
| 735 |
+
print(f"Error enrolling in course: {str(e)}")
|
| 736 |
+
raise HTTPException(status_code=500, detail=f"Failed to enroll in course: {str(e)}")
|
| 737 |
+
|
| 738 |
+
except HTTPException as he:
|
| 739 |
+
raise he
|
| 740 |
+
except Exception as e:
|
| 741 |
+
print(f"Error enrolling in course: {str(e)}")
|
| 742 |
+
raise HTTPException(status_code=500, detail=f"Error enrolling in course: {str(e)}")
|
| 743 |
+
finally:
|
| 744 |
+
if 'conn' in locals():
|
| 745 |
+
conn.close()
|
| 746 |
+
|
| 747 |
+
# Get enrolled courses for the current user
|
| 748 |
+
@router.get("/user/courses", response_model=List[Course])
|
| 749 |
+
async def get_enrolled_courses(
|
| 750 |
+
request: Request,
|
| 751 |
+
auth_token: str = Cookie(None)
|
| 752 |
+
):
|
| 753 |
+
try:
|
| 754 |
+
# Get token from header if not in cookie
|
| 755 |
+
if not auth_token:
|
| 756 |
+
auth_header = request.headers.get('Authorization')
|
| 757 |
+
if auth_header and auth_header.startswith('Bearer '):
|
| 758 |
+
auth_token = auth_header.split(' ')[1]
|
| 759 |
+
else:
|
| 760 |
+
raise HTTPException(status_code=401, detail="No authentication token provided")
|
| 761 |
+
|
| 762 |
+
# Verify token and get user data
|
| 763 |
+
try:
|
| 764 |
+
user_data = decode_token(auth_token)
|
| 765 |
+
|
| 766 |
+
# Get LearnerID from Learners table using the username
|
| 767 |
+
conn = connect_db()
|
| 768 |
+
with conn.cursor(pymysql.cursors.DictCursor) as cursor:
|
| 769 |
+
cursor.execute("""
|
| 770 |
+
SELECT LearnerID
|
| 771 |
+
FROM Learners
|
| 772 |
+
WHERE AccountName = %s
|
| 773 |
+
""", (user_data['username'],))
|
| 774 |
+
learner = cursor.fetchone()
|
| 775 |
+
if not learner:
|
| 776 |
+
raise HTTPException(status_code=404, detail="Learner not found")
|
| 777 |
+
learner_id = learner['LearnerID']
|
| 778 |
+
except Exception as e:
|
| 779 |
+
print(f"Token/user verification error: {str(e)}")
|
| 780 |
+
raise HTTPException(status_code=401, detail="Invalid authentication token or user not found")
|
| 781 |
+
|
| 782 |
+
# Get enrolled courses
|
| 783 |
+
courses = []
|
| 784 |
+
with conn.cursor(pymysql.cursors.DictCursor) as cursor:
|
| 785 |
+
query = """
|
| 786 |
+
SELECT
|
| 787 |
+
c.CourseID as id,
|
| 788 |
+
c.CourseName as name,
|
| 789 |
+
CONCAT(i.InstructorName, ' (', i.AccountName, ')') as instructor,
|
| 790 |
+
c.Descriptions as description
|
| 791 |
+
FROM Courses c
|
| 792 |
+
JOIN Instructors i ON c.InstructorID = i.InstructorID
|
| 793 |
+
JOIN Enrollments e ON c.CourseID = e.CourseID
|
| 794 |
+
WHERE e.LearnerID = %s
|
| 795 |
+
"""
|
| 796 |
+
cursor.execute(query, (learner_id,))
|
| 797 |
+
courses = cursor.fetchall()
|
| 798 |
+
|
| 799 |
+
# Get ratings and enrollment count for each course
|
| 800 |
+
for course in courses:
|
| 801 |
+
cursor.execute("""
|
| 802 |
+
SELECT AVG(Rating) as avg_rating, COUNT(*) as count
|
| 803 |
+
FROM Enrollments
|
| 804 |
+
WHERE CourseID = %s AND Rating IS NOT NULL
|
| 805 |
+
""", (course['id'],))
|
| 806 |
+
|
| 807 |
+
rating_data = cursor.fetchone()
|
| 808 |
+
if rating_data and rating_data['avg_rating']:
|
| 809 |
+
course['rating'] = float(rating_data['avg_rating'])
|
| 810 |
+
else:
|
| 811 |
+
course['rating'] = None
|
| 812 |
+
|
| 813 |
+
# Get enrollment count
|
| 814 |
+
cursor.execute("""
|
| 815 |
+
SELECT COUNT(*) as enrolled
|
| 816 |
+
FROM Enrollments
|
| 817 |
+
WHERE CourseID = %s
|
| 818 |
+
""", (course['id'],))
|
| 819 |
+
|
| 820 |
+
enrolled_data = cursor.fetchone()
|
| 821 |
+
if enrolled_data:
|
| 822 |
+
course['enrolled'] = enrolled_data['enrolled']
|
| 823 |
+
else:
|
| 824 |
+
course['enrolled'] = 0
|
| 825 |
+
|
| 826 |
+
return courses
|
| 827 |
+
|
| 828 |
+
except HTTPException as he:
|
| 829 |
+
raise he
|
| 830 |
+
except Exception as e:
|
| 831 |
+
print(f"Error fetching enrolled courses: {str(e)}")
|
| 832 |
+
raise HTTPException(status_code=500, detail=f"Error fetching enrolled courses: {str(e)}")
|
| 833 |
+
finally:
|
| 834 |
+
if 'conn' in locals():
|
| 835 |
+
conn.close()
|
| 836 |
+
|
| 837 |
+
# Get user profile
|
| 838 |
+
@router.get("/user/profile")
|
| 839 |
+
async def get_user_profile(
|
| 840 |
+
request: Request,
|
| 841 |
+
auth_token: str = Cookie(None)
|
| 842 |
+
):
|
| 843 |
+
try:
|
| 844 |
+
# Get token from header if not in cookie
|
| 845 |
+
if not auth_token:
|
| 846 |
+
auth_header = request.headers.get('Authorization')
|
| 847 |
+
if auth_header and auth_header.startswith('Bearer '):
|
| 848 |
+
auth_token = auth_header.split(' ')[1]
|
| 849 |
+
else:
|
| 850 |
+
raise HTTPException(status_code=401, detail="No authentication token provided")
|
| 851 |
+
|
| 852 |
+
# Verify token and get user data
|
| 853 |
+
try:
|
| 854 |
+
user_data = decode_token(auth_token)
|
| 855 |
+
username = user_data['username']
|
| 856 |
+
role = user_data['role']
|
| 857 |
+
|
| 858 |
+
# Connect to database
|
| 859 |
+
conn = connect_db()
|
| 860 |
+
|
| 861 |
+
# Get user information based on role
|
| 862 |
+
with conn.cursor(pymysql.cursors.DictCursor) as cursor:
|
| 863 |
+
if role == "Learner":
|
| 864 |
+
cursor.execute("""
|
| 865 |
+
SELECT
|
| 866 |
+
LearnerName as name,
|
| 867 |
+
Email as email,
|
| 868 |
+
PhoneNumber as phoneNumber
|
| 869 |
+
FROM Learners
|
| 870 |
+
WHERE AccountName = %s
|
| 871 |
+
""", (username,))
|
| 872 |
+
user_info = cursor.fetchone()
|
| 873 |
+
|
| 874 |
+
if not user_info:
|
| 875 |
+
raise HTTPException(status_code=404, detail="Learner not found")
|
| 876 |
+
|
| 877 |
+
elif role == "Instructor":
|
| 878 |
+
cursor.execute("""
|
| 879 |
+
SELECT
|
| 880 |
+
InstructorName as name,
|
| 881 |
+
Email as email,
|
| 882 |
+
Expertise as expertise
|
| 883 |
+
FROM Instructors
|
| 884 |
+
WHERE AccountName = %s
|
| 885 |
+
""", (username,))
|
| 886 |
+
user_info = cursor.fetchone()
|
| 887 |
+
|
| 888 |
+
if not user_info:
|
| 889 |
+
raise HTTPException(status_code=404, detail="Instructor not found")
|
| 890 |
+
else:
|
| 891 |
+
raise HTTPException(status_code=403, detail="Invalid user role")
|
| 892 |
+
|
| 893 |
+
# Add role and username to the response
|
| 894 |
+
user_info['role'] = role
|
| 895 |
+
user_info['username'] = username
|
| 896 |
+
|
| 897 |
+
return user_info
|
| 898 |
+
|
| 899 |
+
except Exception as e:
|
| 900 |
+
print(f"Token/user verification error: {str(e)}")
|
| 901 |
+
raise HTTPException(status_code=401, detail="Invalid authentication token or user not found")
|
| 902 |
+
except HTTPException as he:
|
| 903 |
+
raise he
|
| 904 |
+
except Exception as e:
|
| 905 |
+
print(f"Error fetching user profile: {str(e)}")
|
| 906 |
+
raise HTTPException(status_code=500, detail=f"Error fetching user profile: {str(e)}")
|
| 907 |
+
finally:
|
| 908 |
+
if 'conn' in locals():
|
| 909 |
+
conn.close()
|
| 910 |
+
|
| 911 |
+
# Update user profile
|
| 912 |
+
@router.put("/user/profile")
|
| 913 |
+
async def update_user_profile(
|
| 914 |
+
request: Request,
|
| 915 |
+
auth_token: str = Cookie(None)
|
| 916 |
+
):
|
| 917 |
+
try:
|
| 918 |
+
# Get token from header if not in cookie
|
| 919 |
+
if not auth_token:
|
| 920 |
+
auth_header = request.headers.get('Authorization')
|
| 921 |
+
if auth_header and auth_header.startswith('Bearer '):
|
| 922 |
+
auth_token = auth_header.split(' ')[1]
|
| 923 |
+
else:
|
| 924 |
+
raise HTTPException(status_code=401, detail="No authentication token provided")
|
| 925 |
+
|
| 926 |
+
# Verify token and get user data
|
| 927 |
+
try:
|
| 928 |
+
user_data = decode_token(auth_token)
|
| 929 |
+
username = user_data['username']
|
| 930 |
+
role = user_data['role']
|
| 931 |
+
|
| 932 |
+
# Get request body
|
| 933 |
+
profile_data = await request.json()
|
| 934 |
+
|
| 935 |
+
# Connect to database
|
| 936 |
+
conn = connect_db()
|
| 937 |
+
|
| 938 |
+
# Update user information based on role
|
| 939 |
+
with conn.cursor(pymysql.cursors.DictCursor) as cursor:
|
| 940 |
+
if role == "Learner":
|
| 941 |
+
# Prepare update fields
|
| 942 |
+
update_fields = []
|
| 943 |
+
params = []
|
| 944 |
+
|
| 945 |
+
if 'name' in profile_data:
|
| 946 |
+
update_fields.append("LearnerName = %s")
|
| 947 |
+
params.append(profile_data['name'])
|
| 948 |
+
|
| 949 |
+
if 'email' in profile_data:
|
| 950 |
+
update_fields.append("Email = %s")
|
| 951 |
+
params.append(profile_data['email'])
|
| 952 |
+
|
| 953 |
+
if 'phoneNumber' in profile_data:
|
| 954 |
+
update_fields.append("PhoneNumber = %s")
|
| 955 |
+
params.append(profile_data['phoneNumber'])
|
| 956 |
+
|
| 957 |
+
if not update_fields:
|
| 958 |
+
return {"message": "No fields to update"}
|
| 959 |
+
|
| 960 |
+
# Add username to params
|
| 961 |
+
params.append(username)
|
| 962 |
+
|
| 963 |
+
# Construct and execute SQL
|
| 964 |
+
sql = f"""
|
| 965 |
+
UPDATE Learners
|
| 966 |
+
SET {', '.join(update_fields)}
|
| 967 |
+
WHERE AccountName = %s
|
| 968 |
+
"""
|
| 969 |
+
cursor.execute(sql, params)
|
| 970 |
+
|
| 971 |
+
elif role == "Instructor":
|
| 972 |
+
# Prepare update fields
|
| 973 |
+
update_fields = []
|
| 974 |
+
params = []
|
| 975 |
+
|
| 976 |
+
if 'name' in profile_data:
|
| 977 |
+
update_fields.append("InstructorName = %s")
|
| 978 |
+
params.append(profile_data['name'])
|
| 979 |
+
|
| 980 |
+
if 'email' in profile_data:
|
| 981 |
+
update_fields.append("Email = %s")
|
| 982 |
+
params.append(profile_data['email'])
|
| 983 |
+
|
| 984 |
+
if 'expertise' in profile_data:
|
| 985 |
+
update_fields.append("Expertise = %s")
|
| 986 |
+
params.append(profile_data['expertise'])
|
| 987 |
+
|
| 988 |
+
if not update_fields:
|
| 989 |
+
return {"message": "No fields to update"}
|
| 990 |
+
|
| 991 |
+
# Add username to params
|
| 992 |
+
params.append(username)
|
| 993 |
+
|
| 994 |
+
# Construct and execute SQL
|
| 995 |
+
sql = f"""
|
| 996 |
+
UPDATE Instructors
|
| 997 |
+
SET {', '.join(update_fields)}
|
| 998 |
+
WHERE AccountName = %s
|
| 999 |
+
"""
|
| 1000 |
+
cursor.execute(sql, params)
|
| 1001 |
+
|
| 1002 |
+
else:
|
| 1003 |
+
raise HTTPException(status_code=403, detail="Invalid user role")
|
| 1004 |
+
|
| 1005 |
+
conn.commit()
|
| 1006 |
+
return {"message": "Profile updated successfully"}
|
| 1007 |
+
|
| 1008 |
+
except Exception as e:
|
| 1009 |
+
print(f"Token/user verification error: {str(e)}")
|
| 1010 |
+
raise HTTPException(status_code=401, detail="Invalid authentication token or user not found")
|
| 1011 |
+
except HTTPException as he:
|
| 1012 |
+
raise he
|
| 1013 |
+
except Exception as e:
|
| 1014 |
+
print(f"Error updating user profile: {str(e)}")
|
| 1015 |
+
raise HTTPException(status_code=500, detail=f"Error updating user profile: {str(e)}")
|
| 1016 |
+
finally:
|
| 1017 |
+
if 'conn' in locals():
|
| 1018 |
+
conn.close()
|
| 1019 |
+
|
| 1020 |
+
# Get dashboard data for the current user
|
| 1021 |
+
@router.get("/user/dashboard")
|
| 1022 |
+
async def get_user_dashboard(
|
| 1023 |
+
request: Request,
|
| 1024 |
+
auth_token: str = Cookie(None)
|
| 1025 |
+
):
|
| 1026 |
+
try:
|
| 1027 |
+
# Get token from header if not in cookie
|
| 1028 |
+
if not auth_token:
|
| 1029 |
+
auth_header = request.headers.get('Authorization')
|
| 1030 |
+
if auth_header and auth_header.startswith('Bearer '):
|
| 1031 |
+
auth_token = auth_header.split(' ')[1]
|
| 1032 |
+
else:
|
| 1033 |
+
raise HTTPException(status_code=401, detail="No authentication token provided")
|
| 1034 |
+
|
| 1035 |
+
# Verify token and get user data
|
| 1036 |
+
try:
|
| 1037 |
+
user_data = decode_token(auth_token)
|
| 1038 |
+
|
| 1039 |
+
# Get LearnerID from Learners table using the username
|
| 1040 |
+
conn = connect_db()
|
| 1041 |
+
with conn.cursor(pymysql.cursors.DictCursor) as cursor:
|
| 1042 |
+
cursor.execute("""
|
| 1043 |
+
SELECT LearnerID, LearnerName
|
| 1044 |
+
FROM Learners
|
| 1045 |
+
WHERE AccountName = %s
|
| 1046 |
+
""", (user_data['username'],))
|
| 1047 |
+
learner = cursor.fetchone()
|
| 1048 |
+
if not learner:
|
| 1049 |
+
raise HTTPException(status_code=404, detail="Learner not found")
|
| 1050 |
+
learner_id = learner['LearnerID']
|
| 1051 |
+
learner_name = learner['LearnerName']
|
| 1052 |
+
except Exception as e:
|
| 1053 |
+
print(f"Token/user verification error: {str(e)}")
|
| 1054 |
+
raise HTTPException(status_code=401, detail="Invalid authentication token or user not found")
|
| 1055 |
+
|
| 1056 |
+
# Calculate dashboard metrics
|
| 1057 |
+
dashboard_data = {
|
| 1058 |
+
"learnerName": learner_name,
|
| 1059 |
+
"enrolled": 0,
|
| 1060 |
+
"completed": 0,
|
| 1061 |
+
"completionRate": "0%",
|
| 1062 |
+
"lecturesPassed": 0,
|
| 1063 |
+
"statistics": {
|
| 1064 |
+
"lecturesPassed": [],
|
| 1065 |
+
"averageScores": []
|
| 1066 |
+
},
|
| 1067 |
+
"enrolledCourses": []
|
| 1068 |
+
}
|
| 1069 |
+
|
| 1070 |
+
with conn.cursor(pymysql.cursors.DictCursor) as cursor:
|
| 1071 |
+
# Get enrollment count
|
| 1072 |
+
cursor.execute("SELECT COUNT(*) as count FROM Enrollments WHERE LearnerID = %s", (learner_id,))
|
| 1073 |
+
enrolled_data = cursor.fetchone()
|
| 1074 |
+
dashboard_data["enrolled"] = enrolled_data['count'] if enrolled_data else 0
|
| 1075 |
+
|
| 1076 |
+
# Get completed courses count
|
| 1077 |
+
cursor.execute(
|
| 1078 |
+
"SELECT COUNT(*) as count FROM Enrollments WHERE LearnerID = %s AND Percentage = 100",
|
| 1079 |
+
(learner_id,)
|
| 1080 |
+
)
|
| 1081 |
+
completed_data = cursor.fetchone()
|
| 1082 |
+
completed = completed_data['count'] if completed_data else 0
|
| 1083 |
+
dashboard_data["completed"] = completed
|
| 1084 |
+
|
| 1085 |
+
# Calculate completion rate
|
| 1086 |
+
if dashboard_data["enrolled"] > 0:
|
| 1087 |
+
rate = (completed / dashboard_data["enrolled"]) * 100
|
| 1088 |
+
dashboard_data["completionRate"] = f"{rate:.1f}%"
|
| 1089 |
+
|
| 1090 |
+
# Get passed lectures count
|
| 1091 |
+
cursor.execute(
|
| 1092 |
+
"SELECT COUNT(*) as count FROM LectureResults WHERE LearnerID = %s AND State = 'passed'",
|
| 1093 |
+
(learner_id,)
|
| 1094 |
+
)
|
| 1095 |
+
passed_data = cursor.fetchone()
|
| 1096 |
+
dashboard_data["lecturesPassed"] = passed_data['count'] if passed_data else 0
|
| 1097 |
+
|
| 1098 |
+
# Get statistics data - passed lectures over time
|
| 1099 |
+
cursor.execute("""
|
| 1100 |
+
SELECT Date, Score,
|
| 1101 |
+
DATE_FORMAT(Date, '%%Y-%%m-%%d') as formatted_date
|
| 1102 |
+
FROM LectureResults
|
| 1103 |
+
WHERE LearnerID = %s AND State = 'passed'
|
| 1104 |
+
ORDER BY Date
|
| 1105 |
+
""", (learner_id,))
|
| 1106 |
+
|
| 1107 |
+
stats_data = cursor.fetchall()
|
| 1108 |
+
date_groups = {}
|
| 1109 |
+
score_groups = {}
|
| 1110 |
+
|
| 1111 |
+
for row in stats_data:
|
| 1112 |
+
date_str = row['formatted_date']
|
| 1113 |
+
if date_str not in date_groups:
|
| 1114 |
+
date_groups[date_str] = 0
|
| 1115 |
+
date_groups[date_str] += 1
|
| 1116 |
+
|
| 1117 |
+
if date_str not in score_groups:
|
| 1118 |
+
score_groups[date_str] = {"total": 0, "count": 0}
|
| 1119 |
+
score_groups[date_str]["total"] += row['Score']
|
| 1120 |
+
score_groups[date_str]["count"] += 1
|
| 1121 |
+
|
| 1122 |
+
# Format the statistics data
|
| 1123 |
+
for date_str in date_groups:
|
| 1124 |
+
dashboard_data["statistics"]["lecturesPassed"].append({
|
| 1125 |
+
"date": date_str,
|
| 1126 |
+
"count": date_groups[date_str]
|
| 1127 |
+
})
|
| 1128 |
+
|
| 1129 |
+
avg_score = score_groups[date_str]["total"] / score_groups[date_str]["count"]
|
| 1130 |
+
dashboard_data["statistics"]["averageScores"].append({
|
| 1131 |
+
"date": date_str,
|
| 1132 |
+
"score": round(avg_score, 2)
|
| 1133 |
+
})
|
| 1134 |
+
|
| 1135 |
+
# Get enrolled courses with percentage
|
| 1136 |
+
cursor.execute("""
|
| 1137 |
+
SELECT
|
| 1138 |
+
c.CourseID as id,
|
| 1139 |
+
c.CourseName as name,
|
| 1140 |
+
CONCAT(i.InstructorName, ' (', i.AccountName, ')') as instructor,
|
| 1141 |
+
c.Descriptions as description,
|
| 1142 |
+
e.Percentage as percentage
|
| 1143 |
+
FROM Courses c
|
| 1144 |
+
JOIN Instructors i ON c.InstructorID = i.InstructorID
|
| 1145 |
+
JOIN Enrollments e ON c.CourseID = e.CourseID
|
| 1146 |
+
WHERE e.LearnerID = %s
|
| 1147 |
+
""", (learner_id,))
|
| 1148 |
+
|
| 1149 |
+
courses = cursor.fetchall()
|
| 1150 |
+
dashboard_data["enrolledCourses"] = courses
|
| 1151 |
+
|
| 1152 |
+
return dashboard_data
|
| 1153 |
+
|
| 1154 |
+
except HTTPException as he:
|
| 1155 |
+
raise he
|
| 1156 |
+
except Exception as e:
|
| 1157 |
+
print(f"Error fetching dashboard data: {str(e)}")
|
| 1158 |
+
raise HTTPException(status_code=500, detail=f"Error fetching dashboard data: {str(e)}")
|
| 1159 |
+
finally:
|
| 1160 |
+
if 'conn' in locals():
|
| 1161 |
+
conn.close()
|
| 1162 |
+
|
| 1163 |
+
# Get dashboard data for instructor
|
| 1164 |
+
@router.get("/instructor/dashboard")
|
| 1165 |
+
async def get_instructor_dashboard(
|
| 1166 |
+
request: Request,
|
| 1167 |
+
auth_token: str = Cookie(None)
|
| 1168 |
+
):
|
| 1169 |
+
try:
|
| 1170 |
+
# Get token from header if not in cookie
|
| 1171 |
+
if not auth_token:
|
| 1172 |
+
auth_header = request.headers.get('Authorization')
|
| 1173 |
+
if auth_header and auth_header.startswith('Bearer '):
|
| 1174 |
+
auth_token = auth_header.split(' ')[1]
|
| 1175 |
+
else:
|
| 1176 |
+
raise HTTPException(status_code=401, detail="No authentication token provided")
|
| 1177 |
+
|
| 1178 |
+
# Verify token and get user data
|
| 1179 |
+
try:
|
| 1180 |
+
user_data = decode_token(auth_token)
|
| 1181 |
+
username = user_data.get('username')
|
| 1182 |
+
role = user_data.get('role')
|
| 1183 |
+
|
| 1184 |
+
if not username or not role:
|
| 1185 |
+
raise HTTPException(status_code=401, detail="Invalid token data")
|
| 1186 |
+
|
| 1187 |
+
# Verify user is an instructor
|
| 1188 |
+
if role != "Instructor":
|
| 1189 |
+
raise HTTPException(status_code=403, detail="Only instructors can access this endpoint")
|
| 1190 |
+
|
| 1191 |
+
# Connect to database
|
| 1192 |
+
conn = connect_db()
|
| 1193 |
+
|
| 1194 |
+
with conn.cursor(pymysql.cursors.DictCursor) as cursor:
|
| 1195 |
+
# Get instructor ID
|
| 1196 |
+
try:
|
| 1197 |
+
cursor.execute("""
|
| 1198 |
+
SELECT InstructorID
|
| 1199 |
+
FROM Instructors
|
| 1200 |
+
WHERE AccountName = %s
|
| 1201 |
+
""", (username,))
|
| 1202 |
+
|
| 1203 |
+
instructor = cursor.fetchone()
|
| 1204 |
+
if not instructor:
|
| 1205 |
+
raise HTTPException(status_code=404, detail="Instructor not found")
|
| 1206 |
+
|
| 1207 |
+
instructor_id = instructor['InstructorID']
|
| 1208 |
+
except Exception as e:
|
| 1209 |
+
print(f"Error getting instructor ID: {str(e)}")
|
| 1210 |
+
raise HTTPException(status_code=500, detail="Error retrieving instructor information")
|
| 1211 |
+
|
| 1212 |
+
try:
|
| 1213 |
+
# Get total courses
|
| 1214 |
+
cursor.execute("""
|
| 1215 |
+
SELECT COUNT(*) as total_courses
|
| 1216 |
+
FROM Courses
|
| 1217 |
+
WHERE InstructorID = %s
|
| 1218 |
+
""", (instructor_id,))
|
| 1219 |
+
total_courses = cursor.fetchone()['total_courses']
|
| 1220 |
+
|
| 1221 |
+
# Get total students and stats
|
| 1222 |
+
cursor.execute("""
|
| 1223 |
+
SELECT
|
| 1224 |
+
COUNT(DISTINCT e.LearnerID) as total_students,
|
| 1225 |
+
COALESCE(AVG(e.Rating), 0) as average_rating,
|
| 1226 |
+
COUNT(*) as total_enrollments,
|
| 1227 |
+
SUM(CASE WHEN e.Percentage = 100 THEN 1 ELSE 0 END) as completed_enrollments
|
| 1228 |
+
FROM Courses c
|
| 1229 |
+
LEFT JOIN Enrollments e ON c.CourseID = e.CourseID
|
| 1230 |
+
WHERE c.InstructorID = %s
|
| 1231 |
+
""", (instructor_id,))
|
| 1232 |
+
|
| 1233 |
+
stats = cursor.fetchone()
|
| 1234 |
+
if not stats:
|
| 1235 |
+
stats = {
|
| 1236 |
+
'total_students': 0,
|
| 1237 |
+
'average_rating': 0,
|
| 1238 |
+
'total_enrollments': 0,
|
| 1239 |
+
'completed_enrollments': 0
|
| 1240 |
+
}
|
| 1241 |
+
|
| 1242 |
+
completion_rate = round((stats['completed_enrollments'] / stats['total_enrollments'] * 100)
|
| 1243 |
+
if stats['total_enrollments'] > 0 else 0, 1)
|
| 1244 |
+
|
| 1245 |
+
# Get student growth
|
| 1246 |
+
cursor.execute("""
|
| 1247 |
+
SELECT
|
| 1248 |
+
DATE_FORMAT(EnrollmentDate, '%%Y-%%m') as month,
|
| 1249 |
+
COUNT(DISTINCT LearnerID) as students
|
| 1250 |
+
FROM Courses c
|
| 1251 |
+
JOIN Enrollments e ON c.CourseID = e.CourseID
|
| 1252 |
+
WHERE c.InstructorID = %s
|
| 1253 |
+
AND EnrollmentDate >= DATE_SUB(CURRENT_DATE, INTERVAL 2 MONTH)
|
| 1254 |
+
GROUP BY DATE_FORMAT(EnrollmentDate, '%%Y-%%m')
|
| 1255 |
+
ORDER BY month DESC
|
| 1256 |
+
LIMIT 2
|
| 1257 |
+
""", (instructor_id,))
|
| 1258 |
+
|
| 1259 |
+
growth_data = cursor.fetchall()
|
| 1260 |
+
|
| 1261 |
+
current_month = growth_data[0]['students'] if growth_data else 0
|
| 1262 |
+
prev_month = growth_data[1]['students'] if len(growth_data) > 1 else 0
|
| 1263 |
+
student_growth = round(((current_month - prev_month) / prev_month * 100)
|
| 1264 |
+
if prev_month > 0 else 0, 1)
|
| 1265 |
+
|
| 1266 |
+
# Get courses
|
| 1267 |
+
cursor.execute("""
|
| 1268 |
+
SELECT
|
| 1269 |
+
c.CourseID as id,
|
| 1270 |
+
c.CourseName as name,
|
| 1271 |
+
c.Descriptions as description,
|
| 1272 |
+
c.AverageRating as rating,
|
| 1273 |
+
COUNT(DISTINCT e.LearnerID) as enrollments,
|
| 1274 |
+
AVG(e.Percentage) as completionRate
|
| 1275 |
+
FROM Courses c
|
| 1276 |
+
LEFT JOIN Enrollments e ON c.CourseID = e.CourseID
|
| 1277 |
+
WHERE c.InstructorID = %s
|
| 1278 |
+
GROUP BY c.CourseID, c.CourseName, c.Descriptions, c.AverageRating
|
| 1279 |
+
ORDER BY c.CreatedAt DESC
|
| 1280 |
+
""", (instructor_id,))
|
| 1281 |
+
|
| 1282 |
+
courses = cursor.fetchall() or []
|
| 1283 |
+
|
| 1284 |
+
# Get enrollment trends
|
| 1285 |
+
cursor.execute("""
|
| 1286 |
+
SELECT
|
| 1287 |
+
DATE_FORMAT(e.EnrollmentDate, '%%Y-%%m-%%d') as date,
|
| 1288 |
+
COUNT(*) as value
|
| 1289 |
+
FROM Courses c
|
| 1290 |
+
JOIN Enrollments e ON c.CourseID = e.CourseID
|
| 1291 |
+
WHERE c.InstructorID = %s
|
| 1292 |
+
AND e.EnrollmentDate >= DATE_SUB(CURRENT_DATE, INTERVAL 30 DAY)
|
| 1293 |
+
GROUP BY DATE_FORMAT(e.EnrollmentDate, '%%Y-%%m-%%d')
|
| 1294 |
+
ORDER BY date
|
| 1295 |
+
""", (instructor_id,))
|
| 1296 |
+
|
| 1297 |
+
enrollment_trends = cursor.fetchall() or []
|
| 1298 |
+
|
| 1299 |
+
# Get rating trends
|
| 1300 |
+
cursor.execute("""
|
| 1301 |
+
SELECT
|
| 1302 |
+
DATE_FORMAT(e.EnrollmentDate, '%%Y-%%m-%%d') as date,
|
| 1303 |
+
AVG(e.Rating) as value
|
| 1304 |
+
FROM Courses c
|
| 1305 |
+
JOIN Enrollments e ON c.CourseID = e.CourseID
|
| 1306 |
+
WHERE c.InstructorID = %s
|
| 1307 |
+
AND e.EnrollmentDate >= DATE_SUB(CURRENT_DATE, INTERVAL 30 DAY)
|
| 1308 |
+
AND e.Rating IS NOT NULL
|
| 1309 |
+
GROUP BY DATE_FORMAT(e.EnrollmentDate, '%%Y-%%m-%%d')
|
| 1310 |
+
ORDER BY date
|
| 1311 |
+
""", (instructor_id,))
|
| 1312 |
+
|
| 1313 |
+
rating_trends = cursor.fetchall() or []
|
| 1314 |
+
|
| 1315 |
+
# Format the data
|
| 1316 |
+
formatted_courses = [
|
| 1317 |
+
{
|
| 1318 |
+
'id': course['id'],
|
| 1319 |
+
'name': course['name'],
|
| 1320 |
+
'description': course['description'] or "",
|
| 1321 |
+
'enrollments': course['enrollments'] or 0,
|
| 1322 |
+
'rating': round(float(course['rating']), 1) if course['rating'] else 0.0,
|
| 1323 |
+
'completionRate': round(float(course['completionRate']), 1) if course['completionRate'] else 0.0
|
| 1324 |
+
} for course in courses
|
| 1325 |
+
]
|
| 1326 |
+
|
| 1327 |
+
formatted_enrollment_trends = [
|
| 1328 |
+
{
|
| 1329 |
+
'date': trend['date'],
|
| 1330 |
+
'value': int(trend['value'])
|
| 1331 |
+
} for trend in enrollment_trends
|
| 1332 |
+
]
|
| 1333 |
+
|
| 1334 |
+
formatted_rating_trends = [
|
| 1335 |
+
{
|
| 1336 |
+
'date': trend['date'],
|
| 1337 |
+
'value': round(float(trend['value']), 1)
|
| 1338 |
+
} for trend in rating_trends if trend['value'] is not None
|
| 1339 |
+
]
|
| 1340 |
+
|
| 1341 |
+
dashboard_data = {
|
| 1342 |
+
"metrics": {
|
| 1343 |
+
"totalCourses": total_courses,
|
| 1344 |
+
"totalStudents": stats['total_students'],
|
| 1345 |
+
"averageRating": round(float(stats['average_rating']), 1),
|
| 1346 |
+
"completionRate": completion_rate,
|
| 1347 |
+
"studentGrowth": student_growth
|
| 1348 |
+
},
|
| 1349 |
+
"courses": formatted_courses,
|
| 1350 |
+
"enrollmentTrends": formatted_enrollment_trends,
|
| 1351 |
+
"ratingTrends": formatted_rating_trends,
|
| 1352 |
+
"courseEnrollments": [
|
| 1353 |
+
{
|
| 1354 |
+
"courseName": course['name'],
|
| 1355 |
+
"enrollments": course['enrollments'] or 0
|
| 1356 |
+
} for course in courses
|
| 1357 |
+
]
|
| 1358 |
+
}
|
| 1359 |
+
|
| 1360 |
+
return dashboard_data
|
| 1361 |
+
|
| 1362 |
+
except Exception as e:
|
| 1363 |
+
print(f"Error processing dashboard data: {str(e)}")
|
| 1364 |
+
raise HTTPException(status_code=500, detail="Error processing dashboard data")
|
| 1365 |
+
|
| 1366 |
+
except HTTPException as he:
|
| 1367 |
+
raise he
|
| 1368 |
+
except Exception as e:
|
| 1369 |
+
print(f"Database error: {str(e)}")
|
| 1370 |
+
raise HTTPException(status_code=500, detail=f"Database error: {str(e)}")
|
| 1371 |
+
|
| 1372 |
+
except HTTPException as he:
|
| 1373 |
+
raise he
|
| 1374 |
+
except Exception as e:
|
| 1375 |
+
print(f"Error fetching instructor dashboard: {str(e)}")
|
| 1376 |
+
raise HTTPException(status_code=500, detail=str(e))
|
| 1377 |
+
finally:
|
| 1378 |
+
if 'conn' in locals():
|
| 1379 |
+
conn.close()
|
| 1380 |
+
|
| 1381 |
+
# Quiz submission model
|
| 1382 |
+
class QuizSubmission(BaseModel):
|
| 1383 |
+
answers: dict[int, str] # questionId -> selected answer text
|
| 1384 |
+
|
| 1385 |
+
@router.post("/lectures/{lecture_id}/quiz/submit")
|
| 1386 |
+
async def submit_quiz_answers(
|
| 1387 |
+
lecture_id: int,
|
| 1388 |
+
submission: QuizSubmission,
|
| 1389 |
+
request: Request,
|
| 1390 |
+
auth_token: str = Cookie(None)
|
| 1391 |
+
):
|
| 1392 |
+
try:
|
| 1393 |
+
# Get token from header if not in cookie
|
| 1394 |
+
if not auth_token:
|
| 1395 |
+
auth_header = request.headers.get('Authorization')
|
| 1396 |
+
if auth_header and auth_header.startswith('Bearer '):
|
| 1397 |
+
auth_token = auth_header.split(' ')[1]
|
| 1398 |
+
else:
|
| 1399 |
+
raise HTTPException(status_code=401, detail="No authentication token provided")
|
| 1400 |
+
|
| 1401 |
+
# Verify token and get user data
|
| 1402 |
+
try:
|
| 1403 |
+
user_data = decode_token(auth_token)
|
| 1404 |
+
# Get LearnerID from Learners table using the username
|
| 1405 |
+
conn = connect_db()
|
| 1406 |
+
with conn.cursor(pymysql.cursors.DictCursor) as cursor:
|
| 1407 |
+
cursor.execute("""
|
| 1408 |
+
SELECT LearnerID
|
| 1409 |
+
FROM Learners
|
| 1410 |
+
WHERE AccountName = %s
|
| 1411 |
+
""", (user_data['username'],))
|
| 1412 |
+
learner = cursor.fetchone()
|
| 1413 |
+
if not learner:
|
| 1414 |
+
raise HTTPException(status_code=404, detail="Learner not found")
|
| 1415 |
+
learner_id = learner['LearnerID']
|
| 1416 |
+
except Exception as e:
|
| 1417 |
+
print(f"Token/user verification error: {str(e)}")
|
| 1418 |
+
raise HTTPException(status_code=401, detail="Invalid authentication token or user not found")
|
| 1419 |
+
|
| 1420 |
+
try:
|
| 1421 |
+
with conn.cursor(pymysql.cursors.DictCursor) as cursor:
|
| 1422 |
+
# First verify the quiz exists for this lecture
|
| 1423 |
+
cursor.execute("""
|
| 1424 |
+
SELECT QuizID
|
| 1425 |
+
FROM Quizzes
|
| 1426 |
+
WHERE LectureID = %s
|
| 1427 |
+
""", (lecture_id,))
|
| 1428 |
+
quiz = cursor.fetchone()
|
| 1429 |
+
if not quiz:
|
| 1430 |
+
raise HTTPException(status_code=404, detail="Quiz not found for this lecture")
|
| 1431 |
+
|
| 1432 |
+
quiz_id = quiz['QuizID']
|
| 1433 |
+
|
| 1434 |
+
# Get correct answers for validation
|
| 1435 |
+
cursor.execute("""
|
| 1436 |
+
SELECT q.QuestionID, o.OptionText
|
| 1437 |
+
FROM Questions q
|
| 1438 |
+
JOIN Options o ON q.QuestionID = o.QuestionID
|
| 1439 |
+
WHERE q.QuizID = %s AND o.IsCorrect = 1
|
| 1440 |
+
""", (quiz_id,))
|
| 1441 |
+
|
| 1442 |
+
correct_answers = {row['QuestionID']: row['OptionText'] for row in cursor.fetchall()}
|
| 1443 |
+
|
| 1444 |
+
# Calculate score
|
| 1445 |
+
total_questions = len(correct_answers)
|
| 1446 |
+
if total_questions == 0:
|
| 1447 |
+
raise HTTPException(status_code=500, detail="No questions found for this quiz")
|
| 1448 |
+
|
| 1449 |
+
correct_count = sum(
|
| 1450 |
+
1 for q_id, answer in submission.answers.items()
|
| 1451 |
+
if str(q_id) in map(str, correct_answers.keys()) and answer == correct_answers[int(q_id)]
|
| 1452 |
+
)
|
| 1453 |
+
|
| 1454 |
+
score = (correct_count / total_questions) * 100
|
| 1455 |
+
|
| 1456 |
+
# Get the CourseID for this lecture
|
| 1457 |
+
cursor.execute("""
|
| 1458 |
+
SELECT CourseID
|
| 1459 |
+
FROM Lectures
|
| 1460 |
+
WHERE LectureID = %s
|
| 1461 |
+
""", (lecture_id,))
|
| 1462 |
+
lecture_data = cursor.fetchone()
|
| 1463 |
+
if not lecture_data:
|
| 1464 |
+
raise HTTPException(status_code=404, detail="Lecture not found")
|
| 1465 |
+
|
| 1466 |
+
course_id = lecture_data['CourseID']
|
| 1467 |
+
|
| 1468 |
+
# Save or update the score using direct SQL instead of stored procedure
|
| 1469 |
+
try:
|
| 1470 |
+
# Use stored procedure to update or insert the lecture result
|
| 1471 |
+
cursor.execute(
|
| 1472 |
+
"CALL sp_update_lecture_result(%s, %s, %s, %s)",
|
| 1473 |
+
(learner_id, course_id, lecture_id, score)
|
| 1474 |
+
)
|
| 1475 |
+
|
| 1476 |
+
# Update course completion percentage
|
| 1477 |
+
try:
|
| 1478 |
+
# Get total lectures in the course
|
| 1479 |
+
cursor.execute("""
|
| 1480 |
+
SELECT COUNT(*) as total_lectures
|
| 1481 |
+
FROM Lectures
|
| 1482 |
+
WHERE CourseID = %s
|
| 1483 |
+
""", (course_id,))
|
| 1484 |
+
total_lectures = cursor.fetchone()['total_lectures']
|
| 1485 |
+
|
| 1486 |
+
# Get passed lectures
|
| 1487 |
+
cursor.execute("""
|
| 1488 |
+
SELECT COUNT(*) as passed_lectures
|
| 1489 |
+
FROM LectureResults
|
| 1490 |
+
WHERE LearnerID = %s AND CourseID = %s AND State = 'passed'
|
| 1491 |
+
""", (learner_id, course_id))
|
| 1492 |
+
passed_lectures = cursor.fetchone()['passed_lectures']
|
| 1493 |
+
|
| 1494 |
+
# Calculate percentage
|
| 1495 |
+
if total_lectures > 0:
|
| 1496 |
+
percentage_raw = (passed_lectures * 100.0) / total_lectures
|
| 1497 |
+
|
| 1498 |
+
# Convert to percentage scale
|
| 1499 |
+
if percentage_raw < 10:
|
| 1500 |
+
percentage = 0
|
| 1501 |
+
elif percentage_raw < 30:
|
| 1502 |
+
percentage = 20
|
| 1503 |
+
elif percentage_raw < 50:
|
| 1504 |
+
percentage = 40
|
| 1505 |
+
elif percentage_raw < 70:
|
| 1506 |
+
percentage = 60
|
| 1507 |
+
elif percentage_raw < 90:
|
| 1508 |
+
percentage = 80
|
| 1509 |
+
else:
|
| 1510 |
+
percentage = 100
|
| 1511 |
+
|
| 1512 |
+
# Update enrollment record
|
| 1513 |
+
cursor.execute("""
|
| 1514 |
+
UPDATE Enrollments
|
| 1515 |
+
SET Percentage = %s
|
| 1516 |
+
WHERE LearnerID = %s AND CourseID = %s
|
| 1517 |
+
""", (percentage, learner_id, course_id))
|
| 1518 |
+
except Exception as e:
|
| 1519 |
+
print(f"Error updating course percentage: {str(e)}")
|
| 1520 |
+
# Continue even if percentage update fails
|
| 1521 |
+
|
| 1522 |
+
conn.commit()
|
| 1523 |
+
print(f"Score updated successfully for learner {learner_id}, lecture {lecture_id}")
|
| 1524 |
+
except Exception as e:
|
| 1525 |
+
print(f"Error saving quiz score: {str(e)}")
|
| 1526 |
+
conn.rollback()
|
| 1527 |
+
raise HTTPException(status_code=500, detail=f"Failed to save quiz score: {str(e)}")
|
| 1528 |
+
|
| 1529 |
+
return {
|
| 1530 |
+
"score": score,
|
| 1531 |
+
"total_questions": total_questions,
|
| 1532 |
+
"correct_answers": correct_count
|
| 1533 |
+
}
|
| 1534 |
+
|
| 1535 |
+
finally:
|
| 1536 |
+
conn.close()
|
| 1537 |
+
|
| 1538 |
+
except HTTPException as he:
|
| 1539 |
+
raise he
|
| 1540 |
+
except Exception as e:
|
| 1541 |
+
print(f"Error submitting quiz: {str(e)}")
|
| 1542 |
+
raise HTTPException(status_code=500, detail=f"Error submitting quiz: {str(e)}")
|
| 1543 |
+
|
| 1544 |
+
@router.get("/lectures/{lecture_id}/quiz/results")
|
| 1545 |
+
async def get_quiz_results(
|
| 1546 |
+
lecture_id: int,
|
| 1547 |
+
request: Request,
|
| 1548 |
+
auth_token: str = Cookie(None)
|
| 1549 |
+
):
|
| 1550 |
+
try:
|
| 1551 |
+
# Verify auth token
|
| 1552 |
+
if not auth_token:
|
| 1553 |
+
auth_header = request.headers.get('Authorization')
|
| 1554 |
+
if auth_header and auth_header.startswith('Bearer '):
|
| 1555 |
+
auth_token = auth_header.split(' ')[1]
|
| 1556 |
+
|
| 1557 |
+
if not auth_token:
|
| 1558 |
+
raise HTTPException(status_code=401, detail="No authentication token provided")
|
| 1559 |
+
|
| 1560 |
+
try:
|
| 1561 |
+
user_data = decode_token(auth_token)
|
| 1562 |
+
except Exception as e:
|
| 1563 |
+
print(f"Token decode error: {str(e)}")
|
| 1564 |
+
raise HTTPException(status_code=401, detail="Invalid authentication token")
|
| 1565 |
+
|
| 1566 |
+
conn = connect_db()
|
| 1567 |
+
try:
|
| 1568 |
+
with conn.cursor(pymysql.cursors.DictCursor) as cursor:
|
| 1569 |
+
cursor.execute("""
|
| 1570 |
+
SELECT Score, State, Date
|
| 1571 |
+
FROM LectureResults
|
| 1572 |
+
WHERE LearnerID = %s AND LectureID = %s
|
| 1573 |
+
ORDER BY Date DESC
|
| 1574 |
+
LIMIT 1
|
| 1575 |
+
""", (user_data["id"], lecture_id))
|
| 1576 |
+
|
| 1577 |
+
result = cursor.fetchone()
|
| 1578 |
+
if not result:
|
| 1579 |
+
return None
|
| 1580 |
+
|
| 1581 |
+
return {
|
| 1582 |
+
"score": float(result["Score"]),
|
| 1583 |
+
"status": result["State"],
|
| 1584 |
+
"date": result["Date"].isoformat()
|
| 1585 |
+
}
|
| 1586 |
+
|
| 1587 |
+
finally:
|
| 1588 |
+
conn.close()
|
| 1589 |
+
|
| 1590 |
+
except HTTPException:
|
| 1591 |
+
raise
|
| 1592 |
+
except Exception as e:
|
| 1593 |
+
print(f"Error in get_quiz_results: {str(e)}")
|
| 1594 |
+
raise HTTPException(status_code=500, detail=str(e))
|
| 1595 |
+
|
| 1596 |
+
# Test endpoints for debugging route registration
|
| 1597 |
+
@router.get("/instructor/courses/test")
|
| 1598 |
+
async def test_instructor_courses_get():
|
| 1599 |
+
return {"message": "GET /instructor/courses/test works"}
|
| 1600 |
+
|
| 1601 |
+
@router.post("/instructor/courses/test")
|
| 1602 |
+
async def test_instructor_courses_post():
|
| 1603 |
+
return {"message": "POST /instructor/courses/test works"}
|
| 1604 |
+
|
| 1605 |
+
# Get instructor course details
|
| 1606 |
+
@router.get("/instructor/courses/{course_id}", response_model=Course)
|
| 1607 |
+
async def get_instructor_course_details(
|
| 1608 |
+
request: Request,
|
| 1609 |
+
course_id: int,
|
| 1610 |
+
auth_token: str = Cookie(None)
|
| 1611 |
+
):
|
| 1612 |
+
try:
|
| 1613 |
+
# Get token from header if not in cookie
|
| 1614 |
+
if not auth_token:
|
| 1615 |
+
auth_header = request.headers.get('Authorization')
|
| 1616 |
+
if auth_header and auth_header.startswith('Bearer '):
|
| 1617 |
+
auth_token = auth_header.split(' ')[1]
|
| 1618 |
+
else:
|
| 1619 |
+
raise HTTPException(status_code=401, detail="No authentication token provided")
|
| 1620 |
+
|
| 1621 |
+
# Verify token and get user data
|
| 1622 |
+
try:
|
| 1623 |
+
user_data = decode_token(auth_token)
|
| 1624 |
+
username = user_data['username']
|
| 1625 |
+
role = user_data['role']
|
| 1626 |
+
|
| 1627 |
+
# Verify user is an instructor
|
| 1628 |
+
if role != "Instructor":
|
| 1629 |
+
raise HTTPException(status_code=403, detail="Only instructors can access this endpoint")
|
| 1630 |
+
|
| 1631 |
+
# Connect to database
|
| 1632 |
+
conn = connect_db()
|
| 1633 |
+
|
| 1634 |
+
with conn.cursor(pymysql.cursors.DictCursor) as cursor:
|
| 1635 |
+
# Get instructor ID
|
| 1636 |
+
cursor.execute("""
|
| 1637 |
+
SELECT InstructorID
|
| 1638 |
+
FROM Instructors
|
| 1639 |
+
WHERE AccountName = %s
|
| 1640 |
+
""", (username,))
|
| 1641 |
+
|
| 1642 |
+
instructor = cursor.fetchone()
|
| 1643 |
+
if not instructor:
|
| 1644 |
+
raise HTTPException(status_code=404, detail="Instructor not found")
|
| 1645 |
+
|
| 1646 |
+
instructor_id = instructor['InstructorID']
|
| 1647 |
+
|
| 1648 |
+
# Get course details, ensuring it belongs to this instructor
|
| 1649 |
+
query = """
|
| 1650 |
+
SELECT
|
| 1651 |
+
c.CourseID as id,
|
| 1652 |
+
c.CourseName as name,
|
| 1653 |
+
CONCAT(i.InstructorName, ' (', i.AccountName, ')') as instructor,
|
| 1654 |
+
c.Descriptions as description,
|
| 1655 |
+
(SELECT COUNT(*) FROM Enrollments WHERE CourseID = c.CourseID) as enrolled,
|
| 1656 |
+
COALESCE(
|
| 1657 |
+
(SELECT AVG(Rating)
|
| 1658 |
+
FROM Enrollments
|
| 1659 |
+
WHERE CourseID = c.CourseID AND Rating IS NOT NULL),
|
| 1660 |
+
0
|
| 1661 |
+
) as rating,
|
| 1662 |
+
c.Skills as skills,
|
| 1663 |
+
c.Difficulty as difficulty,
|
| 1664 |
+
c.EstimatedDuration as duration
|
| 1665 |
+
FROM Courses c
|
| 1666 |
+
JOIN Instructors i ON c.InstructorID = i.InstructorID
|
| 1667 |
+
WHERE c.CourseID = %s AND c.InstructorID = %s
|
| 1668 |
+
"""
|
| 1669 |
+
|
| 1670 |
+
cursor.execute(query, (course_id, instructor_id))
|
| 1671 |
+
course = cursor.fetchone()
|
| 1672 |
+
|
| 1673 |
+
if not course:
|
| 1674 |
+
raise HTTPException(status_code=404, detail=f"Course with ID {course_id} not found or not owned by this instructor")
|
| 1675 |
+
|
| 1676 |
+
# Format the course data
|
| 1677 |
+
if course['rating']:
|
| 1678 |
+
course['rating'] = float(course['rating'])
|
| 1679 |
+
|
| 1680 |
+
# Convert skills from JSON string if needed
|
| 1681 |
+
if isinstance(course.get('skills'), str):
|
| 1682 |
+
try:
|
| 1683 |
+
course['skills'] = json.loads(course['skills'])
|
| 1684 |
+
except:
|
| 1685 |
+
course['skills'] = []
|
| 1686 |
+
|
| 1687 |
+
return course
|
| 1688 |
+
|
| 1689 |
+
except Exception as e:
|
| 1690 |
+
print(f"Database error: {str(e)}")
|
| 1691 |
+
raise HTTPException(status_code=500, detail=f"Database error: {str(e)}")
|
| 1692 |
+
|
| 1693 |
+
except HTTPException as he:
|
| 1694 |
+
raise he
|
| 1695 |
+
except Exception as e:
|
| 1696 |
+
print(f"Error fetching instructor course details: {str(e)}")
|
| 1697 |
+
raise HTTPException(status_code=500, detail=str(e))
|
| 1698 |
+
finally:
|
| 1699 |
+
if 'conn' in locals():
|
| 1700 |
+
conn.close()
|
| 1701 |
+
|
| 1702 |
+
# CreateLecture model and create_lecture endpoint to handle lecture creation with video upload and quiz
|
| 1703 |
+
class CreateLecture(BaseModel):
|
| 1704 |
+
title: str
|
| 1705 |
+
description: str
|
| 1706 |
+
content: str
|
| 1707 |
+
quiz: Optional[dict] = None
|
| 1708 |
+
|
| 1709 |
+
@router.post("/courses/{course_id}/lectures")
|
| 1710 |
+
async def create_lecture(
|
| 1711 |
+
request: Request,
|
| 1712 |
+
course_id: int,
|
| 1713 |
+
auth_token: str = Cookie(None),
|
| 1714 |
+
title: str = Form(...),
|
| 1715 |
+
description: str = Form(...),
|
| 1716 |
+
content: str = Form(...),
|
| 1717 |
+
video: Optional[UploadFile] = File(None),
|
| 1718 |
+
quiz: Optional[str] = Form(None)
|
| 1719 |
+
):
|
| 1720 |
+
try:
|
| 1721 |
+
# Get token from header if not in cookie
|
| 1722 |
+
if not auth_token:
|
| 1723 |
+
auth_header = request.headers.get('Authorization')
|
| 1724 |
+
if auth_header and auth_header.startswith('Bearer '):
|
| 1725 |
+
auth_token = auth_header.split(' ')[1]
|
| 1726 |
+
else:
|
| 1727 |
+
raise HTTPException(status_code=401, detail="No authentication token provided")
|
| 1728 |
+
|
| 1729 |
+
# Verify token and get user data
|
| 1730 |
+
try:
|
| 1731 |
+
user_data = decode_token(auth_token)
|
| 1732 |
+
username = user_data['username']
|
| 1733 |
+
role = user_data['role']
|
| 1734 |
+
|
| 1735 |
+
# Verify user is an instructor
|
| 1736 |
+
if role != "Instructor":
|
| 1737 |
+
raise HTTPException(status_code=403, detail="Only instructors can access this endpoint")
|
| 1738 |
+
|
| 1739 |
+
# Connect to database
|
| 1740 |
+
conn = connect_db()
|
| 1741 |
+
cursor = conn.cursor(pymysql.cursors.DictCursor)
|
| 1742 |
+
|
| 1743 |
+
try:
|
| 1744 |
+
# Get instructor ID
|
| 1745 |
+
cursor.execute("""
|
| 1746 |
+
SELECT InstructorID
|
| 1747 |
+
FROM Instructors
|
| 1748 |
+
WHERE AccountName = %s
|
| 1749 |
+
""", (username,))
|
| 1750 |
+
|
| 1751 |
+
instructor = cursor.fetchone()
|
| 1752 |
+
if not instructor:
|
| 1753 |
+
raise HTTPException(status_code=404, detail="Instructor not found")
|
| 1754 |
+
|
| 1755 |
+
instructor_id = instructor['InstructorID']
|
| 1756 |
+
|
| 1757 |
+
# Verify this instructor owns this course
|
| 1758 |
+
cursor.execute("""
|
| 1759 |
+
SELECT CourseID
|
| 1760 |
+
FROM Courses
|
| 1761 |
+
WHERE CourseID = %s AND InstructorID = %s
|
| 1762 |
+
""", (course_id, instructor_id))
|
| 1763 |
+
|
| 1764 |
+
if not cursor.fetchone():
|
| 1765 |
+
raise HTTPException(status_code=403, detail="Not authorized to modify this course")
|
| 1766 |
+
|
| 1767 |
+
# Create the lecture
|
| 1768 |
+
cursor.execute("""
|
| 1769 |
+
INSERT INTO Lectures (CourseID, Title, Description, Content)
|
| 1770 |
+
VALUES (%s, %s, %s, %s)
|
| 1771 |
+
""", (course_id, title, description, content))
|
| 1772 |
+
conn.commit()
|
| 1773 |
+
|
| 1774 |
+
# Get the newly created lecture ID
|
| 1775 |
+
lecture_id = cursor.lastrowid
|
| 1776 |
+
|
| 1777 |
+
# Upload video if provided
|
| 1778 |
+
if video:
|
| 1779 |
+
# Check file size (100MB limit)
|
| 1780 |
+
video_content = await video.read()
|
| 1781 |
+
if len(video_content) > 100 * 1024 * 1024: # 100MB in bytes
|
| 1782 |
+
raise HTTPException(status_code=400, detail="Video file size must be less than 100MB")
|
| 1783 |
+
|
| 1784 |
+
# Check file type
|
| 1785 |
+
if not video.content_type.startswith('video/'):
|
| 1786 |
+
raise HTTPException(status_code=400, detail="Invalid file type. Please upload a video file")
|
| 1787 |
+
|
| 1788 |
+
# Upload to S3
|
| 1789 |
+
media_path = f"videos/cid{course_id}/lid{lecture_id}/vid_lecture.mp4"
|
| 1790 |
+
s3.put_object(
|
| 1791 |
+
Bucket="tlhmaterials",
|
| 1792 |
+
Key=media_path,
|
| 1793 |
+
Body=video_content,
|
| 1794 |
+
ContentType=video.content_type,
|
| 1795 |
+
ACL="public-read",
|
| 1796 |
+
ContentDisposition="inline"
|
| 1797 |
+
)
|
| 1798 |
+
|
| 1799 |
+
# Create quiz if provided
|
| 1800 |
+
if quiz:
|
| 1801 |
+
quiz_data = json.loads(quiz)
|
| 1802 |
+
if quiz_data and quiz_data.get('questions'):
|
| 1803 |
+
# Insert quiz
|
| 1804 |
+
cursor.execute("""
|
| 1805 |
+
INSERT INTO Quizzes (LectureID, Title, Description)
|
| 1806 |
+
VALUES (%s, %s, %s)
|
| 1807 |
+
""", (lecture_id, f"Quiz for {title}", description))
|
| 1808 |
+
conn.commit()
|
| 1809 |
+
|
| 1810 |
+
quiz_id = cursor.lastrowid
|
| 1811 |
+
|
| 1812 |
+
# Insert questions and options
|
| 1813 |
+
for question in quiz_data['questions']:
|
| 1814 |
+
cursor.execute("""
|
| 1815 |
+
INSERT INTO Questions (QuizID, QuestionText)
|
| 1816 |
+
VALUES (%s, %s)
|
| 1817 |
+
""", (quiz_id, question['question']))
|
| 1818 |
+
conn.commit()
|
| 1819 |
+
|
| 1820 |
+
question_id = cursor.lastrowid
|
| 1821 |
+
|
| 1822 |
+
# Insert options
|
| 1823 |
+
for i, option in enumerate(question['options']):
|
| 1824 |
+
cursor.execute("""
|
| 1825 |
+
INSERT INTO Options (QuestionID, OptionText, IsCorrect)
|
| 1826 |
+
VALUES (%s, %s, %s)
|
| 1827 |
+
""", (question_id, option, i == question['correctAnswer']))
|
| 1828 |
+
conn.commit()
|
| 1829 |
+
|
| 1830 |
+
return {
|
| 1831 |
+
"id": lecture_id,
|
| 1832 |
+
"title": title,
|
| 1833 |
+
"message": "Lecture created successfully"
|
| 1834 |
+
}
|
| 1835 |
+
|
| 1836 |
+
except HTTPException:
|
| 1837 |
+
raise
|
| 1838 |
+
except Exception as e:
|
| 1839 |
+
print(f"Error in create_lecture: {str(e)}")
|
| 1840 |
+
raise HTTPException(status_code=500, detail=f"Failed to create lecture: {str(e)}")
|
| 1841 |
+
|
| 1842 |
+
except Exception as e:
|
| 1843 |
+
print(f"Token/user verification error: {str(e)}")
|
| 1844 |
+
raise HTTPException(status_code=401, detail="Invalid authentication token or user not found")
|
| 1845 |
+
|
| 1846 |
+
except HTTPException:
|
| 1847 |
+
raise
|
| 1848 |
+
except Exception as e:
|
| 1849 |
+
print(f"Error in create_lecture: {str(e)}")
|
| 1850 |
+
raise HTTPException(status_code=500, detail=str(e))
|
| 1851 |
+
finally:
|
| 1852 |
+
if 'conn' in locals():
|
| 1853 |
+
conn.close()
|
services/api/chat_endpoints.py
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from fastapi import APIRouter, Depends, HTTPException, Request, Cookie
|
| 2 |
+
from typing import Dict
|
| 3 |
+
from services.api.db.token_utils import decode_token
|
| 4 |
+
from services.api.chatbot.core import get_chat_response, get_chat_response_lecture
|
| 5 |
+
from pydantic import BaseModel
|
| 6 |
+
|
| 7 |
+
router = APIRouter()
|
| 8 |
+
|
| 9 |
+
class ChatMessage(BaseModel):
|
| 10 |
+
message: str
|
| 11 |
+
|
| 12 |
+
@router.post("/chat")
|
| 13 |
+
async def chat_endpoint(message: ChatMessage, request: Request, auth_token: str = Cookie(None)):
|
| 14 |
+
try:
|
| 15 |
+
# Verify auth token
|
| 16 |
+
if not auth_token:
|
| 17 |
+
auth_header = request.headers.get('Authorization')
|
| 18 |
+
if auth_header and auth_header.startswith('Bearer '):
|
| 19 |
+
auth_token = auth_header.split(' ')[1]
|
| 20 |
+
|
| 21 |
+
if not auth_token:
|
| 22 |
+
raise HTTPException(status_code=401, detail="No authentication token provided")
|
| 23 |
+
|
| 24 |
+
try:
|
| 25 |
+
user_data = decode_token(auth_token)
|
| 26 |
+
except Exception as e:
|
| 27 |
+
print(f"Token decode error: {str(e)}")
|
| 28 |
+
raise HTTPException(status_code=401, detail="Invalid authentication token")
|
| 29 |
+
|
| 30 |
+
# Get chatbot response
|
| 31 |
+
response = get_chat_response(message.message)
|
| 32 |
+
return {"answer": response}
|
| 33 |
+
|
| 34 |
+
except HTTPException as he:
|
| 35 |
+
raise he
|
| 36 |
+
except Exception as e:
|
| 37 |
+
print(f"Chat error: {str(e)}")
|
| 38 |
+
raise HTTPException(status_code=500, detail=str(e))
|
| 39 |
+
|
| 40 |
+
@router.post("/chat/lecture/{lecture_id}")
|
| 41 |
+
async def lecture_chat_endpoint(
|
| 42 |
+
lecture_id: int,
|
| 43 |
+
message: ChatMessage,
|
| 44 |
+
request: Request,
|
| 45 |
+
auth_token: str = Cookie(None)
|
| 46 |
+
):
|
| 47 |
+
try:
|
| 48 |
+
# Verify auth token
|
| 49 |
+
if not auth_token:
|
| 50 |
+
auth_header = request.headers.get('Authorization')
|
| 51 |
+
if auth_header and auth_header.startswith('Bearer '):
|
| 52 |
+
auth_token = auth_header.split(' ')[1]
|
| 53 |
+
|
| 54 |
+
if not auth_token:
|
| 55 |
+
raise HTTPException(status_code=401, detail="No authentication token provided")
|
| 56 |
+
|
| 57 |
+
try:
|
| 58 |
+
user_data = decode_token(auth_token)
|
| 59 |
+
except Exception as e:
|
| 60 |
+
print(f"Token decode error: {str(e)}")
|
| 61 |
+
raise HTTPException(status_code=401, detail="Invalid authentication token")
|
| 62 |
+
|
| 63 |
+
# Get chatbot response for specific lecture
|
| 64 |
+
response = get_chat_response_lecture(message.message, lecture_id)
|
| 65 |
+
return {"answer": response}
|
| 66 |
+
|
| 67 |
+
except HTTPException as he:
|
| 68 |
+
raise he
|
| 69 |
+
except Exception as e:
|
| 70 |
+
print(f"Lecture chat error: {str(e)}")
|
| 71 |
+
raise HTTPException(status_code=500, detail=str(e))
|
| 72 |
+
|
| 73 |
+
@router.delete("/chat/history")
|
| 74 |
+
async def clear_history_endpoint(request: Request, auth_token: str = Cookie(None)):
|
| 75 |
+
try:
|
| 76 |
+
# Verify auth token
|
| 77 |
+
if not auth_token:
|
| 78 |
+
auth_header = request.headers.get('Authorization')
|
| 79 |
+
if auth_header and auth_header.startswith('Bearer '):
|
| 80 |
+
auth_token = auth_header.split(' ')[1]
|
| 81 |
+
|
| 82 |
+
if not auth_token:
|
| 83 |
+
raise HTTPException(status_code=401, detail="No authentication token provided")
|
| 84 |
+
|
| 85 |
+
try:
|
| 86 |
+
user_data = decode_token(auth_token)
|
| 87 |
+
except Exception as e:
|
| 88 |
+
print(f"Token decode error: {str(e)}")
|
| 89 |
+
raise HTTPException(status_code=401, detail="Invalid authentication token")
|
| 90 |
+
|
| 91 |
+
# Clear both regular and lecture chat history
|
| 92 |
+
from services.api.chatbot.core import clear_chat_history, clear_lecture_chat_history
|
| 93 |
+
clear_chat_history()
|
| 94 |
+
clear_lecture_chat_history()
|
| 95 |
+
|
| 96 |
+
return {"status": "success", "message": "Chat history cleared"}
|
| 97 |
+
|
| 98 |
+
except HTTPException as he:
|
| 99 |
+
raise he
|
| 100 |
+
except Exception as e:
|
| 101 |
+
print(f"Clear history error: {str(e)}")
|
| 102 |
+
raise HTTPException(status_code=500, detail=str(e))
|
services/api/chatbot/__pycache__/config.cpython-312.pyc
ADDED
|
Binary file (958 Bytes). View file
|
|
|
services/api/chatbot/__pycache__/config.cpython-313.pyc
ADDED
|
Binary file (956 Bytes). View file
|
|
|
services/api/chatbot/__pycache__/core.cpython-312.pyc
ADDED
|
Binary file (4.91 kB). View file
|
|
|
services/api/chatbot/__pycache__/core.cpython-313.pyc
ADDED
|
Binary file (4.25 kB). View file
|
|
|
services/api/chatbot/__pycache__/llm.cpython-312.pyc
ADDED
|
Binary file (1.98 kB). View file
|
|
|
services/api/chatbot/__pycache__/llm.cpython-313.pyc
ADDED
|
Binary file (2.06 kB). View file
|
|
|
services/api/chatbot/__pycache__/prompts.cpython-312.pyc
ADDED
|
Binary file (2.68 kB). View file
|
|
|
services/api/chatbot/__pycache__/prompts.cpython-313.pyc
ADDED
|
Binary file (2.5 kB). View file
|
|
|
services/api/chatbot/__pycache__/retrieval.cpython-312.pyc
ADDED
|
Binary file (23.7 kB). View file
|
|
|
services/api/chatbot/__pycache__/retrieval.cpython-313.pyc
ADDED
|
Binary file (15.1 kB). View file
|
|
|
services/api/chatbot/config.py
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import google.generativeai as genai
|
| 2 |
+
from langchain.llms.base import LLM
|
| 3 |
+
from typing import List, Optional
|
| 4 |
+
from dotenv import load_dotenv
|
| 5 |
+
import os
|
| 6 |
+
|
| 7 |
+
load_dotenv()
|
| 8 |
+
|
| 9 |
+
GEMINI_API_KEY = os.getenv("GEMINI_API_KEY")
|
| 10 |
+
QDRANT_HOST = os.getenv("QDRANT_HOST")
|
| 11 |
+
QDRANT_API_KEY = os.getenv("QDRANT_API_KEY")
|
| 12 |
+
|
| 13 |
+
QDRANT_COLLECTION_NAME = "dbms_collection_courses"
|
| 14 |
+
QDRANT_COLLECTION_NAME_LECTURES = "dbms_collection_lectures"
|
| 15 |
+
MODEL_NAME = "gemini-2.0-flash"
|
| 16 |
+
|
| 17 |
+
EMBEDDING_MODEL = "sentence-transformers/all-MiniLM-L6-v2"
|
| 18 |
+
EMBEDDING_SIZE = 384
|
services/api/chatbot/core.py
ADDED
|
@@ -0,0 +1,117 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from .llm import gemini_llm
|
| 2 |
+
from langchain.chains import ConversationalRetrievalChain
|
| 3 |
+
from langchain.memory import ConversationBufferMemory
|
| 4 |
+
from .retrieval import get_vectorstore, sync_courses_to_qdrant, get_vectorstore_lectures
|
| 5 |
+
from langchain.chains import RetrievalQA
|
| 6 |
+
from .prompts import courses_prompt, condense_prompt, lectures_prompt
|
| 7 |
+
from qdrant_client.http import models
|
| 8 |
+
|
| 9 |
+
|
| 10 |
+
import qdrant_client
|
| 11 |
+
from .config import (
|
| 12 |
+
EMBEDDING_MODEL,
|
| 13 |
+
QDRANT_HOST,
|
| 14 |
+
QDRANT_API_KEY,
|
| 15 |
+
QDRANT_COLLECTION_NAME,
|
| 16 |
+
EMBEDDING_SIZE,
|
| 17 |
+
QDRANT_COLLECTION_NAME_LECTURES,
|
| 18 |
+
)
|
| 19 |
+
# from langchain.retrievers.self_query.base import SelfQueryRetriever
|
| 20 |
+
# from langchain.retrievers.self_query.qdrant import QdrantTranslator
|
| 21 |
+
client = qdrant_client.QdrantClient(QDRANT_HOST, api_key=QDRANT_API_KEY)
|
| 22 |
+
|
| 23 |
+
vector_store = get_vectorstore()
|
| 24 |
+
vector_store_lecture = get_vectorstore_lectures()
|
| 25 |
+
memory = ConversationBufferMemory(
|
| 26 |
+
memory_key="chat_history",
|
| 27 |
+
return_messages=True,
|
| 28 |
+
output_key="answer"
|
| 29 |
+
)
|
| 30 |
+
|
| 31 |
+
memory_lecture = ConversationBufferMemory(
|
| 32 |
+
memory_key="chat_history",
|
| 33 |
+
return_messages=True,
|
| 34 |
+
output_key="answer"
|
| 35 |
+
)
|
| 36 |
+
|
| 37 |
+
qa_chain = ConversationalRetrievalChain.from_llm(
|
| 38 |
+
llm=gemini_llm,
|
| 39 |
+
retriever=vector_store.as_retriever(search_kwargs={"k": 5}),
|
| 40 |
+
memory=memory,
|
| 41 |
+
condense_question_prompt=condense_prompt,
|
| 42 |
+
combine_docs_chain_kwargs={
|
| 43 |
+
"prompt": courses_prompt
|
| 44 |
+
},
|
| 45 |
+
return_source_documents=True
|
| 46 |
+
)
|
| 47 |
+
|
| 48 |
+
def bulid_qa_chain(lecture_id: int):
|
| 49 |
+
return ConversationalRetrievalChain.from_llm(
|
| 50 |
+
llm=gemini_llm,
|
| 51 |
+
retriever = vector_store_lecture.as_retriever(
|
| 52 |
+
search_kwargs={
|
| 53 |
+
"k": 5,
|
| 54 |
+
"filter": models.Filter(
|
| 55 |
+
must=[
|
| 56 |
+
models.FieldCondition(
|
| 57 |
+
key="metadata.LectureID",
|
| 58 |
+
match=models.MatchValue(value=lecture_id),
|
| 59 |
+
)
|
| 60 |
+
]
|
| 61 |
+
)
|
| 62 |
+
}
|
| 63 |
+
),
|
| 64 |
+
memory=memory_lecture,
|
| 65 |
+
condense_question_prompt=condense_prompt,
|
| 66 |
+
combine_docs_chain_kwargs={
|
| 67 |
+
"prompt": lectures_prompt
|
| 68 |
+
},
|
| 69 |
+
return_source_documents=True
|
| 70 |
+
)
|
| 71 |
+
|
| 72 |
+
def get_chat_response_lecture(user_input: str, lecture_id: int) -> str:
|
| 73 |
+
print(f"\n[DEBUG] Sample points from Qdrant for LectureID={lecture_id}")
|
| 74 |
+
points, _ = client.scroll(
|
| 75 |
+
collection_name=QDRANT_COLLECTION_NAME_LECTURES,
|
| 76 |
+
limit=10,
|
| 77 |
+
with_payload=True,
|
| 78 |
+
with_vectors=False,
|
| 79 |
+
scroll_filter=models.Filter(
|
| 80 |
+
must=[
|
| 81 |
+
models.FieldCondition(
|
| 82 |
+
key="metadata.LectureID",
|
| 83 |
+
match=models.MatchValue(value=lecture_id)
|
| 84 |
+
)
|
| 85 |
+
]
|
| 86 |
+
)
|
| 87 |
+
)
|
| 88 |
+
print(points[0].payload)
|
| 89 |
+
qa_chain_lecture = bulid_qa_chain(lecture_id)
|
| 90 |
+
response = qa_chain_lecture({"question": user_input})
|
| 91 |
+
print(lecture_id)
|
| 92 |
+
print()
|
| 93 |
+
print("Source Documents:")
|
| 94 |
+
for i, doc in enumerate(response["source_documents"], start=1):
|
| 95 |
+
cid = doc.metadata.get("LectureID", "N/A")
|
| 96 |
+
content = doc.page_content
|
| 97 |
+
print(f"{i}. CourseID={cid}\n {content}\n")
|
| 98 |
+
return response["answer"]
|
| 99 |
+
|
| 100 |
+
def get_chat_response(user_input: str) -> str:
|
| 101 |
+
response = qa_chain({"question": user_input})
|
| 102 |
+
print("Source Documents:")
|
| 103 |
+
for i, doc in enumerate(response["source_documents"], start=1):
|
| 104 |
+
cid = doc.metadata.get("CourseID", "N/A")
|
| 105 |
+
content = doc.page_content
|
| 106 |
+
print(f"{i}. CourseID={cid}\n {content}\n")
|
| 107 |
+
return response["answer"]
|
| 108 |
+
|
| 109 |
+
def clear_chat_history():
|
| 110 |
+
"""Clear the conversation memory for general chat"""
|
| 111 |
+
memory.clear()
|
| 112 |
+
return {"status": "success", "message": "Chat history cleared"}
|
| 113 |
+
|
| 114 |
+
def clear_lecture_chat_history():
|
| 115 |
+
"""Clear the conversation memory for lecture-specific chat"""
|
| 116 |
+
memory_lecture.clear()
|
| 117 |
+
return {"status": "success", "message": "Lecture chat history cleared"}
|
services/api/chatbot/llm.py
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import google.generativeai as genai
|
| 2 |
+
from langchain.llms.base import LLM
|
| 3 |
+
from typing import Optional, List
|
| 4 |
+
from .config import GEMINI_API_KEY, MODEL_NAME
|
| 5 |
+
|
| 6 |
+
genai.configure(api_key=GEMINI_API_KEY)
|
| 7 |
+
|
| 8 |
+
class GeminiWrapper(LLM):
|
| 9 |
+
"""Wrapper để sử dụng Gemini với LangChain."""
|
| 10 |
+
|
| 11 |
+
model: str = MODEL_NAME
|
| 12 |
+
|
| 13 |
+
def _call(self, prompt: str, stop: Optional[List[str]] = None) -> str:
|
| 14 |
+
"""Gửi prompt đến Gemini và trả về kết quả."""
|
| 15 |
+
model = genai.GenerativeModel(self.model)
|
| 16 |
+
response = model.generate_content(prompt)
|
| 17 |
+
return response.text if response and hasattr(response, 'text') else "Không có phản hồi từ Gemini."
|
| 18 |
+
|
| 19 |
+
@property
|
| 20 |
+
def _identifying_params(self) -> dict:
|
| 21 |
+
"""Trả về tham số nhận diện của mô hình."""
|
| 22 |
+
return {"model": self.model}
|
| 23 |
+
|
| 24 |
+
@property
|
| 25 |
+
def _llm_type(self) -> str:
|
| 26 |
+
return "gemini"
|
| 27 |
+
|
| 28 |
+
gemini_llm = GeminiWrapper()
|
| 29 |
+
|
services/api/chatbot/memory.py
ADDED
|
File without changes
|
services/api/chatbot/prompts.py
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from langchain.prompts import PromptTemplate
|
| 2 |
+
# from .llm import gemini_llm
|
| 3 |
+
# from langchain_core.prompts import ChatPromptTemplate
|
| 4 |
+
|
| 5 |
+
courses_prompt = PromptTemplate(
|
| 6 |
+
input_variables=["context", "question"],
|
| 7 |
+
template="""
|
| 8 |
+
Your name is Edumate. You are an AI assistant of THE LEARNING HOUSE — an online learning platform that provides a wide variety of courses.
|
| 9 |
+
|
| 10 |
+
Your mission is to help users find courses that match their needs and answer any additional questions they might have.
|
| 11 |
+
|
| 12 |
+
If users didn't show their demand to learning(e.g: They gretting, introduce, ...) introduce about yourself, that you're here to help them find most suitable course.
|
| 13 |
+
If enough information that show their preference, lets priotize recommend user with suitable course
|
| 14 |
+
|
| 15 |
+
Below is a list of courses retrieved from our database. Recommend relevant courses to the user by providing helpful information about each course.
|
| 16 |
+
{context}
|
| 17 |
+
If none of the available courses match the user's demand, politely inform them that we currently do not have a suitable course and that their request has been noted for future development.
|
| 18 |
+
|
| 19 |
+
This is the user's question:
|
| 20 |
+
{question}
|
| 21 |
+
|
| 22 |
+
You must answer in the same language as users.
|
| 23 |
+
---
|
| 24 |
+
|
| 25 |
+
### Response:
|
| 26 |
+
"""
|
| 27 |
+
)
|
| 28 |
+
|
| 29 |
+
|
| 30 |
+
lectures_prompt = PromptTemplate(
|
| 31 |
+
input_variables=["context", "question"],
|
| 32 |
+
template="""
|
| 33 |
+
You are an AI Teaching Assistant at THE LEARNING HOUSE. Your mission is to help learners better understand and engage with the course content.
|
| 34 |
+
|
| 35 |
+
Below is a passage from the course the learner is currently studying. You are an expert in this field, and your role is to support the learner by doing any of the following:
|
| 36 |
+
- Explain difficult parts in a simpler way
|
| 37 |
+
- Provide relevant examples or analogies
|
| 38 |
+
- Share fun or interesting facts
|
| 39 |
+
- Ask questions to reinforce understanding
|
| 40 |
+
- Summarize the content if needed
|
| 41 |
+
|
| 42 |
+
Course content:
|
| 43 |
+
{context}
|
| 44 |
+
|
| 45 |
+
Now, here is the learner's question:
|
| 46 |
+
{question}
|
| 47 |
+
|
| 48 |
+
If none of the available content seems related to the learner's question, kindly inform them that we currently do not have suitable material for this request and that their feedback has been noted for future development.
|
| 49 |
+
|
| 50 |
+
You must answer in the same language as users.
|
| 51 |
+
---
|
| 52 |
+
|
| 53 |
+
### Response:
|
| 54 |
+
"""
|
| 55 |
+
)
|
| 56 |
+
|
| 57 |
+
|
| 58 |
+
condense_prompt = PromptTemplate(
|
| 59 |
+
input_variables=["question", "chat_history"],
|
| 60 |
+
template="""
|
| 61 |
+
Given the following conversation and a follow-up question, rephrase the follow-up question to be a standalone question.
|
| 62 |
+
|
| 63 |
+
Chat History:
|
| 64 |
+
{chat_history}
|
| 65 |
+
|
| 66 |
+
Follow-Up Input:
|
| 67 |
+
{question}
|
| 68 |
+
|
| 69 |
+
Standalone question:
|
| 70 |
+
|
| 71 |
+
You must answer in the same language as users.
|
| 72 |
+
"""
|
| 73 |
+
)
|
services/api/chatbot/retrieval.py
ADDED
|
@@ -0,0 +1,531 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
import re
|
| 3 |
+
import hashlib
|
| 4 |
+
import pymysql
|
| 5 |
+
import qdrant_client
|
| 6 |
+
import asyncio
|
| 7 |
+
import aiomysql
|
| 8 |
+
from dotenv import load_dotenv
|
| 9 |
+
from langchain.schema import Document
|
| 10 |
+
from langchain.vectorstores import Qdrant
|
| 11 |
+
from langchain_huggingface import HuggingFaceEmbeddings
|
| 12 |
+
from qdrant_client.http.models import PointIdsList, Distance, VectorParams
|
| 13 |
+
from typing import List, Tuple, Dict, Set
|
| 14 |
+
from .config import (
|
| 15 |
+
EMBEDDING_MODEL,
|
| 16 |
+
QDRANT_HOST,
|
| 17 |
+
QDRANT_API_KEY,
|
| 18 |
+
QDRANT_COLLECTION_NAME,
|
| 19 |
+
EMBEDDING_SIZE,
|
| 20 |
+
QDRANT_COLLECTION_NAME_LECTURES,
|
| 21 |
+
)
|
| 22 |
+
|
| 23 |
+
load_dotenv()
|
| 24 |
+
|
| 25 |
+
#--- MySQL connection ---
|
| 26 |
+
MYSQL_USER = os.getenv("MYSQL_USER")
|
| 27 |
+
MYSQL_PASSWORD = os.getenv("MYSQL_PASSWORD")
|
| 28 |
+
MYSQL_HOST = os.getenv("MYSQL_HOST")
|
| 29 |
+
MYSQL_DB = os.getenv("MYSQL_DB")
|
| 30 |
+
MYSQL_PORT = int(os.getenv("MYSQL_PORT", 3306))
|
| 31 |
+
|
| 32 |
+
# Initialize embedding model with a more compatible configuration
|
| 33 |
+
embedding_model = HuggingFaceEmbeddings(
|
| 34 |
+
model_name="sentence-transformers/all-MiniLM-L6-v2",
|
| 35 |
+
model_kwargs={'device': 'cpu'},
|
| 36 |
+
encode_kwargs={'normalize_embeddings': True}
|
| 37 |
+
)
|
| 38 |
+
client = qdrant_client.QdrantClient(QDRANT_HOST, api_key=QDRANT_API_KEY)
|
| 39 |
+
|
| 40 |
+
def connect_db():
|
| 41 |
+
return pymysql.connect(
|
| 42 |
+
host=MYSQL_HOST,
|
| 43 |
+
user=MYSQL_USER,
|
| 44 |
+
password=MYSQL_PASSWORD,
|
| 45 |
+
database=MYSQL_DB,
|
| 46 |
+
port=MYSQL_PORT,
|
| 47 |
+
cursorclass=pymysql.cursors.DictCursor,
|
| 48 |
+
)
|
| 49 |
+
|
| 50 |
+
async def connect_db_async():
|
| 51 |
+
return await aiomysql.connect(
|
| 52 |
+
host=MYSQL_HOST,
|
| 53 |
+
user=MYSQL_USER,
|
| 54 |
+
password=MYSQL_PASSWORD,
|
| 55 |
+
database=MYSQL_DB,
|
| 56 |
+
port=MYSQL_PORT,
|
| 57 |
+
cursorclass=aiomysql.DictCursor,
|
| 58 |
+
)
|
| 59 |
+
|
| 60 |
+
#---- Lectures processing
|
| 61 |
+
|
| 62 |
+
def hash_lectures(l: dict) ->str:
|
| 63 |
+
text = "|".join([
|
| 64 |
+
str(l.get("CourseName", "")),
|
| 65 |
+
str(l.get("Descriptions", "")),
|
| 66 |
+
str(l.get("Skills", "")),
|
| 67 |
+
str(l.get("EstimatedDuration", "")),
|
| 68 |
+
str(l.get("Difficulty", "")),
|
| 69 |
+
str(l.get("AverageRating", "")),
|
| 70 |
+
])
|
| 71 |
+
return hashlib.md5(text.encode("utf-8")).hexdigest()
|
| 72 |
+
|
| 73 |
+
def load_sql_lectures(): # -> list[dict]
|
| 74 |
+
with connect_db() as conn:
|
| 75 |
+
with conn.cursor() as cursor:
|
| 76 |
+
cursor.execute("""
|
| 77 |
+
SELECT LectureID, Title, Description, Content
|
| 78 |
+
FROM Lectures
|
| 79 |
+
""")
|
| 80 |
+
return cursor.fetchall()
|
| 81 |
+
|
| 82 |
+
async def load_sql_lectures_async() -> List[Dict]:
|
| 83 |
+
async with await connect_db_async() as conn:
|
| 84 |
+
async with conn.cursor() as cursor:
|
| 85 |
+
await cursor.execute("""
|
| 86 |
+
SELECT LectureID, Title, Description, Content
|
| 87 |
+
FROM Lectures
|
| 88 |
+
""")
|
| 89 |
+
return await cursor.fetchall()
|
| 90 |
+
|
| 91 |
+
def convert_to_documents_lectures(lectures: list[dict]) -> list[Document]:
|
| 92 |
+
documents: list[Document] = []
|
| 93 |
+
for l in lectures:
|
| 94 |
+
parts = [
|
| 95 |
+
f"Lecture Title: {l.get('Title', 'No title')}",
|
| 96 |
+
f"Description: {l.get('Description', 'No description')}",
|
| 97 |
+
f"Content: {l.get('Content', 'None')}",
|
| 98 |
+
]
|
| 99 |
+
text = ", ".join(parts)
|
| 100 |
+
text = re.sub(r"\s+", " ", text).strip()
|
| 101 |
+
|
| 102 |
+
metadata = {
|
| 103 |
+
"LectureID": l["LectureID"],
|
| 104 |
+
"hash": hash_lectures(l)
|
| 105 |
+
}
|
| 106 |
+
|
| 107 |
+
documents.append(Document(page_content=text, metadata=metadata))
|
| 108 |
+
return documents
|
| 109 |
+
|
| 110 |
+
def get_existing_qdrant_data_lectures() -> tuple[set[int], dict[int,str]]:
|
| 111 |
+
qdrant_ids: set[int] = set()
|
| 112 |
+
qdrant_hash_map: dict[int,str] = {}
|
| 113 |
+
|
| 114 |
+
scroll_offset = None
|
| 115 |
+
while True:
|
| 116 |
+
points, scroll_offset = client.scroll(
|
| 117 |
+
collection_name=QDRANT_COLLECTION_NAME_LECTURES,
|
| 118 |
+
limit=1000,
|
| 119 |
+
with_payload=True,
|
| 120 |
+
offset=scroll_offset,
|
| 121 |
+
)
|
| 122 |
+
for pt in points:
|
| 123 |
+
cid = pt.payload["metadata"]["LectureID"]
|
| 124 |
+
qdrant_ids.add(cid)
|
| 125 |
+
qdrant_hash_map[cid] = pt.payload["metadata"].get("hash", "")
|
| 126 |
+
if scroll_offset is None:
|
| 127 |
+
break
|
| 128 |
+
|
| 129 |
+
return qdrant_ids, qdrant_hash_map
|
| 130 |
+
|
| 131 |
+
async def get_existing_qdrant_data_lectures_async() -> Tuple[Set[int], Dict[int, str]]:
|
| 132 |
+
qdrant_ids: set[int] = set()
|
| 133 |
+
qdrant_hash_map: dict[int,str] = {}
|
| 134 |
+
|
| 135 |
+
scroll_offset = None
|
| 136 |
+
while True:
|
| 137 |
+
# Note: client.scroll is synchronous but we're keeping the async pattern
|
| 138 |
+
points, scroll_offset = client.scroll(
|
| 139 |
+
collection_name=QDRANT_COLLECTION_NAME_LECTURES,
|
| 140 |
+
limit=1000,
|
| 141 |
+
with_payload=True,
|
| 142 |
+
offset=scroll_offset,
|
| 143 |
+
)
|
| 144 |
+
for pt in points:
|
| 145 |
+
cid = pt.payload["metadata"]["LectureID"]
|
| 146 |
+
qdrant_ids.add(cid)
|
| 147 |
+
qdrant_hash_map[cid] = pt.payload["metadata"].get("hash", "")
|
| 148 |
+
if scroll_offset is None:
|
| 149 |
+
break
|
| 150 |
+
|
| 151 |
+
return qdrant_ids, qdrant_hash_map
|
| 152 |
+
|
| 153 |
+
def sync_lectures_to_qdrant():
|
| 154 |
+
cols = client.get_collections().collections
|
| 155 |
+
if not any(c.name == QDRANT_COLLECTION_NAME_LECTURES for c in cols):
|
| 156 |
+
client.create_collection(
|
| 157 |
+
collection_name=QDRANT_COLLECTION_NAME_LECTURES,
|
| 158 |
+
vectors_config=VectorParams(size=EMBEDDING_SIZE, distance=Distance.COSINE),
|
| 159 |
+
)
|
| 160 |
+
|
| 161 |
+
# 2) Load data from MySQL
|
| 162 |
+
db_lectures = load_sql_lectures()
|
| 163 |
+
db_map = {l["LectureID"]: l for l in db_lectures}
|
| 164 |
+
db_ids = set(db_map.keys())
|
| 165 |
+
|
| 166 |
+
# 3) Load data from Qdrant
|
| 167 |
+
qdrant_ids, qdrant_hash_map = get_existing_qdrant_data_lectures()
|
| 168 |
+
|
| 169 |
+
# 4) detemine new / removed / updated
|
| 170 |
+
new_ids = db_ids - qdrant_ids
|
| 171 |
+
removed_ids = qdrant_ids - db_ids
|
| 172 |
+
updated_ids = {
|
| 173 |
+
cid
|
| 174 |
+
for cid in db_ids & qdrant_ids
|
| 175 |
+
if hash_lectures(db_map[cid]) != qdrant_hash_map.get(cid, "")
|
| 176 |
+
}
|
| 177 |
+
|
| 178 |
+
# 5) Upsert & update
|
| 179 |
+
to_upsert = [db_map[cid] for cid in new_ids | updated_ids]
|
| 180 |
+
if to_upsert:
|
| 181 |
+
docs = convert_to_documents_lectures(to_upsert)
|
| 182 |
+
vs = Qdrant(
|
| 183 |
+
client=client,
|
| 184 |
+
collection_name=QDRANT_COLLECTION_NAME_LECTURES,
|
| 185 |
+
embeddings=embedding_model,
|
| 186 |
+
content_payload_key="page_content",
|
| 187 |
+
metadata_payload_key="metadata",
|
| 188 |
+
)
|
| 189 |
+
vs.add_documents(docs)
|
| 190 |
+
print(f"Added/Updated: {len(docs)} documents.")
|
| 191 |
+
|
| 192 |
+
# 6) Delete Unavailable courses
|
| 193 |
+
if removed_ids:
|
| 194 |
+
client.delete(
|
| 195 |
+
collection_name=QDRANT_COLLECTION_NAME_LECTURES,
|
| 196 |
+
points_selector=PointIdsList(points=list(removed_ids)),
|
| 197 |
+
)
|
| 198 |
+
print(f"Removed: {len(removed_ids)} documents.")
|
| 199 |
+
|
| 200 |
+
print(
|
| 201 |
+
f"Sync completed. "
|
| 202 |
+
f"New: {len(new_ids)}, "
|
| 203 |
+
f"Updated: {len(updated_ids)}, "
|
| 204 |
+
f"Removed: {len(removed_ids)}"
|
| 205 |
+
)
|
| 206 |
+
collection_info = client.get_collection(QDRANT_COLLECTION_NAME_LECTURES)
|
| 207 |
+
total_points = collection_info.points_count
|
| 208 |
+
print(f"Number of vector in Vectordb: {total_points}")
|
| 209 |
+
|
| 210 |
+
async def sync_lectures_to_qdrant_async():
|
| 211 |
+
cols = client.get_collections().collections
|
| 212 |
+
if not any(c.name == QDRANT_COLLECTION_NAME_LECTURES for c in cols):
|
| 213 |
+
client.create_collection(
|
| 214 |
+
collection_name=QDRANT_COLLECTION_NAME_LECTURES,
|
| 215 |
+
vectors_config=VectorParams(size=EMBEDDING_SIZE, distance=Distance.COSINE),
|
| 216 |
+
)
|
| 217 |
+
|
| 218 |
+
# 2) Load data from MySQL
|
| 219 |
+
db_lectures = await load_sql_lectures_async()
|
| 220 |
+
db_map = {l["LectureID"]: l for l in db_lectures}
|
| 221 |
+
db_ids = set(db_map.keys())
|
| 222 |
+
|
| 223 |
+
# 3) Load data from Qdrant
|
| 224 |
+
qdrant_ids, qdrant_hash_map = await get_existing_qdrant_data_lectures_async()
|
| 225 |
+
|
| 226 |
+
# 4) determine new / removed / updated
|
| 227 |
+
new_ids = db_ids - qdrant_ids
|
| 228 |
+
removed_ids = qdrant_ids - db_ids
|
| 229 |
+
updated_ids = {
|
| 230 |
+
cid
|
| 231 |
+
for cid in db_ids & qdrant_ids
|
| 232 |
+
if hash_lectures(db_map[cid]) != qdrant_hash_map.get(cid, "")
|
| 233 |
+
}
|
| 234 |
+
|
| 235 |
+
# 5) Upsert & update
|
| 236 |
+
to_upsert = [db_map[cid] for cid in new_ids | updated_ids]
|
| 237 |
+
if to_upsert:
|
| 238 |
+
docs = convert_to_documents_lectures(to_upsert)
|
| 239 |
+
vs = Qdrant(
|
| 240 |
+
client=client,
|
| 241 |
+
collection_name=QDRANT_COLLECTION_NAME_LECTURES,
|
| 242 |
+
embeddings=embedding_model,
|
| 243 |
+
content_payload_key="page_content",
|
| 244 |
+
metadata_payload_key="metadata",
|
| 245 |
+
)
|
| 246 |
+
await asyncio.to_thread(vs.add_documents, docs)
|
| 247 |
+
print(f"Added/Updated: {len(docs)} documents.")
|
| 248 |
+
|
| 249 |
+
# 6) Delete Unavailable courses
|
| 250 |
+
if removed_ids:
|
| 251 |
+
await asyncio.to_thread(
|
| 252 |
+
client.delete,
|
| 253 |
+
collection_name=QDRANT_COLLECTION_NAME_LECTURES,
|
| 254 |
+
points_selector=PointIdsList(points=list(removed_ids)),
|
| 255 |
+
)
|
| 256 |
+
print(f"Removed: {len(removed_ids)} documents.")
|
| 257 |
+
|
| 258 |
+
print(
|
| 259 |
+
f"Sync completed. "
|
| 260 |
+
f"New: {len(new_ids)}, "
|
| 261 |
+
f"Updated: {len(updated_ids)}, "
|
| 262 |
+
f"Removed: {len(removed_ids)}"
|
| 263 |
+
)
|
| 264 |
+
collection_info = client.get_collection(QDRANT_COLLECTION_NAME_LECTURES)
|
| 265 |
+
total_points = collection_info.points_count
|
| 266 |
+
print(f"Number of vectors in Vectordb: {total_points}")
|
| 267 |
+
|
| 268 |
+
def get_vectorstore_lectures() -> Qdrant:
|
| 269 |
+
return Qdrant(
|
| 270 |
+
client=client,
|
| 271 |
+
collection_name=QDRANT_COLLECTION_NAME_LECTURES,
|
| 272 |
+
embeddings=embedding_model,
|
| 273 |
+
content_payload_key="page_content",
|
| 274 |
+
metadata_payload_key="metadata",
|
| 275 |
+
)
|
| 276 |
+
|
| 277 |
+
def get_vectorstore() -> Qdrant:
|
| 278 |
+
"""Alias for get_vectorstore_lectures for backward compatibility"""
|
| 279 |
+
return get_vectorstore_lectures()
|
| 280 |
+
|
| 281 |
+
async def reset_qdrant_collection_async():
|
| 282 |
+
collections = client.get_collections().collections
|
| 283 |
+
if any(c.name == QDRANT_COLLECTION_NAME_LECTURES for c in collections):
|
| 284 |
+
await asyncio.to_thread(client.delete_collection, QDRANT_COLLECTION_NAME_LECTURES)
|
| 285 |
+
print(f"Đã xoá collection: {QDRANT_COLLECTION_NAME_LECTURES}")
|
| 286 |
+
|
| 287 |
+
await asyncio.to_thread(
|
| 288 |
+
client.create_collection,
|
| 289 |
+
collection_name=QDRANT_COLLECTION_NAME_LECTURES,
|
| 290 |
+
vectors_config=VectorParams(size=EMBEDDING_SIZE, distance=Distance.COSINE),
|
| 291 |
+
)
|
| 292 |
+
print(f"Đã khởi tạo lại collection: {QDRANT_COLLECTION_NAME_LECTURES}")
|
| 293 |
+
|
| 294 |
+
|
| 295 |
+
|
| 296 |
+
|
| 297 |
+
|
| 298 |
+
|
| 299 |
+
|
| 300 |
+
|
| 301 |
+
|
| 302 |
+
|
| 303 |
+
|
| 304 |
+
|
| 305 |
+
|
| 306 |
+
|
| 307 |
+
|
| 308 |
+
|
| 309 |
+
|
| 310 |
+
|
| 311 |
+
|
| 312 |
+
|
| 313 |
+
#---- Courses processing
|
| 314 |
+
def hash_course(c: dict) -> str:
|
| 315 |
+
text = "|".join([
|
| 316 |
+
str(c.get("CourseName", "")),
|
| 317 |
+
str(c.get("Descriptions", "")),
|
| 318 |
+
str(c.get("Skills", "")),
|
| 319 |
+
str(c.get("EstimatedDuration", "")),
|
| 320 |
+
str(c.get("Difficulty", "")),
|
| 321 |
+
str(c.get("AverageRating", "")),
|
| 322 |
+
])
|
| 323 |
+
return hashlib.md5(text.encode("utf-8")).hexdigest()
|
| 324 |
+
|
| 325 |
+
def load_sql(): #-> list[dict]
|
| 326 |
+
with connect_db() as conn:
|
| 327 |
+
with conn.cursor() as cursor:
|
| 328 |
+
cursor.execute("SELECT * FROM Courses")
|
| 329 |
+
return cursor.fetchall()
|
| 330 |
+
|
| 331 |
+
async def load_sql_async() -> List[Dict]:
|
| 332 |
+
async with await connect_db_async() as conn:
|
| 333 |
+
async with conn.cursor() as cursor:
|
| 334 |
+
await cursor.execute("SELECT * FROM Courses")
|
| 335 |
+
return await cursor.fetchall()
|
| 336 |
+
|
| 337 |
+
def convert_to_documents(courses: list[dict]) -> list[Document]:
|
| 338 |
+
documents: list[Document] = []
|
| 339 |
+
for c in courses:
|
| 340 |
+
# 1) Build the textual content
|
| 341 |
+
parts = [
|
| 342 |
+
f"CourseName: {c.get('CourseName', 'No title')}",
|
| 343 |
+
f"Descriptions: {c.get('Descriptions', 'No description')}",
|
| 344 |
+
f"Skills: {c.get('Skills', 'None')}",
|
| 345 |
+
f"EstimatedDuration (hours): {c.get('EstimatedDuration', 'Unknown')}",
|
| 346 |
+
f"Difficulty: {c.get('Difficulty', 'Unknown')}",
|
| 347 |
+
f"AverageRating: {c.get('AverageRating', '0.00')}",
|
| 348 |
+
]
|
| 349 |
+
text = ", ".join(parts)
|
| 350 |
+
text = re.sub(r"\s+", " ", text).strip()
|
| 351 |
+
|
| 352 |
+
# 2) Assemble metadata
|
| 353 |
+
metadata = {
|
| 354 |
+
"CourseID": c["CourseID"],
|
| 355 |
+
"Skills": c.get("Skills", ""),
|
| 356 |
+
"EstimatedDuration": c.get("EstimatedDuration", 0),
|
| 357 |
+
"Difficulty": c.get("Difficulty", ""),
|
| 358 |
+
"AverageRating": float(c.get("AverageRating", 0.0)),
|
| 359 |
+
"hash": hash_course(c),
|
| 360 |
+
}
|
| 361 |
+
|
| 362 |
+
documents.append(Document(page_content=text, metadata=metadata))
|
| 363 |
+
return documents
|
| 364 |
+
|
| 365 |
+
|
| 366 |
+
def get_existing_qdrant_data() -> tuple[set[int], dict[int,str]]:
|
| 367 |
+
qdrant_ids: set[int] = set()
|
| 368 |
+
qdrant_hash_map: dict[int,str] = {}
|
| 369 |
+
|
| 370 |
+
scroll_offset = None
|
| 371 |
+
while True:
|
| 372 |
+
points, scroll_offset = client.scroll(
|
| 373 |
+
collection_name=QDRANT_COLLECTION_NAME,
|
| 374 |
+
limit=1000,
|
| 375 |
+
with_payload=True,
|
| 376 |
+
offset=scroll_offset,
|
| 377 |
+
)
|
| 378 |
+
for pt in points:
|
| 379 |
+
cid = pt.payload["metadata"]["CourseID"]
|
| 380 |
+
qdrant_ids.add(cid)
|
| 381 |
+
qdrant_hash_map[cid] = pt.payload["metadata"].get("hash", "")
|
| 382 |
+
if scroll_offset is None:
|
| 383 |
+
break
|
| 384 |
+
|
| 385 |
+
return qdrant_ids, qdrant_hash_map
|
| 386 |
+
|
| 387 |
+
async def get_existing_qdrant_data_async() -> Tuple[Set[int], Dict[int, str]]:
|
| 388 |
+
qdrant_ids: set[int] = set()
|
| 389 |
+
qdrant_hash_map: dict[int,str] = {}
|
| 390 |
+
|
| 391 |
+
scroll_offset = None
|
| 392 |
+
while True:
|
| 393 |
+
points, scroll_offset = client.scroll(
|
| 394 |
+
collection_name=QDRANT_COLLECTION_NAME,
|
| 395 |
+
limit=1000,
|
| 396 |
+
with_payload=True,
|
| 397 |
+
offset=scroll_offset,
|
| 398 |
+
)
|
| 399 |
+
for pt in points:
|
| 400 |
+
cid = pt.payload["metadata"]["CourseID"]
|
| 401 |
+
qdrant_ids.add(cid)
|
| 402 |
+
qdrant_hash_map[cid] = pt.payload["metadata"].get("hash", "")
|
| 403 |
+
if scroll_offset is None:
|
| 404 |
+
break
|
| 405 |
+
|
| 406 |
+
return qdrant_ids, qdrant_hash_map
|
| 407 |
+
|
| 408 |
+
def sync_courses_to_qdrant():
|
| 409 |
+
cols = client.get_collections().collections
|
| 410 |
+
if not any(c.name == QDRANT_COLLECTION_NAME for c in cols):
|
| 411 |
+
client.create_collection(
|
| 412 |
+
collection_name=QDRANT_COLLECTION_NAME,
|
| 413 |
+
vectors_config=VectorParams(size=EMBEDDING_SIZE, distance=Distance.COSINE),
|
| 414 |
+
)
|
| 415 |
+
|
| 416 |
+
# 2) Load data from MySQL
|
| 417 |
+
db_courses = load_sql()
|
| 418 |
+
db_map = {c["CourseID"]: c for c in db_courses}
|
| 419 |
+
db_ids = set(db_map.keys())
|
| 420 |
+
|
| 421 |
+
# 3) Load data from Qdrant
|
| 422 |
+
qdrant_ids, qdrant_hash_map = get_existing_qdrant_data()
|
| 423 |
+
|
| 424 |
+
# 4) detemine new / removed / updated
|
| 425 |
+
new_ids = db_ids - qdrant_ids
|
| 426 |
+
removed_ids = qdrant_ids - db_ids
|
| 427 |
+
updated_ids = {
|
| 428 |
+
cid
|
| 429 |
+
for cid in db_ids & qdrant_ids
|
| 430 |
+
if hash_course(db_map[cid]) != qdrant_hash_map.get(cid, "")
|
| 431 |
+
}
|
| 432 |
+
|
| 433 |
+
# 5) Upsert & update
|
| 434 |
+
to_upsert = [db_map[cid] for cid in new_ids | updated_ids]
|
| 435 |
+
if to_upsert:
|
| 436 |
+
docs = convert_to_documents(to_upsert)
|
| 437 |
+
vs = Qdrant(
|
| 438 |
+
client=client,
|
| 439 |
+
collection_name=QDRANT_COLLECTION_NAME,
|
| 440 |
+
embeddings=embedding_model,
|
| 441 |
+
content_payload_key="page_content",
|
| 442 |
+
metadata_payload_key="metadata",
|
| 443 |
+
)
|
| 444 |
+
vs.add_documents(docs)
|
| 445 |
+
print(f"Added/Updated: {len(docs)} documents.")
|
| 446 |
+
|
| 447 |
+
# 6) Delete Unavailable courses
|
| 448 |
+
if removed_ids:
|
| 449 |
+
client.delete(
|
| 450 |
+
collection_name=QDRANT_COLLECTION_NAME,
|
| 451 |
+
points_selector=PointIdsList(points=list(removed_ids)),
|
| 452 |
+
)
|
| 453 |
+
print(f"🗑 Removed: {len(removed_ids)} documents.")
|
| 454 |
+
|
| 455 |
+
print(
|
| 456 |
+
f"Sync completed. "
|
| 457 |
+
f"New: {len(new_ids)}, "
|
| 458 |
+
f"Updated: {len(updated_ids)}, "
|
| 459 |
+
f"Removed: {len(removed_ids)}"
|
| 460 |
+
)
|
| 461 |
+
collection_info = client.get_collection(QDRANT_COLLECTION_NAME)
|
| 462 |
+
total_points = collection_info.points_count
|
| 463 |
+
print(f"Number of vector in Vectordb: {total_points}")
|
| 464 |
+
|
| 465 |
+
async def sync_courses_to_qdrant_async():
|
| 466 |
+
cols = client.get_collections().collections
|
| 467 |
+
if not any(c.name == QDRANT_COLLECTION_NAME for c in cols):
|
| 468 |
+
client.create_collection(
|
| 469 |
+
collection_name=QDRANT_COLLECTION_NAME,
|
| 470 |
+
vectors_config=VectorParams(size=EMBEDDING_SIZE, distance=Distance.COSINE),
|
| 471 |
+
)
|
| 472 |
+
|
| 473 |
+
# 2) Load data from MySQL
|
| 474 |
+
db_courses = await load_sql_async()
|
| 475 |
+
db_map = {c["CourseID"]: c for c in db_courses}
|
| 476 |
+
db_ids = set(db_map.keys())
|
| 477 |
+
|
| 478 |
+
# 3) Load data from Qdrant
|
| 479 |
+
qdrant_ids, qdrant_hash_map = await get_existing_qdrant_data_async()
|
| 480 |
+
|
| 481 |
+
# 4) determine new / removed / updated
|
| 482 |
+
new_ids = db_ids - qdrant_ids
|
| 483 |
+
removed_ids = qdrant_ids - db_ids
|
| 484 |
+
updated_ids = {
|
| 485 |
+
cid
|
| 486 |
+
for cid in db_ids & qdrant_ids
|
| 487 |
+
if hash_course(db_map[cid]) != qdrant_hash_map.get(cid, "")
|
| 488 |
+
}
|
| 489 |
+
|
| 490 |
+
# 5) Upsert & update
|
| 491 |
+
to_upsert = [db_map[cid] for cid in new_ids | updated_ids]
|
| 492 |
+
if to_upsert:
|
| 493 |
+
docs = convert_to_documents(to_upsert)
|
| 494 |
+
vs = Qdrant(
|
| 495 |
+
client=client,
|
| 496 |
+
collection_name=QDRANT_COLLECTION_NAME,
|
| 497 |
+
embeddings=embedding_model,
|
| 498 |
+
content_payload_key="page_content",
|
| 499 |
+
metadata_payload_key="metadata",
|
| 500 |
+
)
|
| 501 |
+
await asyncio.to_thread(vs.add_documents, docs)
|
| 502 |
+
print(f"Added/Updated: {len(docs)} documents.")
|
| 503 |
+
|
| 504 |
+
# 6) Delete Unavailable courses
|
| 505 |
+
if removed_ids:
|
| 506 |
+
await asyncio.to_thread(
|
| 507 |
+
client.delete,
|
| 508 |
+
collection_name=QDRANT_COLLECTION_NAME,
|
| 509 |
+
points_selector=PointIdsList(points=list(removed_ids)),
|
| 510 |
+
)
|
| 511 |
+
print(f"🗑 Removed: {len(removed_ids)} documents.")
|
| 512 |
+
|
| 513 |
+
print(
|
| 514 |
+
f"Sync completed. "
|
| 515 |
+
f"New: {len(new_ids)}, "
|
| 516 |
+
f"Updated: {len(updated_ids)}, "
|
| 517 |
+
f"Removed: {len(removed_ids)}"
|
| 518 |
+
)
|
| 519 |
+
|
| 520 |
+
|
| 521 |
+
def reset_qdrant_collection():
|
| 522 |
+
collections = client.get_collections().collections
|
| 523 |
+
if any(c.name == QDRANT_COLLECTION_NAME for c in collections):
|
| 524 |
+
client.delete_collection(QDRANT_COLLECTION_NAME)
|
| 525 |
+
print(f"Đã xoá collection: {QDRANT_COLLECTION_NAME}")
|
| 526 |
+
|
| 527 |
+
client.create_collection(
|
| 528 |
+
collection_name=QDRANT_COLLECTION_NAME,
|
| 529 |
+
vectors_config=VectorParams(size=EMBEDDING_SIZE, distance=Distance.COSINE),
|
| 530 |
+
)
|
| 531 |
+
print(f"Đã khởi tạo lại collection: {QDRANT_COLLECTION_NAME}")
|
services/api/chatbot/self_query.py
ADDED
|
File without changes
|
services/api/db/__pycache__/__init__.cpython-312.pyc
ADDED
|
Binary file (231 Bytes). View file
|
|
|
services/api/db/__pycache__/auth.cpython-312.pyc
ADDED
|
Binary file (20.5 kB). View file
|
|
|
services/api/db/__pycache__/db_connection.cpython-312.pyc
ADDED
|
Binary file (1.5 kB). View file
|
|
|
services/api/db/__pycache__/token_utils.cpython-312.pyc
ADDED
|
Binary file (2.24 kB). View file
|
|
|
services/api/db/auth.py
ADDED
|
@@ -0,0 +1,437 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
auth.py – Lightweight authentication micro-service
|
| 3 |
+
----------------------------------------------------
|
| 4 |
+
• Exposes /auth/verify_user (login check)
|
| 5 |
+
• Optional /auth/register_user for sign-up
|
| 6 |
+
• Uses MySQL, bcrypt, and Pydantic
|
| 7 |
+
• No Streamlit, cookies, or front-end logic
|
| 8 |
+
"""
|
| 9 |
+
|
| 10 |
+
from fastapi import FastAPI, Request, Response, Cookie, HTTPException, Depends
|
| 11 |
+
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
|
| 12 |
+
from fastapi.responses import HTMLResponse, JSONResponse
|
| 13 |
+
from fastapi.middleware.cors import CORSMiddleware
|
| 14 |
+
from fastapi.security import OAuth2PasswordRequestForm
|
| 15 |
+
from jose import jwt, JWTError
|
| 16 |
+
from pydantic import BaseModel
|
| 17 |
+
from dotenv import load_dotenv
|
| 18 |
+
import os
|
| 19 |
+
import time
|
| 20 |
+
import requests
|
| 21 |
+
import os
|
| 22 |
+
import pymysql
|
| 23 |
+
import bcrypt
|
| 24 |
+
import sys
|
| 25 |
+
import importlib.util
|
| 26 |
+
from services.api.db.token_utils import create_token, decode_token
|
| 27 |
+
from sqlalchemy.orm import Session
|
| 28 |
+
|
| 29 |
+
# Add parent directory to path to enable imports
|
| 30 |
+
sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
| 31 |
+
|
| 32 |
+
# Create FastAPI app
|
| 33 |
+
app = FastAPI()
|
| 34 |
+
|
| 35 |
+
# Configure CORS
|
| 36 |
+
app.add_middleware(
|
| 37 |
+
CORSMiddleware,
|
| 38 |
+
allow_origins=["http://localhost:3000, https://tlong-ds.github.io/thelearninghouse/"], # Your React frontend URL
|
| 39 |
+
allow_credentials=True, # Important for cookies
|
| 40 |
+
allow_methods=["*"],
|
| 41 |
+
allow_headers=["*"],
|
| 42 |
+
expose_headers=["*"]
|
| 43 |
+
)
|
| 44 |
+
|
| 45 |
+
# Import API endpoints
|
| 46 |
+
try:
|
| 47 |
+
from services.api.api_endpoints import router as api_router
|
| 48 |
+
except ImportError:
|
| 49 |
+
# Try a different import strategy for direct module execution
|
| 50 |
+
spec = importlib.util.spec_from_file_location(
|
| 51 |
+
"api_endpoints",
|
| 52 |
+
os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "api_endpoints.py")
|
| 53 |
+
)
|
| 54 |
+
api_endpoints = importlib.util.module_from_spec(spec)
|
| 55 |
+
spec.loader.exec_module(api_endpoints)
|
| 56 |
+
api_router = api_endpoints.router
|
| 57 |
+
|
| 58 |
+
|
| 59 |
+
# Load environment variables
|
| 60 |
+
load_dotenv()
|
| 61 |
+
SECRET_KEY = os.getenv("SECRET_TOKEN", "dev-secret")
|
| 62 |
+
ALGORITHM = os.getenv("ALGORITHM", "HS256")
|
| 63 |
+
MYSQL_USER = os.getenv("MYSQL_USER")
|
| 64 |
+
MYSQL_PASSWORD = os.getenv("MYSQL_PASSWORD")
|
| 65 |
+
MYSQL_HOST = os.getenv("MYSQL_HOST", "localhost")
|
| 66 |
+
MYSQL_DB = os.getenv("MYSQL_DB")
|
| 67 |
+
MYSQL_PORT = int(os.getenv("MYSQL_PORT", "3306"))
|
| 68 |
+
|
| 69 |
+
|
| 70 |
+
|
| 71 |
+
# Initialize FastAPI app
|
| 72 |
+
app = FastAPI()
|
| 73 |
+
app.add_middleware(
|
| 74 |
+
CORSMiddleware,
|
| 75 |
+
allow_origins=["http://localhost:8503", "http://localhost:3000", "http://127.0.0.1:3000", "https://tlong-ds.github.io"],
|
| 76 |
+
allow_credentials=True,
|
| 77 |
+
allow_methods=["GET", "POST", "PUT", "DELETE", "OPTIONS"],
|
| 78 |
+
allow_headers=["*"],
|
| 79 |
+
expose_headers=["*"],
|
| 80 |
+
)
|
| 81 |
+
|
| 82 |
+
# Import and include chat router
|
| 83 |
+
try:
|
| 84 |
+
from services.api.chat_endpoints import router as chat_router
|
| 85 |
+
except ImportError:
|
| 86 |
+
spec = importlib.util.spec_from_file_location(
|
| 87 |
+
"chat_endpoints",
|
| 88 |
+
os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "chat_endpoints.py")
|
| 89 |
+
)
|
| 90 |
+
chat_endpoints = importlib.util.module_from_spec(spec)
|
| 91 |
+
spec.loader.exec_module(chat_endpoints)
|
| 92 |
+
chat_router = chat_endpoints.router
|
| 93 |
+
|
| 94 |
+
# Debug print of available routes
|
| 95 |
+
print("DEBUG: Available routes in api_router:")
|
| 96 |
+
for route in api_router.routes:
|
| 97 |
+
print(f"Route: {route.path}, Methods: {route.methods}")
|
| 98 |
+
|
| 99 |
+
# Include API routers
|
| 100 |
+
app.include_router(api_router)
|
| 101 |
+
app.include_router(chat_router, prefix="/api")
|
| 102 |
+
|
| 103 |
+
|
| 104 |
+
class LoginPayload(BaseModel):
|
| 105 |
+
username: str
|
| 106 |
+
password: str
|
| 107 |
+
role: str
|
| 108 |
+
|
| 109 |
+
class PasswordChangePayload(BaseModel):
|
| 110 |
+
current_password: str
|
| 111 |
+
new_password: str
|
| 112 |
+
|
| 113 |
+
class PasswordChangePayload(BaseModel):
|
| 114 |
+
current_password: str
|
| 115 |
+
new_password: str
|
| 116 |
+
|
| 117 |
+
# Token functions moved to token_utils.py
|
| 118 |
+
|
| 119 |
+
@app.post("/login")
|
| 120 |
+
async def login(response: Response, payload: LoginPayload):
|
| 121 |
+
try:
|
| 122 |
+
print(f"Login attempt: {payload.username}, role: {payload.role}")
|
| 123 |
+
|
| 124 |
+
# Directly verify the user credentials
|
| 125 |
+
table_map = {
|
| 126 |
+
"Learner": "Learners",
|
| 127 |
+
"Instructor": "Instructors"
|
| 128 |
+
}
|
| 129 |
+
table = table_map.get(payload.role)
|
| 130 |
+
if not table:
|
| 131 |
+
print(f"Invalid role: {payload.role}")
|
| 132 |
+
raise HTTPException(status_code=400, detail="Invalid role")
|
| 133 |
+
|
| 134 |
+
conn = connect_db()
|
| 135 |
+
try:
|
| 136 |
+
with conn.cursor() as cur:
|
| 137 |
+
query = f"SELECT Password FROM {table} WHERE AccountName=%s LIMIT 1"
|
| 138 |
+
print(f"Executing query: {query} with username: {payload.username}")
|
| 139 |
+
|
| 140 |
+
cur.execute(query, (payload.username,))
|
| 141 |
+
row = cur.fetchone()
|
| 142 |
+
|
| 143 |
+
if not row:
|
| 144 |
+
print(f"No user found with username: {payload.username} in table: {table}")
|
| 145 |
+
raise HTTPException(status_code=401, detail="Incorrect username or password")
|
| 146 |
+
|
| 147 |
+
password_valid = check_password(payload.password, row[0])
|
| 148 |
+
print(f"Password check result: {password_valid}")
|
| 149 |
+
|
| 150 |
+
if not password_valid:
|
| 151 |
+
print(f"Authentication failed: Invalid password")
|
| 152 |
+
raise HTTPException(status_code=401, detail="Incorrect username or password")
|
| 153 |
+
except Exception as db_err:
|
| 154 |
+
print(f"Database error: {str(db_err)}")
|
| 155 |
+
raise HTTPException(status_code=500, detail=f"Database error: {str(db_err)}")
|
| 156 |
+
finally:
|
| 157 |
+
conn.close()
|
| 158 |
+
|
| 159 |
+
# User authenticated successfully
|
| 160 |
+
user_data = {"username": payload.username, "role": payload.role}
|
| 161 |
+
print(f"Authentication successful for: {payload.username}")
|
| 162 |
+
|
| 163 |
+
token = create_token(user_data)
|
| 164 |
+
response.set_cookie(
|
| 165 |
+
key="auth_token",
|
| 166 |
+
value=token,
|
| 167 |
+
httponly=False, # Cookie cannot be accessed by JavaScript
|
| 168 |
+
samesite="Lax", # More secure than None
|
| 169 |
+
secure=True, # Only send cookie over HTTPS
|
| 170 |
+
path="/",
|
| 171 |
+
max_age=604800 # 7 days
|
| 172 |
+
)
|
| 173 |
+
return {
|
| 174 |
+
"message": f"Login successful for {user_data['username']}",
|
| 175 |
+
"username": user_data["username"],
|
| 176 |
+
"role": user_data["role"],
|
| 177 |
+
"token": token
|
| 178 |
+
}
|
| 179 |
+
except Exception as e:
|
| 180 |
+
print(f"Login exception: {str(e)}")
|
| 181 |
+
raise HTTPException(status_code=500, detail=f"Login error: {str(e)}")
|
| 182 |
+
|
| 183 |
+
@app.get("/logout")
|
| 184 |
+
def logout(response: Response):
|
| 185 |
+
response.delete_cookie("auth_token")
|
| 186 |
+
return HTMLResponse("<h3>Logged out — cookie cleared!</h3>")
|
| 187 |
+
|
| 188 |
+
@app.get("/whoami")
|
| 189 |
+
async def whoami(auth_token: str = Cookie(None)):
|
| 190 |
+
payload = decode_token(auth_token)
|
| 191 |
+
return {"username": payload["username"], "role": payload["role"]}
|
| 192 |
+
|
| 193 |
+
@app.get("/protected")
|
| 194 |
+
def protected_route(auth_token: str = Cookie(None)):
|
| 195 |
+
payload = decode_token(auth_token)
|
| 196 |
+
return {"msg": "Access granted", "user": payload}
|
| 197 |
+
|
| 198 |
+
@app.put("/api/user/password")
|
| 199 |
+
async def change_password(payload: PasswordChangePayload, request: Request, auth_token: str = Cookie(None)):
|
| 200 |
+
"""Change user password endpoint."""
|
| 201 |
+
# Get token from cookie or Authorization header
|
| 202 |
+
token = auth_token
|
| 203 |
+
if not token and "Authorization" in request.headers:
|
| 204 |
+
auth_header = request.headers["Authorization"]
|
| 205 |
+
if auth_header.startswith("Bearer "):
|
| 206 |
+
token = auth_header[7:] # Remove 'Bearer ' prefix
|
| 207 |
+
|
| 208 |
+
if not token:
|
| 209 |
+
raise HTTPException(status_code=401, detail="No authentication token provided")
|
| 210 |
+
try:
|
| 211 |
+
# Decode token to get user info
|
| 212 |
+
user_data = decode_token(token)
|
| 213 |
+
if not user_data:
|
| 214 |
+
raise HTTPException(status_code=401, detail="Invalid token")
|
| 215 |
+
|
| 216 |
+
username = user_data["username"]
|
| 217 |
+
role = user_data["role"]
|
| 218 |
+
|
| 219 |
+
# Get the correct table based on role
|
| 220 |
+
table_map = {
|
| 221 |
+
"Learner": "Learners",
|
| 222 |
+
"Instructor": "Instructors"
|
| 223 |
+
}
|
| 224 |
+
table = table_map.get(role)
|
| 225 |
+
if not table:
|
| 226 |
+
raise HTTPException(status_code=400, detail="Invalid role")
|
| 227 |
+
|
| 228 |
+
conn = connect_db()
|
| 229 |
+
try:
|
| 230 |
+
with conn.cursor() as cur:
|
| 231 |
+
# First verify current password
|
| 232 |
+
query = f"SELECT Password FROM {table} WHERE AccountName=%s LIMIT 1"
|
| 233 |
+
cur.execute(query, (username,))
|
| 234 |
+
row = cur.fetchone()
|
| 235 |
+
|
| 236 |
+
if not row or not check_password(payload.current_password, row[0]):
|
| 237 |
+
raise HTTPException(status_code=401, detail="Current password is incorrect")
|
| 238 |
+
|
| 239 |
+
# Hash the new password
|
| 240 |
+
new_password_hash = hash_password(payload.new_password)
|
| 241 |
+
|
| 242 |
+
# Update the password
|
| 243 |
+
update_query = f"UPDATE {table} SET Password=%s WHERE AccountName=%s"
|
| 244 |
+
cur.execute(update_query, (new_password_hash, username))
|
| 245 |
+
conn.commit()
|
| 246 |
+
|
| 247 |
+
return {"message": "Password updated successfully"}
|
| 248 |
+
|
| 249 |
+
except Exception as db_err:
|
| 250 |
+
conn.rollback()
|
| 251 |
+
print(f"Database error: {str(db_err)}")
|
| 252 |
+
raise HTTPException(status_code=500, detail=f"Database error: {str(db_err)}")
|
| 253 |
+
finally:
|
| 254 |
+
conn.close()
|
| 255 |
+
|
| 256 |
+
except HTTPException:
|
| 257 |
+
raise
|
| 258 |
+
except Exception as e:
|
| 259 |
+
print(f"Unexpected error in change_password: {str(e)}")
|
| 260 |
+
raise HTTPException(status_code=500, detail=f"Error: {str(e)}")
|
| 261 |
+
|
| 262 |
+
def connect_db():
|
| 263 |
+
return pymysql.connect(
|
| 264 |
+
host=MYSQL_HOST,
|
| 265 |
+
user=MYSQL_USER,
|
| 266 |
+
password=MYSQL_PASSWORD,
|
| 267 |
+
database=MYSQL_DB,
|
| 268 |
+
port=MYSQL_PORT
|
| 269 |
+
)
|
| 270 |
+
|
| 271 |
+
def check_password(plain: str, hashed: str) -> bool:
|
| 272 |
+
"""Compare plaintext vs bcrypt hash stored in DB."""
|
| 273 |
+
return bcrypt.checkpw(plain.encode("utf-8"), hashed.encode("utf-8"))
|
| 274 |
+
|
| 275 |
+
def hash_password(plain: str) -> str:
|
| 276 |
+
"""Return bcrypt hash for storage."""
|
| 277 |
+
return bcrypt.hashpw(plain.encode("utf-8"), bcrypt.gensalt()).decode("utf-8")
|
| 278 |
+
|
| 279 |
+
# ───────────────────────────────────────────��────────────────────────────────────
|
| 280 |
+
# Pydantic models
|
| 281 |
+
# ────────────────────────────────────────────────────────────────────────────────
|
| 282 |
+
class UserCredentials(BaseModel):
|
| 283 |
+
username: str
|
| 284 |
+
password: str
|
| 285 |
+
role: str # "Learner" or "Instructor"
|
| 286 |
+
|
| 287 |
+
class NewUser(UserCredentials):
|
| 288 |
+
name: str
|
| 289 |
+
email: str
|
| 290 |
+
|
| 291 |
+
|
| 292 |
+
@app.post("/auth/verify_user")
|
| 293 |
+
def verify_user(creds: UserCredentials):
|
| 294 |
+
"""
|
| 295 |
+
Validate user credentials.
|
| 296 |
+
Returns {username, role} on success; 401 on failure.
|
| 297 |
+
"""
|
| 298 |
+
try:
|
| 299 |
+
print(f"Verify user attempt for: {creds.username}, role: {creds.role}")
|
| 300 |
+
|
| 301 |
+
table_map = {
|
| 302 |
+
"Learner": "Learners",
|
| 303 |
+
"Instructor": "Instructors"
|
| 304 |
+
}
|
| 305 |
+
table = table_map.get(creds.role)
|
| 306 |
+
if not table:
|
| 307 |
+
print(f"Invalid role: {creds.role}")
|
| 308 |
+
raise HTTPException(status_code=400, detail="Invalid role")
|
| 309 |
+
|
| 310 |
+
conn = connect_db()
|
| 311 |
+
try:
|
| 312 |
+
with conn.cursor() as cur:
|
| 313 |
+
query = f"SELECT Password FROM {table} WHERE AccountName=%s LIMIT 1"
|
| 314 |
+
print(f"Executing query: {query} with username: {creds.username}")
|
| 315 |
+
|
| 316 |
+
cur.execute(query, (creds.username,))
|
| 317 |
+
row = cur.fetchone()
|
| 318 |
+
|
| 319 |
+
if not row:
|
| 320 |
+
print(f"No user found with username: {creds.username} in table: {table}")
|
| 321 |
+
except Exception as db_err:
|
| 322 |
+
print(f"Database error: {str(db_err)}")
|
| 323 |
+
raise HTTPException(status_code=500, detail=f"Database error: {str(db_err)}")
|
| 324 |
+
finally:
|
| 325 |
+
conn.close()
|
| 326 |
+
|
| 327 |
+
if not row:
|
| 328 |
+
print(f"Authentication failed: User not found")
|
| 329 |
+
raise HTTPException(status_code=401, detail="Incorrect username or password")
|
| 330 |
+
|
| 331 |
+
password_valid = check_password(creds.password, row[0])
|
| 332 |
+
print(f"Password check result: {password_valid}")
|
| 333 |
+
|
| 334 |
+
if not password_valid:
|
| 335 |
+
print(f"Authentication failed: Invalid password")
|
| 336 |
+
raise HTTPException(status_code=401, detail="Incorrect username or password")
|
| 337 |
+
|
| 338 |
+
print(f"Authentication successful for: {creds.username}")
|
| 339 |
+
return {"username": creds.username, "role": creds.role}
|
| 340 |
+
except HTTPException:
|
| 341 |
+
raise
|
| 342 |
+
except Exception as e:
|
| 343 |
+
print(f"Unexpected error in verify_user: {str(e)}")
|
| 344 |
+
raise HTTPException(status_code=500, detail=f"Error: {str(e)}")
|
| 345 |
+
|
| 346 |
+
|
| 347 |
+
@app.post("/auth/register_user")
|
| 348 |
+
def register_user(user: NewUser):
|
| 349 |
+
"""
|
| 350 |
+
Simple sign-up endpoint.
|
| 351 |
+
Returns 201 Created on success, 409 if username/email exists.
|
| 352 |
+
"""
|
| 353 |
+
table_cfg = {
|
| 354 |
+
"Learner": ("Learners", "LearnerName"),
|
| 355 |
+
"Instructor": ("Instructors", "InstructorName")
|
| 356 |
+
}
|
| 357 |
+
cfg = table_cfg.get(user.role)
|
| 358 |
+
if not cfg:
|
| 359 |
+
raise HTTPException(status_code=400, detail="Invalid role")
|
| 360 |
+
|
| 361 |
+
table, name_col = cfg
|
| 362 |
+
hashed_pw = hash_password(user.password)
|
| 363 |
+
|
| 364 |
+
conn = connect_db()
|
| 365 |
+
try:
|
| 366 |
+
with conn.cursor() as cur:
|
| 367 |
+
# Uniqueness check
|
| 368 |
+
cur.execute(
|
| 369 |
+
f"SELECT 1 FROM {table} WHERE AccountName=%s OR Email=%s LIMIT 1",
|
| 370 |
+
(user.username, user.email)
|
| 371 |
+
)
|
| 372 |
+
if cur.fetchone():
|
| 373 |
+
raise HTTPException(status_code=409, detail="Username or email already exists")
|
| 374 |
+
|
| 375 |
+
# Insert new record
|
| 376 |
+
cur.execute(
|
| 377 |
+
f"INSERT INTO {table} ({name_col}, Email, AccountName, Password) "
|
| 378 |
+
f"VALUES (%s, %s, %s, %s)",
|
| 379 |
+
(user.name, user.email, user.username, hashed_pw)
|
| 380 |
+
)
|
| 381 |
+
conn.commit()
|
| 382 |
+
finally:
|
| 383 |
+
conn.close()
|
| 384 |
+
|
| 385 |
+
return {"msg": "User created", "username": user.username, "role": user.role}, 201
|
| 386 |
+
|
| 387 |
+
|
| 388 |
+
@app.post("/api/auth/logout")
|
| 389 |
+
async def logout():
|
| 390 |
+
response = JSONResponse(content={"message": "Logged out successfully"})
|
| 391 |
+
# Clear all auth-related cookies
|
| 392 |
+
response.delete_cookie(key="auth_token", path="/")
|
| 393 |
+
response.delete_cookie(key="session_id", path="/")
|
| 394 |
+
return response
|
| 395 |
+
|
| 396 |
+
@app.get("/api/users")
|
| 397 |
+
async def get_users(role: str):
|
| 398 |
+
table_map = {
|
| 399 |
+
"Learner": "Learners",
|
| 400 |
+
"Instructor": "Instructors"
|
| 401 |
+
}
|
| 402 |
+
table = table_map.get(role)
|
| 403 |
+
if not table:
|
| 404 |
+
raise HTTPException(status_code=400, detail="Invalid role")
|
| 405 |
+
|
| 406 |
+
conn = connect_db()
|
| 407 |
+
try:
|
| 408 |
+
with conn.cursor() as cur:
|
| 409 |
+
cur.execute(f"SELECT * FROM {table}")
|
| 410 |
+
columns = [desc[0] for desc in cur.description]
|
| 411 |
+
users = [dict(zip(columns, row)) for row in cur.fetchall()]
|
| 412 |
+
return users
|
| 413 |
+
except Exception as e:
|
| 414 |
+
raise HTTPException(status_code=500, detail=str(e))
|
| 415 |
+
finally:
|
| 416 |
+
conn.close()
|
| 417 |
+
|
| 418 |
+
@app.get("/api/statistics/users/count")
|
| 419 |
+
async def get_user_count(role: str):
|
| 420 |
+
table_map = {
|
| 421 |
+
"Learner": "Learners",
|
| 422 |
+
"Instructor": "Instructors"
|
| 423 |
+
}
|
| 424 |
+
table = table_map.get(role)
|
| 425 |
+
if not table:
|
| 426 |
+
raise HTTPException(status_code=400, detail="Invalid role")
|
| 427 |
+
|
| 428 |
+
conn = connect_db()
|
| 429 |
+
try:
|
| 430 |
+
with conn.cursor() as cur:
|
| 431 |
+
cur.execute(f"SELECT COUNT(*) FROM {table}")
|
| 432 |
+
count = cur.fetchone()[0]
|
| 433 |
+
return {"count": count}
|
| 434 |
+
except Exception as e:
|
| 435 |
+
raise HTTPException(status_code=500, detail=str(e))
|
| 436 |
+
finally:
|
| 437 |
+
conn.close()
|
services/api/db/token_utils.py
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
token_utils.py - JWT token utilities for authentication
|
| 3 |
+
------------------------------------------------------
|
| 4 |
+
This module contains functions for JWT token encoding and decoding
|
| 5 |
+
Extracted to avoid circular imports between auth.py and api_endpoints.py
|
| 6 |
+
"""
|
| 7 |
+
|
| 8 |
+
from fastapi import HTTPException
|
| 9 |
+
from jose import jwt, JWTError
|
| 10 |
+
import time
|
| 11 |
+
import os
|
| 12 |
+
from dotenv import load_dotenv
|
| 13 |
+
|
| 14 |
+
# Load environment variables
|
| 15 |
+
load_dotenv()
|
| 16 |
+
SECRET_KEY = os.getenv("SECRET_TOKEN", "somesecretkey")
|
| 17 |
+
ALGORITHM = os.getenv("ALGORITHM", "HS256")
|
| 18 |
+
|
| 19 |
+
def create_token(data: dict, expires_in: int = 86400):
|
| 20 |
+
"""
|
| 21 |
+
Create a JWT token from user data
|
| 22 |
+
|
| 23 |
+
Args:
|
| 24 |
+
data: Dictionary containing user information
|
| 25 |
+
expires_in: Token expiration time in seconds (default: 24 hours)
|
| 26 |
+
|
| 27 |
+
Returns:
|
| 28 |
+
JWT token string
|
| 29 |
+
"""
|
| 30 |
+
to_encode = data.copy()
|
| 31 |
+
to_encode.update({"exp": time.time() + expires_in})
|
| 32 |
+
return jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)
|
| 33 |
+
|
| 34 |
+
def decode_token(token: str):
|
| 35 |
+
"""
|
| 36 |
+
Decode and validate a JWT token
|
| 37 |
+
|
| 38 |
+
Args:
|
| 39 |
+
token: JWT token string
|
| 40 |
+
|
| 41 |
+
Returns:
|
| 42 |
+
Dictionary with decoded token data
|
| 43 |
+
|
| 44 |
+
Raises:
|
| 45 |
+
HTTPException: If token is invalid or expired
|
| 46 |
+
"""
|
| 47 |
+
try:
|
| 48 |
+
return jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
|
| 49 |
+
except JWTError:
|
| 50 |
+
raise HTTPException(status_code=403, detail="Invalid or expired token")
|
services/api/skills.csv
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
skill,
|
| 2 |
+
"Computer Science",
|
| 3 |
+
"Fundamentals",
|
| 4 |
+
"Advanced Techniques",
|
| 5 |
+
"Mathematics",
|
| 6 |
+
"Data Science",
|
| 7 |
+
"Economics",
|
| 8 |
+
"Finance",
|
| 9 |
+
"Marketing",
|
| 10 |
+
"Graphic Design",
|
| 11 |
+
"English Language",
|
| 12 |
+
"Project Management",
|
| 13 |
+
"Web Development",
|
| 14 |
+
"Artificial Intelligence",
|
| 15 |
+
"Cryptography",
|
| 16 |
+
"Network Security",
|
| 17 |
+
"Business Analysis",
|
| 18 |
+
"Human Resources",
|
| 19 |
+
"Digital Marketing",
|
| 20 |
+
"Entrepreneurship",
|
| 21 |
+
"Public Speaking"
|