Coverage for src/couchers/servicers/public.py: 63%
63 statements
« prev ^ index » next coverage.py v7.6.10, created at 2025-07-30 15:36 +0000
« prev ^ index » next coverage.py v7.6.10, created at 2025-07-30 15:36 +0000
1import logging
3import grpc
4from cachetools import TTLCache, cached
5from sqlalchemy.sql import func, union_all
7from couchers import errors, urls
8from couchers.materialized_views import LiteUser
9from couchers.models import Cluster, Node, ProfilePublicVisibility, Reference, User, Volunteer
10from couchers.resources import get_static_badge_dict
11from couchers.servicers.account import _format_volunteer_link
12from couchers.servicers.api import fluency2api, hostingstatus2api, meetupstatus2api, user_model_to_pb
13from couchers.servicers.gis import _statement_to_geojson_response
14from couchers.sql import couchers_select as select
15from couchers.utils import Timestamp_from_datetime, make_logged_out_context
16from proto import api_pb2, public_pb2, public_pb2_grpc
18logger = logging.getLogger(__name__)
21@cached(cache=TTLCache(maxsize=1, ttl=600), key=lambda _: None)
22def _get_public_users(session):
23 with_geom = (
24 select(User.username, User.geom)
25 .where(User.is_visible)
26 .where(User.public_visibility != ProfilePublicVisibility.nothing)
27 .where(User.public_visibility != ProfilePublicVisibility.map_only)
28 )
30 without_geom = (
31 select(None, User.randomized_geom)
32 .where(User.is_visible)
33 .where(User.randomized_geom != None)
34 .where(User.public_visibility == ProfilePublicVisibility.map_only)
35 )
36 return _statement_to_geojson_response(session, union_all(with_geom, without_geom))
39@cached(cache=TTLCache(maxsize=1, ttl=60), key=lambda _: None)
40def _get_signup_page_info(session):
41 # last user who signed up
42 last_signup, geom = session.execute(
43 select(User.joined, User.geom).where(User.is_visible).order_by(User.id.desc()).limit(1)
44 ).one_or_none()
46 communities = (
47 session.execute(
48 select(Cluster.name)
49 .join(Node, Node.id == Cluster.parent_node_id)
50 .where(Cluster.is_official_cluster)
51 .where(func.ST_Contains(Node.geom, geom))
52 .order_by(Cluster.id.asc())
53 )
54 .scalars()
55 .all()
56 )
58 if len(communities) <= 1:
59 # either no community or just global community
60 last_location = "The World"
61 elif len(communities) == 3:
62 # probably global, continent, region, so let's just return the region
63 last_location = communities[-1]
64 else:
65 # probably global, continent, region, city
66 last_location = f"{communities[-1]}, {communities[-2]}"
68 user_count = session.execute(select(func.count()).select_from(User).where(User.is_visible)).scalar_one()
70 return public_pb2.GetSignupPageInfoRes(
71 last_signup=Timestamp_from_datetime(last_signup.replace(second=0, microsecond=0)),
72 last_location=last_location,
73 user_count=user_count,
74 )
77@cached(cache=TTLCache(maxsize=1, ttl=60), key=lambda _: None)
78def _get_volunteers(session):
79 volunteers = session.execute(
80 select(Volunteer, LiteUser)
81 .join(LiteUser, LiteUser.id == Volunteer.user_id)
82 .where(LiteUser.is_visible)
83 .where(Volunteer.show_on_team_page)
84 .order_by(
85 Volunteer.sort_key.asc().nulls_last(),
86 Volunteer.stopped_volunteering.desc().nulls_first(),
87 Volunteer.started_volunteering.asc(),
88 )
89 ).all()
91 board_members = set(get_static_badge_dict()["board_member"])
93 def format_volunteer(volunteer, lite_user):
94 if volunteer.link_type:
95 link_type = volunteer.link_type
96 link_text = volunteer.link_text
97 link_url = volunteer.link_url
98 else:
99 link_type = "couchers"
100 link_text = f"@{lite_user.username}"
101 link_url = urls.user_link(username=lite_user.username)
103 return public_pb2.Volunteer(
104 name=volunteer.display_name or lite_user.name,
105 username=lite_user.username,
106 is_board_member=lite_user.id in board_members,
107 role=volunteer.role,
108 location=volunteer.display_location or lite_user.city,
109 img=urls.media_url(filename=lite_user.avatar_filename, size="thumbnail")
110 if lite_user.avatar_filename
111 else None,
112 **_format_volunteer_link(volunteer, lite_user.username),
113 )
115 return public_pb2.GetVolunteersRes(
116 current_volunteers=[
117 format_volunteer(volunteer, lite_user)
118 for volunteer, lite_user in volunteers
119 if volunteer.stopped_volunteering is None
120 ],
121 past_volunteers=[
122 format_volunteer(volunteer, lite_user)
123 for volunteer, lite_user in volunteers
124 if volunteer.stopped_volunteering is not None
125 ],
126 )
129class Public(public_pb2_grpc.PublicServicer):
130 """
131 Public (logged out) APIs for getting public info
132 """
134 def GetPublicUsers(self, request, context, session):
135 return _get_public_users(session)
137 def GetPublicUser(self, request, context, session):
138 user = session.execute(
139 select(User)
140 .where(User.is_visible)
141 .where(User.username == request.user)
142 .where(
143 User.public_visibility.in_(
144 [ProfilePublicVisibility.limited, ProfilePublicVisibility.most, ProfilePublicVisibility.full]
145 )
146 )
147 ).scalar_one_or_none()
149 if not user:
150 context.abort(grpc.StatusCode.NOT_FOUND, errors.USER_NOT_FOUND)
152 if user.public_visibility == ProfilePublicVisibility.full:
153 return public_pb2.GetPublicUserRes(full_user=user_model_to_pb(user, session, make_logged_out_context()))
155 num_references = session.execute(
156 select(func.count())
157 .select_from(Reference)
158 .join(User, User.id == Reference.from_user_id)
159 .where(User.is_visible)
160 .where(Reference.to_user_id == user.id)
161 ).scalar_one()
163 if user.public_visibility == ProfilePublicVisibility.limited:
164 return public_pb2.GetPublicUserRes(
165 limited_user=public_pb2.LimitedUser(
166 username=user.username,
167 name=user.name,
168 city=user.city,
169 hometown=user.hometown,
170 num_references=num_references,
171 joined=Timestamp_from_datetime(user.display_joined),
172 hosting_status=hostingstatus2api[user.hosting_status],
173 meetup_status=meetupstatus2api[user.meetup_status],
174 badges=[badge.badge_id for badge in user.badges],
175 )
176 )
178 if user.public_visibility == ProfilePublicVisibility.most:
179 return public_pb2.GetPublicUserRes(
180 most_user=public_pb2.MostUser(
181 username=user.username,
182 name=user.name,
183 city=user.city,
184 hometown=user.hometown,
185 timezone=user.timezone,
186 num_references=num_references,
187 gender=user.gender,
188 pronouns=user.pronouns,
189 age=user.age,
190 joined=Timestamp_from_datetime(user.display_joined),
191 last_active=Timestamp_from_datetime(user.display_last_active),
192 hosting_status=hostingstatus2api[user.hosting_status],
193 meetup_status=meetupstatus2api[user.meetup_status],
194 occupation=user.occupation,
195 education=user.education,
196 about_me=user.about_me,
197 things_i_like=user.things_i_like,
198 language_abilities=[
199 api_pb2.LanguageAbility(code=ability.language_code, fluency=fluency2api[ability.fluency])
200 for ability in user.language_abilities
201 ],
202 regions_visited=[region.code for region in user.regions_visited],
203 regions_lived=[region.code for region in user.regions_lived],
204 avatar_url=user.avatar.full_url if user.avatar else None,
205 avatar_thumbnail_url=user.avatar.thumbnail_url if user.avatar else None,
206 badges=[badge.badge_id for badge in user.badges],
207 )
208 )
210 def GetSignupPageInfo(self, request, context, session):
211 return _get_signup_page_info(session)
213 def GetVolunteers(self, request, context, session):
214 return _get_volunteers(session)