|
3 | 3 | from rest_framework.permissions import IsAuthenticated |
4 | 4 | from rest_framework.response import Response |
5 | 5 | from worldtravel.models import Region, City, VisitedRegion, VisitedCity |
6 | | -from adventures.models import Location |
| 6 | +from adventures.models import Location, Lodging, Transportation |
7 | 7 | from adventures.serializers import LocationSerializer |
8 | 8 | from adventures.geocoding import reverse_geocode |
9 | 9 | from django.conf import settings |
| 10 | +from django.db.models import Q |
10 | 11 | from adventures.geocoding import search_google, search_osm |
11 | 12 |
|
| 13 | +SEARCH_MODE_SUFFIXES = { |
| 14 | + 'airport': ' Airport', |
| 15 | + 'train': ' Station', |
| 16 | + 'bus': ' Bus Station', |
| 17 | + 'location': '', |
| 18 | + 'cab': '', |
| 19 | + 'vtc': '', |
| 20 | +} |
| 21 | + |
12 | 22 | class ReverseGeocodeViewSet(viewsets.ViewSet): |
13 | 23 | permission_classes = [IsAuthenticated] |
14 | 24 |
|
@@ -37,12 +47,186 @@ def search(self, request): |
37 | 47 | try: |
38 | 48 | if getattr(settings, 'GOOGLE_MAPS_API_KEY', None): |
39 | 49 | results = search_google(query) |
| 50 | + # Google returned error dict - fallback to OSM |
| 51 | + if isinstance(results, dict): |
| 52 | + results = search_osm(query) |
40 | 53 | else: |
41 | 54 | results = search_osm(query) |
| 55 | + # Final check: if still an error dict, return it as error |
| 56 | + if isinstance(results, dict): |
| 57 | + return Response(results, status=500) |
42 | 58 | return Response(results) |
43 | 59 | except Exception: |
44 | 60 | return Response({"error": "An internal error occurred while processing the request"}, status=500) |
45 | 61 |
|
| 62 | + @action(detail=False, methods=['get']) |
| 63 | + def unified_search(self, request): |
| 64 | + """ |
| 65 | + Unified search endpoint that combines: |
| 66 | + 1. Geocoding results (addresses from Google Maps/OSM) |
| 67 | + 2. User's own locations |
| 68 | + 3. User's own lodgings |
| 69 | + 4. User's transportation departures and arrivals |
| 70 | +
|
| 71 | + The search_mode parameter controls suffix appended to geocoding queries |
| 72 | + (e.g., 'airport' appends ' Airport') without affecting internal entity queries. |
| 73 | +
|
| 74 | + Returns grouped results by source type for intelligent autocomplete. |
| 75 | + """ |
| 76 | + query = request.query_params.get('query', '') |
| 77 | + search_mode = request.query_params.get('search_mode', 'location') |
| 78 | + include_geocode = request.query_params.get('include_geocode', 'true').lower() == 'true' |
| 79 | + include_locations = request.query_params.get('include_locations', 'true').lower() == 'true' |
| 80 | + include_lodging = request.query_params.get('include_lodging', 'true').lower() == 'true' |
| 81 | + include_transportation = request.query_params.get('include_transportation', 'true').lower() == 'true' |
| 82 | + |
| 83 | + if not query or len(query) < 2: |
| 84 | + return Response({"error": "Query parameter must be at least 2 characters"}, status=400) |
| 85 | + |
| 86 | + results = { |
| 87 | + "addresses": [], |
| 88 | + "locations": [], |
| 89 | + "lodging": [], |
| 90 | + "departures": [], |
| 91 | + "arrivals": [] |
| 92 | + } |
| 93 | + |
| 94 | + # Search user's locations (raw query, no suffix) |
| 95 | + if include_locations: |
| 96 | + locations = Location.objects.filter( |
| 97 | + Q(user=self.request.user) | Q(is_public=True), |
| 98 | + Q(name__icontains=query) | Q(location__icontains=query) |
| 99 | + ).exclude( |
| 100 | + latitude__isnull=True |
| 101 | + ).exclude( |
| 102 | + longitude__isnull=True |
| 103 | + ).order_by('-updated_at')[:10] |
| 104 | + |
| 105 | + results["locations"] = [ |
| 106 | + { |
| 107 | + "id": str(loc.id), |
| 108 | + "name": loc.name, |
| 109 | + "display_name": loc.location or loc.name, |
| 110 | + "lat": float(loc.latitude), |
| 111 | + "lon": float(loc.longitude), |
| 112 | + "type": "location", |
| 113 | + "category": loc.category.name if loc.category else None, |
| 114 | + "source": "location" |
| 115 | + } |
| 116 | + for loc in locations |
| 117 | + ] |
| 118 | + |
| 119 | + # Search user's lodging (raw query, no suffix) |
| 120 | + if include_lodging: |
| 121 | + lodging = Lodging.objects.filter( |
| 122 | + Q(user=self.request.user) | Q(is_public=True), |
| 123 | + Q(name__icontains=query) | Q(location__icontains=query) |
| 124 | + ).exclude( |
| 125 | + latitude__isnull=True |
| 126 | + ).exclude( |
| 127 | + longitude__isnull=True |
| 128 | + ).order_by('-updated_at')[:10] |
| 129 | + |
| 130 | + results["lodging"] = [ |
| 131 | + { |
| 132 | + "id": str(ldg.id), |
| 133 | + "name": ldg.name, |
| 134 | + "display_name": ldg.location or ldg.name, |
| 135 | + "lat": float(ldg.latitude), |
| 136 | + "lon": float(ldg.longitude), |
| 137 | + "type": ldg.type, |
| 138 | + "category": "lodging", |
| 139 | + "source": "lodging" |
| 140 | + } |
| 141 | + for ldg in lodging |
| 142 | + ] |
| 143 | + |
| 144 | + # Search user's transportation departures and arrivals (raw query, no suffix) |
| 145 | + if include_transportation: |
| 146 | + # Search departures (from_location) |
| 147 | + departures = Transportation.objects.filter( |
| 148 | + Q(user=self.request.user) | Q(is_public=True), |
| 149 | + Q(from_location__icontains=query) | Q(name__icontains=query) |
| 150 | + ).exclude( |
| 151 | + origin_latitude__isnull=True |
| 152 | + ).exclude( |
| 153 | + origin_longitude__isnull=True |
| 154 | + ).order_by('-updated_at')[:10] |
| 155 | + |
| 156 | + # Use a set to deduplicate by coordinates |
| 157 | + seen_departures = set() |
| 158 | + for t in departures: |
| 159 | + key = (float(t.origin_latitude), float(t.origin_longitude)) |
| 160 | + if key not in seen_departures: |
| 161 | + seen_departures.add(key) |
| 162 | + results["departures"].append({ |
| 163 | + "id": str(t.id), |
| 164 | + "name": t.from_location or t.name, |
| 165 | + "display_name": t.from_location or t.name, |
| 166 | + "lat": float(t.origin_latitude), |
| 167 | + "lon": float(t.origin_longitude), |
| 168 | + "type": t.type, |
| 169 | + "category": "departure", |
| 170 | + "source": "departure", |
| 171 | + "code": t.start_code or None |
| 172 | + }) |
| 173 | + |
| 174 | + # Search arrivals (to_location) |
| 175 | + arrivals = Transportation.objects.filter( |
| 176 | + Q(user=self.request.user) | Q(is_public=True), |
| 177 | + Q(to_location__icontains=query) | Q(name__icontains=query) |
| 178 | + ).exclude( |
| 179 | + destination_latitude__isnull=True |
| 180 | + ).exclude( |
| 181 | + destination_longitude__isnull=True |
| 182 | + ).order_by('-updated_at')[:10] |
| 183 | + |
| 184 | + # Use a set to deduplicate by coordinates |
| 185 | + seen_arrivals = set() |
| 186 | + for t in arrivals: |
| 187 | + key = (float(t.destination_latitude), float(t.destination_longitude)) |
| 188 | + if key not in seen_arrivals: |
| 189 | + seen_arrivals.add(key) |
| 190 | + results["arrivals"].append({ |
| 191 | + "id": str(t.id), |
| 192 | + "name": t.to_location or t.name, |
| 193 | + "display_name": t.to_location or t.name, |
| 194 | + "lat": float(t.destination_latitude), |
| 195 | + "lon": float(t.destination_longitude), |
| 196 | + "type": t.type, |
| 197 | + "category": "arrival", |
| 198 | + "source": "arrival", |
| 199 | + "code": t.end_code or None |
| 200 | + }) |
| 201 | + |
| 202 | + # Search addresses via geocoding (suffix applied here only) |
| 203 | + if include_geocode: |
| 204 | + try: |
| 205 | + geocode_query = query + SEARCH_MODE_SUFFIXES.get(search_mode, '') |
| 206 | + geocode_results = None |
| 207 | + |
| 208 | + if getattr(settings, 'GOOGLE_MAPS_API_KEY', None): |
| 209 | + geocode_results = search_google(geocode_query) |
| 210 | + # Google returned error dict - fallback to OSM |
| 211 | + if isinstance(geocode_results, dict): |
| 212 | + geocode_results = search_osm(geocode_query) |
| 213 | + else: |
| 214 | + geocode_results = search_osm(geocode_query) |
| 215 | + |
| 216 | + if isinstance(geocode_results, list): |
| 217 | + results["addresses"] = [ |
| 218 | + { |
| 219 | + **r, |
| 220 | + "source": "address" |
| 221 | + } |
| 222 | + for r in geocode_results[:10] |
| 223 | + ] |
| 224 | + except Exception: |
| 225 | + # Geocoding failed, but we can still return internal results |
| 226 | + pass |
| 227 | + |
| 228 | + return Response(results) |
| 229 | + |
46 | 230 | @action(detail=False, methods=['post']) |
47 | 231 | def mark_visited_region(self, request): |
48 | 232 | """ |
|
0 commit comments