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

update backend

Browse files
services/api/api_endpoints.py CHANGED
@@ -8,12 +8,15 @@ 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
  from services.utils.cache_utils import cache_data, cache_with_fallback, clear_cache
15
  from services.config.valkey_config import get_redis_client
16
- from services.utils.api_cache import get_cached_data, invalidate_cache
17
 
18
  # Get Valkey client
19
  redis_client = get_redis_client()
@@ -60,7 +63,7 @@ def connect_db():
60
  password=MYSQL_PASSWORD,
61
  database=MYSQL_DB,
62
  port=MYSQL_PORT,
63
- ssl=SSL
64
  )
65
  print("Database connection successful")
66
  return connection
@@ -111,11 +114,25 @@ class LectureDetails(Lecture):
111
  courseDescription: Optional[str] = None
112
  courseLectures: Optional[List[LectureListItem]] = None
113
 
114
- # Get all courses
 
 
 
 
 
 
 
 
 
 
 
 
 
 
115
  @router.get("/courses", response_model=List[Course])
116
  async def get_courses(request: Request, auth_token: str = Cookie(None)):
117
  try:
118
- # Try to get token from Authorization header if cookie is not present
119
  if not auth_token:
120
  auth_header = request.headers.get('Authorization')
121
  if auth_header and auth_header.startswith('Bearer '):
@@ -124,76 +141,86 @@ async def get_courses(request: Request, auth_token: str = Cookie(None)):
124
  if not auth_token:
125
  raise HTTPException(status_code=401, detail="No authentication token provided")
126
 
127
- # Verify user is authenticated
128
  try:
129
  user_data = decode_token(auth_token)
130
  except Exception as e:
131
  print(f"Token decode error: {str(e)}")
132
  raise HTTPException(status_code=401, detail="Invalid authentication token")
133
 
134
- # Create a unique cache key using the user's ID
135
- cache_key = f"courses:all:user:{user_data.get('user_id', 'anonymous')}"
136
 
137
- # Define the database fetch function
138
  async def fetch_courses_from_db():
139
  conn = connect_db()
140
- courses = []
141
-
142
  try:
143
  with conn.cursor(pymysql.cursors.DictCursor) as cursor:
 
144
  query = """
145
  SELECT
146
  c.CourseID as id,
147
  c.CourseName as name,
148
  CONCAT(i.InstructorName, ' (', i.AccountName, ')') as instructor,
149
- c.Descriptions as description
 
 
150
  FROM Courses c
151
  JOIN Instructors i ON c.InstructorID = i.InstructorID
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
152
  """
 
153
  cursor.execute(query)
154
  courses = cursor.fetchall()
155
 
156
- # Get ratings for each course
 
157
  for course in courses:
158
- cursor.execute("""
159
- SELECT AVG(Rating) as avg_rating, COUNT(*) as count
160
- FROM Enrollments
161
- WHERE CourseID = %s AND Rating IS NOT NULL
162
- """, (course['id'],))
163
-
164
- rating_data = cursor.fetchone()
165
- if rating_data and rating_data['avg_rating']:
166
- course['rating'] = float(rating_data['avg_rating'])
167
- else:
168
- course['rating'] = None
169
-
170
- # Get enrollment count
171
- cursor.execute("""
172
- SELECT COUNT(*) as enrolled
173
- FROM Enrollments
174
- WHERE CourseID = %s
175
- """, (course['id'],))
176
-
177
- enrolled_data = cursor.fetchone()
178
- if enrolled_data:
179
- course['enrolled'] = enrolled_data['enrolled']
180
- else:
181
- course['enrolled'] = 0
182
  finally:
183
  conn.close()
184
-
185
- return courses
186
 
187
- # Use the cached data helper to implement the Valkey caching pattern
188
- return await get_cached_data(cache_key, fetch_courses_from_db, ttl=3600)
 
 
 
 
 
 
189
  except Exception as e:
190
  raise HTTPException(status_code=500, detail=str(e))
191
 
192
  # Get course details
193
- @router.get("/courses/{course_id}", response_model=Course)
194
- async def get_course_details(request: Request, course_id: int, auth_token: str = Cookie(None)):
 
195
  try:
196
- # Try to get token from Authorization header if cookie is not present
197
  if not auth_token:
198
  auth_header = request.headers.get('Authorization')
199
  if auth_header and auth_header.startswith('Bearer '):
@@ -201,66 +228,96 @@ async def get_course_details(request: Request, course_id: int, auth_token: str =
201
 
202
  if not auth_token:
203
  raise HTTPException(status_code=401, detail="No authentication token provided")
204
-
205
- # Verify user is authenticated
206
  try:
207
  user_data = decode_token(auth_token)
 
208
  except Exception as e:
209
  print(f"Token decode error: {str(e)}")
210
  raise HTTPException(status_code=401, detail="Invalid authentication token")
211
 
212
- # Create a unique cache key using the course ID and user ID
213
- cache_key = f"courses:id:{course_id}:user:{user_data.get('user_id', 'anonymous')}"
214
 
215
- # Define the database fetch function
216
- async def fetch_course_from_db():
217
  conn = connect_db()
218
- course = None
219
-
220
  try:
221
  with conn.cursor(pymysql.cursors.DictCursor) as cursor:
222
- # First check if course exists
223
  query = """
224
  SELECT
225
- c.CourseID as id,
226
- c.CourseName as name,
 
 
 
 
227
  CONCAT(i.InstructorName, ' (', i.AccountName, ')') as instructor,
228
- c.Descriptions as description
 
 
 
229
  FROM Courses c
230
  JOIN Instructors i ON c.InstructorID = i.InstructorID
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
231
  WHERE c.CourseID = %s
232
  """
233
- cursor.execute(query, (course_id,))
 
234
  course = cursor.fetchone()
235
 
236
  if not course:
237
- raise HTTPException(status_code=404, detail=f"Course with ID {course_id} not found")
238
-
239
- # Get ratings and enrollment count in one query
240
- cursor.execute("""
241
- SELECT
242
- COUNT(*) as enrolled,
243
- COALESCE(AVG(Rating), 0) as avg_rating,
244
- COUNT(CASE WHEN Rating IS NOT NULL THEN 1 END) as rating_count
245
- FROM Enrollments
246
- WHERE CourseID = %s
247
- """, (course_id,))
248
 
249
- stats = cursor.fetchone()
250
- if stats:
251
- course['enrolled'] = stats['enrolled']
252
- course['rating'] = float(stats['avg_rating']) if stats['rating_count'] > 0 else None
253
- else:
254
- course['enrolled'] = 0
255
- course['rating'] = None
256
 
 
 
 
 
 
 
 
 
 
 
 
 
 
257
  finally:
258
  conn.close()
259
-
260
- return course
261
 
262
- # Use the cached data helper to implement the Valkey caching pattern
263
- return await get_cached_data(cache_key, fetch_course_from_db, ttl=3600)
 
 
 
 
 
 
264
  except HTTPException:
265
  raise
266
  except Exception as e:
@@ -505,7 +562,7 @@ async def get_lecture_details(request: Request, lecture_id: int, auth_token: str
505
  print(f"Error in get_lecture_details: {str(e)}")
506
  raise HTTPException(status_code=500, detail=str(e))
507
 
508
- # Get instructor's courses
509
  @router.get("/instructor/courses", response_model=List[Course])
510
  async def get_instructor_courses(
511
  request: Request,
@@ -521,24 +578,27 @@ async def get_instructor_courses(
521
  raise HTTPException(status_code=401, detail="No authentication token provided")
522
 
523
  # Verify token and get user data
524
- try:
525
- user_data = decode_token(auth_token)
526
- username = user_data['username']
527
- role = user_data['role']
528
-
529
- # Verify user is an instructor
530
- if role != "Instructor":
531
- raise HTTPException(status_code=403, detail="Only instructors can access this endpoint")
532
-
533
- # Create a cache key for instructor courses
534
- cache_key = f"instructor:courses:{username}"
535
-
536
- # Define database fetch function
537
- async def fetch_instructor_courses_from_db():
538
- conn = connect_db()
539
- try:
540
- with conn.cursor(pymysql.cursors.DictCursor) as cursor:
541
- # Get instructor ID
 
 
 
542
  cursor.execute("""
543
  SELECT InstructorID
544
  FROM Instructors
@@ -549,59 +609,55 @@ async def get_instructor_courses(
549
  if not instructor:
550
  raise HTTPException(status_code=404, detail="Instructor not found")
551
 
552
- instructor_id = instructor['InstructorID']
553
-
554
- # Get courses by this instructor
555
- query = """
556
- SELECT
557
- c.CourseID as id,
558
- c.CourseName as name,
559
- CONCAT(i.InstructorName, ' (', i.AccountName, ')') as instructor,
560
- c.Descriptions as description,
561
- (SELECT COUNT(*) FROM Enrollments WHERE CourseID = c.CourseID) as enrolled,
562
- COALESCE(
563
- (SELECT AVG(Rating)
564
- FROM Enrollments
565
- WHERE CourseID = c.CourseID AND Rating IS NOT NULL),
566
- 0
567
- ) as rating
568
- FROM Courses c
569
- JOIN Instructors i ON c.InstructorID = i.InstructorID
570
- WHERE c.InstructorID = %s
571
- ORDER BY c.CourseID DESC
572
- """
573
-
574
- cursor.execute(query, (instructor_id,))
575
- courses = cursor.fetchall()
576
-
577
- # Format the courses data
578
- formatted_courses = []
579
- for course in courses:
580
- formatted_course = {
581
- 'id': course['id'],
582
- 'name': course['name'],
583
- 'instructor': course['instructor'],
584
- 'description': course['description'],
585
- 'enrolled': course['enrolled'],
586
- 'rating': float(course['rating']) if course['rating'] else None
587
- }
588
- formatted_courses.append(formatted_course)
589
-
590
- return formatted_courses
591
- finally:
592
- conn.close()
593
-
594
- # Use the caching mechanism to get the data
595
- return await get_cached_data(
596
- cache_key,
597
- fetch_instructor_courses_from_db,
598
- ttl=1800, # Cache for 30 minutes
599
- use_compression=True # Enable compression for large response
600
- )
601
-
602
- except Exception as e:
603
- print(f"Database error: {str(e)}")
604
- raise HTTPException(status_code=500, detail=f"Database error: {str(e)}")
605
 
606
  except HTTPException as he:
607
  raise he
@@ -637,9 +693,10 @@ async def create_course(
637
  user_data = decode_token(auth_token)
638
  username = user_data['username']
639
  role = user_data['role']
 
640
 
641
  # Log debugging information
642
- print(f"POST /instructor/courses - User: {username}, Role: {role}")
643
  print(f"Course data: {course_data}")
644
 
645
  # Verify user is an instructor
@@ -650,18 +707,20 @@ async def create_course(
650
  conn = connect_db()
651
 
652
  with conn.cursor(pymysql.cursors.DictCursor) as cursor:
653
- # Get instructor ID
654
- cursor.execute("""
655
- SELECT InstructorID
656
- FROM Instructors
657
- WHERE AccountName = %s
658
- """, (username,))
659
-
660
- instructor = cursor.fetchone()
661
- if not instructor:
662
- raise HTTPException(status_code=404, detail="Instructor not found")
663
-
664
- instructor_id = instructor['InstructorID']
 
 
665
 
666
  # Insert new course
667
  cursor.execute("""
@@ -699,11 +758,14 @@ async def create_course(
699
  if not new_course:
700
  raise HTTPException(status_code=500, detail="Course was created but couldn't be retrieved")
701
 
702
- # Invalidate Valkey cache for all courses
703
- redis_client = get_redis_client()
704
- pattern_all = "courses:all:*"
705
- print(f"Invalidating cache pattern: {pattern_all}")
706
- invalidate_cache([pattern_all])
 
 
 
707
 
708
  return new_course
709
 
@@ -741,19 +803,24 @@ async def enroll_in_course(
741
  # Verify token and get user data
742
  try:
743
  user_data = decode_token(auth_token)
 
744
 
745
- # Get LearnerID from Learners table using the username
746
- conn = connect_db()
747
- with conn.cursor(pymysql.cursors.DictCursor) as cursor:
748
- cursor.execute("""
749
- SELECT LearnerID
750
- FROM Learners
751
- WHERE AccountName = %s
752
- """, (user_data['username'],))
753
- learner = cursor.fetchone()
754
- if not learner:
755
- raise HTTPException(status_code=404, detail="Learner not found")
756
- learner_id = learner['LearnerID']
 
 
 
 
757
  except Exception as e:
758
  print(f"Token/user verification error: {str(e)}")
759
  raise HTTPException(status_code=401, detail="Invalid authentication token or user not found")
@@ -796,13 +863,18 @@ async def enroll_in_course(
796
 
797
  conn.commit()
798
 
799
- # Invalidate Valkey cache for this course and all courses
800
  redis_client = get_redis_client()
801
  # Clear specific course cache for all users
802
  pattern_course = f"courses:id:{course_id}:*"
803
- pattern_all = "courses:all:*"
804
- print(f"Invalidating cache patterns: {pattern_course} and {pattern_all}")
805
- invalidate_cache([pattern_course, pattern_all])
 
 
 
 
 
806
 
807
  return {"message": "Successfully enrolled in the course"}
808
  except Exception as e:
@@ -837,19 +909,24 @@ async def get_enrolled_courses(
837
  # Verify token and get user data
838
  try:
839
  user_data = decode_token(auth_token)
 
840
 
841
- # Get LearnerID from Learners table using the username
842
- conn = connect_db()
843
- with conn.cursor(pymysql.cursors.DictCursor) as cursor:
844
- cursor.execute("""
845
- SELECT LearnerID
846
- FROM Learners
847
- WHERE AccountName = %s
848
- """, (user_data['username'],))
849
- learner = cursor.fetchone()
850
- if not learner:
851
- raise HTTPException(status_code=404, detail="Learner not found")
852
- learner_id = learner['LearnerID']
 
 
 
 
853
  except Exception as e:
854
  print(f"Token/user verification error: {str(e)}")
855
  raise HTTPException(status_code=401, detail="Invalid authentication token or user not found")
@@ -1265,6 +1342,7 @@ async def get_instructor_dashboard(
1265
  user_data = decode_token(auth_token)
1266
  username = user_data.get('username')
1267
  role = user_data.get('role')
 
1268
 
1269
  if not username or not role:
1270
  raise HTTPException(status_code=401, detail="Invalid token data")
@@ -1277,8 +1355,9 @@ async def get_instructor_dashboard(
1277
  conn = connect_db()
1278
 
1279
  with conn.cursor(pymysql.cursors.DictCursor) as cursor:
1280
- # Get instructor ID
1281
- try:
 
1282
  cursor.execute("""
1283
  SELECT InstructorID
1284
  FROM Instructors
@@ -1290,9 +1369,6 @@ async def get_instructor_dashboard(
1290
  raise HTTPException(status_code=404, detail="Instructor not found")
1291
 
1292
  instructor_id = instructor['InstructorID']
1293
- except Exception as e:
1294
- print(f"Error getting instructor ID: {str(e)}")
1295
- raise HTTPException(status_code=500, detail="Error retrieving instructor information")
1296
 
1297
  try:
1298
  # Get total courses
@@ -1714,6 +1790,7 @@ async def get_instructor_course_details(
1714
  user_data = decode_token(auth_token)
1715
  username = user_data['username']
1716
  role = user_data['role']
 
1717
 
1718
  # Verify user is an instructor
1719
  if role != "Instructor":
@@ -1723,18 +1800,20 @@ async def get_instructor_course_details(
1723
  conn = connect_db()
1724
 
1725
  with conn.cursor(pymysql.cursors.DictCursor) as cursor:
1726
- # Get instructor ID
1727
- cursor.execute("""
1728
- SELECT InstructorID
1729
- FROM Instructors
1730
- WHERE AccountName = %s
1731
- """, (username,))
1732
-
1733
- instructor = cursor.fetchone()
1734
- if not instructor:
1735
- raise HTTPException(status_code=404, detail="Instructor not found")
1736
-
1737
- instructor_id = instructor['InstructorID']
 
 
1738
 
1739
  # Get course details, ensuring it belongs to this instructor
1740
  query = """
@@ -1833,18 +1912,21 @@ async def create_lecture(
1833
  cursor = conn.cursor(pymysql.cursors.DictCursor)
1834
 
1835
  try:
1836
- # Get instructor ID
1837
- cursor.execute("""
1838
- SELECT InstructorID
1839
- FROM Instructors
1840
- WHERE AccountName = %s
1841
- """, (username,))
1842
-
1843
- instructor = cursor.fetchone()
1844
- if not instructor:
1845
- raise HTTPException(status_code=404, detail="Instructor not found")
1846
-
1847
- instructor_id = instructor['InstructorID']
 
 
 
1848
 
1849
  # Verify this instructor owns this course
1850
  cursor.execute("""
@@ -1861,34 +1943,12 @@ async def create_lecture(
1861
  INSERT INTO Lectures (CourseID, Title, Description, Content)
1862
  VALUES (%s, %s, %s, %s)
1863
  """, (course_id, title, description, content))
1864
- conn.commit()
1865
 
1866
  # Get the newly created lecture ID
1867
  lecture_id = cursor.lastrowid
1868
 
1869
- # Upload video if provided
1870
- if video:
1871
- # Check file size (100MB limit)
1872
- video_content = await video.read()
1873
- if len(video_content) > 100 * 1024 * 1024: # 100MB in bytes
1874
- raise HTTPException(status_code=400, detail="Video file size must be less than 100MB")
1875
-
1876
- # Check file type
1877
- if not video.content_type.startswith('video/'):
1878
- raise HTTPException(status_code=400, detail="Invalid file type. Please upload a video file")
1879
-
1880
- # Upload to S3
1881
- media_path = f"videos/cid{course_id}/lid{lecture_id}/vid_lecture.mp4"
1882
- s3.put_object(
1883
- Bucket="tlhmaterials",
1884
- Key=media_path,
1885
- Body=video_content,
1886
- ContentType=video.content_type,
1887
- ACL="public-read",
1888
- ContentDisposition="inline"
1889
- )
1890
-
1891
- # Create quiz if provided
1892
  if quiz:
1893
  quiz_data = json.loads(quiz)
1894
  if quiz_data and quiz_data.get('questions'):
@@ -1897,34 +1957,117 @@ async def create_lecture(
1897
  INSERT INTO Quizzes (LectureID, Title, Description)
1898
  VALUES (%s, %s, %s)
1899
  """, (lecture_id, f"Quiz for {title}", description))
1900
- conn.commit()
1901
 
1902
  quiz_id = cursor.lastrowid
1903
 
1904
- # Insert questions and options
 
 
 
1905
  for question in quiz_data['questions']:
 
1906
  cursor.execute("""
1907
  INSERT INTO Questions (QuizID, QuestionText)
1908
  VALUES (%s, %s)
1909
  """, (quiz_id, question['question']))
1910
- conn.commit()
1911
 
1912
  question_id = cursor.lastrowid
1913
 
1914
- # Insert options
1915
  for i, option in enumerate(question['options']):
1916
- cursor.execute("""
1917
- INSERT INTO Options (QuestionID, OptionText, IsCorrect)
1918
- VALUES (%s, %s, %s)
1919
- """, (question_id, option, i == question['correctAnswer']))
1920
- conn.commit()
 
 
 
 
 
 
 
1921
 
1922
- return {
 
 
 
 
1923
  "id": lecture_id,
1924
  "title": title,
1925
  "message": "Lecture created successfully"
1926
  }
1927
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1928
  except HTTPException:
1929
  raise
1930
  except Exception as e:
@@ -1942,4 +2085,134 @@ async def create_lecture(
1942
  raise HTTPException(status_code=500, detail=str(e))
1943
  finally:
1944
  if 'conn' in locals():
1945
- conn.close()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
8
  import pandas as pd
9
  import boto3
10
  import json
11
+ import threading
12
+ import asyncio
13
+ from io import BytesIO
14
  from botocore.exceptions import ClientError
15
  from datetime import datetime
16
  from fastapi.middleware.cors import CORSMiddleware
17
  from services.utils.cache_utils import cache_data, cache_with_fallback, clear_cache
18
  from services.config.valkey_config import get_redis_client
19
+ from services.utils.api_cache import get_cached_data, invalidate_cache, invalidate_cache_pattern
20
 
21
  # Get Valkey client
22
  redis_client = get_redis_client()
 
63
  password=MYSQL_PASSWORD,
64
  database=MYSQL_DB,
65
  port=MYSQL_PORT,
66
+ ssl=False
67
  )
68
  print("Database connection successful")
69
  return connection
 
114
  courseDescription: Optional[str] = None
115
  courseLectures: Optional[List[LectureListItem]] = None
116
 
117
+ # Add the missing CourseDetails model
118
+ class CourseDetails(BaseModel):
119
+ id: int
120
+ name: str
121
+ description: Optional[str]
122
+ duration: Optional[str]
123
+ skills: Optional[List[str]] = []
124
+ difficulty: Optional[str]
125
+ instructor: str
126
+ instructor_id: Optional[int]
127
+ enrolled: Optional[int] = 0
128
+ rating: Optional[float] = None
129
+ is_enrolled: Optional[bool] = False
130
+
131
+ # Optimized /courses endpoint
132
  @router.get("/courses", response_model=List[Course])
133
  async def get_courses(request: Request, auth_token: str = Cookie(None)):
134
  try:
135
+ # Authentication (keep existing code)
136
  if not auth_token:
137
  auth_header = request.headers.get('Authorization')
138
  if auth_header and auth_header.startswith('Bearer '):
 
141
  if not auth_token:
142
  raise HTTPException(status_code=401, detail="No authentication token provided")
143
 
 
144
  try:
145
  user_data = decode_token(auth_token)
146
  except Exception as e:
147
  print(f"Token decode error: {str(e)}")
148
  raise HTTPException(status_code=401, detail="Invalid authentication token")
149
 
150
+ # Better cache key with shorter TTL for faster updates
151
+ cache_key = "courses:public:v2"
152
 
153
+ # Optimized database fetch function
154
  async def fetch_courses_from_db():
155
  conn = connect_db()
 
 
156
  try:
157
  with conn.cursor(pymysql.cursors.DictCursor) as cursor:
158
+ # Single optimized query with JOINs instead of N+1 queries
159
  query = """
160
  SELECT
161
  c.CourseID as id,
162
  c.CourseName as name,
163
  CONCAT(i.InstructorName, ' (', i.AccountName, ')') as instructor,
164
+ c.Descriptions as description,
165
+ COALESCE(enrollment_stats.enrolled, 0) as enrolled,
166
+ COALESCE(rating_stats.avg_rating, NULL) as rating
167
  FROM Courses c
168
  JOIN Instructors i ON c.InstructorID = i.InstructorID
169
+ LEFT JOIN (
170
+ SELECT
171
+ CourseID,
172
+ COUNT(*) as enrolled
173
+ FROM Enrollments
174
+ GROUP BY CourseID
175
+ ) enrollment_stats ON c.CourseID = enrollment_stats.CourseID
176
+ LEFT JOIN (
177
+ SELECT
178
+ CourseID,
179
+ AVG(Rating) as avg_rating
180
+ FROM Enrollments
181
+ WHERE Rating IS NOT NULL
182
+ GROUP BY CourseID
183
+ ) rating_stats ON c.CourseID = rating_stats.CourseID
184
+ ORDER BY c.CourseID DESC
185
+ LIMIT 50
186
  """
187
+
188
  cursor.execute(query)
189
  courses = cursor.fetchall()
190
 
191
+ # Format the data efficiently
192
+ formatted_courses = []
193
  for course in courses:
194
+ formatted_courses.append({
195
+ 'id': course['id'],
196
+ 'name': course['name'],
197
+ 'instructor': course['instructor'],
198
+ 'description': course['description'],
199
+ 'enrolled': course['enrolled'],
200
+ 'rating': float(course['rating']) if course['rating'] else None
201
+ })
202
+
203
+ return formatted_courses
 
 
 
 
 
 
 
 
 
 
 
 
 
 
204
  finally:
205
  conn.close()
 
 
206
 
207
+ # Use optimized caching with compression and shorter TTL
208
+ return await get_cached_data(
209
+ cache_key,
210
+ fetch_courses_from_db,
211
+ ttl=900, # 15 minutes instead of 1 hour for faster updates
212
+ use_compression=True # Enable compression for faster transfer
213
+ )
214
+
215
  except Exception as e:
216
  raise HTTPException(status_code=500, detail=str(e))
217
 
218
  # Get course details
219
+ # Optimize the course details endpoint with caching
220
+ @router.get("/courses/{course_id}", response_model=CourseDetails)
221
+ async def get_course_details(course_id: int, request: Request, auth_token: str = Cookie(None)):
222
  try:
223
+ # Authentication
224
  if not auth_token:
225
  auth_header = request.headers.get('Authorization')
226
  if auth_header and auth_header.startswith('Bearer '):
 
228
 
229
  if not auth_token:
230
  raise HTTPException(status_code=401, detail="No authentication token provided")
231
+
 
232
  try:
233
  user_data = decode_token(auth_token)
234
+ user_id = user_data.get('user_id')
235
  except Exception as e:
236
  print(f"Token decode error: {str(e)}")
237
  raise HTTPException(status_code=401, detail="Invalid authentication token")
238
 
239
+ # Cache key for course details
240
+ cache_key = f"course:details:{course_id}:user:{user_id}"
241
 
242
+ # Optimized database fetch function
243
+ async def fetch_course_details_from_db():
244
  conn = connect_db()
 
 
245
  try:
246
  with conn.cursor(pymysql.cursors.DictCursor) as cursor:
247
+ # Single query with all course info, enrollment status, and user's enrollment
248
  query = """
249
  SELECT
250
+ c.CourseID as id,
251
+ c.CourseName as name,
252
+ c.Descriptions as description,
253
+ c.EstimatedDuration as duration,
254
+ c.Skills as skills,
255
+ c.Difficulty as difficulty,
256
  CONCAT(i.InstructorName, ' (', i.AccountName, ')') as instructor,
257
+ i.InstructorID as instructor_id,
258
+ COALESCE(enrollment_stats.enrolled, 0) as enrolled,
259
+ COALESCE(rating_stats.avg_rating, NULL) as rating,
260
+ CASE WHEN user_enrollment.LearnerID IS NOT NULL THEN TRUE ELSE FALSE END as is_enrolled
261
  FROM Courses c
262
  JOIN Instructors i ON c.InstructorID = i.InstructorID
263
+ LEFT JOIN (
264
+ SELECT CourseID, COUNT(*) as enrolled
265
+ FROM Enrollments
266
+ GROUP BY CourseID
267
+ ) enrollment_stats ON c.CourseID = enrollment_stats.CourseID
268
+ LEFT JOIN (
269
+ SELECT CourseID, AVG(Rating) as avg_rating
270
+ FROM Enrollments
271
+ WHERE Rating IS NOT NULL
272
+ GROUP BY CourseID
273
+ ) rating_stats ON c.CourseID = rating_stats.CourseID
274
+ LEFT JOIN (
275
+ SELECT CourseID, LearnerID
276
+ FROM Enrollments e2
277
+ JOIN Learners l ON e2.LearnerID = l.LearnerID
278
+ WHERE l.LearnerID = %s
279
+ ) user_enrollment ON c.CourseID = user_enrollment.CourseID
280
  WHERE c.CourseID = %s
281
  """
282
+
283
+ cursor.execute(query, (user_id, course_id))
284
  course = cursor.fetchone()
285
 
286
  if not course:
287
+ raise HTTPException(status_code=404, detail="Course not found")
 
 
 
 
 
 
 
 
 
 
288
 
289
+ # Format skills if it's JSON
290
+ skills = []
291
+ if course['skills']:
292
+ try:
293
+ skills = json.loads(course['skills'])
294
+ except:
295
+ skills = []
296
 
297
+ return {
298
+ 'id': course['id'],
299
+ 'name': course['name'],
300
+ 'description': course['description'],
301
+ 'duration': course['duration'],
302
+ 'skills': skills,
303
+ 'difficulty': course['difficulty'],
304
+ 'instructor': course['instructor'],
305
+ 'instructor_id': course['instructor_id'],
306
+ 'enrolled': course['enrolled'],
307
+ 'rating': float(course['rating']) if course['rating'] else None,
308
+ 'is_enrolled': course['is_enrolled']
309
+ }
310
  finally:
311
  conn.close()
 
 
312
 
313
+ # Use caching with 30 minutes TTL
314
+ return await get_cached_data(
315
+ cache_key,
316
+ fetch_course_details_from_db,
317
+ ttl=1800, # 30 minutes
318
+ use_compression=True
319
+ )
320
+
321
  except HTTPException:
322
  raise
323
  except Exception as e:
 
562
  print(f"Error in get_lecture_details: {str(e)}")
563
  raise HTTPException(status_code=500, detail=str(e))
564
 
565
+ # Get instructor's courses with proper caching
566
  @router.get("/instructor/courses", response_model=List[Course])
567
  async def get_instructor_courses(
568
  request: Request,
 
578
  raise HTTPException(status_code=401, detail="No authentication token provided")
579
 
580
  # Verify token and get user data
581
+ user_data = decode_token(auth_token)
582
+ username = user_data['username']
583
+ role = user_data['role']
584
+ instructor_id = user_data.get('user_id')
585
+
586
+ # Verify user is an instructor
587
+ if role != "Instructor":
588
+ raise HTTPException(status_code=403, detail="Only instructors can access this endpoint")
589
+
590
+ # Create a cache key for instructor courses
591
+ cache_key = f"instructor:courses:{instructor_id or username}"
592
+
593
+ # Define database fetch function
594
+ async def fetch_instructor_courses_from_db():
595
+ conn = connect_db()
596
+ try:
597
+ with conn.cursor(pymysql.cursors.DictCursor) as cursor:
598
+ # Get instructor ID from token or fallback to database lookup
599
+ current_instructor_id = instructor_id
600
+ if not current_instructor_id:
601
+ # Fallback for old tokens without user_id
602
  cursor.execute("""
603
  SELECT InstructorID
604
  FROM Instructors
 
609
  if not instructor:
610
  raise HTTPException(status_code=404, detail="Instructor not found")
611
 
612
+ current_instructor_id = instructor['InstructorID']
613
+
614
+ # Get courses by this instructor
615
+ query = """
616
+ SELECT
617
+ c.CourseID as id,
618
+ c.CourseName as name,
619
+ CONCAT(i.InstructorName, ' (', i.AccountName, ')') as instructor,
620
+ c.Descriptions as description,
621
+ (SELECT COUNT(*) FROM Enrollments WHERE CourseID = c.CourseID) as enrolled,
622
+ COALESCE(
623
+ (SELECT AVG(Rating)
624
+ FROM Enrollments
625
+ WHERE CourseID = c.CourseID AND Rating IS NOT NULL),
626
+ 0
627
+ ) as rating
628
+ FROM Courses c
629
+ JOIN Instructors i ON c.InstructorID = i.InstructorID
630
+ WHERE c.InstructorID = %s
631
+ ORDER BY c.CourseID DESC
632
+ """
633
+
634
+ cursor.execute(query, (current_instructor_id,))
635
+ courses = cursor.fetchall()
636
+
637
+ # Format the courses data
638
+ formatted_courses = []
639
+ for course in courses:
640
+ formatted_course = {
641
+ 'id': course['id'],
642
+ 'name': course['name'],
643
+ 'instructor': course['instructor'],
644
+ 'description': course['description'],
645
+ 'enrolled': course['enrolled'],
646
+ 'rating': float(course['rating']) if course['rating'] else None
647
+ }
648
+ formatted_courses.append(formatted_course)
649
+
650
+ return formatted_courses
651
+ finally:
652
+ conn.close()
653
+
654
+ # Use the caching mechanism to get the data
655
+ return await get_cached_data(
656
+ cache_key,
657
+ fetch_instructor_courses_from_db,
658
+ ttl=1800, # Cache for 30 minutes
659
+ use_compression=True # Enable compression for large response
660
+ )
 
 
 
 
661
 
662
  except HTTPException as he:
663
  raise he
 
693
  user_data = decode_token(auth_token)
694
  username = user_data['username']
695
  role = user_data['role']
696
+ instructor_id = user_data.get('user_id')
697
 
698
  # Log debugging information
699
+ print(f"POST /instructor/courses - User: {username}, Role: {role}, InstructorID: {instructor_id}")
700
  print(f"Course data: {course_data}")
701
 
702
  # Verify user is an instructor
 
707
  conn = connect_db()
708
 
709
  with conn.cursor(pymysql.cursors.DictCursor) as cursor:
710
+ # Get instructor ID from token or fallback to database lookup
711
+ if not instructor_id:
712
+ # Fallback for old tokens without user_id
713
+ cursor.execute("""
714
+ SELECT InstructorID
715
+ FROM Instructors
716
+ WHERE AccountName = %s
717
+ """, (username,))
718
+
719
+ instructor = cursor.fetchone()
720
+ if not instructor:
721
+ raise HTTPException(status_code=404, detail="Instructor not found")
722
+
723
+ instructor_id = instructor['InstructorID']
724
 
725
  # Insert new course
726
  cursor.execute("""
 
758
  if not new_course:
759
  raise HTTPException(status_code=500, detail="Course was created but couldn't be retrieved")
760
 
761
+ # Invalidate Valkey cache for all courses and instructor-specific cache
762
+ pattern_all = "courses:all"
763
+ pattern_instructor = f"instructor:courses:{instructor_id or username}"
764
+ pattern_course_details = f"courses:id" # Invalidate specific course details too
765
+ print(f"Invalidating cache patterns: {pattern_all}, {pattern_instructor}, and {pattern_course_details}")
766
+ invalidate_cache_pattern(pattern_all)
767
+ invalidate_cache_pattern(pattern_course_details)
768
+ invalidate_cache([pattern_instructor])
769
 
770
  return new_course
771
 
 
803
  # Verify token and get user data
804
  try:
805
  user_data = decode_token(auth_token)
806
+ learner_id = user_data.get('user_id')
807
 
808
+ if not learner_id:
809
+ # Fallback for old tokens without user_id - do database lookup
810
+ conn = connect_db()
811
+ with conn.cursor(pymysql.cursors.DictCursor) as cursor:
812
+ cursor.execute("""
813
+ SELECT LearnerID
814
+ FROM Learners
815
+ WHERE AccountName = %s
816
+ """, (user_data['username'],))
817
+ learner = cursor.fetchone()
818
+ if not learner:
819
+ raise HTTPException(status_code=404, detail="Learner not found")
820
+ learner_id = learner['LearnerID']
821
+ else:
822
+ # Use user_id from token
823
+ conn = connect_db()
824
  except Exception as e:
825
  print(f"Token/user verification error: {str(e)}")
826
  raise HTTPException(status_code=401, detail="Invalid authentication token or user not found")
 
863
 
864
  conn.commit()
865
 
866
+ # Invalidate Valkey cache for this course and user-specific data
867
  redis_client = get_redis_client()
868
  # Clear specific course cache for all users
869
  pattern_course = f"courses:id:{course_id}:*"
870
+ # Clear course preview cache for all users (this is the missing piece!)
871
+ pattern_preview = f"course:preview:{course_id}:*"
872
+ # Clear user-specific enrolled courses cache
873
+ pattern_user_courses = f"learner:courses:{learner_id}"
874
+ print(f"Invalidating cache patterns: {pattern_course}, {pattern_preview}, and {pattern_user_courses}")
875
+ invalidate_cache_pattern(pattern_course)
876
+ invalidate_cache_pattern(pattern_preview)
877
+ invalidate_cache([pattern_user_courses])
878
 
879
  return {"message": "Successfully enrolled in the course"}
880
  except Exception as e:
 
909
  # Verify token and get user data
910
  try:
911
  user_data = decode_token(auth_token)
912
+ learner_id = user_data.get('user_id')
913
 
914
+ if not learner_id:
915
+ # Fallback for old tokens without user_id - do database lookup
916
+ conn = connect_db()
917
+ with conn.cursor(pymysql.cursors.DictCursor) as cursor:
918
+ cursor.execute("""
919
+ SELECT LearnerID
920
+ FROM Learners
921
+ WHERE AccountName = %s
922
+ """, (user_data['username'],))
923
+ learner = cursor.fetchone()
924
+ if not learner:
925
+ raise HTTPException(status_code=404, detail="Learner not found")
926
+ learner_id = learner['LearnerID']
927
+ else:
928
+ # Use user_id from token
929
+ conn = connect_db()
930
  except Exception as e:
931
  print(f"Token/user verification error: {str(e)}")
932
  raise HTTPException(status_code=401, detail="Invalid authentication token or user not found")
 
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")
 
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
 
1369
  raise HTTPException(status_code=404, detail="Instructor not found")
1370
 
1371
  instructor_id = instructor['InstructorID']
 
 
 
1372
 
1373
  try:
1374
  # Get total courses
 
1790
  user_data = decode_token(auth_token)
1791
  username = user_data['username']
1792
  role = user_data['role']
1793
+ instructor_id = user_data.get('user_id')
1794
 
1795
  # Verify user is an instructor
1796
  if role != "Instructor":
 
1800
  conn = connect_db()
1801
 
1802
  with conn.cursor(pymysql.cursors.DictCursor) as cursor:
1803
+ # Get instructor ID from token or fallback to database lookup
1804
+ if not instructor_id:
1805
+ # Fallback for old tokens without user_id
1806
+ cursor.execute("""
1807
+ SELECT InstructorID
1808
+ FROM Instructors
1809
+ WHERE AccountName = %s
1810
+ """, (username,))
1811
+
1812
+ instructor = cursor.fetchone()
1813
+ if not instructor:
1814
+ raise HTTPException(status_code=404, detail="Instructor not found")
1815
+
1816
+ instructor_id = instructor['InstructorID']
1817
 
1818
  # Get course details, ensuring it belongs to this instructor
1819
  query = """
 
1912
  cursor = conn.cursor(pymysql.cursors.DictCursor)
1913
 
1914
  try:
1915
+ # Get instructor ID from token or fallback to database lookup
1916
+ instructor_id = user_data.get('user_id')
1917
+ if not instructor_id:
1918
+ # Fallback for old tokens without user_id
1919
+ cursor.execute("""
1920
+ SELECT InstructorID
1921
+ FROM Instructors
1922
+ WHERE AccountName = %s
1923
+ """, (username,))
1924
+
1925
+ instructor = cursor.fetchone()
1926
+ if not instructor:
1927
+ raise HTTPException(status_code=404, detail="Instructor not found")
1928
+
1929
+ instructor_id = instructor['InstructorID']
1930
 
1931
  # Verify this instructor owns this course
1932
  cursor.execute("""
 
1943
  INSERT INTO Lectures (CourseID, Title, Description, Content)
1944
  VALUES (%s, %s, %s, %s)
1945
  """, (course_id, title, description, content))
 
1946
 
1947
  # Get the newly created lecture ID
1948
  lecture_id = cursor.lastrowid
1949
 
1950
+ # Process quiz data first (faster database operations)
1951
+ quiz_id = None
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1952
  if quiz:
1953
  quiz_data = json.loads(quiz)
1954
  if quiz_data and quiz_data.get('questions'):
 
1957
  INSERT INTO Quizzes (LectureID, Title, Description)
1958
  VALUES (%s, %s, %s)
1959
  """, (lecture_id, f"Quiz for {title}", description))
 
1960
 
1961
  quiz_id = cursor.lastrowid
1962
 
1963
+ # Batch insert questions and options for better performance
1964
+ questions_to_insert = []
1965
+ options_to_insert = []
1966
+
1967
  for question in quiz_data['questions']:
1968
+ # Insert question first to get the ID
1969
  cursor.execute("""
1970
  INSERT INTO Questions (QuizID, QuestionText)
1971
  VALUES (%s, %s)
1972
  """, (quiz_id, question['question']))
 
1973
 
1974
  question_id = cursor.lastrowid
1975
 
1976
+ # Prepare batch options for this question
1977
  for i, option in enumerate(question['options']):
1978
+ options_to_insert.append((
1979
+ question_id,
1980
+ option,
1981
+ i == question['correctAnswer']
1982
+ ))
1983
+
1984
+ # Batch insert all options at once
1985
+ if options_to_insert:
1986
+ cursor.executemany("""
1987
+ INSERT INTO Options (QuestionID, OptionText, IsCorrect)
1988
+ VALUES (%s, %s, %s)
1989
+ """, options_to_insert)
1990
 
1991
+ # Commit all database changes at once
1992
+ conn.commit()
1993
+
1994
+ # Return immediately after database operations - handle video upload asynchronously
1995
+ response = {
1996
  "id": lecture_id,
1997
  "title": title,
1998
  "message": "Lecture created successfully"
1999
  }
2000
 
2001
+ # Handle video upload synchronously but efficiently
2002
+ if video:
2003
+ try:
2004
+ # File validation
2005
+ video.file.seek(0, 2) # Seek to end
2006
+ file_size = video.file.tell()
2007
+ video.file.seek(0) # Reset to beginning
2008
+
2009
+ if file_size > 100 * 1024 * 1024: # 100MB in bytes
2010
+ raise HTTPException(status_code=400, detail="Video file size must be less than 100MB")
2011
+
2012
+ # Check file type
2013
+ if not video.content_type.startswith('video/'):
2014
+ raise HTTPException(status_code=400, detail="Invalid file type. Please upload a video file")
2015
+
2016
+ # Read video content into memory for upload
2017
+ video_content = video.file.read()
2018
+ video.file.seek(0) # Reset file position
2019
+
2020
+ # Upload to S3 with the video content
2021
+ media_path = f"videos/cid{course_id}/lid{lecture_id}/vid_lecture.mp4"
2022
+
2023
+ # Create BytesIO object from video content
2024
+ video_stream = BytesIO(video_content)
2025
+
2026
+ # Upload to S3 using the BytesIO stream
2027
+ s3.upload_fileobj(
2028
+ video_stream,
2029
+ "tlhmaterials",
2030
+ media_path,
2031
+ ExtraArgs={
2032
+ 'ContentType': video.content_type,
2033
+ 'ACL': 'public-read',
2034
+ 'ContentDisposition': 'inline'
2035
+ }
2036
+ )
2037
+
2038
+ print(f"Video upload completed for lecture {lecture_id}")
2039
+ response["video_status"] = "uploaded"
2040
+ response["video_url"] = f"https://tlhmaterials.s3-{REGION}.amazonaws.com/{media_path}"
2041
+ response["message"] = "Lecture created successfully with video."
2042
+
2043
+ except Exception as video_error:
2044
+ print(f"Video upload failed: {str(video_error)}")
2045
+ response["warning"] = f"Lecture created but video upload failed: {str(video_error)}"
2046
+
2047
+
2048
+ # Invalidate cache patterns in background (non-blocking)
2049
+ def background_cache_invalidation():
2050
+ try:
2051
+ # Clear course-specific caches
2052
+ invalidate_cache_pattern(f"courses:id:{course_id}:*")
2053
+ invalidate_cache_pattern(f"instructor:courses:*")
2054
+ # Clear lecture cache if it exists
2055
+ invalidate_cache_pattern(f"lectures:id:*")
2056
+ print(f"Cache invalidation completed for course {course_id}")
2057
+ except Exception as cache_error:
2058
+ print(f"Background cache invalidation failed: {str(cache_error)}")
2059
+
2060
+ # Start background cache invalidation
2061
+ cache_thread = threading.Thread(target=background_cache_invalidation)
2062
+ cache_thread.daemon = True
2063
+ cache_thread.start()
2064
+
2065
+ return response
2066
+ cache_thread.daemon = True
2067
+ cache_thread.start()
2068
+
2069
+ return response
2070
+
2071
  except HTTPException:
2072
  raise
2073
  except Exception as e:
 
2085
  raise HTTPException(status_code=500, detail=str(e))
2086
  finally:
2087
  if 'conn' in locals():
2088
+ conn.close()
2089
+
2090
+ # Optimized preview endpoint for CoursePreview.js - combines course + lectures
2091
+ @router.get("/courses/{course_id}/preview")
2092
+ async def get_course_preview_data(course_id: int, request: Request, auth_token: str = Cookie(None)):
2093
+ try:
2094
+ # Authentication
2095
+ if not auth_token:
2096
+ auth_header = request.headers.get('Authorization')
2097
+ if auth_header and auth_header.startswith('Bearer '):
2098
+ auth_token = auth_header.split(' ')[1]
2099
+
2100
+ if not auth_token:
2101
+ raise HTTPException(status_code=401, detail="No authentication token provided")
2102
+
2103
+ try:
2104
+ user_data = decode_token(auth_token)
2105
+ user_id = user_data.get('user_id')
2106
+ except Exception as e:
2107
+ print(f"Token decode error: {str(e)}")
2108
+ raise HTTPException(status_code=401, detail="Invalid authentication token")
2109
+
2110
+ # Cache key for combined preview data
2111
+ cache_key = f"course:preview:{course_id}:user:{user_id}"
2112
+
2113
+ # Fetch all data in one optimized query
2114
+ async def fetch_preview_data_from_db():
2115
+ conn = connect_db()
2116
+ try:
2117
+ with conn.cursor(pymysql.cursors.DictCursor) as cursor:
2118
+ # Get course details with enrollment status
2119
+ course_query = """
2120
+ SELECT
2121
+ c.CourseID as id,
2122
+ c.CourseName as name,
2123
+ c.Descriptions as description,
2124
+ c.EstimatedDuration as duration,
2125
+ c.Skills as skills,
2126
+ c.Difficulty as difficulty,
2127
+ CONCAT(i.InstructorName, ' (', i.AccountName, ')') as instructor,
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 (
2135
+ SELECT CourseID, COUNT(*) as enrolled
2136
+ FROM Enrollments GROUP BY CourseID
2137
+ ) enrollment_stats ON c.CourseID = enrollment_stats.CourseID
2138
+ LEFT JOIN (
2139
+ SELECT CourseID, AVG(Rating) as avg_rating
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
2147
+ ) user_enrollment ON c.CourseID = user_enrollment.CourseID
2148
+ WHERE c.CourseID = %s
2149
+ """
2150
+
2151
+ cursor.execute(course_query, (user_id, course_id))
2152
+ course = cursor.fetchone()
2153
+
2154
+ if not course:
2155
+ raise HTTPException(status_code=404, detail="Course not found")
2156
+
2157
+ # Get lectures for this course
2158
+ lectures_query = """
2159
+ SELECT
2160
+ LectureID as id,
2161
+ Title as title,
2162
+ Description as description
2163
+ FROM Lectures
2164
+ WHERE CourseID = %s
2165
+ ORDER BY LectureID ASC
2166
+ """
2167
+
2168
+ cursor.execute(lectures_query, (course_id,))
2169
+ lectures = cursor.fetchall()
2170
+
2171
+ # Format skills if it's JSON
2172
+ skills = []
2173
+ if course['skills']:
2174
+ try:
2175
+ skills = json.loads(course['skills'])
2176
+ except:
2177
+ skills = []
2178
+
2179
+ # Format the response
2180
+ return {
2181
+ 'course': {
2182
+ 'id': course['id'],
2183
+ 'name': course['name'],
2184
+ 'description': course['description'],
2185
+ 'duration': course['duration'],
2186
+ 'skills': skills,
2187
+ 'difficulty': course['difficulty'],
2188
+ 'instructor': course['instructor'],
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
+ {
2196
+ 'id': lecture['id'],
2197
+ 'title': lecture['title'],
2198
+ 'description': lecture['description']
2199
+ }
2200
+ for lecture in lectures
2201
+ ]
2202
+ }
2203
+ finally:
2204
+ conn.close()
2205
+
2206
+ # Use caching with shorter TTL since it includes user-specific data
2207
+ return await get_cached_data(
2208
+ cache_key,
2209
+ fetch_preview_data_from_db,
2210
+ ttl=900, # 15 minutes for user-specific data
2211
+ use_compression=True
2212
+ )
2213
+
2214
+ except HTTPException:
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))
services/api/db/auth.py CHANGED
@@ -129,9 +129,15 @@ async def login(response: Response, payload: LoginPayload):
129
  raise HTTPException(status_code=400, detail="Invalid role")
130
 
131
  conn = connect_db()
 
132
  try:
133
  with conn.cursor() as cur:
134
- query = f"SELECT Password FROM {table} WHERE AccountName=%s LIMIT 1"
 
 
 
 
 
135
  print(f"Executing query: {query} with username: {payload.username}")
136
 
137
  cur.execute(query, (payload.username,))
@@ -142,7 +148,8 @@ async def login(response: Response, payload: LoginPayload):
142
  raise HTTPException(status_code=401, detail="Incorrect username or password")
143
 
144
  password_valid = check_password(payload.password, row[0])
145
- print(f"Password check result: {password_valid}")
 
146
 
147
  if not password_valid:
148
  print(f"Authentication failed: Invalid password")
@@ -153,9 +160,13 @@ async def login(response: Response, payload: LoginPayload):
153
  finally:
154
  conn.close()
155
 
156
- # User authenticated successfully
157
- user_data = {"username": payload.username, "role": payload.role}
158
- print(f"Authentication successful for: {payload.username}")
 
 
 
 
159
 
160
  token = create_token(user_data)
161
  # Set cookie with less restrictive settings for cross-origin
@@ -172,6 +183,7 @@ async def login(response: Response, payload: LoginPayload):
172
  "message": f"Login successful for {user_data['username']}",
173
  "username": user_data["username"],
174
  "role": user_data["role"],
 
175
  "token": token
176
  }
177
  except Exception as e:
@@ -186,7 +198,11 @@ def logout(response: Response):
186
  @app.get("/whoami")
187
  async def whoami(auth_token: str = Cookie(None)):
188
  payload = decode_token(auth_token)
189
- return {"username": payload["username"], "role": payload["role"]}
 
 
 
 
190
 
191
  @app.get("/protected")
192
  def protected_route(auth_token: str = Cookie(None)):
 
129
  raise HTTPException(status_code=400, detail="Invalid role")
130
 
131
  conn = connect_db()
132
+ user_id = None
133
  try:
134
  with conn.cursor() as cur:
135
+ # Get both password and user ID for token creation
136
+ if payload.role == "Learner":
137
+ query = f"SELECT Password, LearnerID FROM {table} WHERE AccountName=%s LIMIT 1"
138
+ else: # Instructor
139
+ query = f"SELECT Password, InstructorID FROM {table} WHERE AccountName=%s LIMIT 1"
140
+
141
  print(f"Executing query: {query} with username: {payload.username}")
142
 
143
  cur.execute(query, (payload.username,))
 
148
  raise HTTPException(status_code=401, detail="Incorrect username or password")
149
 
150
  password_valid = check_password(payload.password, row[0])
151
+ user_id = row[1] # Get the LearnerID or InstructorID
152
+ print(f"Password check result: {password_valid}, User ID: {user_id}")
153
 
154
  if not password_valid:
155
  print(f"Authentication failed: Invalid password")
 
160
  finally:
161
  conn.close()
162
 
163
+ # User authenticated successfully - include user_id in token
164
+ user_data = {
165
+ "username": payload.username,
166
+ "role": payload.role,
167
+ "user_id": user_id
168
+ }
169
+ print(f"Authentication successful for: {payload.username} (ID: {user_id})")
170
 
171
  token = create_token(user_data)
172
  # Set cookie with less restrictive settings for cross-origin
 
183
  "message": f"Login successful for {user_data['username']}",
184
  "username": user_data["username"],
185
  "role": user_data["role"],
186
+ "user_id": user_data["user_id"],
187
  "token": token
188
  }
189
  except Exception as e:
 
198
  @app.get("/whoami")
199
  async def whoami(auth_token: str = Cookie(None)):
200
  payload = decode_token(auth_token)
201
+ return {
202
+ "username": payload["username"],
203
+ "role": payload["role"],
204
+ "user_id": payload.get("user_id")
205
+ }
206
 
207
  @app.get("/protected")
208
  def protected_route(auth_token: str = Cookie(None)):
services/config/valkey_config.py CHANGED
@@ -20,8 +20,9 @@ def create_valkey_client():
20
  global valkey_client, connection_available
21
 
22
  try:
23
- # First try with SSL if password is provided
24
  if VALKEY_PASSWORD:
 
25
  valkey_client = valkey.Valkey(
26
  host=VALKEY_HOST,
27
  port=VALKEY_PORT,
@@ -33,11 +34,21 @@ def create_valkey_client():
33
  ssl_cert_reqs="required",
34
  cache_ttl=VALKEY_TTL
35
  )
 
 
 
 
 
 
 
 
 
 
36
 
37
- # Test the connection
38
- valkey_client.ping()
39
- connection_available = True
40
- print("✅ Successfully connected to Valkey")
41
 
42
  except Exception as e:
43
  print(f"⚠️ Valkey connection failed: {e}")
 
20
  global valkey_client, connection_available
21
 
22
  try:
23
+ # Try with SSL if password is provided (production)
24
  if VALKEY_PASSWORD:
25
+ print(f"🔐 Attempting SSL connection to Valkey at {VALKEY_HOST}:{VALKEY_PORT}")
26
  valkey_client = valkey.Valkey(
27
  host=VALKEY_HOST,
28
  port=VALKEY_PORT,
 
34
  ssl_cert_reqs="required",
35
  cache_ttl=VALKEY_TTL
36
  )
37
+ else:
38
+ # Try without SSL for local development
39
+ print(f"🔓 Attempting local connection to Valkey at {VALKEY_HOST}:{VALKEY_PORT}")
40
+ valkey_client = valkey.Valkey(
41
+ host=VALKEY_HOST,
42
+ port=VALKEY_PORT,
43
+ db=VALKEY_DB,
44
+ decode_responses=True,
45
+ cache_ttl=VALKEY_TTL
46
+ )
47
 
48
+ # Test the connection for both SSL and non-SSL
49
+ valkey_client.ping()
50
+ connection_available = True
51
+ print("✅ Successfully connected to Valkey")
52
 
53
  except Exception as e:
54
  print(f"⚠️ Valkey connection failed: {e}")
services/utils/api_cache.py CHANGED
@@ -2,6 +2,7 @@
2
  import json
3
  import zlib
4
  import base64
 
5
  import time
6
  from services.config.valkey_config import get_redis_client, is_connection_available
7
 
@@ -43,15 +44,28 @@ async def get_cached_data(key, db_fetch_func, ttl=3600, use_compression=False):
43
  cache_hits += 1
44
  print(f"Cache HIT: {key}")
45
 
46
- # Check if data is compressed (starts with special prefix)
47
- if isinstance(cached_data, bytes) and cached_data.startswith(b'COMPRESSED:'):
48
- # Remove prefix and decompress
49
- compressed_data = base64.b64decode(cached_data[11:])
50
- decompressed_data = zlib.decompress(compressed_data)
51
- return json.loads(decompressed_data.decode('utf-8'))
52
- else:
53
- # Regular non-compressed data
54
- return json.loads(cached_data)
 
 
 
 
 
 
 
 
 
 
 
 
 
55
 
56
  # Increment miss counter
57
  cache_misses += 1
 
2
  import json
3
  import zlib
4
  import base64
5
+ import binascii
6
  import time
7
  from services.config.valkey_config import get_redis_client, is_connection_available
8
 
 
44
  cache_hits += 1
45
  print(f"Cache HIT: {key}")
46
 
47
+ try:
48
+ # Check if data is compressed (starts with special prefix)
49
+ # Handle both string and bytes (due to decode_responses setting)
50
+ if ((isinstance(cached_data, bytes) and cached_data.startswith(b'COMPRESSED:')) or
51
+ (isinstance(cached_data, str) and cached_data.startswith('COMPRESSED:'))):
52
+ # Remove prefix and decompress
53
+ if isinstance(cached_data, str):
54
+ compressed_data = base64.b64decode(cached_data[11:])
55
+ else:
56
+ compressed_data = base64.b64decode(cached_data[11:])
57
+ decompressed_data = zlib.decompress(compressed_data)
58
+ return json.loads(decompressed_data.decode('utf-8'))
59
+ else:
60
+ # Regular non-compressed data
61
+ return json.loads(cached_data)
62
+ except (json.JSONDecodeError, zlib.error, binascii.Error) as e:
63
+ print(f"Cache ERROR for {key}: {e}")
64
+ print("Falling back to database")
65
+ # Delete corrupted cache entry
66
+ redis_client.delete(key)
67
+ cache_misses += 1
68
+ return await db_fetch_func()
69
 
70
  # Increment miss counter
71
  cache_misses += 1