feat: init trails

This commit is contained in:
swve 2023-11-18 12:22:00 +01:00
parent b04fd64c92
commit eca819b896
5 changed files with 495 additions and 253 deletions

View file

@ -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

View file

@ -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

34
apps/api/src/db/trails.py Normal file
View file

@ -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]

View file

@ -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
"""
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
)

View file

@ -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 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
if trail:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Trail already exists",
)
# 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()
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(**trail)
return trail_read
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"]
# get org_id from orgslug
org = await orgs.find_one({"slug": org_slug})
trail = await trails.find_one({"user_id": user.user_id, "org_id": org["org_id"]})
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()
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
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND, detail="Trail not found"
)
for course in trail["courses"]:
course_id = course["course_id"]
statement = select(TrailRun).where(TrailRun.trail_id == trail.id)
trail_runs = db_session.exec(statement).all()
if course_id not in [course["course_id"] for course in live_courses]:
course["masked"] = True
continue
trail_runs = [
TrailRunRead(**trail_run.__dict__, steps=[]) for trail_run in trail_runs
]
chapters_meta = await get_coursechapters_meta(request, course_id, user)
activities = chapters_meta["activities"]
for trail_run in trail_runs:
statement = select(TrailStep).where(TrailStep.trailrun_id == trail_run.id)
trail_steps = db_session.exec(statement).all()
# get course object without _id
course_object = await courses_mongo.find_one(
{"course_id": course_id}, {"_id": 0}
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,
)
course["course_object"] = course_object
num_activities = len(activities)
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
)
return Trail(**trail)
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:
# get org_id from orgslug
org = await orgs.find_one({"slug": org_slug})
org_id = org["org_id"]
# check if run already exists
statement = select(TrailRun).where(TrailRun.course_id == course_id)
trailrun = db_session.exec(statement).first()
# 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}
)
if user.user_id == "anonymous":
if trailrun:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Anonymous users cannot add activity to trail",
status_code=status.HTTP_400_BAD_REQUEST, detail="TrailRun already exists"
)
if not trail:
return Trail(masked=False, courses=[])
statement = select(Course).where(Course.id == course_id)
course = db_session.exec(statement).first()
# 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:
if not course:
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",
status_code=status.HTTP_404_NOT_FOUND, detail="Course not found"
)
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=[],
statement = select(Trail).where(
Trail.org_id == course.org_id, Trail.user_id == user.id
)
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