From d8f77aec4c26ae94e3b823f6ac71d3e469c83536 Mon Sep 17 00:00:00 2001 From: swve Date: Wed, 18 Dec 2024 16:53:31 +0100 Subject: [PATCH] feat: add explore API endpoints for organizations and courses - Implemented new API routes for exploring organizations and their courses. - Added endpoints for retrieving organizations, searching organizations, and fetching courses associated with a specific organization. - Introduced functionality to get details of a specific course for exploration. --- apps/api/src/routers/ee/cloud_internal.py | 45 ++++++ apps/api/src/services/explore/explore.py | 165 ++++++++++++++++++++++ 2 files changed, 210 insertions(+) create mode 100644 apps/api/src/services/explore/explore.py diff --git a/apps/api/src/routers/ee/cloud_internal.py b/apps/api/src/routers/ee/cloud_internal.py index b4224ddf..8134fd31 100644 --- a/apps/api/src/routers/ee/cloud_internal.py +++ b/apps/api/src/routers/ee/cloud_internal.py @@ -1,8 +1,10 @@ import os +from typing import Optional from fastapi import APIRouter, Depends, HTTPException, Request from sqlmodel import Session from src.core.events.database import get_db_session from src.db.organization_config import OrganizationConfigBase +from src.services.explore.explore import get_course_for_explore, get_courses_for_an_org_explore, get_org_for_explore, get_orgs_for_explore, search_orgs_for_explore from src.services.orgs.orgs import update_org_with_config_no_auth router = APIRouter() @@ -14,6 +16,49 @@ def check_internal_cloud_key(request: Request): ): raise HTTPException(status_code=403, detail="Unauthorized") +@router.get("/explore/orgs") +async def api_get_orgs_for_explore( + request: Request, + page: int = 1, + limit: int = 10, + label: str = "", + salt: str = "", + db_session: Session = Depends(get_db_session), +): + return await get_orgs_for_explore(request, db_session, page, limit, label, salt) + +@router.get("/explore/orgs/search") +async def api_search_orgs_for_explore( + request: Request, + search_query: str, + label: Optional[str] = None, + db_session: Session = Depends(get_db_session), +): + return await search_orgs_for_explore(request, db_session, search_query, label) + +@router.get("/explore/orgs/{org_uuid}/courses") +async def api_get_courses_for_explore( + request: Request, + org_uuid: str, + db_session: Session = Depends(get_db_session), +): + return await get_courses_for_an_org_explore(request, db_session, org_uuid) + +@router.get("/explore/courses/{course_id}") +async def api_get_course_for_explore( + request: Request, + course_id: str, + db_session: Session = Depends(get_db_session), +): + return await get_course_for_explore(request, course_id, db_session) + +@router.get("/explore/orgs/{org_slug}") +async def api_get_org_for_explore( + request: Request, + org_slug: str, + db_session: Session = Depends(get_db_session), +): + return await get_org_for_explore(request, org_slug, db_session) @router.put("/update_org_config") async def update_org_Config( diff --git a/apps/api/src/services/explore/explore.py b/apps/api/src/services/explore/explore.py new file mode 100644 index 00000000..9cc866e5 --- /dev/null +++ b/apps/api/src/services/explore/explore.py @@ -0,0 +1,165 @@ +from typing import Optional +from fastapi import HTTPException, Request +from sqlmodel import Session, select +from sqlalchemy import text + +from src.db.courses.courses import Course, CourseRead +from src.db.organizations import Organization, OrganizationRead + + +def _get_sort_expression(salt: str): + """Helper function to create consistent sort expression""" + if not salt: + return Organization.name + + # Create a deterministic ordering using md5(salt + id) + return text( + f"md5('{salt}' || id)" + ) + +async def get_orgs_for_explore( + request: Request, + db_session: Session, + page: int = 1, + limit: int = 10, + label: str = "", + salt: str = "", +) -> list[OrganizationRead]: + + statement = ( + select(Organization) + .where( + Organization.explore == True, + ) + ) + + # Add label filter if provided + if label: + statement = statement.where(Organization.label == label) #type: ignore + + # Add deterministic ordering based on salt + statement = statement.order_by(_get_sort_expression(salt)) + + # Add pagination + statement = ( + statement + .offset((page - 1) * limit) + .limit(limit) + ) + + result = db_session.exec(statement) + orgs = result.all() + + return [OrganizationRead.model_validate(org) for org in orgs] + + + +async def get_courses_for_an_org_explore( + request: Request, + db_session: Session, + org_uuid: str, +) -> list[CourseRead]: + statement = select(Organization).where(Organization.org_uuid == org_uuid) + result = db_session.exec(statement) + org = result.first() + + if not org: + raise HTTPException( + status_code=404, + detail="Organization not found", + ) + + statement = select(Course).where(Course.org_id == org.id, Course.public == True) + result = db_session.exec(statement) + courses = result.all() + + courses_list = [] + + for course in courses: + courses_list.append(course) + + return courses_list + +async def get_course_for_explore( + request: Request, + course_id: str, + db_session: Session, +) -> CourseRead: + statement = select(Course).where(Course.id == course_id) + result = db_session.exec(statement) + + course = result.first() + + if not course: + raise HTTPException( + status_code=404, + detail="Course not found", + ) + + return CourseRead.model_validate(course) + +async def search_orgs_for_explore( + request: Request, + db_session: Session, + search_query: str, + label: Optional[str] = None, + page: int = 1, + limit: int = 10, + salt: str = "", +) -> list[OrganizationRead]: + # Create a combined search vector + search_terms = search_query.split() + search_conditions = [] + + for term in search_terms: + term_pattern = f"%{term}%" + search_conditions.append( + (Organization.name.ilike(term_pattern)) | #type: ignore + (Organization.about.ilike(term_pattern)) | #type: ignore + (Organization.description.ilike(term_pattern)) | #type: ignore + (Organization.label.ilike(term_pattern)) #type: ignore + ) + + statement = ( + select(Organization) + .where(Organization.explore == True) + ) + + if label and label != "all": + statement = statement.where(Organization.label == label) #type: ignore + + if search_conditions: + statement = statement.where(*search_conditions) + + # Add deterministic ordering based on salt + statement = statement.order_by(_get_sort_expression(salt)) + + # Add pagination + statement = ( + statement + .offset((page - 1) * limit) + .limit(limit) + ) + + result = db_session.exec(statement) + orgs = result.all() + + return [OrganizationRead.model_validate(org) for org in orgs] + +async def get_org_for_explore( + request: Request, + org_slug: str, + db_session: Session, + ) -> OrganizationRead: + statement = select(Organization).where(Organization.slug == org_slug) + result = db_session.exec(statement) + org = result.first() + + if not org: + raise HTTPException( + status_code=404, + detail="Organization not found", + ) + + return OrganizationRead.model_validate(org) + \ No newline at end of file