From 10fbef6557cf3263352e3543be9eeee587030f05 Mon Sep 17 00:00:00 2001 From: Sean Morley Date: Sun, 14 Jun 2026 21:51:35 -0400 Subject: [PATCH] Refactor location label resolution in LocationViewSet and LodgingViewSet to use new utility function. Implement fetchFormattedLocation and resolveQuickAddLocationLabel functions for improved location data handling. Update API calls in frontend to utilize backendApiUrl for consistency. --- .../server/adventures/views/location_view.py | 8 +- .../server/adventures/views/lodging_view.py | 8 +- .../adventures/views/quick_add_utils.py | 14 ++++ frontend/package.json | 2 +- .../components/shared/PlaceQuickStart.svelte | 80 +++++++------------ frontend/src/lib/config.ts | 2 +- frontend/src/lib/index.server.ts | 2 +- frontend/src/lib/map/places.ts | 67 ++++++++++++++++ frontend/src/lib/server/django-proxy.ts | 38 ++++++--- frontend/src/routes/api/[...path]/+server.ts | 15 ++-- .../routes/collections/[id]/+page.server.ts | 6 +- frontend/src/routes/locations/+page.server.ts | 7 +- .../src/routes/locations/[id]/+page.server.ts | 6 +- frontend/src/routes/map/+page.svelte | 6 +- frontend/vite.config.ts | 13 +-- 15 files changed, 164 insertions(+), 110 deletions(-) diff --git a/backend/server/adventures/views/location_view.py b/backend/server/adventures/views/location_view.py index c3206e66..9a6024e1 100644 --- a/backend/server/adventures/views/location_view.py +++ b/backend/server/adventures/views/location_view.py @@ -44,6 +44,7 @@ from .quick_add_utils import ( parse_itinerary_date, preferred_link, resolve_quick_add_collection, + resolve_quick_add_location_label, sanitize_photo_urls, sanitize_tags, ) @@ -245,12 +246,7 @@ class LocationViewSet(viewsets.ModelViewSet): phone_number = str(details.get('phone_number') or payload.get('phone_number') or '').strip() or None - location_label = ( - str(payload.get('location') or '').strip() - or str(reverse_data.get('display_name') or '').strip() - or str(details.get('formatted_address') or '').strip() - or None - ) + location_label = resolve_quick_add_location_label(payload, reverse_data, details) description = build_quick_add_description( base_description=payload.get('description'), diff --git a/backend/server/adventures/views/lodging_view.py b/backend/server/adventures/views/lodging_view.py index 1a80565d..3138e91e 100644 --- a/backend/server/adventures/views/lodging_view.py +++ b/backend/server/adventures/views/lodging_view.py @@ -21,6 +21,7 @@ from .quick_add_utils import ( parse_itinerary_date, preferred_link, resolve_quick_add_collection, + resolve_quick_add_location_label, sanitize_photo_urls, ) @@ -127,12 +128,7 @@ class LodgingViewSet(viewsets.ModelViewSet): if rating is None: rating = coerce_float(details.get('rating')) - location_label = ( - str(payload.get('location') or '').strip() - or str(reverse_data.get('display_name') or '').strip() - or str(details.get('formatted_address') or '').strip() - or None - ) + location_label = resolve_quick_add_location_label(payload, reverse_data, details) place_types = payload.get('types') if not isinstance(place_types, list) or not place_types: diff --git a/backend/server/adventures/views/quick_add_utils.py b/backend/server/adventures/views/quick_add_utils.py index 3c2582f2..af0b49c4 100644 --- a/backend/server/adventures/views/quick_add_utils.py +++ b/backend/server/adventures/views/quick_add_utils.py @@ -111,6 +111,20 @@ def build_quick_add_description(base_description, detailed_description): return description or None +def resolve_quick_add_location_label(payload, reverse_data, details): + """Prefer server reverse-geocoded display_name over raw provider address strings.""" + reverse_data = reverse_data if isinstance(reverse_data, dict) else {} + details = details if isinstance(details, dict) else {} + payload = payload if isinstance(payload, dict) else {} + + return ( + str(reverse_data.get('display_name') or '').strip() + or str(payload.get('location') or '').strip() + or str(details.get('formatted_address') or '').strip() + or None + ) + + def resolve_quick_add_collection(collection_id, validate_permissions, permission_error_message): if not collection_id: return None diff --git a/frontend/package.json b/frontend/package.json index 537caf02..65f9cc45 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -1,6 +1,6 @@ { "name": "adventurelog-frontend", - "version": "0.12.0", + "version": "0.12.1", "scripts": { "dev": "vite dev", "django": "cd .. && cd backend/server && python3 manage.py runserver", diff --git a/frontend/src/lib/components/shared/PlaceQuickStart.svelte b/frontend/src/lib/components/shared/PlaceQuickStart.svelte index 1de15134..07468d93 100644 --- a/frontend/src/lib/components/shared/PlaceQuickStart.svelte +++ b/frontend/src/lib/components/shared/PlaceQuickStart.svelte @@ -6,6 +6,7 @@ import { addToast } from '$lib/toasts'; import CategoryDropdown from '../CategoryDropdown.svelte'; import type { Category } from '$lib/types'; + import { fetchFormattedLocation } from '$lib/map/places'; import SearchIcon from '~icons/mdi/magnify'; import LocationIcon from '~icons/mdi/crosshairs-gps'; @@ -348,59 +349,32 @@ async function performDetailedReverseGeocode(lat: number, lng: number) { try { - const response = await fetch(`/api/places/reverse/?lat=${lat}&lon=${lng}&format=json`); - - if (response.ok) { - const data = await response.json(); - const providerUsed = data.provider_used || data.provider || null; - locationData = { - city: data.city - ? { - name: data.city, - id: data.city_id, - visited: data.city_visited || false - } - : undefined, - region: data.region - ? { - name: data.region, - id: data.region_id, - visited: data.region_visited || false - } - : undefined, - country: data.country - ? { - name: data.country, - country_code: data.country_id, - visited: false - } - : undefined, - display_name: data.display_name, - location_name: data.location_name, - provider: providerUsed - }; - - if (selectedLocation) { - const isCoordinatePlaceholder = selectedLocation.name.startsWith('Location at '); - const shouldAutoEnrichQuickAdd = isCoordinatePlaceholder || !selectedLocation.place_id; - const resolvedLocationName = (data.location_name || '').trim(); - const resolvedDisplayName = (data.display_name || '').trim(); - - selectedLocation = { - ...selectedLocation, - name: isCoordinatePlaceholder - ? resolvedLocationName || resolvedDisplayName || selectedLocation.name - : selectedLocation.name, - location: resolvedDisplayName || `${lat.toFixed(4)}, ${lng.toFixed(4)}` - }; - searchQuery = selectedLocation.name; - - if (shouldAutoEnrichQuickAdd && resolvedLocationName) { - await enrichFromResolvedName(lat, lng, resolvedLocationName); - } - } - } else { + const formatted = await fetchFormattedLocation(lat, lng); + if (!formatted) { locationData = null; + return; + } + + locationData = formatted; + + if (selectedLocation) { + const isCoordinatePlaceholder = selectedLocation.name.startsWith('Location at '); + const shouldAutoEnrichQuickAdd = isCoordinatePlaceholder || !selectedLocation.place_id; + const resolvedLocationName = (formatted.location_name || '').trim(); + const resolvedDisplayName = (formatted.display_name || '').trim(); + + selectedLocation = { + ...selectedLocation, + name: isCoordinatePlaceholder + ? resolvedLocationName || resolvedDisplayName || selectedLocation.name + : selectedLocation.name, + location: resolvedDisplayName || `${lat.toFixed(4)}, ${lng.toFixed(4)}` + }; + searchQuery = selectedLocation.name; + + if (shouldAutoEnrichQuickAdd && resolvedLocationName) { + await enrichFromResolvedName(lat, lng, resolvedLocationName); + } } } catch (error) { console.error('Detailed reverse geocoding error:', error); @@ -519,6 +493,8 @@ } async function quickAdd() { + await ensureAdventureLogFormattedLocation(); + const prefill = buildPrefillPayload(); if (!prefill) { addToast('warning', `Please select a place or drop a pin first`); diff --git a/frontend/src/lib/config.ts b/frontend/src/lib/config.ts index e18717b8..0a46b5e5 100644 --- a/frontend/src/lib/config.ts +++ b/frontend/src/lib/config.ts @@ -1,4 +1,4 @@ -export let appVersion = 'v0.12.1-beta-061326'; +export let appVersion = 'v0.12.1-beta-061426'; export let versionChangelog = 'https://github.com/seanmorley15/AdventureLog/releases/tag/v0.12.1'; export let appTitle = 'AdventureLog'; export let copyrightYear = '2023-2026'; diff --git a/frontend/src/lib/index.server.ts b/frontend/src/lib/index.server.ts index aee1ea0e..aacdaa1f 100644 --- a/frontend/src/lib/index.server.ts +++ b/frontend/src/lib/index.server.ts @@ -1 +1 @@ -export { fetchCSRFToken, getServerEndpoint } from '$lib/server/django-proxy'; +export { backendApiUrl, fetchCSRFToken, getServerEndpoint } from '$lib/server/django-proxy'; diff --git a/frontend/src/lib/map/places.ts b/frontend/src/lib/map/places.ts index aa737861..47a76662 100644 --- a/frontend/src/lib/map/places.ts +++ b/frontend/src/lib/map/places.ts @@ -98,6 +98,73 @@ export async function enrichPlace(place: PlaceSearchResult): Promise { + try { + const response = await fetch(`/api/places/reverse/?lat=${lat}&lon=${lng}&format=json`); + if (!response.ok) { + return null; + } + + const data = await response.json(); + const providerUsed = data.provider_used || data.provider || null; + return { + city: data.city + ? { + name: data.city, + id: data.city_id, + visited: data.city_visited || false + } + : undefined, + region: data.region + ? { + name: data.region, + id: data.region_id, + visited: data.region_visited || false + } + : undefined, + country: data.country + ? { + name: data.country, + country_code: data.country_id, + visited: false + } + : undefined, + display_name: data.display_name, + location_name: data.location_name, + provider: providerUsed + }; + } catch { + return null; + } +} + +export function resolveQuickAddLocationLabel( + formatted: FormattedLocationData | null, + fallbackLocation = '' +): string { + return (formatted?.display_name || '').trim() || fallbackLocation.trim() || ''; +} + +export async function resolveQuickAddPayload(payload: QuickAddPayload): Promise { + const formatted = await fetchFormattedLocation(payload.latitude, payload.longitude); + return { + ...payload, + location: resolveQuickAddLocationLabel(formatted, payload.location) + }; +} + export type QuickAddPayload = { name: string; latitude: number; diff --git a/frontend/src/lib/server/django-proxy.ts b/frontend/src/lib/server/django-proxy.ts index 5f8cef43..38568ad5 100644 --- a/frontend/src/lib/server/django-proxy.ts +++ b/frontend/src/lib/server/django-proxy.ts @@ -22,8 +22,8 @@ type ProxyOptions = { formatSearchParam?: (search: string, method: string) => string; }; -const authTrailingSlashPaths = ['disable-password', 'mobile-qr']; -const authTrailingSlashPrefixes = ['api-keys']; +/** Allauth headless routes omit trailing slashes; custom auth routes use them. */ +const authNoTrailingSlashPrefixes = ['browser/', 'app/']; /** Headers that must not be forwarded to Node fetch (undici rejects hop-by-hop headers). */ const FORBIDDEN_PROXY_HEADERS = new Set([ @@ -60,11 +60,31 @@ function defaultFormatSearchParam(search: string, method: string, basePath: Prox } function authRequiresTrailingSlash(path: string, requireTrailingSlash: boolean): boolean { - return ( - requireTrailingSlash || - authTrailingSlashPaths.includes(path) || - authTrailingSlashPrefixes.some((prefix) => path === prefix || path.startsWith(`${prefix}/`)) - ); + if (authNoTrailingSlashPrefixes.some((prefix) => path.startsWith(prefix))) { + return false; + } + return requireTrailingSlash; +} + +function apiRequiresTrailingSlash(path: string, requireTrailingSlash: boolean): boolean { + if (requireTrailingSlash === false) { + return false; + } + // Preserve trailing slash for nested action paths (e.g. locations//duplicate/). + if (path.endsWith('/')) { + return false; + } + return true; +} + +/** Build a backend API URL with a trailing slash before query params. */ +export function backendApiUrl(path: string, search = ''): string { + const endpoint = getServerEndpoint(); + const normalized = path.startsWith('/') ? path : `/${path}`; + const [pathname, embeddedSearch = ''] = normalized.split('?'); + const slashPath = pathname.endsWith('/') ? pathname : `${pathname}/`; + const query = search || embeddedSearch; + return `${endpoint}${slashPath}${query ? (query.startsWith('?') ? query : `?${query}`) : ''}`; } export async function proxyToDjango( @@ -79,8 +99,8 @@ export async function proxyToDjango( const requireTrailingSlash = basePath === 'auth' - ? authRequiresTrailingSlash(path, options.requireTrailingSlash ?? false) - : (options.requireTrailingSlash ?? false); + ? authRequiresTrailingSlash(path, options.requireTrailingSlash ?? true) + : apiRequiresTrailingSlash(path, options.requireTrailingSlash ?? true); if (requireTrailingSlash && !targetUrl.endsWith('/')) { targetUrl += '/'; diff --git a/frontend/src/routes/api/[...path]/+server.ts b/frontend/src/routes/api/[...path]/+server.ts index b0029013..7857e195 100644 --- a/frontend/src/routes/api/[...path]/+server.ts +++ b/frontend/src/routes/api/[...path]/+server.ts @@ -1,17 +1,12 @@ import { proxyToDjango } from '$lib/server/django-proxy'; import type { RequestHandler } from './$types'; -export const GET: RequestHandler = (event) => - proxyToDjango(event, 'api', { requireTrailingSlash: false }); +export const GET: RequestHandler = (event) => proxyToDjango(event, 'api'); -export const POST: RequestHandler = (event) => - proxyToDjango(event, 'api', { requireTrailingSlash: true }); +export const POST: RequestHandler = (event) => proxyToDjango(event, 'api'); -export const PATCH: RequestHandler = (event) => - proxyToDjango(event, 'api', { requireTrailingSlash: true }); +export const PATCH: RequestHandler = (event) => proxyToDjango(event, 'api'); -export const PUT: RequestHandler = (event) => - proxyToDjango(event, 'api', { requireTrailingSlash: true }); +export const PUT: RequestHandler = (event) => proxyToDjango(event, 'api'); -export const DELETE: RequestHandler = (event) => - proxyToDjango(event, 'api', { requireTrailingSlash: true }); +export const DELETE: RequestHandler = (event) => proxyToDjango(event, 'api'); diff --git a/frontend/src/routes/collections/[id]/+page.server.ts b/frontend/src/routes/collections/[id]/+page.server.ts index 1ecce247..853c7c2a 100644 --- a/frontend/src/routes/collections/[id]/+page.server.ts +++ b/frontend/src/routes/collections/[id]/+page.server.ts @@ -34,9 +34,7 @@ export const load = (async (event) => { }) satisfies PageServerLoad; import type { Actions } from '@sveltejs/kit'; -import { fetchCSRFToken } from '$lib/index.server'; - -const serverEndpoint = PUBLIC_SERVER_URL || 'http://localhost:8000'; +import { backendApiUrl, fetchCSRFToken } from '$lib/index.server'; export const actions: Actions = { delete: async (event) => { @@ -61,7 +59,7 @@ export const actions: Actions = { let csrfToken = await fetchCSRFToken(); - let res = await fetch(`${serverEndpoint}/api/collections/${event.params.id}`, { + let res = await fetch(backendApiUrl(`/api/collections/${event.params.id}`), { method: 'DELETE', headers: { Cookie: `sessionid=${sessionId}; csrftoken=${csrfToken}`, diff --git a/frontend/src/routes/locations/+page.server.ts b/frontend/src/routes/locations/+page.server.ts index fc3a8c94..1cb542a1 100755 --- a/frontend/src/routes/locations/+page.server.ts +++ b/frontend/src/routes/locations/+page.server.ts @@ -4,7 +4,7 @@ const PUBLIC_SERVER_URL = process.env['PUBLIC_SERVER_URL']; import type { Location } from '$lib/types'; import type { Actions } from '@sveltejs/kit'; -import { fetchCSRFToken } from '$lib/index.server'; +import { backendApiUrl, fetchCSRFToken } from '$lib/index.server'; const serverEndpoint = PUBLIC_SERVER_URL || 'http://localhost:8000'; @@ -29,7 +29,10 @@ export const load = (async (event) => { const is_visited = event.url.searchParams.get('is_visited') || 'all'; let initialFetch = await event.fetch( - `${serverEndpoint}/api/locations/filtered?types=${typeString}&order_by=${order_by}&order_direction=${order_direction}&include_collections=${include_collections}&page=${page}&is_visited=${is_visited}`, + backendApiUrl( + '/api/locations/filtered', + `types=${typeString}&order_by=${order_by}&order_direction=${order_direction}&include_collections=${include_collections}&page=${page}&is_visited=${is_visited}` + ), { headers: { Cookie: `sessionid=${event.cookies.get('sessionid')}` diff --git a/frontend/src/routes/locations/[id]/+page.server.ts b/frontend/src/routes/locations/[id]/+page.server.ts index 2c3dc9bb..ee2c3dcf 100644 --- a/frontend/src/routes/locations/[id]/+page.server.ts +++ b/frontend/src/routes/locations/[id]/+page.server.ts @@ -33,9 +33,7 @@ export const load = (async (event) => { }) satisfies PageServerLoad; import { redirect, type Actions } from '@sveltejs/kit'; -import { fetchCSRFToken } from '$lib/index.server'; - -const serverEndpoint = PUBLIC_SERVER_URL || 'http://localhost:8000'; +import { backendApiUrl, fetchCSRFToken } from '$lib/index.server'; export const actions: Actions = { delete: async (event) => { @@ -54,7 +52,7 @@ export const actions: Actions = { let csrfToken = await fetchCSRFToken(); - let res = await fetch(`${serverEndpoint}/api/locations/${event.params.id}`, { + let res = await fetch(backendApiUrl(`/api/locations/${event.params.id}`), { method: 'DELETE', headers: { Referer: event.url.origin, // Include Referer header diff --git a/frontend/src/routes/map/+page.svelte b/frontend/src/routes/map/+page.svelte index 7d8d6612..56e5a27a 100644 --- a/frontend/src/routes/map/+page.svelte +++ b/frontend/src/routes/map/+page.svelte @@ -28,7 +28,8 @@ placeToQuickAddPayload, quickAddLocation, recommendationToLocationPrefill, - recommendationToQuickAddPayload + recommendationToQuickAddPayload, + resolveQuickAddPayload } from '$lib/map/places'; import MapIcon from '~icons/mdi/map'; @@ -572,7 +573,8 @@ selected.kind === 'place' ? placeToQuickAddPayload(selected.place) : recommendationToQuickAddPayload(selected.item); - const created = await quickAddLocation(payload); + const resolved = await resolveQuickAddPayload(payload); + const created = await quickAddLocation(resolved); addToast('success', $t('map.quick_add')); const newPin: Pin = { id: created.id, diff --git a/frontend/vite.config.ts b/frontend/vite.config.ts index 126a520f..7b0edb35 100644 --- a/frontend/vite.config.ts +++ b/frontend/vite.config.ts @@ -3,22 +3,11 @@ import { defineConfig } from 'vite'; import { sveltekit } from '@sveltejs/kit/vite'; import Icons from 'unplugin-icons/vite'; -const backendTarget = process.env.PUBLIC_SERVER_URL ?? 'http://127.0.0.1:8000'; -const useDevProxy = !process.env.CI && process.env.VITEST !== 'true'; - export default defineConfig({ plugins: [ sveltekit(), Icons({ compiler: 'svelte' }) - ], - server: useDevProxy - ? { - proxy: { - '/api': { target: backendTarget, changeOrigin: true }, - '/auth': { target: backendTarget, changeOrigin: true } - } - } - : undefined + ] });