mirror of
https://github.com/seanmorley15/AdventureLog.git
synced 2026-08-02 08:51:32 -04:00
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.
This commit is contained in:
@@ -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'),
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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`);
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -1 +1 @@
|
||||
export { fetchCSRFToken, getServerEndpoint } from '$lib/server/django-proxy';
|
||||
export { backendApiUrl, fetchCSRFToken, getServerEndpoint } from '$lib/server/django-proxy';
|
||||
|
||||
@@ -98,6 +98,73 @@ export async function enrichPlace(place: PlaceSearchResult): Promise<PlaceSearch
|
||||
}
|
||||
}
|
||||
|
||||
export type FormattedLocationData = {
|
||||
city?: { name: string; id: string; visited: boolean };
|
||||
region?: { name: string; id: string; visited: boolean };
|
||||
country?: { name: string; country_code: string; visited: boolean };
|
||||
display_name?: string;
|
||||
location_name?: string;
|
||||
provider?: string;
|
||||
};
|
||||
|
||||
export async function fetchFormattedLocation(
|
||||
lat: number,
|
||||
lng: number
|
||||
): Promise<FormattedLocationData | null> {
|
||||
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<QuickAddPayload> {
|
||||
const formatted = await fetchFormattedLocation(payload.latitude, payload.longitude);
|
||||
return {
|
||||
...payload,
|
||||
location: resolveQuickAddLocationLabel(formatted, payload.location)
|
||||
};
|
||||
}
|
||||
|
||||
export type QuickAddPayload = {
|
||||
name: string;
|
||||
latitude: number;
|
||||
|
||||
@@ -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/<id>/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 += '/';
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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}`,
|
||||
|
||||
@@ -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')}`
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
]
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user