tlong-ds commited on
Commit
2afd9e5
·
1 Parent(s): 0a9b204

update api

Browse files
Files changed (1) hide show
  1. 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
- # Get token from header if not in cookie
1333
  if not auth_token:
1334
- auth_header = request.headers.get('Authorization')
1335
- if auth_header and auth_header.startswith('Bearer '):
1336
- auth_token = auth_header.split(' ')[1]
1337
  else:
1338
  raise HTTPException(status_code=401, detail="No authentication token provided")
1339
-
1340
- # Verify token and get user data
1341
- try:
1342
- user_data = decode_token(auth_token)
1343
- username = user_data.get('username')
1344
- role = user_data.get('role')
1345
- instructor_id = user_data.get('user_id')
1346
-
1347
- if not username or not role:
1348
- raise HTTPException(status_code=401, detail="Invalid token data")
1349
-
1350
- # Verify user is an instructor
1351
- if role != "Instructor":
1352
- raise HTTPException(status_code=403, detail="Only instructors can access this endpoint")
1353
-
1354
- # Connect to database
1355
- conn = connect_db()
1356
-
1357
- with conn.cursor(pymysql.cursors.DictCursor) as cursor:
1358
- # Get instructor ID from token or fallback to database lookup
1359
- if not instructor_id:
1360
- # Fallback for old tokens without user_id
1361
- cursor.execute("""
1362
- SELECT InstructorID
1363
- FROM Instructors
1364
- WHERE AccountName = %s
1365
- """, (username,))
1366
-
1367
- instructor = cursor.fetchone()
1368
- if not instructor:
1369
- raise HTTPException(status_code=404, detail="Instructor not found")
1370
-
1371
- instructor_id = instructor['InstructorID']
1372
-
1373
- try:
1374
- # Get total courses
1375
- cursor.execute("""
1376
- SELECT COUNT(*) as total_courses
1377
- FROM Courses
1378
- WHERE InstructorID = %s
1379
- """, (instructor_id,))
1380
- total_courses = cursor.fetchone()['total_courses']
1381
-
1382
- # Get total students and stats
1383
- cursor.execute("""
1384
- SELECT
1385
- COUNT(DISTINCT e.LearnerID) as total_students,
1386
- COALESCE(AVG(e.Rating), 0) as average_rating,
1387
- COUNT(*) as total_enrollments,
1388
- SUM(CASE WHEN e.Percentage = 100 THEN 1 ELSE 0 END) as completed_enrollments
1389
- FROM Courses c
1390
- LEFT JOIN Enrollments e ON c.CourseID = e.CourseID
1391
- WHERE c.InstructorID = %s
1392
- """, (instructor_id,))
1393
-
1394
- stats = cursor.fetchone()
1395
- if not stats:
1396
- stats = {
1397
- 'total_students': 0,
1398
- 'average_rating': 0,
1399
- 'total_enrollments': 0,
1400
- 'completed_enrollments': 0
1401
- }
1402
-
1403
- completion_rate = round((stats['completed_enrollments'] / stats['total_enrollments'] * 100)
1404
- if stats['total_enrollments'] > 0 else 0, 1)
1405
-
1406
- # Get student growth
1407
- cursor.execute("""
1408
- SELECT
1409
- DATE_FORMAT(EnrollmentDate, '%%Y-%%m') as month,
1410
- COUNT(DISTINCT LearnerID) as students
1411
- FROM Courses c
1412
- JOIN Enrollments e ON c.CourseID = e.CourseID
1413
- WHERE c.InstructorID = %s
1414
- AND EnrollmentDate >= DATE_SUB(CURRENT_DATE, INTERVAL 2 MONTH)
1415
- GROUP BY DATE_FORMAT(EnrollmentDate, '%%Y-%%m')
1416
- ORDER BY month DESC
1417
- LIMIT 2
1418
- """, (instructor_id,))
1419
-
1420
- growth_data = cursor.fetchall()
1421
-
1422
- current_month = growth_data[0]['students'] if growth_data else 0
1423
- prev_month = growth_data[1]['students'] if len(growth_data) > 1 else 0
1424
- student_growth = round(((current_month - prev_month) / prev_month * 100)
1425
- if prev_month > 0 else 0, 1)
1426
-
1427
- # Get courses
1428
- cursor.execute("""
1429
- SELECT
1430
- c.CourseID as id,
1431
- c.CourseName as name,
1432
- c.Descriptions as description,
1433
- c.AverageRating as rating,
1434
- COUNT(DISTINCT e.LearnerID) as enrollments,
1435
- AVG(e.Percentage) as completionRate
1436
- FROM Courses c
1437
- LEFT JOIN Enrollments e ON c.CourseID = e.CourseID
1438
- WHERE c.InstructorID = %s
1439
- GROUP BY c.CourseID, c.CourseName, c.Descriptions, c.AverageRating
1440
- ORDER BY c.CreatedAt DESC
1441
- """, (instructor_id,))
1442
-
1443
- courses = cursor.fetchall() or []
1444
-
1445
- # Get enrollment trends
1446
- cursor.execute("""
1447
- SELECT
1448
- DATE_FORMAT(e.EnrollmentDate, '%%Y-%%m-%%d') as date,
1449
- COUNT(*) as value
1450
- FROM Courses c
1451
- JOIN Enrollments e ON c.CourseID = e.CourseID
1452
- WHERE c.InstructorID = %s
1453
- AND e.EnrollmentDate >= DATE_SUB(CURRENT_DATE, INTERVAL 30 DAY)
1454
- GROUP BY DATE_FORMAT(e.EnrollmentDate, '%%Y-%%m-%%d')
1455
- ORDER BY date
1456
- """, (instructor_id,))
1457
-
1458
- enrollment_trends = cursor.fetchall() or []
1459
-
1460
- # Get rating trends
1461
- cursor.execute("""
1462
- SELECT
1463
- DATE_FORMAT(e.EnrollmentDate, '%%Y-%%m-%%d') as date,
1464
- AVG(e.Rating) as value
1465
- FROM Courses c
1466
- JOIN Enrollments e ON c.CourseID = e.CourseID
1467
- WHERE c.InstructorID = %s
1468
- AND e.EnrollmentDate >= DATE_SUB(CURRENT_DATE, INTERVAL 30 DAY)
1469
- AND e.Rating IS NOT NULL
1470
- GROUP BY DATE_FORMAT(e.EnrollmentDate, '%%Y-%%m-%%d')
1471
- ORDER BY date
1472
- """, (instructor_id,))
1473
-
1474
- rating_trends = cursor.fetchall() or []
1475
-
1476
- # Format the data
1477
- formatted_courses = [
1478
- {
1479
- 'id': course['id'],
1480
- 'name': course['name'],
1481
- 'description': course['description'] or "",
1482
- 'enrollments': course['enrollments'] or 0,
1483
- 'rating': round(float(course['rating']), 1) if course['rating'] else 0.0,
1484
- 'completionRate': round(float(course['completionRate']), 1) if course['completionRate'] else 0.0
1485
- } for course in courses
1486
- ]
1487
-
1488
- formatted_enrollment_trends = [
1489
- {
1490
- 'date': trend['date'],
1491
- 'value': int(trend['value'])
1492
- } for trend in enrollment_trends
1493
- ]
1494
-
1495
- formatted_rating_trends = [
1496
- {
1497
- 'date': trend['date'],
1498
- 'value': round(float(trend['value']), 1)
1499
- } for trend in rating_trends if trend['value'] is not None
1500
- ]
1501
-
1502
- dashboard_data = {
1503
- "metrics": {
1504
- "totalCourses": total_courses,
1505
- "totalStudents": stats['total_students'],
1506
- "averageRating": round(float(stats['average_rating']), 1),
1507
- "completionRate": completion_rate,
1508
- "studentGrowth": student_growth
1509
- },
1510
- "courses": formatted_courses,
1511
- "enrollmentTrends": formatted_enrollment_trends,
1512
- "ratingTrends": formatted_rating_trends,
1513
- "courseEnrollments": [
1514
- {
1515
- "courseName": course['name'],
1516
- "enrollments": course['enrollments'] or 0
1517
- } for course in courses
1518
- ]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1519
  }
1520
-
1521
- return dashboard_data
1522
-
1523
- except Exception as e:
1524
- print(f"Error processing dashboard data: {str(e)}")
1525
- raise HTTPException(status_code=500, detail="Error processing dashboard data")
1526
-
1527
- except HTTPException as he:
1528
- raise he
1529
- except Exception as e:
1530
- print(f"Database error: {str(e)}")
1531
- raise HTTPException(status_code=500, detail=f"Database error: {str(e)}")
1532
-
1533
- except HTTPException as he:
1534
- raise he
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1535
  except Exception as e:
1536
- print(f"Error fetching instructor dashboard: {str(e)}")
1537
- raise HTTPException(status_code=500, detail=str(e))
1538
  finally:
1539
- if 'conn' in locals():
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()