mirror of
https://github.com/rzmk/learnhouse.git
synced 2025-12-19 04:19:25 +00:00
feat: implement multi search on search bar
This commit is contained in:
parent
ed1ae628cb
commit
0167fecbe8
6 changed files with 413 additions and 73 deletions
|
|
@ -2,7 +2,7 @@ import os
|
|||
from fastapi import APIRouter, Depends
|
||||
from src.routers import health
|
||||
from src.routers import usergroups
|
||||
from src.routers import dev, trail, users, auth, orgs, roles
|
||||
from src.routers import dev, trail, users, auth, orgs, roles, search
|
||||
from src.routers.ai import ai
|
||||
from src.routers.courses import chapters, collections, courses, assignments
|
||||
from src.routers.courses.activities import activities, blocks
|
||||
|
|
@ -23,6 +23,7 @@ v1_router.include_router(orgs.router, prefix="/orgs", tags=["orgs"])
|
|||
v1_router.include_router(roles.router, prefix="/roles", tags=["roles"])
|
||||
v1_router.include_router(blocks.router, prefix="/blocks", tags=["blocks"])
|
||||
v1_router.include_router(courses.router, prefix="/courses", tags=["courses"])
|
||||
v1_router.include_router(search.router, prefix="/search", tags=["search"])
|
||||
v1_router.include_router(
|
||||
assignments.router, prefix="/assignments", tags=["assignments"]
|
||||
)
|
||||
|
|
|
|||
31
apps/api/src/routers/search.py
Normal file
31
apps/api/src/routers/search.py
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
from fastapi import APIRouter, Depends, Request
|
||||
from sqlmodel import Session
|
||||
from src.core.events.database import get_db_session
|
||||
from src.db.users import PublicUser
|
||||
from src.security.auth import get_current_user
|
||||
from src.services.search.search import search_across_org, SearchResult
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
@router.get("/org_slug/{org_slug}", response_model=SearchResult)
|
||||
async def api_search_across_org(
|
||||
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),
|
||||
) -> SearchResult:
|
||||
"""
|
||||
Search across courses, collections and users within an organization
|
||||
"""
|
||||
return await search_across_org(
|
||||
request=request,
|
||||
current_user=current_user,
|
||||
org_slug=org_slug,
|
||||
search_query=query,
|
||||
db_session=db_session,
|
||||
page=page,
|
||||
limit=limit
|
||||
)
|
||||
|
|
@ -288,10 +288,22 @@ async def get_courses_orgslug(
|
|||
# Create CourseRead objects with authors
|
||||
course_reads = []
|
||||
for course in courses:
|
||||
course_read = CourseRead(
|
||||
**course.model_dump(),
|
||||
authors=course_authors.get(course.course_uuid, [])
|
||||
)
|
||||
course_read = CourseRead.model_validate({
|
||||
"id": course.id or 0, # Ensure id is never None
|
||||
"org_id": course.org_id,
|
||||
"name": course.name,
|
||||
"description": course.description or "",
|
||||
"about": course.about or "",
|
||||
"learnings": course.learnings or "",
|
||||
"tags": course.tags or "",
|
||||
"thumbnail_image": course.thumbnail_image or "",
|
||||
"public": course.public,
|
||||
"open_to_contributors": course.open_to_contributors,
|
||||
"course_uuid": course.course_uuid,
|
||||
"creation_date": course.creation_date,
|
||||
"update_date": course.update_date,
|
||||
"authors": course_authors.get(course.course_uuid, [])
|
||||
})
|
||||
course_reads.append(course_read)
|
||||
|
||||
return course_reads
|
||||
|
|
@ -380,8 +392,22 @@ async def search_courses(
|
|||
for resource_author, user in author_results
|
||||
]
|
||||
|
||||
course_read = CourseRead.model_validate(course)
|
||||
course_read.authors = authors
|
||||
course_read = CourseRead.model_validate({
|
||||
"id": course.id or 0, # Ensure id is never None
|
||||
"org_id": course.org_id,
|
||||
"name": course.name,
|
||||
"description": course.description or "",
|
||||
"about": course.about or "",
|
||||
"learnings": course.learnings or "",
|
||||
"tags": course.tags or "",
|
||||
"thumbnail_image": course.thumbnail_image or "",
|
||||
"public": course.public,
|
||||
"open_to_contributors": course.open_to_contributors,
|
||||
"course_uuid": course.course_uuid,
|
||||
"creation_date": course.creation_date,
|
||||
"update_date": course.update_date,
|
||||
"authors": authors
|
||||
})
|
||||
course_reads.append(course_read)
|
||||
|
||||
return course_reads
|
||||
|
|
@ -700,22 +726,22 @@ async def get_user_courses(
|
|||
)
|
||||
|
||||
# Create CourseRead object
|
||||
course_read = CourseRead(
|
||||
id=course.id,
|
||||
org_id=course.org_id,
|
||||
name=course.name,
|
||||
description=course.description,
|
||||
about=course.about,
|
||||
learnings=course.learnings,
|
||||
tags=course.tags,
|
||||
thumbnail_image=course.thumbnail_image,
|
||||
public=course.public,
|
||||
open_to_contributors=course.open_to_contributors,
|
||||
course_uuid=course.course_uuid,
|
||||
creation_date=course.creation_date,
|
||||
update_date=course.update_date,
|
||||
authors=authors_with_role,
|
||||
)
|
||||
course_read = CourseRead.model_validate({
|
||||
"id": course.id or 0, # Ensure id is never None
|
||||
"org_id": course.org_id,
|
||||
"name": course.name,
|
||||
"description": course.description or "",
|
||||
"about": course.about or "",
|
||||
"learnings": course.learnings or "",
|
||||
"tags": course.tags or "",
|
||||
"thumbnail_image": course.thumbnail_image or "",
|
||||
"public": course.public,
|
||||
"open_to_contributors": course.open_to_contributors,
|
||||
"course_uuid": course.course_uuid,
|
||||
"creation_date": course.creation_date,
|
||||
"update_date": course.update_date,
|
||||
"authors": authors_with_role
|
||||
})
|
||||
|
||||
result.append(course_read)
|
||||
|
||||
|
|
|
|||
118
apps/api/src/services/search/search.py
Normal file
118
apps/api/src/services/search/search.py
Normal file
|
|
@ -0,0 +1,118 @@
|
|||
from typing import List, TypeVar
|
||||
from fastapi import Request
|
||||
from sqlmodel import Session, select, or_, text, and_
|
||||
from sqlalchemy import true as sa_true
|
||||
from pydantic import BaseModel
|
||||
from src.db.users import PublicUser, AnonymousUser, UserRead, User
|
||||
from src.db.courses.courses import Course, CourseRead
|
||||
from src.db.collections import Collection, CollectionRead
|
||||
from src.db.collections_courses import CollectionCourse
|
||||
from src.db.organizations import Organization
|
||||
from src.services.courses.courses import search_courses
|
||||
|
||||
T = TypeVar('T')
|
||||
|
||||
class SearchResult(BaseModel):
|
||||
courses: List[CourseRead]
|
||||
collections: List[CollectionRead]
|
||||
users: List[UserRead]
|
||||
|
||||
class Config:
|
||||
arbitrary_types_allowed = True
|
||||
|
||||
async def search_across_org(
|
||||
request: Request,
|
||||
current_user: PublicUser | AnonymousUser,
|
||||
org_slug: str,
|
||||
search_query: str,
|
||||
db_session: Session,
|
||||
page: int = 1,
|
||||
limit: int = 10,
|
||||
) -> SearchResult:
|
||||
"""
|
||||
Search across courses, collections and users within an organization
|
||||
"""
|
||||
offset = (page - 1) * limit
|
||||
|
||||
# Get organization
|
||||
org_statement = select(Organization).where(Organization.slug == org_slug)
|
||||
org = db_session.exec(org_statement).first()
|
||||
|
||||
if not org:
|
||||
return SearchResult(courses=[], collections=[], users=[])
|
||||
|
||||
# Search courses using existing search_courses function
|
||||
courses = await search_courses(request, current_user, org_slug, search_query, db_session, page, limit)
|
||||
|
||||
# Search collections
|
||||
collections_query = (
|
||||
select(Collection)
|
||||
.where(Collection.org_id == org.id)
|
||||
.where(
|
||||
or_(
|
||||
text('LOWER("collection".name) LIKE LOWER(:pattern)'),
|
||||
text('LOWER("collection".description) LIKE LOWER(:pattern)')
|
||||
)
|
||||
)
|
||||
.params(pattern=f"%{search_query}%")
|
||||
)
|
||||
|
||||
# Search users
|
||||
users_query = (
|
||||
select(User)
|
||||
.where(
|
||||
or_(
|
||||
text('LOWER("user".username) LIKE LOWER(:pattern) OR ' +
|
||||
'LOWER("user".first_name) LIKE LOWER(:pattern) OR ' +
|
||||
'LOWER("user".last_name) LIKE LOWER(:pattern) OR ' +
|
||||
'LOWER("user".bio) LIKE LOWER(:pattern)')
|
||||
)
|
||||
)
|
||||
.params(pattern=f"%{search_query}%")
|
||||
)
|
||||
|
||||
if isinstance(current_user, AnonymousUser):
|
||||
# For anonymous users, only show public collections
|
||||
collections_query = collections_query.where(Collection.public == sa_true())
|
||||
else:
|
||||
# For authenticated users, show public collections and those in their org
|
||||
collections_query = (
|
||||
collections_query
|
||||
.where(
|
||||
or_(
|
||||
Collection.public == sa_true(),
|
||||
Collection.org_id == org.id
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
# Apply pagination to queries
|
||||
collections = db_session.exec(collections_query.offset(offset).limit(limit)).all()
|
||||
users = db_session.exec(users_query.offset(offset).limit(limit)).all()
|
||||
|
||||
# Convert collections to CollectionRead objects with courses
|
||||
collection_reads = []
|
||||
for collection in collections:
|
||||
# Get courses in collection
|
||||
statement = (
|
||||
select(Course)
|
||||
.select_from(Course)
|
||||
.join(CollectionCourse, and_(
|
||||
CollectionCourse.course_id == Course.id,
|
||||
CollectionCourse.collection_id == collection.id,
|
||||
CollectionCourse.org_id == collection.org_id
|
||||
))
|
||||
.distinct()
|
||||
)
|
||||
collection_courses = list(db_session.exec(statement).all())
|
||||
collection_read = CollectionRead(**collection.model_dump(), courses=collection_courses)
|
||||
collection_reads.append(collection_read)
|
||||
|
||||
# Convert users to UserRead objects
|
||||
user_reads = [UserRead.model_validate(user) for user in users]
|
||||
|
||||
return SearchResult(
|
||||
courses=courses,
|
||||
collections=collection_reads,
|
||||
users=user_reads
|
||||
)
|
||||
Loading…
Add table
Add a link
Reference in a new issue