From 01132c374556724e4b5991f060ead366a4b122fd Mon Sep 17 00:00:00 2001 From: swve Date: Tue, 18 Feb 2025 15:54:48 +0100 Subject: [PATCH] feat: add course search functionality with advanced filtering backend --- apps/api/src/routers/courses/courses.py | 19 ++++++ apps/api/src/services/courses/courses.py | 76 +++++++++++++++++++++++- 2 files changed, 94 insertions(+), 1 deletion(-) diff --git a/apps/api/src/routers/courses/courses.py b/apps/api/src/routers/courses/courses.py index 2afd99e4..700ae7f6 100644 --- a/apps/api/src/routers/courses/courses.py +++ b/apps/api/src/routers/courses/courses.py @@ -24,6 +24,7 @@ from src.services.courses.courses import ( update_course, delete_course, update_course_thumbnail, + search_courses, ) from src.services.courses.updates import ( create_update, @@ -146,6 +147,24 @@ async def api_get_course_by_orgslug( ) +@router.get("/org_slug/{org_slug}/search") +async def api_search_courses( + request: Request, + org_slug: str, + query: str, + page: int = 1, + limit: int = 10, + db_session: Session = Depends(get_db_session), + current_user: PublicUser = Depends(get_current_user), +) -> List[CourseRead]: + """ + Search courses by title and description + """ + return await search_courses( + request, current_user, org_slug, query, db_session, page, limit + ) + + @router.put("/{course_uuid}") async def api_update_course( request: Request, diff --git a/apps/api/src/services/courses/courses.py b/apps/api/src/services/courses/courses.py index 2233d9c9..42b9b49d 100644 --- a/apps/api/src/services/courses/courses.py +++ b/apps/api/src/services/courses/courses.py @@ -1,6 +1,6 @@ from typing import Literal, List from uuid import uuid4 -from sqlmodel import Session, select, or_, and_ +from sqlmodel import Session, select, or_, and_, text from src.db.usergroup_resources import UserGroupResource from src.db.usergroup_user import UserGroupUser from src.db.organizations import Organization @@ -214,6 +214,80 @@ async def get_courses_orgslug( return course_reads +async def search_courses( + request: Request, + current_user: PublicUser | AnonymousUser, + org_slug: str, + search_query: str, + db_session: Session, + page: int = 1, + limit: int = 10, +) -> List[CourseRead]: + offset = (page - 1) * limit + + # Base query + query = ( + select(Course) + .join(Organization) + .where(Organization.slug == org_slug) + .where( + or_( + text(f"LOWER(course.name) LIKE LOWER('%{search_query}%')"), + text(f"LOWER(course.description) LIKE LOWER('%{search_query}%')"), + text(f"LOWER(course.about) LIKE LOWER('%{search_query}%')"), + text(f"LOWER(course.learnings) LIKE LOWER('%{search_query}%')"), + text(f"LOWER(course.tags) LIKE LOWER('%{search_query}%')") + ) + ) + ) + + if isinstance(current_user, AnonymousUser): + # For anonymous users, only show public courses + query = query.where(Course.public == True) + else: + # For authenticated users, show: + # 1. Public courses + # 2. Courses not in any UserGroup + # 3. Courses in UserGroups where the user is a member + # 4. Courses where the user is a resource author + query = ( + query + .outerjoin(UserGroupResource, UserGroupResource.resource_uuid == Course.course_uuid) # type: ignore + .outerjoin(UserGroupUser, and_( + UserGroupUser.usergroup_id == UserGroupResource.usergroup_id, + UserGroupUser.user_id == current_user.id + )) + .outerjoin(ResourceAuthor, ResourceAuthor.resource_uuid == Course.course_uuid) # type: ignore + .where(or_( + Course.public == True, + UserGroupResource.resource_uuid == None, # Courses not in any UserGroup # noqa: E711 + UserGroupUser.user_id == current_user.id, # Courses in UserGroups where user is a member + ResourceAuthor.user_id == current_user.id # Courses where user is a resource author + )) + ) + + # Apply pagination + query = query.offset(offset).limit(limit).distinct() + + courses = db_session.exec(query).all() + + # Fetch authors for each course + course_reads = [] + for course in courses: + authors_query = ( + select(User) + .join(ResourceAuthor, ResourceAuthor.user_id == User.id) # type: ignore + .where(ResourceAuthor.resource_uuid == course.course_uuid) + ) + authors = db_session.exec(authors_query).all() + + course_read = CourseRead.model_validate(course) + course_read.authors = [UserRead.model_validate(author) for author in authors] + course_reads.append(course_read) + + return course_reads + + async def create_course( request: Request, org_id: int,