Python · 13998 bytes Raw Blame History
1 from django.http import JsonResponse
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 get(self, request):
43 lat = request.query_params.get('lat')
44 lng = request.query_params.get('lng')
45 radius = float(request.query_params.get('radius', 5)) # km
46
47 if not lat or not lng:
48 return Response(
49 {'error': 'Latitude and longitude are required'},
50 status=status.HTTP_400_BAD_REQUEST
51 )
52
53 try:
54 lat = float(lat)
55 lng = float(lng)
56 except ValueError:
57 return Response(
58 {'error': 'Invalid latitude or longitude'},
59 status=status.HTTP_400_BAD_REQUEST
60 )
61
62 # Simple bounding box calculation
63 lat_diff = radius / 111 # 1 degree latitude ≈ 111 km
64 lng_diff = radius / (111 * math.cos(math.radians(lat)))
65
66 restaurants = Restaurant.objects.filter(
67 latitude__range=(lat - lat_diff, lat + lat_diff),
68 longitude__range=(lng - lng_diff, lng + lng_diff)
69 )
70
71 # Calculate actual distances and filter
72 results = []
73 for restaurant in restaurants:
74 distance = calculate_distance(
75 lat, lng,
76 restaurant.latitude,
77 restaurant.longitude
78 )
79 if distance <= radius:
80 serializer = RestaurantSerializer(restaurant)
81 data = serializer.data
82 data['distance'] = round(distance, 2)
83 results.append(data)
84
85 # Sort by distance
86 results.sort(key=lambda x: x['distance'])
87
88 return Response(results)
89
90
91 class RestaurantListCreateView(generics.ListCreateAPIView):
92 """List all restaurants or create a new one"""
93 queryset = Restaurant.objects.all()
94 serializer_class = RestaurantSerializer
95
96 def create(self, request, *args, **kwargs):
97 # Check if restaurant already exists
98 place_id = request.data.get('place_id')
99 if place_id:
100 existing = Restaurant.objects.filter(place_id=place_id).first()
101 if existing:
102 serializer = self.get_serializer(existing)
103 return Response(serializer.data, status=status.HTTP_200_OK)
104
105 return super().create(request, *args, **kwargs)
106
107
108 class RestaurantDetailView(generics.RetrieveAPIView):
109 """Get detailed info about a restaurant"""
110 queryset = Restaurant.objects.all()
111 serializer_class = RestaurantDetailSerializer
112
113
114 class RestaurantRatingView(APIView):
115 """Add a rating to a restaurant or get all ratings"""
116
117 def get(self, request, pk):
118 try:
119 restaurant = Restaurant.objects.get(pk=pk)
120 except Restaurant.DoesNotExist:
121 return Response(
122 {'error': 'Restaurant not found'},
123 status=status.HTTP_404_NOT_FOUND
124 )
125
126 ratings = restaurant.ratings.all()
127 serializer = RatingSerializer(ratings, many=True)
128 return Response(serializer.data)
129
130 def post(self, request, pk):
131 try:
132 restaurant = Restaurant.objects.get(pk=pk)
133 except Restaurant.DoesNotExist:
134 return Response(
135 {'error': 'Restaurant not found'},
136 status=status.HTTP_404_NOT_FOUND
137 )
138
139 serializer = CreateRatingSerializer(data=request.data)
140 if serializer.is_valid():
141 rating = serializer.save(restaurant=restaurant)
142 return Response(
143 {
144 'id': rating.id,
145 'message': 'Toast rating added successfully! 🍞'
146 },
147 status=status.HTTP_201_CREATED
148 )
149 return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
150
151
152 class RestaurantToastStatusView(APIView):
153 """Update restaurant's toast status"""
154
155 def patch(self, request, pk):
156 try:
157 restaurant = Restaurant.objects.get(pk=pk)
158 except Restaurant.DoesNotExist:
159 return Response(
160 {'error': 'Restaurant not found'},
161 status=status.HTTP_404_NOT_FOUND
162 )
163
164 has_toast = request.data.get('has_toast')
165 if has_toast is None:
166 return Response(
167 {'error': 'has_toast field is required'},
168 status=status.HTTP_400_BAD_REQUEST
169 )
170
171 restaurant.has_toast = has_toast
172 restaurant.save()
173
174 return Response({
175 'id': restaurant.id,
176 'has_toast': restaurant.has_toast,
177 'message': f'Toast status updated! {"🍞" if has_toast else "❌"}'
178 })
179
180
181 class SearchPlacesView(APIView):
182 """Search for places that might serve toast"""
183
184 def get(self, request):
185 lat = request.query_params.get('lat')
186 lng = request.query_params.get('lng')
187 radius = request.query_params.get('radius', '1000')
188
189 if not lat or not lng:
190 return Response(
191 {'error': 'Latitude and longitude are required'},
192 status=status.HTTP_400_BAD_REQUEST
193 )
194
195 places = []
196
197 # Try Google Places API first if available
198 if settings.GOOGLE_PLACES_API_KEY:
199 try:
200 google_results = self.search_google_places(lat, lng, radius)
201 places.extend(google_results)
202 except Exception as e:
203 print(f"Google Places API error: {e}")
204
205 # If no results from Google, try OpenStreetMap
206 if not places:
207 try:
208 osm_results = self.search_openstreetmap(lat, lng, radius)
209 places.extend(osm_results)
210 except Exception as e:
211 print(f"OpenStreetMap error: {e}")
212
213 # If still no results, return mock data
214 if not places:
215 places = self.get_mock_places(float(lat), float(lng))
216
217 # Calculate distances and sort
218 user_lat, user_lng = float(lat), float(lng)
219 for place in places:
220 place['distance'] = calculate_distance(
221 user_lat, user_lng,
222 place['latitude'], place['longitude']
223 )
224
225 places.sort(key=lambda x: x['distance'])
226
227 # Auto-create restaurants for all found places
228 created_restaurants = []
229 for place in places[:20]: # Limit to top 20
230 restaurant, created = Restaurant.objects.get_or_create(
231 place_id=place['place_id'],
232 defaults={
233 'name': place['name'],
234 'address': place['address'],
235 'latitude': place['latitude'],
236 'longitude': place['longitude'],
237 'has_toast': None # Unknown status initially
238 }
239 )
240 # Add restaurant data to place info
241 place['restaurant_id'] = restaurant.id
242 place['has_toast'] = restaurant.has_toast
243 created_restaurants.append(place)
244
245 serializer = PlaceSearchSerializer(created_restaurants, many=True)
246 return Response(serializer.data)
247
248 def search_google_places(self, lat, lng, radius):
249 """Search using Google Places API"""
250 url = 'https://maps.googleapis.com/maps/api/place/textsearch/json'
251 params = {
252 'query': 'cafe bakery breakfast toast',
253 'location': f'{lat},{lng}',
254 'radius': radius,
255 'type': 'restaurant|cafe|bakery',
256 'key': settings.GOOGLE_PLACES_API_KEY
257 }
258
259 response = requests.get(url, params=params, timeout=10)
260 response.raise_for_status()
261 data = response.json()
262
263 places = []
264 for place in data.get('results', []):
265 confidence = 'high' if 'toast' in place['name'].lower() else 'medium'
266 if any(word in place['name'].lower() for word in ['cafe', 'bakery', 'breakfast']):
267 confidence = 'high'
268
269 places.append({
270 'place_id': place['place_id'],
271 'name': place['name'],
272 'address': place.get('formatted_address', place.get('vicinity', '')),
273 'latitude': place['geometry']['location']['lat'],
274 'longitude': place['geometry']['location']['lng'],
275 'category': 'restaurant',
276 'confidence': confidence,
277 'source': 'google_places'
278 })
279
280 return places
281
282 def search_openstreetmap(self, lat, lng, radius):
283 """Search using OpenStreetMap Overpass API"""
284 overpass_query = f"""
285 [out:json][timeout:25];
286 (
287 node["amenity"="cafe"](around:{radius},{lat},{lng});
288 node["shop"="bakery"](around:{radius},{lat},{lng});
289 node["amenity"="restaurant"]["cuisine"~"breakfast"](around:{radius},{lat},{lng});
290 );
291 out body;
292 """
293
294 url = 'https://overpass-api.de/api/interpreter'
295 response = requests.post(
296 url,
297 data={'data': overpass_query},
298 timeout=10
299 )
300 response.raise_for_status()
301 data = response.json()
302
303 places = []
304 for element in data.get('elements', []):
305 if not element.get('tags', {}).get('name'):
306 continue
307
308 tags = element['tags']
309 category = tags.get('amenity') or tags.get('shop', 'unknown')
310
311 # Determine confidence
312 confidence = 'low'
313 if category in ['cafe', 'bakery']:
314 confidence = 'medium'
315 if 'breakfast' in tags.get('cuisine', '').lower():
316 confidence = 'high'
317
318 places.append({
319 'place_id': f"osm_{element['id']}",
320 'name': tags['name'],
321 'address': self.format_osm_address(tags),
322 'latitude': element['lat'],
323 'longitude': element['lon'],
324 'category': category,
325 'cuisine': tags.get('cuisine', ''),
326 'confidence': confidence,
327 'source': 'openstreetmap'
328 })
329
330 return places
331
332 def format_osm_address(self, tags):
333 """Format address from OSM tags"""
334 parts = []
335 for key in ['addr:housenumber', 'addr:street', 'addr:city']:
336 if key in tags:
337 parts.append(tags[key])
338 return ' '.join(parts) if parts else 'Address not available'
339
340 def get_mock_places(self, lat, lng):
341 """Return mock data as fallback"""
342 return [
343 {
344 'place_id': 'mock_1',
345 'name': 'The Breakfast Club',
346 'address': '123 Toast Lane',
347 'latitude': lat + 0.01,
348 'longitude': lng + 0.01,
349 'category': 'restaurant',
350 'confidence': 'low',
351 'source': 'mock'
352 }
353 ]
354
355
356 @api_view(['POST'])
357 def seed_data(request):
358 """Seed the database with sample restaurants"""
359 lat = float(request.data.get('lat', 40.7128))
360 lng = float(request.data.get('lng', -74.0060))
361
362 seed_restaurants = [
363 {
364 'place_id': 'seed_1',
365 'name': 'Toast & Jam Café',
366 'address': '100 Breakfast Blvd',
367 'latitude': lat + 0.005,
368 'longitude': lng + 0.005
369 },
370 {
371 'place_id': 'seed_2',
372 'name': 'The Golden Toast',
373 'address': '200 Butter Lane',
374 'latitude': lat - 0.005,
375 'longitude': lng + 0.005
376 },
377 {
378 'place_id': 'seed_3',
379 'name': 'Morning Glory Diner',
380 'address': '300 Sunrise Ave',
381 'latitude': lat + 0.005,
382 'longitude': lng - 0.005
383 },
384 {
385 'place_id': 'seed_4',
386 'name': 'Crispy Corner',
387 'address': '400 Crunch St',
388 'latitude': lat - 0.005,
389 'longitude': lng - 0.005
390 }
391 ]
392
393 created = 0
394 for restaurant_data in seed_restaurants:
395 restaurant, was_created = Restaurant.objects.get_or_create(
396 place_id=restaurant_data['place_id'],
397 defaults=restaurant_data
398 )
399 if was_created:
400 created += 1
401
402 return Response({
403 'message': f'Seeded {created} restaurants with toast! 🍞',
404 'total_restaurants': Restaurant.objects.count()
405 })