diff --git a/apps/api/src/db/trail_runs.py b/apps/api/src/db/trail_runs.py new file mode 100644 index 00000000..ce177589 --- /dev/null +++ b/apps/api/src/db/trail_runs.py @@ -0,0 +1,53 @@ +from typing import Optional +from pydantic import BaseModel +from sqlalchemy import JSON, Column +from sqlmodel import Field, SQLModel +from enum import Enum + +from src.db.trail_steps import TrailStep + + +class TrailRunEnum(str, Enum): + RUN_TYPE_COURSE = "RUN_TYPE_COURSE" + + +class StatusEnum(str, Enum): + STATUS_IN_PROGRESS = "STATUS_IN_PROGRESS" + STATUS_COMPLETED = "STATUS_COMPLETED" + STATUS_PAUSED = "STATUS_PAUSED" + STATUS_CANCELLED = "STATUS_CANCELLED" + + +class TrailRun(SQLModel, table=True): + id: Optional[int] = Field(default=None, primary_key=True) + data: dict = Field(default={}, sa_column=Column(JSON)) + status: StatusEnum = StatusEnum.STATUS_IN_PROGRESS + # foreign keys + trail_id: int = Field(default=None, foreign_key="trail.id") + course_id: int = Field(default=None, foreign_key="course.id") + org_id: int = Field(default=None, foreign_key="organization.id") + user_id: int = Field(default=None, foreign_key="user.id") + # timestamps + creation_date: str + update_date: str + + +class TrailRunCreate(TrailRun): + pass + + +# trick because Lists are not supported in SQLModel (runs: list[TrailStep] ) +class TrailRunRead(BaseModel): + id: Optional[int] = Field(default=None, primary_key=True) + data: dict = Field(default={}, sa_column=Column(JSON)) + status: StatusEnum = StatusEnum.STATUS_IN_PROGRESS + # foreign keys + trail_id: int = Field(default=None, foreign_key="trail.id") + course_id: int = Field(default=None, foreign_key="course.id") + org_id: int = Field(default=None, foreign_key="organization.id") + user_id: int = Field(default=None, foreign_key="user.id") + # timestamps + creation_date: str + update_date: str + steps: list[TrailStep] + pass diff --git a/apps/api/src/db/trail_steps.py b/apps/api/src/db/trail_steps.py new file mode 100644 index 00000000..3afba947 --- /dev/null +++ b/apps/api/src/db/trail_steps.py @@ -0,0 +1,32 @@ +from enum import Enum +from typing import Optional +from sqlalchemy import JSON, Column +from sqlmodel import Field, SQLModel + + +class TrailStepTypeEnum(str, Enum): + STEP_TYPE_READABLE_ACTIVITY = "STEP_TYPE_READABLE_ACTIVITY" + STEP_TYPE_ASSIGNMENT_ACTIVITY = "STEP_TYPE_ASSIGNMENT_ACTIVITY" + STEP_TYPE_CUSTOM_ACTIVITY = "STEP_TYPE_CUSTOM_ACTIVITY" + + +class TrailStep(SQLModel, table=True): + id: Optional[int] = Field(default=None, primary_key=True) + complete: bool + teacher_verified: bool + grade: str + data: dict = Field(default={}, sa_column=Column(JSON)) + # foreign keys + trailrun_id: int = Field(default=None, foreign_key="trailrun.id") + trail_id: int = Field(default=None, foreign_key="trail.id") + activity_id: int = Field(default=None, foreign_key="activity.id") + course_id: int = Field(default=None, foreign_key="course.id") + org_id: int = Field(default=None, foreign_key="organization.id") + user_id: int = Field(default=None, foreign_key="user.id") + # timestamps + creation_date: str + update_date: str + + +# note : prepare assignments support +# an assignment object will be linked to a trail step object in the future diff --git a/apps/api/src/db/trails.py b/apps/api/src/db/trails.py new file mode 100644 index 00000000..94c19f31 --- /dev/null +++ b/apps/api/src/db/trails.py @@ -0,0 +1,34 @@ +from typing import Optional +from pydantic import BaseModel +from sqlmodel import Field, SQLModel +from enum import Enum +from src.db.trail_runs import TrailRun, TrailRunRead + +from src.db.trail_steps import TrailStep + + +class TrailBase(SQLModel): + org_id: int = Field(default=None, foreign_key="organization.id") + user_id: int = Field(default=None, foreign_key="user.id") + + +class Trail(TrailBase, table=True): + id: Optional[int] = Field(default=None, primary_key=True) + trail_uuid: str = "" + creation_date: str = "" + update_date: str = "" + + +class TrailCreate(TrailBase): + pass + + +# trick because Lists are not supported in SQLModel (runs: list[TrailRun] ) +class TrailRead(BaseModel): + id: Optional[int] = Field(default=None, primary_key=True) + trail_uuid: str + org_id: int = Field(default=None, foreign_key="organization.id") + user_id: int = Field(default=None, foreign_key="user.id") + creation_date: str + update_date: str + runs: list[TrailRunRead] diff --git a/apps/api/src/routers/trail.py b/apps/api/src/routers/trail.py index f2eb38b5..0cbcaeff 100644 --- a/apps/api/src/routers/trail.py +++ b/apps/api/src/routers/trail.py @@ -1,56 +1,103 @@ from fastapi import APIRouter, Depends, Request +from src.core.events.database import get_db_session +from src.db.trails import TrailCreate, TrailRead from src.security.auth import get_current_user -from src.services.trail.trail import Trail, add_activity_to_trail, add_course_to_trail, create_trail, get_user_trail_with_orgslug, get_user_trail, remove_course_from_trail +from src.services.trail.trail import ( + Trail, + add_activity_to_trail, + add_course_to_trail, + create_user_trail, + get_user_trails, + get_user_trail_with_orgid, + remove_course_from_trail, +) router = APIRouter() @router.post("/start") -async def api_start_trail(request: Request, trail_object: Trail, org_id: str, user=Depends(get_current_user)) -> Trail: +async def api_start_trail( + request: Request, + trail_object: TrailCreate, + user=Depends(get_current_user), + db_session=Depends(get_db_session), +) -> Trail: """ - Start trail + Start trail """ - return await create_trail(request, user, org_id, trail_object) + return await create_user_trail(request, user, trail_object, db_session) -@router.get("/org_id/{org_id}/trail") -async def api_get_trail_by_orgid(request: Request, org_slug: str, user=Depends(get_current_user)): +@router.get("/") +async def api_get_user_trail( + request: Request, + user=Depends(get_current_user), + db_session=Depends(get_db_session), +) -> TrailRead: """ Get a user trails """ - return await get_user_trail(request, user=user, org_slug=org_slug) + return await get_user_trails( + request, user=user, db_session=db_session + ) -@router.get("/org_slug/{org_slug}/trail") -async def api_get_trail_by_orgslug(request: Request, org_slug: str, user=Depends(get_current_user)): +@router.get("/org_slug/{org_id}/trail") +async def api_get_trail_by_org_id( + request: Request, + org_id: int, + user=Depends(get_current_user), + db_session=Depends(get_db_session), +) -> TrailRead: """ Get a user trails using org slug """ - return await get_user_trail_with_orgslug(request, user, org_slug=org_slug) + return await get_user_trail_with_orgid( + request, user, org_id=org_id, db_session=db_session + ) + # Courses in trail -@router.post("/org_slug/{org_slug}/add_course/{course_id}") -async def api_add_course_to_trail(request: Request, course_id: str, org_slug: str, user=Depends(get_current_user)): +@router.post("/add_course/{course_id}") +async def api_add_course_to_trail( + request: Request, + course_id: str, + user=Depends(get_current_user), + db_session=Depends(get_db_session), +) -> TrailRead: """ Add Course to trail """ - return await add_course_to_trail(request, user, org_slug, course_id) + return await add_course_to_trail(request, user, course_id, db_session) -@router.post("/org_slug/{org_slug}/remove_course/{course_id}") -async def api_remove_course_to_trail(request: Request, course_id: str, org_slug: str, user=Depends(get_current_user)): +@router.post("/remove_course/{course_id}") +async def api_remove_course_to_trail( + request: Request, + course_id: str, + user=Depends(get_current_user), + db_session=Depends(get_db_session), +) -> TrailRead: """ Remove Course from trail """ - return await remove_course_from_trail(request, user, org_slug, course_id) + return await remove_course_from_trail(request, user, course_id, db_session) -@router.post("/org_slug/{org_slug}/add_activity/course_id/{course_id}/activity_id/{activity_id}") -async def api_add_activity_to_trail(request: Request, activity_id: str, course_id: str, org_slug: str, user=Depends(get_current_user)): +@router.post("/add_activity/course_id/{course_id}/activity_id/{activity_id}") +async def api_add_activity_to_trail( + request: Request, + activity_id: int, + course_id: int, + user=Depends(get_current_user), + db_session=Depends(get_db_session), +) -> TrailRead: """ Add Course to trail """ - return await add_activity_to_trail(request, user, course_id, org_slug, activity_id) + return await add_activity_to_trail( + request, user, course_id, activity_id, db_session + ) diff --git a/apps/api/src/services/trail/trail.py b/apps/api/src/services/trail/trail.py index 1a9e9d9d..ff7a8f56 100644 --- a/apps/api/src/services/trail/trail.py +++ b/apps/api/src/services/trail/trail.py @@ -1,286 +1,362 @@ from datetime import datetime +import stat from typing import List, Literal, Optional from uuid import uuid4 from fastapi import HTTPException, Request, status from pydantic import BaseModel +from sqlmodel import Session, select +from src.db.courses import Course +from src.db.trail_runs import TrailRun, TrailRunCreate, TrailRunRead +from src.db.trail_steps import TrailStep +from src.db.trails import Trail, TrailCreate, TrailRead +from src.db.users import PublicUser from src.services.orgs.schemas.orgs import PublicOrganization from src.services.courses.chapters import get_coursechapters_meta -from src.services.users.users import PublicUser -#### Classes #################################################### - - -class ActivityData(BaseModel): - activity_id: str - activity_type: str - data: Optional[dict] - - -class TrailCourse(BaseModel): - course_id: str - elements_type: Optional[Literal["course"]] = "course" - status: Optional[Literal["ongoing", "done", "closed"]] = "ongoing" - course_object: dict - masked: Optional[bool] = False - activities_marked_complete: Optional[List[str]] - activities_data: Optional[List[ActivityData]] - progress: Optional[int] - - -class Trail(BaseModel): - status: Optional[Literal["ongoing", "done", "closed"]] = "ongoing" - masked: Optional[bool] = False - courses: Optional[List[TrailCourse]] - - -class TrailInDB(Trail): - trail_id: str - org_id: str - user_id: str - creationDate: str = datetime.now().isoformat() - updateDate: str = datetime.now().isoformat() - - -#### Classes #################################################### - - -async def create_trail( - request: Request, user: PublicUser, org_id: str, trail_object: Trail +async def create_user_trail( + request: Request, + user: PublicUser, + trail_object: TrailCreate, + db_session: Session, ) -> Trail: - trails = request.app.db["trails"] + statement = select(Trail).where(Trail.org_id == trail_object.org_id) + trail = db_session.exec(statement).first() - # get list of courses - if trail_object.courses: - courses = trail_object.courses - # get course ids - course_ids = [course.course_id for course in courses] - - # find if the user has already started the course - existing_trail = await trails.find_one( - {"user_id": user.user_id, "courses.course_id": {"$in": course_ids}} + if trail: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail="Trail already exists", ) - if existing_trail: - # update the status of the element with the matching course_id to "ongoing" - for element in existing_trail["courses"]: - if element["course_id"] in course_ids: - element["status"] = "ongoing" - # update the existing trail in the database - await trails.replace_one( - {"trail_id": existing_trail["trail_id"]}, existing_trail - ) - # create trail id - trail_id = f"trail_{uuid4()}" + trail = Trail.from_orm(trail_object) + + trail.creation_date = str(datetime.now()) + trail.update_date = str(datetime.now()) + trail.org_id = trail_object.org_id + trail.trail_uuid = str(f"trail_{uuid4()}") # create trail - trail = TrailInDB( - **trail_object.dict(), trail_id=trail_id, user_id=user.user_id, org_id=org_id - ) - - await trails.insert_one(trail.dict()) + db_session.add(trail) + db_session.commit() + db_session.refresh(trail) return trail -async def get_user_trail(request: Request, org_slug: str, user: PublicUser) -> Trail: - trails = request.app.db["trails"] - trail = await trails.find_one({"user_id": user.user_id}) +async def get_user_trails( + request: Request, + user: PublicUser, + db_session: Session, +) -> TrailRead: + statement = select(Trail).where(Trail.user_id == user.id) + trail = db_session.exec(statement).first() + if not trail: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail="Trail not found" ) - for element in trail["courses"]: - course_id = element["course_id"] - chapters_meta = await get_coursechapters_meta(request, course_id, user) - activities = chapters_meta["activities"] - num_activities = len(activities) - num_completed_activities = len(element.get("activities_marked_complete", [])) - element["progress"] = ( - round((num_completed_activities / num_activities) * 100, 2) - if num_activities > 0 - else 0 - ) + statement = select(TrailRun).where(TrailRun.trail_id == trail.id) + trail_runs = db_session.exec(statement).all() - return Trail(**trail) + trail_runs = [ + TrailRunRead(**trail_run.__dict__, steps=[]) for trail_run in trail_runs + ] + for trail_run in trail_runs: + statement = select(TrailStep).where(TrailStep.trailrun_id == trail_run.id) + trail_steps = db_session.exec(statement).all() -async def get_user_trail_with_orgslug( - request: Request, user: PublicUser, org_slug: str -) -> Trail: - trails = request.app.db["trails"] - orgs = request.app.db["organizations"] - courses_mongo = request.app.db["courses"] + trail_steps = [TrailStep(**trail_step.__dict__) for trail_step in trail_steps] + trail_run.steps = trail_steps - # get org_id from orgslug - org = await orgs.find_one({"slug": org_slug}) + for trail_step in trail_steps: + statement = select(Course).where(Course.id == trail_step.course_id) + course = db_session.exec(statement).first() + trail_step.data = dict(course=course) - trail = await trails.find_one({"user_id": user.user_id, "org_id": org["org_id"]}) - - if not trail: - return Trail(masked=False, courses=[]) - - course_ids = [course["course_id"] for course in trail["courses"]] - - live_courses = await courses_mongo.find({"course_id": {"$in": course_ids}}).to_list( - length=None + trail_read = TrailRead( + **trail.dict(), + runs=trail_runs, ) - for course in trail["courses"]: - course_id = course["course_id"] + return trail_read - if course_id not in [course["course_id"] for course in live_courses]: - course["masked"] = True - continue - chapters_meta = await get_coursechapters_meta(request, course_id, user) - activities = chapters_meta["activities"] +async def get_user_trail_with_orgid( + request: Request, user: PublicUser, org_id: int, db_session: Session +) -> TrailRead: + statement = select(Trail).where(Trail.org_id == org_id, Trail.user_id == user.id) + trail = db_session.exec(statement).first() - # get course object without _id - course_object = await courses_mongo.find_one( - {"course_id": course_id}, {"_id": 0} + if not trail: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, detail="Trail not found" ) - course["course_object"] = course_object - num_activities = len(activities) + statement = select(TrailRun).where(TrailRun.trail_id == trail.id) + trail_runs = db_session.exec(statement).all() - num_completed_activities = len(course.get("activities_marked_complete", [])) - course["progress"] = ( - round((num_completed_activities / num_activities) * 100, 2) - if num_activities > 0 - else 0 - ) + trail_runs = [ + TrailRunRead(**trail_run.__dict__, steps=[]) for trail_run in trail_runs + ] - return Trail(**trail) + for trail_run in trail_runs: + statement = select(TrailStep).where(TrailStep.trailrun_id == trail_run.id) + trail_steps = db_session.exec(statement).all() + + trail_steps = [TrailStep(**trail_step.__dict__) for trail_step in trail_steps] + trail_run.steps = trail_steps + + for trail_step in trail_steps: + statement = select(Course).where(Course.id == trail_step.course_id) + course = db_session.exec(statement).first() + trail_step.data = dict(course=course) + + trail_read = TrailRead( + **trail.dict(), + runs=trail_runs, + ) + + return trail_read async def add_activity_to_trail( - request: Request, user: PublicUser, course_id: str, org_slug: str, activity_id: str -) -> Trail: - trails = request.app.db["trails"] - orgs = request.app.db["organizations"] - courseid = "course_" + course_id - activityid = "activity_" + activity_id + request: Request, + user: PublicUser, + course_id: int, + activity_id: int, + db_session: Session, +) -> TrailRead: + + # check if run already exists + statement = select(TrailRun).where(TrailRun.course_id == course_id) + trailrun = db_session.exec(statement).first() - # get org_id from orgslug - org = await orgs.find_one({"slug": org_slug}) - org_id = org["org_id"] + if trailrun: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, detail="TrailRun already exists" + ) - # find a trail with the user_id and course_id in the courses array - trail = await trails.find_one( - {"user_id": user.user_id, "courses.course_id": courseid, "org_id": org_id} + statement = select(Course).where(Course.id == course_id) + course = db_session.exec(statement).first() + + if not course: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, detail="Course not found" + ) + + statement = select(Trail).where( + Trail.org_id == course.org_id, Trail.user_id == user.id ) - - if user.user_id == "anonymous": - raise HTTPException( - status_code=status.HTTP_401_UNAUTHORIZED, - detail="Anonymous users cannot add activity to trail", - ) - - if not trail: - return Trail(masked=False, courses=[]) - - # if a trail has course_id in the courses array, then add the activity_id to the activities_marked_complete array - for element in trail["courses"]: - if element["course_id"] == courseid: - if "activities_marked_complete" in element: - # check if activity_id is already in the array - if activityid not in element["activities_marked_complete"]: - element["activities_marked_complete"].append(activityid) - else: - raise HTTPException( - status_code=status.HTTP_400_BAD_REQUEST, - detail="Activity already marked complete", - ) - else: - element["activities_marked_complete"] = [activity_id] - - # modify trail object - await trails.replace_one({"trail_id": trail["trail_id"]}, trail) - - return Trail(**trail) - - -async def add_course_to_trail( - request: Request, user: PublicUser, orgslug: str, course_id: str -) -> Trail: - trails = request.app.db["trails"] - orgs = request.app.db["organizations"] - - if user.user_id == "anonymous": - raise HTTPException( - status_code=status.HTTP_401_UNAUTHORIZED, - detail="Anonymous users cannot add activity to trail", - ) - - org = await orgs.find_one({"slug": orgslug}) - - org = PublicOrganization(**org) - - trail = await trails.find_one({"user_id": user.user_id, "org_id": org["org_id"]}) - - if not trail: - trail_to_insert = TrailInDB( - trail_id=f"trail_{uuid4()}", - user_id=user.user_id, - org_id=org["org_id"], - courses=[], - ) - trail_to_insert = await trails.insert_one(trail_to_insert.dict()) - - trail = await trails.find_one({"_id": trail_to_insert.inserted_id}) - - # check if course is already present in the trail - for element in trail["courses"]: - if element["course_id"] == course_id: - raise HTTPException( - status_code=status.HTTP_400_BAD_REQUEST, - detail="Course already present in the trail", - ) - - updated_trail = TrailCourse( - course_id=course_id, - activities_data=[], - activities_marked_complete=[], - progress=0, - course_object={}, - status="ongoing", - masked=False, - ) - trail["courses"].append(updated_trail.dict()) - await trails.replace_one({"trail_id": trail["trail_id"]}, trail) - return Trail(**trail) - - -async def remove_course_from_trail( - request: Request, user: PublicUser, orgslug: str, course_id: str -) -> Trail: - trails = request.app.db["trails"] - orgs = request.app.db["organizations"] - - if user.user_id == "anonymous": - raise HTTPException( - status_code=status.HTTP_401_UNAUTHORIZED, - detail="Anonymous users cannot add activity to trail", - ) - - org = await orgs.find_one({"slug": orgslug}) - - org = PublicOrganization(**org) - trail = await trails.find_one({"user_id": user.user_id, "org_id": org["org_id"]}) + trail = db_session.exec(statement).first() if not trail: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail="Trail not found" ) - # check if course is already present in the trail + statement = select(TrailRun).where( + TrailRun.trail_id == trail.id, TrailRun.course_id == course.id + ) + trailrun = db_session.exec(statement).first() - for element in trail["courses"]: - if element["course_id"] == course_id: - trail["courses"].remove(element) - break + if not trailrun: + trailrun = TrailRun( + trail_id=trail.id is not None, + course_id=course.id is not None, + org_id=course.org_id, + user_id=user.id, + creation_date=str(datetime.now()), + update_date=str(datetime.now()), + ) + db_session.add(trailrun) + db_session.commit() + db_session.refresh(trailrun) - await trails.replace_one({"trail_id": trail["trail_id"]}, trail) - return Trail(**trail) + statement = select(TrailStep).where( + TrailStep.trailrun_id == trailrun.id, TrailStep.activity_id == activity_id + ) + trailstep = db_session.exec(statement).first() + + if not trailstep: + trailstep = TrailStep( + trailrun_id=trailrun.id is not None, + activity_id=activity_id, + course_id=course.id is not None, + org_id=course.org_id, + complete=False, + teacher_verified=False, + grade="", + user_id=user.id, + creation_date=str(datetime.now()), + update_date=str(datetime.now()), + ) + db_session.add(trailstep) + db_session.commit() + db_session.refresh(trailstep) + + statement = select(TrailRun).where(TrailRun.trail_id == trail.id) + trail_runs = db_session.exec(statement).all() + + trail_runs = [ + TrailRunRead(**trail_run.__dict__, steps=[]) for trail_run in trail_runs + ] + + for trail_run in trail_runs: + statement = select(TrailStep).where(TrailStep.trailrun_id == trail_run.id) + trail_steps = db_session.exec(statement).all() + + trail_steps = [TrailStep(**trail_step.__dict__) for trail_step in trail_steps] + trail_run.steps = trail_steps + + for trail_step in trail_steps: + statement = select(Course).where(Course.id == trail_step.course_id) + course = db_session.exec(statement).first() + trail_step.data = dict(course=course) + + trail_read = TrailRead( + **trail.dict(), + runs=trail_runs, + ) + + return trail_read + + +async def add_course_to_trail( + request: Request, + user: PublicUser, + course_id: str, + db_session: Session, +) -> TrailRead: + + # check if run already exists + statement = select(TrailRun).where(TrailRun.course_id == course_id) + trailrun = db_session.exec(statement).first() + + if trailrun: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, detail="TrailRun already exists" + ) + + statement = select(Course).where(Course.id == course_id) + course = db_session.exec(statement).first() + + if not course: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, detail="Course not found" + ) + + statement = select(Trail).where( + Trail.org_id == course.org_id, Trail.user_id == user.id + ) + trail = db_session.exec(statement).first() + + if not trail: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, detail="Trail not found" + ) + + statement = select(TrailRun).where( + TrailRun.trail_id == trail.id, TrailRun.course_id == course.id + ) + trail_run = db_session.exec(statement).first() + + if not trail_run: + trail_run = TrailRun( + trail_id=trail.id is not None, + course_id=course.id is not None, + org_id=course.org_id, + user_id=user.id, + creation_date=str(datetime.now()), + update_date=str(datetime.now()), + ) + db_session.add(trail_run) + db_session.commit() + db_session.refresh(trail_run) + + statement = select(TrailRun).where(TrailRun.trail_id == trail.id) + trail_runs = db_session.exec(statement).all() + + trail_runs = [ + TrailRunRead(**trail_run.__dict__, steps=[]) for trail_run in trail_runs + ] + + for trail_run in trail_runs: + statement = select(TrailStep).where(TrailStep.trailrun_id == trail_run.id) + trail_steps = db_session.exec(statement).all() + + trail_steps = [TrailStep(**trail_step.__dict__) for trail_step in trail_steps] + trail_run.steps = trail_steps + + for trail_step in trail_steps: + statement = select(Course).where(Course.id == trail_step.course_id) + course = db_session.exec(statement).first() + trail_step.data = dict(course=course) + + trail_read = TrailRead( + **trail.dict(), + runs=trail_runs, + ) + + return trail_read + + +async def remove_course_from_trail( + request: Request, + user: PublicUser, + course_id: str, + db_session: Session, +) -> TrailRead: + statement = select(Course).where(Course.id == course_id) + course = db_session.exec(statement).first() + + if not course: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, detail="Course not found" + ) + + statement = select(Trail).where( + Trail.org_id == course.org_id, Trail.user_id == user.id + ) + trail = db_session.exec(statement).first() + + if not trail: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, detail="Trail not found" + ) + + statement = select(TrailRun).where( + TrailRun.trail_id == trail.id, TrailRun.course_id == course.id + ) + trail_run = db_session.exec(statement).first() + + if trail_run: + db_session.delete(trail_run) + db_session.commit() + + statement = select(TrailRun).where(TrailRun.trail_id == trail.id) + trail_runs = db_session.exec(statement).all() + + trail_runs = [ + TrailRunRead(**trail_run.__dict__, steps=[]) for trail_run in trail_runs + ] + for trail_run in trail_runs: + statement = select(TrailStep).where(TrailStep.trailrun_id == trail_run.id) + trail_steps = db_session.exec(statement).all() + + trail_steps = [TrailStep(**trail_step.__dict__) for trail_step in trail_steps] + trail_run.steps = trail_steps + + for trail_step in trail_steps: + statement = select(Course).where(Course.id == trail_step.course_id) + course = db_session.exec(statement).first() + trail_step.data = dict(course=course) + + trail_read = TrailRead( + **trail.dict(), + runs=trail_runs, + ) + + return trail_read