Port NomadCNA city_details/content/routes seeding into PocketBase and surface full destination pages with related meetups and discussions. Co-authored-by: Cursor <cursoragent@cursor.com>
139 lines
4.5 KiB
Python
139 lines
4.5 KiB
Python
"""City / destination depth aggregation (city_details + related content)."""
|
|
from __future__ import annotations
|
|
|
|
from typing import Any
|
|
|
|
from app.data.city_details_data import (
|
|
CITY_DETAILS_BY_SLUG,
|
|
CONTENT_ITEMS,
|
|
EXTRA_ROUTES,
|
|
)
|
|
from app.data import community_data
|
|
from app.services.pb_repo import q, safe_first, safe_list, use_pb
|
|
from app.services.pocketbase import pb
|
|
from app.services import community_store
|
|
|
|
|
|
def get_city_detail_record(slug: str) -> dict[str, Any] | None:
|
|
if use_pb():
|
|
row = safe_first("city_details", filter=f"citySlug={q(slug)}")
|
|
if row:
|
|
return {
|
|
"citySlug": row.get("citySlug", slug),
|
|
"guide": row.get("guide") or {},
|
|
"pros": row.get("pros") or {},
|
|
"reviews": row.get("reviews") or {},
|
|
"cost": row.get("cost") or {},
|
|
"people": row.get("people") or {},
|
|
"chat": row.get("chat") or {},
|
|
"photos": row.get("photos") or {},
|
|
"weather": row.get("weather") or {},
|
|
"trends": row.get("trends") or {},
|
|
"demographics": row.get("demographics") or {},
|
|
"relatedContent": row.get("relatedContent") or [],
|
|
}
|
|
return CITY_DETAILS_BY_SLUG.get(slug)
|
|
|
|
|
|
async def get_destination_full(slug: str) -> dict[str, Any] | None:
|
|
city = await pb.get_destination(slug)
|
|
if not city:
|
|
return None
|
|
|
|
detail = get_city_detail_record(slug)
|
|
content: list[dict] = []
|
|
if use_pb():
|
|
for item in safe_list("content_items"):
|
|
slugs = item.get("citySlugs") or []
|
|
if slug in slugs and item.get("status", "published") == "published":
|
|
content.append(_public_content(item))
|
|
if not content:
|
|
content = [_public_content(c) for c in CONTENT_ITEMS if slug in (c.get("citySlugs") or [])]
|
|
|
|
city_name = city.get("name", "")
|
|
meetups = [
|
|
m for m in community_store.list_meetups(upcoming=True)
|
|
if m.get("destination_slug") == slug or m.get("city") == city_name
|
|
][:10]
|
|
discussions = [
|
|
d for d in community_store.list_discussions()
|
|
if city_name and (city_name in d.get("title", "") or city_name in d.get("excerpt", ""))
|
|
][:10]
|
|
|
|
return {
|
|
"ok": True,
|
|
"city": city,
|
|
"detail": detail,
|
|
"content": content,
|
|
"meetups": meetups,
|
|
"discussions": discussions,
|
|
}
|
|
|
|
|
|
def _public_content(item: dict) -> dict:
|
|
return {
|
|
"slug": item.get("slug", ""),
|
|
"type": item.get("type", "guide"),
|
|
"title": item.get("title", ""),
|
|
"subtitle": item.get("subtitle") or item.get("subtitleEn") or "",
|
|
"description": item.get("description", ""),
|
|
"targetUrl": item.get("targetUrl", ""),
|
|
"ctaLabel": item.get("ctaLabel", "查看"),
|
|
"citySlugs": item.get("citySlugs") or [],
|
|
}
|
|
|
|
|
|
def list_routes_enriched(destinations: list[dict] | None = None) -> list[dict]:
|
|
dest_by_slug = {d.get("slug"): d for d in (destinations or [])}
|
|
routes: list[dict] = []
|
|
|
|
if use_pb():
|
|
for r in safe_list("routes"):
|
|
if r.get("status") and r.get("status") != "published":
|
|
continue
|
|
routes.append(_route_from_pb(r, dest_by_slug))
|
|
|
|
if not routes:
|
|
for r in community_data.NOMAD_ROUTES:
|
|
routes.append({
|
|
"id": r.get("id") or r.get("slug", ""),
|
|
"slug": r.get("slug", ""),
|
|
"title": r.get("title", ""),
|
|
"duration_days": r.get("duration_days", 0),
|
|
"budget": r.get("budget", 0),
|
|
"description": r.get("description", ""),
|
|
"stops": r.get("stops") or [],
|
|
"emoji": r.get("emoji", "🗺️"),
|
|
"cities": [dest_by_slug[s] for s in (r.get("stops") or []) if s in dest_by_slug],
|
|
})
|
|
for r in EXTRA_ROUTES:
|
|
if any(x["slug"] == r["slug"] for x in routes):
|
|
continue
|
|
routes.append({
|
|
"id": r["slug"],
|
|
"slug": r["slug"],
|
|
"title": r["title"],
|
|
"duration_days": r.get("durationDays", 0),
|
|
"budget": r.get("budget", 0),
|
|
"description": r.get("description", ""),
|
|
"stops": r.get("citySlugs") or [],
|
|
"emoji": r.get("emoji", "🗺️"),
|
|
"cities": [dest_by_slug[s] for s in (r.get("citySlugs") or []) if s in dest_by_slug],
|
|
})
|
|
return routes
|
|
|
|
|
|
def _route_from_pb(r: dict, dest_by_slug: dict) -> dict:
|
|
slugs = r.get("citySlugs") or []
|
|
return {
|
|
"id": r.get("id") or r.get("slug", ""),
|
|
"slug": r.get("slug", ""),
|
|
"title": r.get("title", ""),
|
|
"duration_days": int(r.get("durationDays") or 0),
|
|
"budget": int(r.get("budget") or 0),
|
|
"description": r.get("description", ""),
|
|
"stops": slugs,
|
|
"emoji": r.get("emoji", "🗺️"),
|
|
"cities": [dest_by_slug[s] for s in slugs if s in dest_by_slug],
|
|
}
|