Skip to content

Commit 9fe811f

Browse files
kevinortiz43claude
andcommitted
Communities: order by NodeType enum, drop home-first, keyset pagination
Address @aapeliv review on #9270: order ListUserCommunities directly by the NodeType ordinal (node_type.desc()) instead of a CASE rank; remove the is_home/ST_Contains home-first sort; replace offset with a keyset page token (node_type, name, id) so filtering happens in SQL. Revert hand-edits to the Weblate-managed non-English locale files — only en.json changes by hand. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 4f0343b commit 9fe811f

15 files changed

Lines changed: 130 additions & 59 deletions

File tree

app/backend/src/couchers/servicers/communities.py

Lines changed: 29 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
1+
import json
12
import logging
23
from collections.abc import Sequence
34
from datetime import timedelta
45

56
import grpc
67
from google.protobuf import empty_pb2
7-
from sqlalchemy import case, select
8+
from sqlalchemy import select
89
from sqlalchemy.orm import Session, selectinload
9-
from sqlalchemy.sql import delete, func, or_
10+
from sqlalchemy.sql import and_, delete, func, or_
1011

1112
from couchers.constants import COMMUNITIES_SEARCH_FUZZY_SIMILARITY_THRESHOLD
1213
from couchers.context import CouchersContext
@@ -570,40 +571,47 @@ def ListUserCommunities(
570571
self, request: communities_pb2.ListUserCommunitiesReq, context: CouchersContext, session: Session
571572
) -> communities_pb2.ListUserCommunitiesRes:
572573
page_size = min(MAX_PAGINATION_LENGTH, request.page_size or MAX_PAGINATION_LENGTH)
573-
offset = int(decrypt_page_token(request.page_token)) if request.page_token else 0
574574
user_id = request.user_id or context.user_id
575575

576-
user_geom = select(User.geom).where(User.id == user_id).scalar_subquery()
577-
type_rank = case(
578-
(Node.node_type == NodeType.locality, 0),
579-
(Node.node_type == NodeType.sublocality, 1),
580-
(Node.node_type == NodeType.subregion, 2),
581-
(Node.node_type == NodeType.region, 3),
582-
(Node.node_type == NodeType.macroregion, 4),
583-
(Node.node_type == NodeType.world, 5),
584-
else_=6,
576+
query = (
577+
select(Node)
578+
.join(Cluster, Cluster.parent_node_id == Node.id)
579+
.join(ClusterSubscription, ClusterSubscription.cluster_id == Cluster.id)
580+
.where(ClusterSubscription.user_id == user_id)
581+
.where(Cluster.is_official_cluster)
585582
)
586-
is_home = func.ST_Contains(Node.geom, user_geom)
583+
584+
if request.page_token:
585+
cursor_node_type_name, cursor_name, cursor_id = json.loads(decrypt_page_token(request.page_token))
586+
cursor_node_type = NodeType[cursor_node_type_name]
587+
query = query.where(
588+
or_(
589+
Node.node_type < cursor_node_type,
590+
and_(Node.node_type == cursor_node_type, Cluster.name > cursor_name),
591+
and_(Node.node_type == cursor_node_type, Cluster.name == cursor_name, Node.id > cursor_id),
592+
)
593+
)
587594

588595
nodes = (
589596
session.execute(
590-
select(Node)
591-
.join(Cluster, Cluster.parent_node_id == Node.id)
592-
.join(ClusterSubscription, ClusterSubscription.cluster_id == Cluster.id)
593-
.where(ClusterSubscription.user_id == user_id)
594-
.where(Cluster.is_official_cluster)
595-
.order_by(type_rank, is_home.desc(), Cluster.name.asc(), Node.id.asc())
597+
query.order_by(Node.node_type.desc(), Cluster.name.asc(), Node.id.asc())
596598
.limit(page_size + 1)
597-
.offset(offset)
598599
.options(selectinload(Node.official_cluster))
599600
)
600601
.scalars()
601602
.all()
602603
)
603604

605+
next_page_token = None
606+
if len(nodes) > page_size:
607+
last_node = nodes[page_size - 1]
608+
next_page_token = encrypt_page_token(
609+
json.dumps([last_node.node_type.name, last_node.official_cluster.name, last_node.id])
610+
)
611+
604612
return communities_pb2.ListUserCommunitiesRes(
605613
communities=communities_to_pb(session, nodes[:page_size], context),
606-
next_page_token=encrypt_page_token(str(offset + page_size)) if len(nodes) > page_size else None,
614+
next_page_token=next_page_token,
607615
)
608616

609617
def ListAllCommunities(

app/backend/src/tests/test_communities.py

Lines changed: 36 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1248,46 +1248,52 @@ def test_LeaveCommunity_regression(db):
12481248
assert not api.GetCommunity(communities_pb2.GetCommunityReq(community_id=c2_id)).member
12491249

12501250

1251-
def test_ListUserCommunities_orders_locality_first_then_home_then_name(db):
1252-
user, token = generate_user(username="orderuser", geom=create_1d_point(20), geom_radius=0.1)
1253-
other_admin, _ = generate_user(username="orderuser_stateadmin")
1251+
def test_ListUserCommunities_orders_locality_first_then_name(db):
1252+
user, token = generate_user(username="orderuser")
12541253

12551254
with session_scope() as session:
12561255
world = create_community(session, 0, 100, "World", [user], [], None)
12571256
continent = create_community(session, 0, 90, "Continent", [user], [], world)
1258-
home_country = create_community(session, 10, 50, "Zzz Home Country", [user], [], continent)
1259-
other_country = create_community(session, 60, 80, "Aaa Country", [user], [], continent)
1260-
state = create_community(session, 15, 30, "State", [other_admin], [], home_country)
1261-
home_city = create_community(session, 18, 22, "Zzz Home City", [user], [], state)
1262-
other_city = create_community(session, 23, 26, "Aaa City", [user], [], state)
1257+
country_a = create_community(session, 0, 30, "Aaa Country", [user], [], continent)
1258+
country_z = create_community(session, 30, 60, "Zzz Country", [user], [], continent)
1259+
city_a = create_community(session, 0, 10, "Aaa City", [user], [], country_a)
1260+
city_m = create_community(session, 10, 20, "Mmm City", [user], [], country_a)
1261+
city_z = create_community(session, 20, 30, "Zzz City", [user], [], country_a)
12631262

12641263
world_id = world.id
12651264
continent_id = continent.id
1266-
home_country_id = home_country.id
1267-
other_country_id = other_country.id
1268-
home_city_id = home_city.id
1269-
other_city_id = other_city.id
1265+
country_a_id = country_a.id
1266+
country_z_id = country_z.id
1267+
city_a_id = city_a.id
1268+
city_m_id = city_m.id
1269+
city_z_id = city_z.id
1270+
1271+
expected_order = [
1272+
city_a_id,
1273+
city_m_id,
1274+
city_z_id,
1275+
country_a_id,
1276+
country_z_id,
1277+
continent_id,
1278+
world_id,
1279+
]
12701280

12711281
with communities_session(token) as api:
12721282
res = api.ListUserCommunities(communities_pb2.ListUserCommunitiesReq())
1273-
assert [c.community_id for c in res.communities] == [
1274-
home_city_id,
1275-
other_city_id,
1276-
home_country_id,
1277-
other_country_id,
1278-
continent_id,
1279-
world_id,
1280-
]
1281-
1282-
res = api.ListUserCommunities(communities_pb2.ListUserCommunitiesReq(page_size=3))
1283-
assert [c.community_id for c in res.communities] == [home_city_id, other_city_id, home_country_id]
1284-
assert res.next_page_token
1285-
1286-
res = api.ListUserCommunities(
1287-
communities_pb2.ListUserCommunitiesReq(page_size=3, page_token=res.next_page_token)
1288-
)
1289-
assert [c.community_id for c in res.communities] == [other_country_id, continent_id, world_id]
1290-
assert not res.next_page_token
1283+
assert [c.community_id for c in res.communities] == expected_order
1284+
1285+
seen: list[int] = []
1286+
page_token = ""
1287+
for _ in range(len(expected_order) + 1):
1288+
res = api.ListUserCommunities(communities_pb2.ListUserCommunitiesReq(page_size=2, page_token=page_token))
1289+
seen.extend(c.community_id for c in res.communities)
1290+
if not res.next_page_token:
1291+
break
1292+
page_token = res.next_page_token
1293+
else:
1294+
pytest.fail("pagination did not terminate within the expected number of pages")
1295+
1296+
assert seen == expected_order
12911297

12921298

12931299
def test_enforce_community_memberships_for_user(testing_communities):

app/web/features/dashboard/locales/ca.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
"welcome": "Benvingut a Couchers.org!",
1616
"new_pill": "Nou",
1717
"your_communities_helper_text": "T'han afegit a totes les comunitats basant-se en la teva ubicació. Pots <1>explorar comunitats</1> en altres ubicacions.",
18+
"your_communities_helper_text2": "No trobes la teva comunitat? <1>Posa-la en marxa!</1>",
1819
"community_builder_form_text": "Crea la teva pròpia comunitat local",
1920
"communities_welcome_title": "Benvingut a les Comunitats",
2021
"community_builder": "Vols ser un ambaixador de la teva comunitat i ajudar-la a créixer? Sigues un <1>Impulsor de Comunitat!</1>",

app/web/features/dashboard/locales/cs.json

Lines changed: 33 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
"all_communities_heading": "Všechny komunity",
1313
"no_community": "Momentálně nejsi v žádné komunitě.",
1414
"your_communities_helper_text": "Přidali jsme tě do všech komunit na základě tvé polohy. Neváhej však <1>procházet komunity</1> i v jiných lokalitách.",
15+
"your_communities_helper_text2": "Nevidíš svou komunitu? <1>Založ ji!</1>",
1516
"community_builder_form_text": "Založ si vlastní místní komunitu",
1617
"communities_welcome_title": "Vítej v Komunitách",
1718
"community_builder": "Chceš být ambasadorem své komunity a pomoci jí růst? Staň se <1>budovatelem komunity!</1>",
@@ -44,10 +45,14 @@
4445
"hero_image_attribution": "Foto: <1>Mesut Kaya</1> na <3>Unsplash</3>",
4546
"reminder": {
4647
"complete_profile": {
47-
"button": "Upravit svůj profil"
48+
"button": "Upravit svůj profil",
49+
"title": "Dokonči svůj profil",
50+
"description": "Vyplň svojí sekci \"Kdo jsem\" a nahraj profilovou fotku"
4851
},
4952
"strong_verification": {
50-
"description": "Ověř svoji identitu pro bezpečnější komunitu"
53+
"description": "Ověř svoji identitu pro bezpečnější komunitu",
54+
"title": "Silné ověření",
55+
"button": "Ověř svůj účet"
5156
},
5257
"respond_to_host_request": {
5358
"description": "Nezapomeň odpovědět na žádost o ubytování od {{name}}.",
@@ -56,14 +61,36 @@
5661
},
5762
"write_reference": {
5863
"title": "Napsat referenci",
59-
"description_surfed": "Zanech referenci na svůj pobyt u {{name}}"
64+
"description_surfed": "Zanech referenci na svůj pobyt u {{name}}",
65+
"button": "Napiš referenci",
66+
"description_hosted": "Zanech referenci na pobyt {{name}} u tebe"
67+
},
68+
"complete_my_home": {
69+
"title": "Dokonči sekci \"Můj domov\" ve svém profilu",
70+
"description": "Vyplň informace o uspořádání spaní, domácí řád a popis vašeho ubytování",
71+
"button": "Uprav svůj domov"
72+
},
73+
"carousel_dismiss_button_a11y": "Zavřít upozornění",
74+
"carousel_scroller": {
75+
"left_arrow_a11y": "Posunout doleva",
76+
"right_arrow_a11y": "Posunout doprava"
77+
},
78+
"confirm_host_request": {
79+
"title": "Potvrď svůj pobyt u {{name}}",
80+
"button": "Potvrď pobyt"
6081
}
6182
},
6283
"public_trips": {
6384
"when_tomorrow": "Zítra",
6485
"when_today": "Dnes",
6586
"manage_link": "Spravovat",
66-
"when_now": "Nyní"
87+
"when_now": "Nyní",
88+
"empty_description": "Chystáš se někam? Zveřejni veřejnou cestu v komunitě a získej tak nabídky od hostitelů v okolí.",
89+
"same_gender_only_indicator": "Stejné pohlaví",
90+
"offers_count_one": "{{count}} nabídka",
91+
"offers_count_few": "{{count}} nabídky",
92+
"offers_count_other": "{{count}} nabídek",
93+
"no_offers": "Zatím žádné nabídky"
6794
},
6895
"prev_page_button_a11y": "Předchozí stránka",
6996
"next_page_button_a11y": "Následující stránka",
@@ -88,6 +115,7 @@
88115
"night_count_one": "{{count}} noc",
89116
"night_count_few": "{{count}} noci",
90117
"night_count_other": "{{count}} nocí",
91-
"no_upcoming_guests": "Zatím žádní nadcházející hosté."
118+
"no_upcoming_guests": "Zatím žádní nadcházející hosté.",
119+
"no_upcoming_trips": "Žádné nadcházející cesty. Připraveni objevovat?"
92120
}
93121
}

app/web/features/dashboard/locales/de.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
"landing_text": "Wir entwickeln neue <1>Funktionen</1> wie Events, Local Guides, Moderation und Hangouts. Wir danken euch für eure Geduld und <3>Unterstützung</3> bei der Entwicklung.",
2020
"your_communities_helper_text": "Du wurdest zu allen Communitys hinzugefügt, die sich auf deinen Standort beziehen. Du kannst auch an anderen Orten <1>Communitys durchsuchen</1>.",
2121
"community_builder_form_text": "Gründen deine eigene lokale Community",
22+
"your_communities_helper_text2": "Siehst du deine Community nicht? <1>Gründe sie!</1>",
2223
"member_count_one": "{{count}} Mitglied",
2324
"member_count_other": "{{count}} Mitglieder",
2425
"profile_summary_edit": "Profil bearbeiten",

app/web/features/dashboard/locales/es.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
"profile_mobile_summary_view": "Ir al perfil",
1717
"find_a_host": "Buscar anfitrión",
1818
"your_communities_helper_text": "Se te ha añadido a todas las comunidades basándonos en tu ubicación. También eres libre de <1>explorar comunidades</1> en otras ubicaciones.",
19+
"your_communities_helper_text2": "¿No ves tu comunidad? <1>¡Créala!</1>",
1920
"community_builder_form_text": "Crear tu propia comunidad local",
2021
"no_sub_communities": "Sin sub-comunidades.",
2122
"browse_communities": "Explorar las comunidades",

app/web/features/dashboard/locales/fr.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
"member_count_other": "{{count}} membres",
1717
"profile_summary_edit": "Rédiger mon profil",
1818
"profile_summary_view": "Voir mon profil",
19+
"your_communities_helper_text2": "Ne voyez-vous pas votre communauté ? <1>Créez la vôtre !</1>",
1920
"community_builder_form_text": "Créez votre propre communauté locale",
2021
"landing_text": "Nous développons actuellement de nouvelles <1>fonctionnalités</1>, telles que les événements, les guides locaux, la modération et la fonction de traîner. Nous vous remercions de votre patience et de votre soutien pendant leur développement.",
2122
"show_map": "Montrer la carte",

app/web/features/dashboard/locales/it.json

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,29 @@
33
"please_complete_profile": "Completa il tuo profilo in modo da comparire nei risultati di ricerca:",
44
"fill_in_who_i_am": "1. Completa la sezione \"Chi sono\" con alcune frasi su di te",
55
"upload_photo": "2. Carica una foto (cliccando sull'avatar nella pagina di modifica)",
6-
"your_communities_heading": "Le tue comunità",
7-
"all_communities_heading": "Tutte le comunità",
6+
"your_communities_heading": "Le tue community",
7+
"all_communities_heading": "Tutte le community",
88
"load_more": "Carica altro",
99
"welcome": "Benvenuto in Couchers.org!",
1010
"new_pill": "Nuovo",
1111
"landing_text": "Stiamo sviluppando nuove <1>funzionalità</1> come eventi, guide locali, moderazione e ritrovi. Apprezziamo la tua pazienza ed il tuo <3>supporto</3> durante lo sviluppo.",
12-
"your_communities_helper_text": "Sei stato aggiunto a tutte le comunità in base alla tua posizione. Sentiti libero di <1>esplorare le comunità</1> anche in altre località."
12+
"your_communities_helper_text": "Fai parte di tutte le community relative alla tua posizione. Se ti va, puoi sempre <1>esplorare le community</1> anche in altre località.",
13+
"complete_profile_explanation": "Non odi il fatto che su altre piattaforme ci siano profili \"fantasma\" vuoti e senza informazioni? Per favore non ghostarci! 👻👻👻",
14+
"no_community": "Attualmente non sei in alcuna community.",
15+
"no_sub_communities": "Nessuna sotto-community.",
16+
"your_communities_helper_text2": "Non vedi la tua community? <1>Creala!</1>",
17+
"community_builder_form_text": "Crea la tua community locale",
18+
"communities_welcome_title": "Benvenuto nel portale community",
19+
"communities_intro": "Per aiutare la gente in tutto il mondo a organizzare eventi, discussioni e incontri, abbiamo creato il portale delle community, che sono riportate qui sotto. <strong>Fai automaticamente parte di alcune di queste in base alla tua posizione geografica, ma puoi unirti a qualsiasi community tu voglia!</strong> Le tue community sono riportate nella bacheca.",
20+
"community_builder": "Vuoi essere un ambasciatore per la tua community e aiutare a farla crescere? Diventa un <1>Community Builder!</1>",
21+
"my_communities_heading": "Le mie community",
22+
"find_your_community": "Trova una community",
23+
"find_your_community_intro": "Qui sotto puoi cercare o navigare tutte le community che esistono finora. Manca il tuo paese o la tua città? <1>Usa questo form</1> per richiederlo!",
24+
"find_your_community_intro_simplified": "Cerca la tua community o naviga qui sotto. <1>Non trovi la tua? Richiedila!</1>",
25+
"browse_communities": "Esplora le community",
26+
"browse_all_communities": "Esplora tutte le community",
27+
"browse_communities_text": "Clicca sul nome di una regione per vedere le altre community contenute all'interno. Clicca sul link per vedere la pagina della community di quella specifica città o regione.",
28+
"show_all_communities": "Mostra tutte le community",
29+
"hide_all_communities": "Nascondi tutte le community",
30+
"all_communities_section": "Tutte le community"
1331
}

app/web/features/dashboard/locales/nl.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
"load_more": "Meer laden",
1313
"new_pill": "Nieuw",
1414
"your_communities_helper_text": "Je bent op basis van je locatie automatisch toegevoegd aan een aantal communities. Natuurlijk kan je ook door <1>communities bladeren</1> van andere locaties.",
15+
"your_communities_helper_text2": "Zie je jouw community niet? <1>Begin hem zelf!</1>",
1516
"community_builder_form_text": "Begin jouw eigen lokale community",
1617
"communities_welcome_title": "Welkom bij Communities",
1718
"landing_text": "We bouwen nieuwe <1>functionaliteiten</1> zoals events, lokale gidsen, moderatie en hangouts. We waarderen je geduld en <3>support</3> tijdens het wachten hierop.",

app/web/features/dashboard/locales/pt.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
"hero_image_alt": "Balões de ar quente na Capadócia",
77
"hero_image_attribution": "Fotografia por <1>Mesut Kaya</1> em <3>Unsplash</3>",
88
"become_a_host": "Torna-te um anfitrião",
9+
"your_communities_helper_text2": "Não vês a tua comunidade? <1>Começa-a aqui!</1>",
910
"search_input_label": "Para onde vais?",
1011
"complete_profile_explanation": "Não detestas quando as outras plataformas têm perfis \"fantasma\", vazios, sem nenhuma informação. Por favor, não sejas um fantasma aqui! 👻👻👻",
1112
"no_sub_communities": "Sem subcomunidades.",

0 commit comments

Comments
 (0)