Coverage for app/backend/src/couchers/helpers/completed_profile.py: 100%
13 statements
« prev ^ index » next coverage.py v7.15.3, created at 2026-08-04 22:32 +0000
« prev ^ index » next coverage.py v7.15.3, created at 2026-08-04 22:32 +0000
1from sqlalchemy import and_, select
2from sqlalchemy.orm import Session
3from sqlalchemy.sql.elements import ColumnElement
4from sqlalchemy.sql.selectable import Subquery
6from couchers.constants import COMPLETED_PROFILE_MINIMUM_CHAR_LENGTH
7from couchers.models import User
8from couchers.models.uploads import has_avatar_photo_expression
11def has_completed_profile(session: Session, user: User) -> bool:
12 """
13 Check if a user has completed their profile (has photo + 150 char about_me).
14 """
15 if not user.profile_gallery_id or not user.about_me or len(user.about_me) < COMPLETED_PROFILE_MINIMUM_CHAR_LENGTH:
16 return False
17 return bool(session.execute(select(has_avatar_photo_expression(user))).scalar())
20def has_completed_profile_expression(galleries_with_photos: Subquery | None = None) -> ColumnElement[bool]:
21 """
22 Returns a SQL expression for checking if a user has completed their profile.
24 Use this in SQLAlchemy queries where you need to filter by profile completeness.
26 The avatar is checked with a correlated EXISTS, which the planner flattens into a semi-join in a WHERE clause.
27 Where it can't do that -- inside an aggregate FILTER clause it stays a subplan and runs once per row -- pass a
28 subquery of distinct photo_gallery_items.gallery_id that the statement outer joins on User.profile_gallery_id,
29 and the check reads that join instead.
31 Usage:
32 statement = select(User).where(has_completed_profile_expression())
33 """
34 return and_(
35 User.profile_gallery_id != None,
36 has_avatar_photo_expression(User)
37 if galleries_with_photos is None
38 else galleries_with_photos.c.gallery_id.isnot(None),
39 User.about_me_length >= COMPLETED_PROFILE_MINIMUM_CHAR_LENGTH,
40 )