mirror of
https://github.com/rzmk/learnhouse.git
synced 2025-12-18 20:09:25 +00:00
feat: backend endpoints
This commit is contained in:
parent
86f7a80eb7
commit
b0d492a116
6 changed files with 566 additions and 3 deletions
65
apps/api/src/db/courses/certifications.py
Normal file
65
apps/api/src/db/courses/certifications.py
Normal file
|
|
@ -0,0 +1,65 @@
|
|||
from typing import Optional
|
||||
from sqlalchemy import JSON, Column, ForeignKey
|
||||
from sqlmodel import Field, SQLModel
|
||||
from enum import Enum
|
||||
from datetime import datetime
|
||||
|
||||
class CertificationBase(SQLModel):
|
||||
course_id: int = Field(sa_column= Column("course_id", ForeignKey("course.id", ondelete="CASCADE")))
|
||||
config: dict = Field(default={}, sa_column= Column("config", JSON))
|
||||
|
||||
class Certifications(CertificationBase, table=True):
|
||||
id: Optional[int] = Field(default=None, primary_key=True)
|
||||
certification_uuid: str = Field(unique=True)
|
||||
course_id: int = Field(sa_column= Column("course_id", ForeignKey("course.id", ondelete="CASCADE")))
|
||||
config: dict = Field(default={}, sa_column= Column("config", JSON))
|
||||
creation_date: str = ""
|
||||
update_date: str = ""
|
||||
|
||||
class CertificationCreate(SQLModel):
|
||||
course_id: int
|
||||
config: dict = Field(default={})
|
||||
|
||||
class CertificationUpdate(SQLModel):
|
||||
config: Optional[dict] = None
|
||||
|
||||
class CertificationRead(SQLModel):
|
||||
id: int
|
||||
certification_uuid: str
|
||||
course_id: int
|
||||
config: dict
|
||||
creation_date: str
|
||||
update_date: str
|
||||
|
||||
|
||||
class CertificateUserBase(SQLModel):
|
||||
user_id: int = Field(sa_column= Column("user_id", ForeignKey("user.id", ondelete="CASCADE")))
|
||||
certification_id: int = Field(sa_column= Column("certification_id", ForeignKey("certifications.id", ondelete="CASCADE")))
|
||||
user_certification_uuid: str
|
||||
|
||||
class CertificateUser(CertificateUserBase, table=True):
|
||||
id: Optional[int] = Field(default=None, primary_key=True)
|
||||
user_id: int = Field(sa_column= Column("user_id", ForeignKey("user.id", ondelete="CASCADE")))
|
||||
certification_id: int = Field(sa_column= Column("certification_id", ForeignKey("certifications.id", ondelete="CASCADE")))
|
||||
user_certification_uuid: str
|
||||
created_at: str = ""
|
||||
updated_at: str = ""
|
||||
|
||||
class CertificateUserCreate(SQLModel):
|
||||
user_id: int
|
||||
certification_id: int
|
||||
user_certification_uuid: str
|
||||
|
||||
class CertificateUserRead(SQLModel):
|
||||
id: int
|
||||
user_id: int
|
||||
certification_id: int
|
||||
user_certification_uuid: str
|
||||
created_at: str
|
||||
updated_at: str
|
||||
|
||||
class CertificateUserUpdate(SQLModel):
|
||||
user_id: Optional[int] = None
|
||||
certification_id: Optional[int] = None
|
||||
user_certification_uuid: Optional[str] = None
|
||||
|
||||
|
|
@ -4,7 +4,7 @@ from src.routers import health
|
|||
from src.routers import usergroups
|
||||
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 import chapters, collections, courses, assignments, certifications
|
||||
from src.routers.courses.activities import activities, blocks
|
||||
from src.routers.ee import cloud_internal, payments
|
||||
from src.routers.install import install
|
||||
|
|
@ -33,6 +33,9 @@ v1_router.include_router(activities.router, prefix="/activities", tags=["activit
|
|||
v1_router.include_router(
|
||||
collections.router, prefix="/collections", tags=["collections"]
|
||||
)
|
||||
v1_router.include_router(
|
||||
certifications.router, prefix="/certifications", tags=["certifications"]
|
||||
)
|
||||
v1_router.include_router(trail.router, prefix="/trail", tags=["trail"])
|
||||
v1_router.include_router(ai.router, prefix="/ai", tags=["ai"])
|
||||
v1_router.include_router(payments.router, prefix="/payments", tags=["payments"])
|
||||
|
|
|
|||
96
apps/api/src/routers/courses/certifications.py
Normal file
96
apps/api/src/routers/courses/certifications.py
Normal file
|
|
@ -0,0 +1,96 @@
|
|||
from typing import List
|
||||
from fastapi import APIRouter, Depends, Request
|
||||
from sqlmodel import Session
|
||||
from src.core.events.database import get_db_session
|
||||
from src.db.courses.certifications import (
|
||||
CertificationCreate,
|
||||
CertificationRead,
|
||||
CertificationUpdate,
|
||||
)
|
||||
from src.db.users import PublicUser
|
||||
from src.security.auth import get_current_user
|
||||
from src.services.courses.certifications import (
|
||||
create_certification,
|
||||
get_certification,
|
||||
get_certifications_by_course,
|
||||
update_certification,
|
||||
delete_certification,
|
||||
)
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.post("/")
|
||||
async def api_create_certification(
|
||||
request: Request,
|
||||
certification_object: CertificationCreate,
|
||||
current_user: PublicUser = Depends(get_current_user),
|
||||
db_session: Session = Depends(get_db_session),
|
||||
) -> CertificationRead:
|
||||
"""
|
||||
Create new certification for a course
|
||||
"""
|
||||
return await create_certification(
|
||||
request, certification_object, current_user, db_session
|
||||
)
|
||||
|
||||
|
||||
@router.get("/{certification_uuid}")
|
||||
async def api_get_certification(
|
||||
request: Request,
|
||||
certification_uuid: str,
|
||||
current_user: PublicUser = Depends(get_current_user),
|
||||
db_session: Session = Depends(get_db_session),
|
||||
) -> CertificationRead:
|
||||
"""
|
||||
Get single certification by certification_id
|
||||
"""
|
||||
return await get_certification(
|
||||
request, certification_uuid, current_user, db_session
|
||||
)
|
||||
|
||||
|
||||
@router.get("/course/{course_uuid}")
|
||||
async def api_get_certifications_by_course(
|
||||
request: Request,
|
||||
course_uuid: str,
|
||||
current_user: PublicUser = Depends(get_current_user),
|
||||
db_session: Session = Depends(get_db_session),
|
||||
) -> List[CertificationRead]:
|
||||
"""
|
||||
Get all certifications for a specific course
|
||||
"""
|
||||
return await get_certifications_by_course(
|
||||
request, course_uuid, current_user, db_session
|
||||
)
|
||||
|
||||
|
||||
@router.put("/{certification_uuid}")
|
||||
async def api_update_certification(
|
||||
request: Request,
|
||||
certification_uuid: str,
|
||||
certification_object: CertificationUpdate,
|
||||
current_user: PublicUser = Depends(get_current_user),
|
||||
db_session: Session = Depends(get_db_session),
|
||||
) -> CertificationRead:
|
||||
"""
|
||||
Update certification by certification_id
|
||||
"""
|
||||
return await update_certification(
|
||||
request, certification_uuid, certification_object, current_user, db_session
|
||||
)
|
||||
|
||||
|
||||
@router.delete("/{certification_uuid}")
|
||||
async def api_delete_certification(
|
||||
request: Request,
|
||||
certification_uuid: str,
|
||||
current_user: PublicUser = Depends(get_current_user),
|
||||
db_session: Session = Depends(get_db_session),
|
||||
):
|
||||
"""
|
||||
Delete certification by certification_id
|
||||
"""
|
||||
return await delete_certification(
|
||||
request, certification_uuid, current_user, db_session
|
||||
)
|
||||
|
|
@ -44,6 +44,7 @@ from src.services.courses.activities.uploads.tasks_ref_files import (
|
|||
upload_reference_file,
|
||||
)
|
||||
from src.services.trail.trail import check_trail_presence
|
||||
from src.services.courses.certifications import check_course_completion_and_create_certificate
|
||||
|
||||
## > Assignments CRUD
|
||||
|
||||
|
|
@ -1237,6 +1238,12 @@ async def create_assignment_submission(
|
|||
db_session.commit()
|
||||
db_session.refresh(trailstep)
|
||||
|
||||
# Check if all activities in the course are completed and create certificate if so
|
||||
if course and course.id and user and user.id:
|
||||
await check_course_completion_and_create_certificate(
|
||||
request, user.id, course.id, db_session
|
||||
)
|
||||
|
||||
# return assignment user submission read
|
||||
return AssignmentUserSubmissionRead.model_validate(assignment_user_submission)
|
||||
|
||||
|
|
@ -1658,6 +1665,12 @@ async def mark_activity_as_done_for_user(
|
|||
db_session.commit()
|
||||
db_session.refresh(trailstep)
|
||||
|
||||
# Check if all activities in the course are completed and create certificate if so
|
||||
if course and course.id:
|
||||
await check_course_completion_and_create_certificate(
|
||||
request, int(user_id), course.id, db_session
|
||||
)
|
||||
|
||||
# return OK
|
||||
return {"message": "Activity marked as done for user"}
|
||||
|
||||
|
|
|
|||
379
apps/api/src/services/courses/certifications.py
Normal file
379
apps/api/src/services/courses/certifications.py
Normal file
|
|
@ -0,0 +1,379 @@
|
|||
from typing import List, Literal
|
||||
from uuid import uuid4
|
||||
from datetime import datetime
|
||||
from sqlmodel import Session, select
|
||||
from fastapi import HTTPException, Request
|
||||
from src.db.courses.certifications import (
|
||||
Certifications,
|
||||
CertificationCreate,
|
||||
CertificationRead,
|
||||
CertificationUpdate,
|
||||
CertificateUser,
|
||||
CertificateUserCreate,
|
||||
CertificateUserRead,
|
||||
)
|
||||
from src.db.courses.courses import Course
|
||||
from src.db.courses.chapter_activities import ChapterActivity
|
||||
from src.db.trail_steps import TrailStep
|
||||
from src.db.users import PublicUser, AnonymousUser
|
||||
from src.security.rbac.rbac import (
|
||||
authorization_verify_based_on_roles_and_authorship,
|
||||
authorization_verify_if_element_is_public,
|
||||
authorization_verify_if_user_is_anon,
|
||||
)
|
||||
|
||||
|
||||
####################################################
|
||||
# CRUD
|
||||
####################################################
|
||||
|
||||
|
||||
async def create_certification(
|
||||
request: Request,
|
||||
certification_object: CertificationCreate,
|
||||
current_user: PublicUser | AnonymousUser,
|
||||
db_session: Session,
|
||||
) -> CertificationRead:
|
||||
"""Create a new certification for a course"""
|
||||
|
||||
# Check if course exists
|
||||
statement = select(Course).where(Course.id == certification_object.course_id)
|
||||
course = db_session.exec(statement).first()
|
||||
|
||||
if not course:
|
||||
raise HTTPException(
|
||||
status_code=404,
|
||||
detail="Course not found",
|
||||
)
|
||||
|
||||
# RBAC check
|
||||
await rbac_check(request, course.course_uuid, current_user, "create", db_session)
|
||||
|
||||
# Create certification
|
||||
certification = Certifications(
|
||||
course_id=certification_object.course_id,
|
||||
config=certification_object.config or {},
|
||||
certification_uuid=str(f"certification_{uuid4()}"),
|
||||
creation_date=str(datetime.now()),
|
||||
update_date=str(datetime.now()),
|
||||
)
|
||||
|
||||
# Insert certification in DB
|
||||
db_session.add(certification)
|
||||
db_session.commit()
|
||||
db_session.refresh(certification)
|
||||
|
||||
return CertificationRead(**certification.model_dump())
|
||||
|
||||
|
||||
async def get_certification(
|
||||
request: Request,
|
||||
certification_uuid: str,
|
||||
current_user: PublicUser | AnonymousUser,
|
||||
db_session: Session,
|
||||
) -> CertificationRead:
|
||||
"""Get a single certification by certification_id"""
|
||||
|
||||
statement = select(Certifications).where(Certifications.certification_uuid == certification_uuid)
|
||||
certification = db_session.exec(statement).first()
|
||||
|
||||
if not certification:
|
||||
raise HTTPException(
|
||||
status_code=404,
|
||||
detail="Certification not found",
|
||||
)
|
||||
|
||||
# Get course for RBAC check
|
||||
statement = select(Course).where(Course.id == certification.course_id)
|
||||
course = db_session.exec(statement).first()
|
||||
|
||||
if not course:
|
||||
raise HTTPException(
|
||||
status_code=404,
|
||||
detail="Course not found",
|
||||
)
|
||||
|
||||
# RBAC check
|
||||
await rbac_check(request, course.course_uuid, current_user, "read", db_session)
|
||||
|
||||
return CertificationRead(**certification.model_dump())
|
||||
|
||||
|
||||
async def get_certifications_by_course(
|
||||
request: Request,
|
||||
course_uuid: str,
|
||||
current_user: PublicUser | AnonymousUser,
|
||||
db_session: Session,
|
||||
) -> List[CertificationRead]:
|
||||
"""Get all certifications for a course"""
|
||||
|
||||
# Get course for RBAC check
|
||||
statement = select(Course).where(Course.course_uuid == course_uuid)
|
||||
course = db_session.exec(statement).first()
|
||||
|
||||
if not course:
|
||||
raise HTTPException(
|
||||
status_code=404,
|
||||
detail="Course not found",
|
||||
)
|
||||
|
||||
# RBAC check
|
||||
await rbac_check(request, course_uuid, current_user, "read", db_session)
|
||||
|
||||
# Get certifications for this course
|
||||
statement = select(Certifications).where(Certifications.course_id == course.id)
|
||||
certifications = db_session.exec(statement).all()
|
||||
|
||||
return [CertificationRead(**certification.model_dump()) for certification in certifications]
|
||||
|
||||
|
||||
async def update_certification(
|
||||
request: Request,
|
||||
certification_uuid: str,
|
||||
certification_object: CertificationUpdate,
|
||||
current_user: PublicUser | AnonymousUser,
|
||||
db_session: Session,
|
||||
) -> CertificationRead:
|
||||
"""Update a certification"""
|
||||
|
||||
statement = select(Certifications).where(Certifications.certification_uuid == certification_uuid)
|
||||
certification = db_session.exec(statement).first()
|
||||
|
||||
if not certification:
|
||||
raise HTTPException(
|
||||
status_code=404,
|
||||
detail="Certification not found",
|
||||
)
|
||||
|
||||
# Get course for RBAC check
|
||||
statement = select(Course).where(Course.id == certification.course_id)
|
||||
course = db_session.exec(statement).first()
|
||||
|
||||
if not course:
|
||||
raise HTTPException(
|
||||
status_code=404,
|
||||
detail="Course not found",
|
||||
)
|
||||
|
||||
# RBAC check
|
||||
await rbac_check(request, course.course_uuid, current_user, "update", db_session)
|
||||
|
||||
# Update only the fields that were passed in
|
||||
for var, value in vars(certification_object).items():
|
||||
if value is not None:
|
||||
setattr(certification, var, value)
|
||||
|
||||
# Update the update_date
|
||||
certification.update_date = str(datetime.now())
|
||||
|
||||
db_session.add(certification)
|
||||
db_session.commit()
|
||||
db_session.refresh(certification)
|
||||
|
||||
return CertificationRead(**certification.model_dump())
|
||||
|
||||
|
||||
async def delete_certification(
|
||||
request: Request,
|
||||
certification_uuid: str,
|
||||
current_user: PublicUser | AnonymousUser,
|
||||
db_session: Session,
|
||||
) -> dict:
|
||||
"""Delete a certification"""
|
||||
|
||||
statement = select(Certifications).where(Certifications.certification_uuid == certification_uuid)
|
||||
certification = db_session.exec(statement).first()
|
||||
|
||||
if not certification:
|
||||
raise HTTPException(
|
||||
status_code=404,
|
||||
detail="Certification not found",
|
||||
)
|
||||
|
||||
# Get course for RBAC check
|
||||
statement = select(Course).where(Course.id == certification.course_id)
|
||||
course = db_session.exec(statement).first()
|
||||
|
||||
if not course:
|
||||
raise HTTPException(
|
||||
status_code=404,
|
||||
detail="Course not found",
|
||||
)
|
||||
|
||||
# RBAC check
|
||||
await rbac_check(request, course.course_uuid, current_user, "delete", db_session)
|
||||
|
||||
db_session.delete(certification)
|
||||
db_session.commit()
|
||||
|
||||
return {"detail": "Certification deleted successfully"}
|
||||
|
||||
|
||||
####################################################
|
||||
# Certificate User Functions
|
||||
####################################################
|
||||
|
||||
|
||||
async def create_certificate_user(
|
||||
request: Request,
|
||||
user_id: int,
|
||||
certification_id: int,
|
||||
db_session: Session,
|
||||
) -> CertificateUserRead:
|
||||
"""Create a certificate user link"""
|
||||
|
||||
# Check if certification exists
|
||||
statement = select(Certifications).where(Certifications.id == certification_id)
|
||||
certification = db_session.exec(statement).first()
|
||||
|
||||
if not certification:
|
||||
raise HTTPException(
|
||||
status_code=404,
|
||||
detail="Certification not found",
|
||||
)
|
||||
|
||||
# Check if certificate user already exists
|
||||
statement = select(CertificateUser).where(
|
||||
CertificateUser.user_id == user_id,
|
||||
CertificateUser.certification_id == certification_id
|
||||
)
|
||||
existing_certificate_user = db_session.exec(statement).first()
|
||||
|
||||
if existing_certificate_user:
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail="User already has a certificate for this course",
|
||||
)
|
||||
|
||||
# Generate readable certificate user UUID
|
||||
current_year = datetime.now().year
|
||||
current_month = datetime.now().month
|
||||
current_day = datetime.now().day
|
||||
|
||||
# Get user to extract user_uuid
|
||||
from src.db.users import User
|
||||
statement = select(User).where(User.id == user_id)
|
||||
user = db_session.exec(statement).first()
|
||||
|
||||
if not user:
|
||||
raise HTTPException(
|
||||
status_code=404,
|
||||
detail="User not found",
|
||||
)
|
||||
|
||||
# Extract last 4 characters from user_uuid for uniqueness (since all start with "user_")
|
||||
user_uuid_short = user.user_uuid[-4:] if user.user_uuid else "USER"
|
||||
|
||||
# Generate random 2-letter prefix
|
||||
import random
|
||||
import string
|
||||
random_prefix = ''.join(random.choices(string.ascii_uppercase, k=2))
|
||||
|
||||
# Get the count of existing certificate users for this user today
|
||||
today_user_prefix = f"{random_prefix}-{current_year}{current_month:02d}{current_day:02d}-{user_uuid_short}-"
|
||||
statement = select(CertificateUser).where(
|
||||
CertificateUser.user_certification_uuid.startswith(today_user_prefix)
|
||||
)
|
||||
existing_certificates = db_session.exec(statement).all()
|
||||
|
||||
# Generate next sequential number for this user today
|
||||
next_number = len(existing_certificates) + 1
|
||||
certificate_number = f"{next_number:03d}" # Format as 3-digit number with leading zeros
|
||||
|
||||
user_certification_uuid = f"{today_user_prefix}{certificate_number}"
|
||||
|
||||
# Create certificate user
|
||||
certificate_user = CertificateUser(
|
||||
user_id=user_id,
|
||||
certification_id=certification_id,
|
||||
user_certification_uuid=user_certification_uuid,
|
||||
created_at=str(datetime.now()),
|
||||
updated_at=str(datetime.now()),
|
||||
)
|
||||
|
||||
db_session.add(certificate_user)
|
||||
db_session.commit()
|
||||
db_session.refresh(certificate_user)
|
||||
|
||||
return CertificateUserRead(**certificate_user.model_dump())
|
||||
|
||||
|
||||
async def check_course_completion_and_create_certificate(
|
||||
request: Request,
|
||||
user_id: int,
|
||||
course_id: int,
|
||||
db_session: Session,
|
||||
) -> bool:
|
||||
"""Check if all activities in a course are completed and create certificate if so"""
|
||||
|
||||
# Get all activities in the course
|
||||
statement = select(ChapterActivity).where(ChapterActivity.course_id == course_id)
|
||||
course_activities = db_session.exec(statement).all()
|
||||
|
||||
if not course_activities:
|
||||
return False # No activities in course
|
||||
|
||||
# Get all completed activities for this user in this course
|
||||
statement = select(TrailStep).where(
|
||||
TrailStep.user_id == user_id,
|
||||
TrailStep.course_id == course_id,
|
||||
TrailStep.complete == True
|
||||
)
|
||||
completed_activities = db_session.exec(statement).all()
|
||||
|
||||
# Check if all activities are completed
|
||||
if len(completed_activities) >= len(course_activities):
|
||||
# All activities completed, check if certification exists for this course
|
||||
statement = select(Certifications).where(Certifications.course_id == course_id)
|
||||
certification = db_session.exec(statement).first()
|
||||
|
||||
if certification and certification.id:
|
||||
# Create certificate user link
|
||||
try:
|
||||
await create_certificate_user(request, user_id, certification.id, db_session)
|
||||
return True
|
||||
except HTTPException as e:
|
||||
if e.status_code == 400 and "already has a certificate" in e.detail:
|
||||
# Certificate already exists, which is fine
|
||||
return True
|
||||
else:
|
||||
raise e
|
||||
|
||||
return False
|
||||
|
||||
|
||||
####################################################
|
||||
# RBAC Utils
|
||||
####################################################
|
||||
|
||||
|
||||
async def rbac_check(
|
||||
request: Request,
|
||||
course_uuid: str,
|
||||
current_user: PublicUser | AnonymousUser,
|
||||
action: Literal["create", "read", "update", "delete"],
|
||||
db_session: Session,
|
||||
):
|
||||
if action == "read":
|
||||
if current_user.id == 0: # Anonymous user
|
||||
res = await authorization_verify_if_element_is_public(
|
||||
request, course_uuid, action, db_session
|
||||
)
|
||||
return res
|
||||
else:
|
||||
res = (
|
||||
await authorization_verify_based_on_roles_and_authorship(
|
||||
request, current_user.id, action, course_uuid, db_session
|
||||
)
|
||||
)
|
||||
return res
|
||||
else:
|
||||
await authorization_verify_if_user_is_anon(current_user.id)
|
||||
|
||||
await authorization_verify_based_on_roles_and_authorship(
|
||||
request,
|
||||
current_user.id,
|
||||
action,
|
||||
course_uuid,
|
||||
db_session,
|
||||
)
|
||||
|
|
@ -9,6 +9,7 @@ from src.db.trail_runs import TrailRun, TrailRunRead
|
|||
from src.db.trail_steps import TrailStep
|
||||
from src.db.trails import Trail, TrailCreate, TrailRead
|
||||
from src.db.users import AnonymousUser, PublicUser
|
||||
from src.services.courses.certifications import check_course_completion_and_create_certificate
|
||||
|
||||
|
||||
async def create_user_trail(
|
||||
|
|
@ -68,7 +69,7 @@ async def get_user_trails(
|
|||
for trail_run in trail_runs:
|
||||
statement = select(Course).where(Course.id == trail_run.course_id)
|
||||
course = db_session.exec(statement).first()
|
||||
trail_run.course = course
|
||||
trail_run.course = course.model_dump() if course else {}
|
||||
|
||||
# Add number of activities (steps) in a course
|
||||
statement = select(ChapterActivity).where(
|
||||
|
|
@ -153,7 +154,7 @@ async def get_user_trail_with_orgid(
|
|||
for trail_run in trail_runs:
|
||||
statement = select(Course).where(Course.id == trail_run.course_id)
|
||||
course = db_session.exec(statement).first()
|
||||
trail_run.course = course
|
||||
trail_run.course = course.model_dump() if course else {}
|
||||
|
||||
# Add number of activities (steps) in a course
|
||||
statement = select(ChapterActivity).where(
|
||||
|
|
@ -255,6 +256,12 @@ async def add_activity_to_trail(
|
|||
db_session.commit()
|
||||
db_session.refresh(trailstep)
|
||||
|
||||
# Check if all activities in the course are completed and create certificate if so
|
||||
if course and course.id:
|
||||
await check_course_completion_and_create_certificate(
|
||||
request, user.id, course.id, db_session
|
||||
)
|
||||
|
||||
statement = select(TrailRun).where(TrailRun.trail_id == trail.id , TrailRun.user_id == user.id)
|
||||
trail_runs = db_session.exec(statement).all()
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue