Python · 17250 bytes Raw Blame History
1 from django.http import JsonResponse, HttpResponse
2 from django.utils import timezone
3 from django.conf import settings
4 from django.db.models import Q
5 from rest_framework import status, generics
6 from rest_framework.decorators import api_view
7 from rest_framework.response import Response
8 from rest_framework.views import APIView
9 import requests
10 import math
11 from .models import Restaurant, Rating
12 from .serializers import (
13 RestaurantSerializer, RestaurantDetailSerializer,
14 CreateRatingSerializer, RatingSerializer, PlaceSearchSerializer
15 )
16
17
18 def health_check(request):
19 """Simple health check endpoint"""
20 return JsonResponse({
21 'status': 'LocalToast is cooking! 🍞',
22 'timestamp': timezone.now().isoformat(),
23 'version': '2.0'
24 })
25
26
27 def calculate_distance(lat1, lon1, lat2, lon2):
28 """Calculate distance between two points in kilometers"""
29 R = 6371 # Radius of Earth in km
30 dlat = math.radians(lat2 - lat1)
31 dlon = math.radians(lon2 - lon1)
32 a = (math.sin(dlat/2) * math.sin(dlat/2) +
33 math.cos(math.radians(lat1)) * math.cos(math.radians(lat2)) *
34 math.sin(dlon/2) * math.sin(dlon/2))
35 c = 2 * math.atan2(math.sqrt(a), math.sqrt(1-a))
36 return R * c
37
38
39 class NearbyRestaurantsView(APIView):
40 """Get restaurants near a location"""
41
42 def options(self, request, *args, **kwargs):
43 """Handle preflight requests"""
44 response = HttpResponse()
45 response['Access-Control-Allow-Origin'] = request.headers.get('Origin', '*')
46 response['Access-Control-Allow-Methods'] = 'GET, POST, OPTIONS'
47 response['Access-Control-Allow-Headers'] = 'Content-Type, Accept, X-Requested-With'
48 response['Access-Control-Max-Age'] = '3600'
49 return response
50
51 def get(self, request):
52 lat = request.query_params.get('lat')
53 lng = request.query_params.get('lng')
54 radius = float(request.query_params.get('radius', 5)) # km
55
56 if not lat or not lng:
57 return Response(
58 {'error': 'Latitude and longitude are required'},
59 status=status.HTTP_400_BAD_REQUEST
60 )
61
62 try:
63 lat = float(lat)
64 lng = float(lng)
65 except ValueError:
66 return Response(
67 {'error': 'Invalid latitude or longitude'},
68 status=status.HTTP_400_BAD_REQUEST
69 )
70
71 # Simple bounding box calculation
72 lat_diff = radius / 111 # 1 degree latitude ≈ 111 km
73 lng_diff = radius / (111 * math.cos(math.radians(lat)))
74
75 restaurants = Restaurant.objects.filter(
76 latitude__range=(lat - lat_diff, lat + lat_diff),
77 longitude__range=(lng - lng_diff, lng + lng_diff)
78 )
79
80 # Calculate actual distances and filter
81 results = []
82 for restaurant in restaurants:
83 distance = calculate_distance(
84 lat, lng,
85 restaurant.latitude,
86 restaurant.longitude
87 )
88 if distance <= radius:
89 serializer = RestaurantSerializer(restaurant)
90 data = serializer.data
91 data['distance'] = round(distance, 2)
92 results.append(data)
93
94 # Sort by distance
95 results.sort(key=lambda x: x['distance'])
96
97 return Response(results)
98
99
100 class RestaurantListCreateView(generics.ListCreateAPIView):
101 """List all restaurants or create a new one"""
102 queryset = Restaurant.objects.all()
103 serializer_class = RestaurantSerializer
104
105 def options(self, request, *args, **kwargs):
106 """Handle preflight requests"""
107 response = HttpResponse()
108 response['Access-Control-Allow-Origin'] = request.headers.get('Origin', '*')
109 response['Access-Control-Allow-Methods'] = 'GET, POST, OPTIONS'
110 response['Access-Control-Allow-Headers'] = 'Content-Type, Accept, X-Requested-With'
111 response['Access-Control-Max-Age'] = '3600'
112 return response
113
114 def create(self, request, *args, **kwargs):
115 # Check if restaurant already exists
116 place_id = request.data.get('place_id')
117 if place_id:
118 existing = Restaurant.objects.filter(place_id=place_id).first()
119 if existing:
120 serializer = self.get_serializer(existing)
121 return Response(serializer.data, status=status.HTTP_200_OK)
122
123 return super().create(request, *args, **kwargs)
124
125
126 class RestaurantDetailView(generics.RetrieveAPIView):
127 """Get detailed info about a restaurant"""
128 queryset = Restaurant.objects.all()
129 serializer_class = RestaurantDetailSerializer
130
131 def options(self, request, *args, **kwargs):
132 """Handle preflight requests"""
133 response = HttpResponse()
134 response['Access-Control-Allow-Origin'] = request.headers.get('Origin', '*')
135 response['Access-Control-Allow-Methods'] = 'GET, OPTIONS'
136 response['Access-Control-Allow-Headers'] = 'Content-Type, Accept, X-Requested-With'
137 response['Access-Control-Max-Age'] = '3600'
138 return response
139
140
141 class RestaurantRatingView(APIView):
142 """Add a rating to a restaurant or get all ratings"""
143
144 def options(self, request, *args, **kwargs):
145 """Handle preflight requests"""
146 response = HttpResponse()
147 response['Access-Control-Allow-Origin'] = request.headers.get('Origin', '*')
148 response['Access-Control-Allow-Methods'] = 'GET, POST, OPTIONS'
149 response['Access-Control-Allow-Headers'] = 'Content-Type, Accept, X-Requested-With'
150 response['Access-Control-Max-Age'] = '3600'
151 return response
152
153 def get(self, request, pk):
154 try:
155 restaurant = Restaurant.objects.get(pk=pk)
156 except Restaurant.DoesNotExist:
157 return Response(
158 {'error': 'Restaurant not found'},
159 status=status.HTTP_404_NOT_FOUND
160 )
161
162 ratings = restaurant.ratings.all()
163 serializer = RatingSerializer(ratings, many=True)
164 return Response(serializer.data)
165
166 def post(self, request, pk):
167 try:
168 restaurant = Restaurant.objects.get(pk=pk)
169 except Restaurant.DoesNotExist:
170 return Response(
171 {'error': 'Restaurant not found'},
172 status=status.HTTP_404_NOT_FOUND
173 )
174
175 serializer = CreateRatingSerializer(data=request.data)
176 if serializer.is_valid():
177 rating = serializer.save(restaurant=restaurant)
178 return Response(
179 {
180 'id': rating.id,
181 'message': 'Toast rating added successfully! 🍞'
182 },
183 status=status.HTTP_201_CREATED
184 )
185 return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
186
187
188 class RestaurantToastStatusView(APIView):
189 """Update restaurant's toast status"""
190
191 def options(self, request, *args, **kwargs):
192 """Handle preflight requests"""
193 response = HttpResponse()
194 response['Access-Control-Allow-Origin'] = request.headers.get('Origin', '*')
195 response['Access-Control-Allow-Methods'] = 'PATCH, OPTIONS'
196 response['Access-Control-Allow-Headers'] = 'Content-Type, Accept, X-Requested-With'
197 response['Access-Control-Max-Age'] = '3600'
198 return response
199
200 def patch(self, request, pk):
201 try:
202 restaurant = Restaurant.objects.get(pk=pk)
203 except Restaurant.DoesNotExist:
204 return Response(
205 {'error': 'Restaurant not found'},
206 status=status.HTTP_404_NOT_FOUND
207 )
208
209 has_toast = request.data.get('has_toast')
210 if has_toast is None:
211 return Response(
212 {'error': 'has_toast field is required'},
213 status=status.HTTP_400_BAD_REQUEST
214 )
215
216 restaurant.has_toast = has_toast
217 restaurant.save()
218
219 return Response({
220 'id': restaurant.id,
221 'has_toast': restaurant.has_toast,
222 'message': f'Toast status updated! {"🍞" if has_toast else "❌"}'
223 })
224
225
226 class SearchPlacesView(APIView):
227 """Search for places that might serve toast"""
228
229 def options(self, request, *args, **kwargs):
230 """Handle preflight requests"""
231 response = HttpResponse()
232 response['Access-Control-Allow-Origin'] = request.headers.get('Origin', '*')
233 response['Access-Control-Allow-Methods'] = 'GET, OPTIONS'
234 response['Access-Control-Allow-Headers'] = 'Content-Type, Accept, X-Requested-With'
235 response['Access-Control-Max-Age'] = '3600'
236 return response
237
238 def get(self, request):
239 lat = request.query_params.get('lat')
240 lng = request.query_params.get('lng')
241 radius = request.query_params.get('radius', '1000')
242
243 if not lat or not lng:
244 return Response(
245 {'error': 'Latitude and longitude are required'},
246 status=status.HTTP_400_BAD_REQUEST
247 )
248
249 places = []
250
251 # Try Google Places API first if available
252 if settings.GOOGLE_PLACES_API_KEY:
253 try:
254 google_results = self.search_google_places(lat, lng, radius)
255 places.extend(google_results)
256 except Exception as e:
257 print(f"Google Places API error: {e}")
258
259 # If no results from Google, try OpenStreetMap
260 if not places:
261 try:
262 osm_results = self.search_openstreetmap(lat, lng, radius)
263 places.extend(osm_results)
264 except Exception as e:
265 print(f"OpenStreetMap error: {e}")
266
267 # If still no results, return mock data
268 if not places:
269 places = self.get_mock_places(float(lat), float(lng))
270
271 # Calculate distances and sort
272 user_lat, user_lng = float(lat), float(lng)
273 for place in places:
274 place['distance'] = calculate_distance(
275 user_lat, user_lng,
276 place['latitude'], place['longitude']
277 )
278
279 places.sort(key=lambda x: x['distance'])
280
281 # Auto-create restaurants for all found places
282 created_restaurants = []
283 for place in places[:20]: # Limit to top 20
284 restaurant, created = Restaurant.objects.get_or_create(
285 place_id=place['place_id'],
286 defaults={
287 'name': place['name'],
288 'address': place['address'],
289 'latitude': place['latitude'],
290 'longitude': place['longitude'],
291 'has_toast': None # Unknown status initially
292 }
293 )
294 # Add restaurant data to place info
295 place['restaurant_id'] = restaurant.id
296 place['has_toast'] = restaurant.has_toast
297 created_restaurants.append(place)
298
299 serializer = PlaceSearchSerializer(created_restaurants, many=True)
300 return Response(serializer.data)
301
302 def search_google_places(self, lat, lng, radius):
303 """Search using Google Places API"""
304 url = 'https://maps.googleapis.com/maps/api/place/textsearch/json'
305 params = {
306 'query': 'cafe bakery breakfast toast',
307 'location': f'{lat},{lng}',
308 'radius': radius,
309 'type': 'restaurant|cafe|bakery',
310 'key': settings.GOOGLE_PLACES_API_KEY
311 }
312
313 response = requests.get(url, params=params, timeout=10)
314 response.raise_for_status()
315 data = response.json()
316
317 places = []
318 for place in data.get('results', []):
319 confidence = 'high' if 'toast' in place['name'].lower() else 'medium'
320 if any(word in place['name'].lower() for word in ['cafe', 'bakery', 'breakfast']):
321 confidence = 'high'
322
323 places.append({
324 'place_id': place['place_id'],
325 'name': place['name'],
326 'address': place.get('formatted_address', place.get('vicinity', '')),
327 'latitude': place['geometry']['location']['lat'],
328 'longitude': place['geometry']['location']['lng'],
329 'category': 'restaurant',
330 'confidence': confidence,
331 'source': 'google_places'
332 })
333
334 return places
335
336 def search_openstreetmap(self, lat, lng, radius):
337 """Search using OpenStreetMap Overpass API"""
338 overpass_query = f"""
339 [out:json][timeout:25];
340 (
341 node["amenity"="cafe"](around:{radius},{lat},{lng});
342 node["shop"="bakery"](around:{radius},{lat},{lng});
343 node["amenity"="restaurant"]["cuisine"~"breakfast"](around:{radius},{lat},{lng});
344 );
345 out body;
346 """
347
348 url = 'https://overpass-api.de/api/interpreter'
349 response = requests.post(
350 url,
351 data={'data': overpass_query},
352 timeout=10
353 )
354 response.raise_for_status()
355 data = response.json()
356
357 places = []
358 for element in data.get('elements', []):
359 if not element.get('tags', {}).get('name'):
360 continue
361
362 tags = element['tags']
363 category = tags.get('amenity') or tags.get('shop', 'unknown')
364
365 # Determine confidence
366 confidence = 'low'
367 if category in ['cafe', 'bakery']:
368 confidence = 'medium'
369 if 'breakfast' in tags.get('cuisine', '').lower():
370 confidence = 'high'
371
372 places.append({
373 'place_id': f"osm_{element['id']}",
374 'name': tags['name'],
375 'address': self.format_osm_address(tags),
376 'latitude': element['lat'],
377 'longitude': element['lon'],
378 'category': category,
379 'cuisine': tags.get('cuisine', ''),
380 'confidence': confidence,
381 'source': 'openstreetmap'
382 })
383
384 return places
385
386 def format_osm_address(self, tags):
387 """Format address from OSM tags"""
388 parts = []
389 for key in ['addr:housenumber', 'addr:street', 'addr:city']:
390 if key in tags:
391 parts.append(tags[key])
392 return ' '.join(parts) if parts else 'Address not available'
393
394 def get_mock_places(self, lat, lng):
395 """Return mock data as fallback"""
396 return [
397 {
398 'place_id': 'mock_1',
399 'name': 'The Breakfast Club',
400 'address': '123 Toast Lane',
401 'latitude': lat + 0.01,
402 'longitude': lng + 0.01,
403 'category': 'restaurant',
404 'confidence': 'low',
405 'source': 'mock'
406 }
407 ]
408
409
410 @api_view(['GET', 'OPTIONS'])
411 def cors_test(request):
412 """Test endpoint for CORS"""
413 if request.method == 'OPTIONS':
414 response = HttpResponse()
415 response['Access-Control-Allow-Origin'] = '*'
416 response['Access-Control-Allow-Methods'] = 'GET, OPTIONS'
417 response['Access-Control-Allow-Headers'] = '*'
418 return response
419
420 return Response({
421 'status': 'CORS test successful',
422 'origin': request.headers.get('Origin', 'No origin header'),
423 'method': request.method
424 })
425
426
427 @api_view(['POST'])
428 def seed_data(request):
429 """Seed the database with sample restaurants"""
430 lat = float(request.data.get('lat', 40.7128))
431 lng = float(request.data.get('lng', -74.0060))
432
433 seed_restaurants = [
434 {
435 'place_id': 'seed_1',
436 'name': 'Toast & Jam Café',
437 'address': '100 Breakfast Blvd',
438 'latitude': lat + 0.005,
439 'longitude': lng + 0.005
440 },
441 {
442 'place_id': 'seed_2',
443 'name': 'The Golden Toast',
444 'address': '200 Butter Lane',
445 'latitude': lat - 0.005,
446 'longitude': lng + 0.005
447 },
448 {
449 'place_id': 'seed_3',
450 'name': 'Morning Glory Diner',
451 'address': '300 Sunrise Ave',
452 'latitude': lat + 0.005,
453 'longitude': lng - 0.005
454 },
455 {
456 'place_id': 'seed_4',
457 'name': 'Crispy Corner',
458 'address': '400 Crunch St',
459 'latitude': lat - 0.005,
460 'longitude': lng - 0.005
461 }
462 ]
463
464 created = 0
465 for restaurant_data in seed_restaurants:
466 restaurant, was_created = Restaurant.objects.get_or_create(
467 place_id=restaurant_data['place_id'],
468 defaults=restaurant_data
469 )
470 if was_created:
471 created += 1
472
473 return Response({
474 'message': f'Seeded {created} restaurants with toast! 🍞',
475 'total_restaurants': Restaurant.objects.count()
476 })