Spaces:
Sleeping
Sleeping
update api
Browse files- services/api/api_endpoints.py +593 -207
services/api/api_endpoints.py
CHANGED
|
@@ -1,6 +1,6 @@
|
|
| 1 |
from fastapi import APIRouter, Depends, HTTPException, Request, Cookie, UploadFile, File, Form
|
| 2 |
from typing import List, Optional, Dict, Any, Callable
|
| 3 |
-
from pydantic import BaseModel
|
| 4 |
from services.api.db.token_utils import decode_token
|
| 5 |
from dotenv import load_dotenv
|
| 6 |
import os
|
|
@@ -1258,6 +1258,7 @@ async def get_learner_dashboard(
|
|
| 1258 |
dashboard_data["lecturesPassed"] = passed_data['count'] if passed_data else 0
|
| 1259 |
|
| 1260 |
# Get statistics data - passed lectures over time
|
|
|
|
| 1261 |
cursor.execute("""
|
| 1262 |
SELECT Date, Score,
|
| 1263 |
DATE_FORMAT(Date, '%%Y-%%m-%%d') as formatted_date
|
|
@@ -1326,219 +1327,343 @@ async def get_learner_dashboard(
|
|
| 1326 |
@router.get("/instructor/dashboard")
|
| 1327 |
async def get_instructor_dashboard(
|
| 1328 |
request: Request,
|
|
|
|
| 1329 |
auth_token: str = Cookie(None)
|
| 1330 |
):
|
|
|
|
| 1331 |
try:
|
| 1332 |
-
#
|
| 1333 |
if not auth_token:
|
| 1334 |
-
auth_header = request.headers.get(
|
| 1335 |
-
if auth_header and auth_header.startswith(
|
| 1336 |
-
auth_token = auth_header.split(
|
| 1337 |
else:
|
| 1338 |
raise HTTPException(status_code=401, detail="No authentication token provided")
|
| 1339 |
-
|
| 1340 |
-
#
|
| 1341 |
-
|
| 1342 |
-
|
| 1343 |
-
|
| 1344 |
-
|
| 1345 |
-
|
| 1346 |
-
|
| 1347 |
-
|
| 1348 |
-
|
| 1349 |
-
|
| 1350 |
-
|
| 1351 |
-
|
| 1352 |
-
|
| 1353 |
-
|
| 1354 |
-
#
|
| 1355 |
-
|
| 1356 |
-
|
| 1357 |
-
|
| 1358 |
-
|
| 1359 |
-
|
| 1360 |
-
|
| 1361 |
-
|
| 1362 |
-
|
| 1363 |
-
|
| 1364 |
-
|
| 1365 |
-
|
| 1366 |
-
|
| 1367 |
-
|
| 1368 |
-
|
| 1369 |
-
|
| 1370 |
-
|
| 1371 |
-
|
| 1372 |
-
|
| 1373 |
-
|
| 1374 |
-
|
| 1375 |
-
|
| 1376 |
-
|
| 1377 |
-
|
| 1378 |
-
|
| 1379 |
-
|
| 1380 |
-
|
| 1381 |
-
|
| 1382 |
-
|
| 1383 |
-
|
| 1384 |
-
|
| 1385 |
-
|
| 1386 |
-
|
| 1387 |
-
|
| 1388 |
-
|
| 1389 |
-
|
| 1390 |
-
|
| 1391 |
-
|
| 1392 |
-
|
| 1393 |
-
|
| 1394 |
-
|
| 1395 |
-
|
| 1396 |
-
|
| 1397 |
-
|
| 1398 |
-
|
| 1399 |
-
|
| 1400 |
-
|
| 1401 |
-
|
| 1402 |
-
|
| 1403 |
-
|
| 1404 |
-
|
| 1405 |
-
|
| 1406 |
-
|
| 1407 |
-
|
| 1408 |
-
|
| 1409 |
-
|
| 1410 |
-
|
| 1411 |
-
|
| 1412 |
-
|
| 1413 |
-
|
| 1414 |
-
|
| 1415 |
-
|
| 1416 |
-
|
| 1417 |
-
|
| 1418 |
-
|
| 1419 |
-
|
| 1420 |
-
|
| 1421 |
-
|
| 1422 |
-
|
| 1423 |
-
|
| 1424 |
-
|
| 1425 |
-
|
| 1426 |
-
|
| 1427 |
-
|
| 1428 |
-
|
| 1429 |
-
|
| 1430 |
-
|
| 1431 |
-
|
| 1432 |
-
|
| 1433 |
-
|
| 1434 |
-
|
| 1435 |
-
|
| 1436 |
-
|
| 1437 |
-
|
| 1438 |
-
|
| 1439 |
-
|
| 1440 |
-
|
| 1441 |
-
|
| 1442 |
-
|
| 1443 |
-
|
| 1444 |
-
|
| 1445 |
-
|
| 1446 |
-
|
| 1447 |
-
|
| 1448 |
-
|
| 1449 |
-
|
| 1450 |
-
|
| 1451 |
-
|
| 1452 |
-
|
| 1453 |
-
|
| 1454 |
-
|
| 1455 |
-
|
| 1456 |
-
|
| 1457 |
-
|
| 1458 |
-
|
| 1459 |
-
|
| 1460 |
-
|
| 1461 |
-
|
| 1462 |
-
|
| 1463 |
-
|
| 1464 |
-
|
| 1465 |
-
|
| 1466 |
-
|
| 1467 |
-
|
| 1468 |
-
|
| 1469 |
-
|
| 1470 |
-
|
| 1471 |
-
|
| 1472 |
-
|
| 1473 |
-
|
| 1474 |
-
|
| 1475 |
-
|
| 1476 |
-
|
| 1477 |
-
|
| 1478 |
-
|
| 1479 |
-
|
| 1480 |
-
|
| 1481 |
-
|
| 1482 |
-
|
| 1483 |
-
|
| 1484 |
-
|
| 1485 |
-
|
| 1486 |
-
|
| 1487 |
-
|
| 1488 |
-
|
| 1489 |
-
|
| 1490 |
-
|
| 1491 |
-
|
| 1492 |
-
|
| 1493 |
-
|
| 1494 |
-
|
| 1495 |
-
|
| 1496 |
-
|
| 1497 |
-
|
| 1498 |
-
|
| 1499 |
-
|
| 1500 |
-
|
| 1501 |
-
|
| 1502 |
-
|
| 1503 |
-
|
| 1504 |
-
|
| 1505 |
-
|
| 1506 |
-
|
| 1507 |
-
|
| 1508 |
-
|
| 1509 |
-
|
| 1510 |
-
|
| 1511 |
-
|
| 1512 |
-
|
| 1513 |
-
|
| 1514 |
-
|
| 1515 |
-
|
| 1516 |
-
|
| 1517 |
-
|
| 1518 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1519 |
}
|
| 1520 |
-
|
| 1521 |
-
|
| 1522 |
-
|
| 1523 |
-
|
| 1524 |
-
|
| 1525 |
-
|
| 1526 |
-
|
| 1527 |
-
|
| 1528 |
-
|
| 1529 |
-
|
| 1530 |
-
|
| 1531 |
-
|
| 1532 |
-
|
| 1533 |
-
|
| 1534 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1535 |
except Exception as e:
|
| 1536 |
-
print(f"
|
| 1537 |
-
raise HTTPException(status_code=500, detail=
|
| 1538 |
finally:
|
| 1539 |
-
if
|
| 1540 |
conn.close()
|
| 1541 |
|
|
|
|
| 1542 |
# Quiz submission model
|
| 1543 |
class QuizSubmission(BaseModel):
|
| 1544 |
answers: dict[int, str] # questionId -> selected answer text
|
|
@@ -1870,6 +1995,7 @@ async def get_instructor_course_details(
|
|
| 1870 |
conn.close()
|
| 1871 |
|
| 1872 |
|
|
|
|
| 1873 |
# CreateLecture model and create_lecture endpoint to handle lecture creation with video upload and quiz
|
| 1874 |
class CreateLecture(BaseModel):
|
| 1875 |
title: str
|
|
@@ -1935,6 +2061,8 @@ async def create_lecture(
|
|
| 1935 |
WHERE CourseID = %s AND InstructorID = %s
|
| 1936 |
""", (course_id, instructor_id))
|
| 1937 |
|
|
|
|
|
|
|
| 1938 |
if not cursor.fetchone():
|
| 1939 |
raise HTTPException(status_code=403, detail="Not authorized to modify this course")
|
| 1940 |
|
|
@@ -2128,7 +2256,8 @@ async def get_course_preview_data(course_id: int, request: Request, auth_token:
|
|
| 2128 |
i.InstructorID as instructor_id,
|
| 2129 |
COALESCE(enrollment_stats.enrolled, 0) as enrolled,
|
| 2130 |
COALESCE(rating_stats.avg_rating, NULL) as rating,
|
| 2131 |
-
CASE WHEN user_enrollment.LearnerID IS NOT NULL THEN TRUE ELSE FALSE END as is_enrolled
|
|
|
|
| 2132 |
FROM Courses c
|
| 2133 |
JOIN Instructors i ON c.InstructorID = i.InstructorID
|
| 2134 |
LEFT JOIN (
|
|
@@ -2140,7 +2269,7 @@ async def get_course_preview_data(course_id: int, request: Request, auth_token:
|
|
| 2140 |
FROM Enrollments WHERE Rating IS NOT NULL GROUP BY CourseID
|
| 2141 |
) rating_stats ON c.CourseID = rating_stats.CourseID
|
| 2142 |
LEFT JOIN (
|
| 2143 |
-
SELECT e2.CourseID, e2.LearnerID
|
| 2144 |
FROM Enrollments e2
|
| 2145 |
JOIN Learners l ON e2.LearnerID = l.LearnerID
|
| 2146 |
WHERE l.LearnerID = %s
|
|
@@ -2189,7 +2318,8 @@ async def get_course_preview_data(course_id: int, request: Request, auth_token:
|
|
| 2189 |
'instructor_id': course['instructor_id'],
|
| 2190 |
'enrolled': course['enrolled'],
|
| 2191 |
'rating': float(course['rating']) if course['rating'] else None,
|
| 2192 |
-
'is_enrolled': course['is_enrolled']
|
|
|
|
| 2193 |
},
|
| 2194 |
'lectures': [
|
| 2195 |
{
|
|
@@ -2215,4 +2345,260 @@ async def get_course_preview_data(course_id: int, request: Request, auth_token:
|
|
| 2215 |
raise
|
| 2216 |
except Exception as e:
|
| 2217 |
print(f"Error in get_course_preview_data: {str(e)}")
|
| 2218 |
-
raise HTTPException(status_code=500, detail=str(e))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
from fastapi import APIRouter, Depends, HTTPException, Request, Cookie, UploadFile, File, Form
|
| 2 |
from typing import List, Optional, Dict, Any, Callable
|
| 3 |
+
from pydantic import BaseModel, Field
|
| 4 |
from services.api.db.token_utils import decode_token
|
| 5 |
from dotenv import load_dotenv
|
| 6 |
import os
|
|
|
|
| 1258 |
dashboard_data["lecturesPassed"] = passed_data['count'] if passed_data else 0
|
| 1259 |
|
| 1260 |
# Get statistics data - passed lectures over time
|
| 1261 |
+
# Fix the learner dashboard query around line 1270
|
| 1262 |
cursor.execute("""
|
| 1263 |
SELECT Date, Score,
|
| 1264 |
DATE_FORMAT(Date, '%%Y-%%m-%%d') as formatted_date
|
|
|
|
| 1327 |
@router.get("/instructor/dashboard")
|
| 1328 |
async def get_instructor_dashboard(
|
| 1329 |
request: Request,
|
| 1330 |
+
course_id: Optional[int] = None,
|
| 1331 |
auth_token: str = Cookie(None)
|
| 1332 |
):
|
| 1333 |
+
conn = None
|
| 1334 |
try:
|
| 1335 |
+
# 1) Auth token via cookie or header
|
| 1336 |
if not auth_token:
|
| 1337 |
+
auth_header = request.headers.get("Authorization")
|
| 1338 |
+
if auth_header and auth_header.startswith("Bearer "):
|
| 1339 |
+
auth_token = auth_header.split(" ", 1)[1]
|
| 1340 |
else:
|
| 1341 |
raise HTTPException(status_code=401, detail="No authentication token provided")
|
| 1342 |
+
|
| 1343 |
+
# 2) Decode and verify
|
| 1344 |
+
user_data = decode_token(auth_token)
|
| 1345 |
+
username = user_data.get("username")
|
| 1346 |
+
role = user_data.get("role")
|
| 1347 |
+
instructor_id = user_data.get("user_id")
|
| 1348 |
+
if role != "Instructor":
|
| 1349 |
+
raise HTTPException(status_code=403, detail="Only instructors can access this endpoint")
|
| 1350 |
+
if not username:
|
| 1351 |
+
raise HTTPException(status_code=401, detail="Invalid token payload")
|
| 1352 |
+
|
| 1353 |
+
# 3) DB connection
|
| 1354 |
+
conn = connect_db()
|
| 1355 |
+
with conn.cursor(pymysql.cursors.DictCursor) as cursor:
|
| 1356 |
+
|
| 1357 |
+
# Fallback for old tokens without user_id
|
| 1358 |
+
if not instructor_id:
|
| 1359 |
+
cursor.execute(
|
| 1360 |
+
"SELECT InstructorID FROM Instructors WHERE AccountName = %s",
|
| 1361 |
+
(username,)
|
| 1362 |
+
)
|
| 1363 |
+
row = cursor.fetchone()
|
| 1364 |
+
if not row:
|
| 1365 |
+
raise HTTPException(status_code=404, detail="Instructor not found")
|
| 1366 |
+
instructor_id = row["InstructorID"]
|
| 1367 |
+
|
| 1368 |
+
# --- General metrics ---
|
| 1369 |
+
cursor.execute("""
|
| 1370 |
+
SELECT COUNT(*) AS total_courses
|
| 1371 |
+
FROM Courses
|
| 1372 |
+
WHERE InstructorID = %s
|
| 1373 |
+
""", (instructor_id,))
|
| 1374 |
+
total_courses = cursor.fetchone()["total_courses"]
|
| 1375 |
+
|
| 1376 |
+
cursor.execute("""
|
| 1377 |
+
SELECT
|
| 1378 |
+
COUNT(DISTINCT e.LearnerID) AS total_students,
|
| 1379 |
+
COALESCE(AVG(e.Rating), 0) AS average_rating,
|
| 1380 |
+
COUNT(*) AS total_enrollments,
|
| 1381 |
+
SUM(CASE WHEN e.Percentage = 100 THEN 1 ELSE 0 END) AS completed_enrollments
|
| 1382 |
+
FROM Courses c
|
| 1383 |
+
LEFT JOIN Enrollments e ON c.CourseID = e.CourseID
|
| 1384 |
+
WHERE c.InstructorID = %s
|
| 1385 |
+
""", (instructor_id,))
|
| 1386 |
+
stats = cursor.fetchone() or {}
|
| 1387 |
+
|
| 1388 |
+
completion_rate = (
|
| 1389 |
+
round(
|
| 1390 |
+
stats.get("completed_enrollments", 0)
|
| 1391 |
+
/ stats.get("total_enrollments", 1)
|
| 1392 |
+
* 100,
|
| 1393 |
+
1
|
| 1394 |
+
)
|
| 1395 |
+
if stats.get("total_enrollments") else 0.0
|
| 1396 |
+
)
|
| 1397 |
+
|
| 1398 |
+
# --- Student growth (last 2 months) ---
|
| 1399 |
+
cursor.execute("""
|
| 1400 |
+
SELECT
|
| 1401 |
+
DATE_FORMAT(EnrollmentDate, '%%Y-%%m') AS month,
|
| 1402 |
+
COUNT(DISTINCT LearnerID) AS students
|
| 1403 |
+
FROM Courses c
|
| 1404 |
+
JOIN Enrollments e ON c.CourseID = e.CourseID
|
| 1405 |
+
WHERE c.InstructorID = %s
|
| 1406 |
+
AND EnrollmentDate >= DATE_SUB(CURRENT_DATE, INTERVAL 2 MONTH)
|
| 1407 |
+
GROUP BY month
|
| 1408 |
+
ORDER BY month DESC
|
| 1409 |
+
LIMIT 2
|
| 1410 |
+
""", (instructor_id,))
|
| 1411 |
+
growth = cursor.fetchall()
|
| 1412 |
+
current = growth[0]["students"] if len(growth) > 0 else 0
|
| 1413 |
+
previous = growth[1]["students"] if len(growth) > 1 else 0
|
| 1414 |
+
student_growth = (
|
| 1415 |
+
round((current - previous) / previous * 100, 1)
|
| 1416 |
+
if previous else 0.0
|
| 1417 |
+
)
|
| 1418 |
+
|
| 1419 |
+
# --- Course list summary ---
|
| 1420 |
+
cursor.execute("""
|
| 1421 |
+
SELECT
|
| 1422 |
+
c.CourseID AS id,
|
| 1423 |
+
c.CourseName AS name,
|
| 1424 |
+
c.Descriptions AS description,
|
| 1425 |
+
c.AverageRating AS rating,
|
| 1426 |
+
COUNT(DISTINCT e.LearnerID) AS enrollments,
|
| 1427 |
+
AVG(e.Percentage) AS completionRate
|
| 1428 |
+
FROM Courses c
|
| 1429 |
+
LEFT JOIN Enrollments e ON c.CourseID = e.CourseID
|
| 1430 |
+
WHERE c.InstructorID = %s
|
| 1431 |
+
GROUP BY c.CourseID
|
| 1432 |
+
ORDER BY c.CreatedAt DESC
|
| 1433 |
+
""", (instructor_id,))
|
| 1434 |
+
raw_courses = cursor.fetchall()
|
| 1435 |
+
formatted_courses = [
|
| 1436 |
+
{
|
| 1437 |
+
"id": c["id"],
|
| 1438 |
+
"name": c["name"],
|
| 1439 |
+
"description": c["description"] or "",
|
| 1440 |
+
"enrollments": c["enrollments"] or 0,
|
| 1441 |
+
"rating": round(float(c["rating"]), 1) if c["rating"] else 0.0,
|
| 1442 |
+
"completionRate": round(float(c["completionRate"]), 1) if c["completionRate"] else 0.0
|
| 1443 |
+
}
|
| 1444 |
+
for c in raw_courses
|
| 1445 |
+
]
|
| 1446 |
+
|
| 1447 |
+
# --- Enrollment trends (last 30 days) ---
|
| 1448 |
+
cursor.execute("""
|
| 1449 |
+
SELECT
|
| 1450 |
+
DATE_FORMAT(e.EnrollmentDate, '%%Y-%%m-%%d') AS date,
|
| 1451 |
+
COUNT(*) AS value
|
| 1452 |
+
FROM Courses c
|
| 1453 |
+
JOIN Enrollments e ON c.CourseID = e.CourseID
|
| 1454 |
+
WHERE c.InstructorID = %s
|
| 1455 |
+
AND e.EnrollmentDate >= DATE_SUB(CURRENT_DATE, INTERVAL 30 DAY)
|
| 1456 |
+
GROUP BY date
|
| 1457 |
+
ORDER BY date
|
| 1458 |
+
""", (instructor_id,))
|
| 1459 |
+
enroll_trends = cursor.fetchall()
|
| 1460 |
+
|
| 1461 |
+
# --- Rating trends (last 30 days) ---
|
| 1462 |
+
cursor.execute("""
|
| 1463 |
+
SELECT
|
| 1464 |
+
DATE_FORMAT(e.EnrollmentDate, '%%Y-%%m-%%d') AS date,
|
| 1465 |
+
AVG(e.Rating) AS value
|
| 1466 |
+
FROM Courses c
|
| 1467 |
+
JOIN Enrollments e ON c.CourseID = e.CourseID
|
| 1468 |
+
WHERE c.InstructorID = %s
|
| 1469 |
+
AND e.Rating IS NOT NULL
|
| 1470 |
+
AND e.EnrollmentDate >= DATE_SUB(CURRENT_DATE, INTERVAL 30 DAY)
|
| 1471 |
+
GROUP BY date
|
| 1472 |
+
ORDER BY date
|
| 1473 |
+
""", (instructor_id,))
|
| 1474 |
+
rating_trends = cursor.fetchall()
|
| 1475 |
+
|
| 1476 |
+
# --- Build base payload ---
|
| 1477 |
+
dashboard_data = {
|
| 1478 |
+
"metrics": {
|
| 1479 |
+
"totalCourses": total_courses,
|
| 1480 |
+
"totalStudents": stats.get("total_students", 0),
|
| 1481 |
+
"averageRating": round(stats.get("average_rating", 0), 1),
|
| 1482 |
+
"completionRate": completion_rate,
|
| 1483 |
+
"studentGrowth": student_growth
|
| 1484 |
+
},
|
| 1485 |
+
"courses": formatted_courses,
|
| 1486 |
+
"enrollmentTrends": [
|
| 1487 |
+
{"date": r["date"], "value": r["value"]} for r in enroll_trends
|
| 1488 |
+
],
|
| 1489 |
+
"ratingTrends": [
|
| 1490 |
+
{"date": r["date"], "value": round(r["value"], 1)}
|
| 1491 |
+
for r in rating_trends if r["value"] is not None
|
| 1492 |
+
],
|
| 1493 |
+
"courseEnrollments": [
|
| 1494 |
+
{"courseName": c["name"], "enrollments": c["enrollments"]}
|
| 1495 |
+
for c in formatted_courses
|
| 1496 |
+
],
|
| 1497 |
+
"courseAnalytics": {}
|
| 1498 |
+
}
|
| 1499 |
+
|
| 1500 |
+
# --- Detailed courseAnalytics if course_id given ---
|
| 1501 |
+
if course_id:
|
| 1502 |
+
# Ownership check
|
| 1503 |
+
cursor.execute("""
|
| 1504 |
+
SELECT CourseID, CourseName
|
| 1505 |
+
FROM Courses
|
| 1506 |
+
WHERE CourseID = %s AND InstructorID = %s
|
| 1507 |
+
""", (course_id, instructor_id))
|
| 1508 |
+
course_row = cursor.fetchone()
|
| 1509 |
+
if not course_row:
|
| 1510 |
+
raise HTTPException(status_code=404, detail="Course not found or not owned")
|
| 1511 |
+
course_name = course_row["CourseName"]
|
| 1512 |
+
|
| 1513 |
+
# Basic course metrics
|
| 1514 |
+
cursor.execute("""
|
| 1515 |
+
SELECT
|
| 1516 |
+
COUNT(DISTINCT e.LearnerID) AS total_enrollments,
|
| 1517 |
+
COALESCE(AVG(e.Rating), 0) AS average_rating,
|
| 1518 |
+
SUM(CASE WHEN e.Percentage = 100 THEN 1 ELSE 0 END) AS completed_enrollments,
|
| 1519 |
+
COUNT(*) AS all_with_progress
|
| 1520 |
+
FROM Enrollments e
|
| 1521 |
+
WHERE e.CourseID = %s
|
| 1522 |
+
""", (course_id,))
|
| 1523 |
+
cm = cursor.fetchone() or {}
|
| 1524 |
+
total_enr = cm.get("total_enrollments", 0)
|
| 1525 |
+
total_with = cm.get("all_with_progress", 0)
|
| 1526 |
+
comp_rate = (
|
| 1527 |
+
round(cm.get("completed_enrollments", 0) / total_with * 100, 1)
|
| 1528 |
+
if total_with else 0.0
|
| 1529 |
+
)
|
| 1530 |
+
|
| 1531 |
+
# Enroll/Ratings trends (60 days)
|
| 1532 |
+
cursor.execute("""
|
| 1533 |
+
SELECT
|
| 1534 |
+
DATE_FORMAT(EnrollmentDate, '%%Y-%%m-%%d') AS date,
|
| 1535 |
+
COUNT(*) AS value
|
| 1536 |
+
FROM Enrollments
|
| 1537 |
+
WHERE CourseID = %s
|
| 1538 |
+
AND EnrollmentDate >= DATE_SUB(CURRENT_DATE, INTERVAL 60 DAY)
|
| 1539 |
+
GROUP BY date
|
| 1540 |
+
ORDER BY date
|
| 1541 |
+
""", (course_id,))
|
| 1542 |
+
ce_trends = cursor.fetchall()
|
| 1543 |
+
|
| 1544 |
+
cursor.execute("""
|
| 1545 |
+
SELECT
|
| 1546 |
+
DATE_FORMAT(EnrollmentDate, '%%Y-%%m-%%d') AS date,
|
| 1547 |
+
AVG(Rating) AS value
|
| 1548 |
+
FROM Enrollments
|
| 1549 |
+
WHERE CourseID = %s
|
| 1550 |
+
AND Rating IS NOT NULL
|
| 1551 |
+
AND EnrollmentDate >= DATE_SUB(CURRENT_DATE, INTERVAL 60 DAY)
|
| 1552 |
+
GROUP BY date
|
| 1553 |
+
ORDER BY date
|
| 1554 |
+
""", (course_id,))
|
| 1555 |
+
cr_trends = cursor.fetchall()
|
| 1556 |
+
|
| 1557 |
+
# Completion via LectureResults (30 days)
|
| 1558 |
+
cursor.execute("""
|
| 1559 |
+
SELECT
|
| 1560 |
+
DATE_FORMAT(lr.Date, '%%Y-%%m-%%d') AS date,
|
| 1561 |
+
COUNT(DISTINCT CASE WHEN e.Percentage = 100 THEN e.LearnerID END) AS completed,
|
| 1562 |
+
COUNT(DISTINCT lr.LearnerID) AS total
|
| 1563 |
+
FROM LectureResults lr
|
| 1564 |
+
JOIN Enrollments e
|
| 1565 |
+
ON lr.LearnerID = e.LearnerID AND lr.CourseID = e.CourseID
|
| 1566 |
+
WHERE lr.CourseID = %s
|
| 1567 |
+
AND lr.Date >= DATE_SUB(CURRENT_DATE, INTERVAL 30 DAY)
|
| 1568 |
+
GROUP BY date
|
| 1569 |
+
ORDER BY date
|
| 1570 |
+
""", (course_id,))
|
| 1571 |
+
comp_rows = cursor.fetchall()
|
| 1572 |
+
completion_trends = [
|
| 1573 |
+
{
|
| 1574 |
+
"date": r["date"],
|
| 1575 |
+
"value": round(r["completed"] / r["total"] * 100, 1) if r["total"] else 0.0
|
| 1576 |
}
|
| 1577 |
+
for r in comp_rows
|
| 1578 |
+
]
|
| 1579 |
+
|
| 1580 |
+
# Lecture-level analytics
|
| 1581 |
+
cursor.execute("""
|
| 1582 |
+
SELECT
|
| 1583 |
+
l.LectureID AS lectureId,
|
| 1584 |
+
l.Title AS lecture_title,
|
| 1585 |
+
COUNT(DISTINCT lr.LearnerID) AS total_attempts,
|
| 1586 |
+
SUM(lr.State = 'passed') AS passed_count,
|
| 1587 |
+
COALESCE(AVG(lr.Score), 0) AS average_score
|
| 1588 |
+
FROM Lectures l
|
| 1589 |
+
LEFT JOIN LectureResults lr
|
| 1590 |
+
ON l.LectureID = lr.LectureID
|
| 1591 |
+
WHERE l.CourseID = %s
|
| 1592 |
+
GROUP BY l.LectureID, l.Title
|
| 1593 |
+
ORDER BY l.LectureID
|
| 1594 |
+
""", (course_id,))
|
| 1595 |
+
lects = cursor.fetchall()
|
| 1596 |
+
lecture_analytics = [
|
| 1597 |
+
{
|
| 1598 |
+
"lectureId": l["lectureId"],
|
| 1599 |
+
"title": l["lecture_title"],
|
| 1600 |
+
"totalAttempts": l["total_attempts"],
|
| 1601 |
+
"passedCount": l["passed_count"],
|
| 1602 |
+
"passRate": round(l["passed_count"] / l["total_attempts"] * 100, 1)
|
| 1603 |
+
if l["total_attempts"] else 0.0,
|
| 1604 |
+
"averageScore": round(float(l["average_score"]), 1)
|
| 1605 |
+
}
|
| 1606 |
+
for l in lects
|
| 1607 |
+
]
|
| 1608 |
+
|
| 1609 |
+
# Student progress distribution
|
| 1610 |
+
cursor.execute("""
|
| 1611 |
+
SELECT
|
| 1612 |
+
CASE
|
| 1613 |
+
WHEN Percentage = 0 THEN 'Not Started'
|
| 1614 |
+
WHEN Percentage < 25 THEN '0-25%%'
|
| 1615 |
+
WHEN Percentage < 50 THEN '25-50%%'
|
| 1616 |
+
WHEN Percentage < 75 THEN '50-75%%'
|
| 1617 |
+
WHEN Percentage < 100 THEN '75-99%%'
|
| 1618 |
+
ELSE 'Completed'
|
| 1619 |
+
END AS progress_range,
|
| 1620 |
+
COUNT(*) AS student_count
|
| 1621 |
+
FROM Enrollments
|
| 1622 |
+
WHERE CourseID = %s
|
| 1623 |
+
GROUP BY progress_range
|
| 1624 |
+
ORDER BY
|
| 1625 |
+
FIELD(progress_range,
|
| 1626 |
+
'Not Started','0-25%%','25-50%%',
|
| 1627 |
+
'50-75%%','75-99%%','Completed')
|
| 1628 |
+
""", (course_id,))
|
| 1629 |
+
pd = cursor.fetchall()
|
| 1630 |
+
progress = [
|
| 1631 |
+
{"range": p["progress_range"], "count": p["student_count"]}
|
| 1632 |
+
for p in pd
|
| 1633 |
+
]
|
| 1634 |
+
|
| 1635 |
+
dashboard_data["courseAnalytics"] = {
|
| 1636 |
+
"courseId": course_id,
|
| 1637 |
+
"courseName": course_name,
|
| 1638 |
+
"totalEnrollments": total_enr,
|
| 1639 |
+
"averageRating": round(float(cm.get("average_rating", 0)), 1),
|
| 1640 |
+
"completionRate": comp_rate,
|
| 1641 |
+
"enrollmentTrends": [
|
| 1642 |
+
{"date": r["date"], "value": int(r["value"])}
|
| 1643 |
+
for r in ce_trends
|
| 1644 |
+
],
|
| 1645 |
+
"ratingTrends": [
|
| 1646 |
+
{"date": r["date"], "value": round(float(r["value"]), 1)}
|
| 1647 |
+
for r in cr_trends if r["value"] is not None
|
| 1648 |
+
],
|
| 1649 |
+
"completionTrends": completion_trends,
|
| 1650 |
+
"lectureAnalytics": lecture_analytics,
|
| 1651 |
+
"studentProgress": progress
|
| 1652 |
+
}
|
| 1653 |
+
|
| 1654 |
+
# 4) Return final payload
|
| 1655 |
+
return dashboard_data
|
| 1656 |
+
|
| 1657 |
+
except HTTPException:
|
| 1658 |
+
raise
|
| 1659 |
except Exception as e:
|
| 1660 |
+
print(f"[ERROR] get_instructor_dashboard: {e}")
|
| 1661 |
+
raise HTTPException(status_code=500, detail="Internal server error")
|
| 1662 |
finally:
|
| 1663 |
+
if conn:
|
| 1664 |
conn.close()
|
| 1665 |
|
| 1666 |
+
|
| 1667 |
# Quiz submission model
|
| 1668 |
class QuizSubmission(BaseModel):
|
| 1669 |
answers: dict[int, str] # questionId -> selected answer text
|
|
|
|
| 1995 |
conn.close()
|
| 1996 |
|
| 1997 |
|
| 1998 |
+
|
| 1999 |
# CreateLecture model and create_lecture endpoint to handle lecture creation with video upload and quiz
|
| 2000 |
class CreateLecture(BaseModel):
|
| 2001 |
title: str
|
|
|
|
| 2061 |
WHERE CourseID = %s AND InstructorID = %s
|
| 2062 |
""", (course_id, instructor_id))
|
| 2063 |
|
| 2064 |
+
|
| 2065 |
+
|
| 2066 |
if not cursor.fetchone():
|
| 2067 |
raise HTTPException(status_code=403, detail="Not authorized to modify this course")
|
| 2068 |
|
|
|
|
| 2256 |
i.InstructorID as instructor_id,
|
| 2257 |
COALESCE(enrollment_stats.enrolled, 0) as enrolled,
|
| 2258 |
COALESCE(rating_stats.avg_rating, NULL) as rating,
|
| 2259 |
+
CASE WHEN user_enrollment.LearnerID IS NOT NULL THEN TRUE ELSE FALSE END as is_enrolled,
|
| 2260 |
+
user_enrollment.Rating as user_rating
|
| 2261 |
FROM Courses c
|
| 2262 |
JOIN Instructors i ON c.InstructorID = i.InstructorID
|
| 2263 |
LEFT JOIN (
|
|
|
|
| 2269 |
FROM Enrollments WHERE Rating IS NOT NULL GROUP BY CourseID
|
| 2270 |
) rating_stats ON c.CourseID = rating_stats.CourseID
|
| 2271 |
LEFT JOIN (
|
| 2272 |
+
SELECT e2.CourseID, e2.LearnerID, e2.Rating
|
| 2273 |
FROM Enrollments e2
|
| 2274 |
JOIN Learners l ON e2.LearnerID = l.LearnerID
|
| 2275 |
WHERE l.LearnerID = %s
|
|
|
|
| 2318 |
'instructor_id': course['instructor_id'],
|
| 2319 |
'enrolled': course['enrolled'],
|
| 2320 |
'rating': float(course['rating']) if course['rating'] else None,
|
| 2321 |
+
'is_enrolled': course['is_enrolled'],
|
| 2322 |
+
'user_rating': int(course['user_rating']) if course['user_rating'] else None
|
| 2323 |
},
|
| 2324 |
'lectures': [
|
| 2325 |
{
|
|
|
|
| 2345 |
raise
|
| 2346 |
except Exception as e:
|
| 2347 |
print(f"Error in get_course_preview_data: {str(e)}")
|
| 2348 |
+
raise HTTPException(status_code=500, detail=str(e))
|
| 2349 |
+
|
| 2350 |
+
# Debug endpoint for instructor to list their courses - no caching, direct DB access
|
| 2351 |
+
@router.get("/instructor/debug/my-courses")
|
| 2352 |
+
async def debug_my_courses(
|
| 2353 |
+
request: Request,
|
| 2354 |
+
auth_token: str = Cookie(None)
|
| 2355 |
+
):
|
| 2356 |
+
try:
|
| 2357 |
+
if not auth_token:
|
| 2358 |
+
auth_header = request.headers.get('Authorization')
|
| 2359 |
+
if auth_header and auth_header.startswith('Bearer '):
|
| 2360 |
+
auth_token = auth_header.split(' ')[1]
|
| 2361 |
+
else:
|
| 2362 |
+
raise HTTPException(status_code=401, detail="No authentication token provided")
|
| 2363 |
+
|
| 2364 |
+
user_data = decode_token(auth_token)
|
| 2365 |
+
username = user_data['username']
|
| 2366 |
+
role = user_data['role']
|
| 2367 |
+
instructor_id = user_data.get('user_id')
|
| 2368 |
+
|
| 2369 |
+
if role != "Instructor":
|
| 2370 |
+
raise HTTPException(status_code=403, detail="Only instructors can access this endpoint")
|
| 2371 |
+
|
| 2372 |
+
conn = connect_db()
|
| 2373 |
+
with conn.cursor(pymysql.cursors.DictCursor) as cursor:
|
| 2374 |
+
if not instructor_id:
|
| 2375 |
+
cursor.execute("""
|
| 2376 |
+
SELECT InstructorID
|
| 2377 |
+
FROM Instructors
|
| 2378 |
+
WHERE AccountName = %s
|
| 2379 |
+
""", (username,))
|
| 2380 |
+
|
| 2381 |
+
instructor = cursor.fetchone()
|
| 2382 |
+
if not instructor:
|
| 2383 |
+
raise HTTPException(status_code=404, detail="Instructor not found")
|
| 2384 |
+
|
| 2385 |
+
instructor_id = instructor['InstructorID']
|
| 2386 |
+
|
| 2387 |
+
cursor.execute("""
|
| 2388 |
+
SELECT CourseID, CourseName, InstructorID
|
| 2389 |
+
FROM Courses
|
| 2390 |
+
WHERE InstructorID = %s
|
| 2391 |
+
ORDER BY CourseID
|
| 2392 |
+
""", (instructor_id,))
|
| 2393 |
+
|
| 2394 |
+
courses = cursor.fetchall()
|
| 2395 |
+
|
| 2396 |
+
return {
|
| 2397 |
+
"instructor_id": instructor_id,
|
| 2398 |
+
"username": username,
|
| 2399 |
+
"courses": courses,
|
| 2400 |
+
"course_count": len(courses)
|
| 2401 |
+
}
|
| 2402 |
+
|
| 2403 |
+
except Exception as e:
|
| 2404 |
+
print(f"Debug error: {str(e)}")
|
| 2405 |
+
raise HTTPException(status_code=500, detail=str(e))
|
| 2406 |
+
finally:
|
| 2407 |
+
if 'conn' in locals():
|
| 2408 |
+
conn.close()
|
| 2409 |
+
|
| 2410 |
+
# Get enrolled learners for a specific course (instructor only)
|
| 2411 |
+
@router.get("/instructor/courses/{course_id}/enrollments")
|
| 2412 |
+
async def get_course_enrollments(
|
| 2413 |
+
course_id: int,
|
| 2414 |
+
request: Request,
|
| 2415 |
+
auth_token: str = Cookie(None)
|
| 2416 |
+
):
|
| 2417 |
+
try:
|
| 2418 |
+
# Get token from header if not in cookie
|
| 2419 |
+
if not auth_token:
|
| 2420 |
+
auth_header = request.headers.get('Authorization')
|
| 2421 |
+
if auth_header and auth_header.startswith('Bearer '):
|
| 2422 |
+
auth_token = auth_header.split(' ')[1]
|
| 2423 |
+
else:
|
| 2424 |
+
raise HTTPException(status_code=401, detail="No authentication token provided")
|
| 2425 |
+
|
| 2426 |
+
# Verify token and get user data
|
| 2427 |
+
user_data = decode_token(auth_token)
|
| 2428 |
+
username = user_data['username']
|
| 2429 |
+
role = user_data['role']
|
| 2430 |
+
instructor_id = user_data.get('user_id')
|
| 2431 |
+
|
| 2432 |
+
# Verify user is an instructor
|
| 2433 |
+
if role != "Instructor":
|
| 2434 |
+
raise HTTPException(status_code=403, detail="Only instructors can access this endpoint")
|
| 2435 |
+
|
| 2436 |
+
conn = connect_db()
|
| 2437 |
+
|
| 2438 |
+
with conn.cursor(pymysql.cursors.DictCursor) as cursor:
|
| 2439 |
+
# Get instructor ID if not in token
|
| 2440 |
+
if not instructor_id:
|
| 2441 |
+
cursor.execute("""
|
| 2442 |
+
SELECT InstructorID
|
| 2443 |
+
FROM Instructors
|
| 2444 |
+
WHERE AccountName = %s
|
| 2445 |
+
""", (username,))
|
| 2446 |
+
|
| 2447 |
+
instructor = cursor.fetchone()
|
| 2448 |
+
if not instructor:
|
| 2449 |
+
raise HTTPException(status_code=404, detail="Instructor not found")
|
| 2450 |
+
|
| 2451 |
+
instructor_id = instructor['InstructorID']
|
| 2452 |
+
|
| 2453 |
+
# Verify the course belongs to this instructor
|
| 2454 |
+
cursor.execute("""
|
| 2455 |
+
SELECT CourseID, CourseName
|
| 2456 |
+
FROM Courses
|
| 2457 |
+
WHERE CourseID = %s AND InstructorID = %s
|
| 2458 |
+
""", (course_id, instructor_id))
|
| 2459 |
+
|
| 2460 |
+
course = cursor.fetchone()
|
| 2461 |
+
if not course:
|
| 2462 |
+
raise HTTPException(status_code=404, detail="Course not found or you don't have permission to access it")
|
| 2463 |
+
|
| 2464 |
+
# Get enrolled learners with their enrollment details
|
| 2465 |
+
cursor.execute("""
|
| 2466 |
+
SELECT
|
| 2467 |
+
l.LearnerID,
|
| 2468 |
+
l.LearnerName,
|
| 2469 |
+
l.Email,
|
| 2470 |
+
e.EnrollmentDate,
|
| 2471 |
+
COALESCE(e.Percentage, 0) as progress,
|
| 2472 |
+
COALESCE(e.Rating, 0) as rating
|
| 2473 |
+
FROM Enrollments e
|
| 2474 |
+
JOIN Learners l ON e.LearnerID = l.LearnerID
|
| 2475 |
+
WHERE e.CourseID = %s
|
| 2476 |
+
ORDER BY e.EnrollmentDate DESC
|
| 2477 |
+
""", (course_id,))
|
| 2478 |
+
|
| 2479 |
+
enrollments = cursor.fetchall()
|
| 2480 |
+
|
| 2481 |
+
# Calculate completion rate
|
| 2482 |
+
total_enrollments = len(enrollments)
|
| 2483 |
+
completed_enrollments = sum(1 for e in enrollments if e['progress'] == 100)
|
| 2484 |
+
completion_rate = (completed_enrollments / total_enrollments * 100) if total_enrollments > 0 else 0
|
| 2485 |
+
|
| 2486 |
+
# Format the data for the frontend
|
| 2487 |
+
formatted_enrollments = []
|
| 2488 |
+
for enrollment in enrollments:
|
| 2489 |
+
formatted_enrollments.append({
|
| 2490 |
+
'learner_id': enrollment['LearnerID'],
|
| 2491 |
+
'learner_name': enrollment['LearnerName'],
|
| 2492 |
+
'email': enrollment['Email'],
|
| 2493 |
+
'enrollment_date': enrollment['EnrollmentDate'].strftime('%b %d, %Y') if enrollment['EnrollmentDate'] else 'N/A',
|
| 2494 |
+
'progress': enrollment['progress'],
|
| 2495 |
+
'rating': enrollment['rating']
|
| 2496 |
+
})
|
| 2497 |
+
|
| 2498 |
+
return {
|
| 2499 |
+
'course_id': course_id,
|
| 2500 |
+
'course_name': course['CourseName'],
|
| 2501 |
+
'total_enrollments': total_enrollments,
|
| 2502 |
+
'completion_rate': round(completion_rate, 1),
|
| 2503 |
+
'enrollments': formatted_enrollments
|
| 2504 |
+
}
|
| 2505 |
+
|
| 2506 |
+
except HTTPException as he:
|
| 2507 |
+
raise he
|
| 2508 |
+
except Exception as e:
|
| 2509 |
+
print(f"Error fetching course enrollments: {str(e)}")
|
| 2510 |
+
raise HTTPException(status_code=500, detail=f"Error fetching course enrollments: {str(e)}")
|
| 2511 |
+
finally:
|
| 2512 |
+
if 'conn' in locals():
|
| 2513 |
+
conn.close()
|
| 2514 |
+
|
| 2515 |
+
# Rating submission model
|
| 2516 |
+
class RatingSubmission(BaseModel):
|
| 2517 |
+
rating: int = Field(..., ge=1, le=5, description="Rating value between 1 and 5")
|
| 2518 |
+
|
| 2519 |
+
# Submit rating for a course
|
| 2520 |
+
@router.put("/courses/{course_id}/rating")
|
| 2521 |
+
async def submit_course_rating(
|
| 2522 |
+
course_id: int,
|
| 2523 |
+
rating_data: RatingSubmission,
|
| 2524 |
+
request: Request,
|
| 2525 |
+
auth_token: str = Cookie(None)
|
| 2526 |
+
):
|
| 2527 |
+
try:
|
| 2528 |
+
# Get token from header if not in cookie
|
| 2529 |
+
if not auth_token:
|
| 2530 |
+
auth_header = request.headers.get('Authorization')
|
| 2531 |
+
if auth_header and auth_header.startswith('Bearer '):
|
| 2532 |
+
auth_token = auth_header.split(' ')[1]
|
| 2533 |
+
else:
|
| 2534 |
+
raise HTTPException(status_code=401, detail="No authentication token provided")
|
| 2535 |
+
|
| 2536 |
+
# Verify token and get user data
|
| 2537 |
+
try:
|
| 2538 |
+
user_data = decode_token(auth_token)
|
| 2539 |
+
learner_id = user_data.get('user_id')
|
| 2540 |
+
|
| 2541 |
+
if not learner_id:
|
| 2542 |
+
# Fallback for old tokens without user_id - do database lookup
|
| 2543 |
+
conn = connect_db()
|
| 2544 |
+
with conn.cursor(pymysql.cursors.DictCursor) as cursor:
|
| 2545 |
+
cursor.execute("""
|
| 2546 |
+
SELECT LearnerID
|
| 2547 |
+
FROM Learners
|
| 2548 |
+
WHERE AccountName = %s
|
| 2549 |
+
""", (user_data['username'],))
|
| 2550 |
+
learner = cursor.fetchone()
|
| 2551 |
+
if not learner:
|
| 2552 |
+
raise HTTPException(status_code=404, detail="Learner not found")
|
| 2553 |
+
learner_id = learner['LearnerID']
|
| 2554 |
+
else:
|
| 2555 |
+
# Use user_id from token
|
| 2556 |
+
conn = connect_db()
|
| 2557 |
+
except Exception as e:
|
| 2558 |
+
print(f"Token/user verification error: {str(e)}")
|
| 2559 |
+
raise HTTPException(status_code=401, detail="Invalid authentication token or user not found")
|
| 2560 |
+
|
| 2561 |
+
# Check if the course exists and user is enrolled
|
| 2562 |
+
with conn.cursor(pymysql.cursors.DictCursor) as cursor:
|
| 2563 |
+
cursor.execute("""
|
| 2564 |
+
SELECT e.EnrollmentID
|
| 2565 |
+
FROM Enrollments e
|
| 2566 |
+
JOIN Courses c ON e.CourseID = c.CourseID
|
| 2567 |
+
WHERE e.CourseID = %s AND e.LearnerID = %s
|
| 2568 |
+
""", (course_id, learner_id))
|
| 2569 |
+
enrollment = cursor.fetchone()
|
| 2570 |
+
if not enrollment:
|
| 2571 |
+
raise HTTPException(status_code=404, detail="Course not found or you are not enrolled")
|
| 2572 |
+
|
| 2573 |
+
# Update the rating in the Enrollments table
|
| 2574 |
+
try:
|
| 2575 |
+
cursor.execute("""
|
| 2576 |
+
UPDATE Enrollments
|
| 2577 |
+
SET Rating = %s
|
| 2578 |
+
WHERE CourseID = %s AND LearnerID = %s
|
| 2579 |
+
""", (rating_data.rating, course_id, learner_id))
|
| 2580 |
+
|
| 2581 |
+
conn.commit()
|
| 2582 |
+
|
| 2583 |
+
# Invalidate cache for course rating data
|
| 2584 |
+
redis_client = get_redis_client()
|
| 2585 |
+
pattern_course = f"courses:id:{course_id}:*"
|
| 2586 |
+
pattern_preview = f"course:preview:{course_id}:*"
|
| 2587 |
+
print(f"Invalidating cache patterns: {pattern_course}, {pattern_preview}")
|
| 2588 |
+
invalidate_cache_pattern(pattern_course)
|
| 2589 |
+
invalidate_cache_pattern(pattern_preview)
|
| 2590 |
+
|
| 2591 |
+
return {"message": "Rating submitted successfully", "rating": rating_data.rating}
|
| 2592 |
+
except Exception as e:
|
| 2593 |
+
conn.rollback()
|
| 2594 |
+
print(f"Error updating rating: {str(e)}")
|
| 2595 |
+
raise HTTPException(status_code=500, detail=f"Failed to submit rating: {str(e)}")
|
| 2596 |
+
|
| 2597 |
+
except HTTPException as he:
|
| 2598 |
+
raise he
|
| 2599 |
+
except Exception as e:
|
| 2600 |
+
print(f"Error submitting rating: {str(e)}")
|
| 2601 |
+
raise HTTPException(status_code=500, detail=f"Error submitting rating: {str(e)}")
|
| 2602 |
+
finally:
|
| 2603 |
+
if 'conn' in locals():
|
| 2604 |
+
conn.close()
|