diff --git a/mobile_api/views/events.py b/mobile_api/views/events.py index ea19217..4240443 100644 --- a/mobile_api/views/events.py +++ b/mobile_api/views/events.py @@ -11,9 +11,21 @@ from django.views.decorators.csrf import csrf_exempt from django.db.models import Q from datetime import datetime, timedelta import calendar +import math from mobile_api.utils import validate_token_and_get_user +def _haversine_km(lat1, lon1, lat2, lon2): + """Great-circle distance between two points in km.""" + R = 6371.0 + dlat = math.radians(lat2 - lat1) + dlon = math.radians(lon2 - lon1) + a = (math.sin(dlat / 2) ** 2 + + math.cos(math.radians(lat1)) * math.cos(math.radians(lat2)) * + math.sin(dlon / 2) ** 2) + return R * 2 * math.atan2(math.sqrt(a), math.sqrt(1 - a)) + + @method_decorator(csrf_exempt, name='dispatch') class EventTypeListAPIView(APIView): permission_classes = [AllowAny] @@ -79,18 +91,83 @@ class EventListAPI(APIView): page_size = int(data.get("page_size", 50)) per_type = int(data.get("per_type", 0)) - # Build base queryset (lazy - no DB hit yet) + # New optional geo params + user_lat = data.get("latitude") + user_lng = data.get("longitude") + try: + radius_km = float(data.get("radius_km", 10)) + except (ValueError, TypeError): + radius_km = 10 + + # Build base queryset MIN_EVENTS_THRESHOLD = 6 qs = Event.objects.all() - if pincode and pincode != 'all': + used_radius = None + + # Priority 1: Haversine radius filtering (if lat/lng provided) + if user_lat is not None and user_lng is not None: + try: + user_lat = float(user_lat) + user_lng = float(user_lng) + + # Bounding box pre-filter (1 degree lat ≈ 111km) + lat_delta = radius_km / 111.0 + lng_delta = radius_km / (111.0 * max(math.cos(math.radians(user_lat)), 0.01)) + + candidates = qs.filter( + latitude__gte=user_lat - lat_delta, + latitude__lte=user_lat + lat_delta, + longitude__gte=user_lng - lng_delta, + longitude__lte=user_lng + lng_delta, + latitude__isnull=False, + longitude__isnull=False, + ) + + # Exact Haversine filter in Python + nearby_ids = [] + for e in candidates: + if e.latitude is not None and e.longitude is not None: + dist = _haversine_km(user_lat, user_lng, float(e.latitude), float(e.longitude)) + if dist <= radius_km: + nearby_ids.append(e.id) + + # Progressive radius expansion if too few results + if len(nearby_ids) < MIN_EVENTS_THRESHOLD: + for expanded_r in [r for r in [25, 50, 100] if r > radius_km]: + lat_delta_ex = expanded_r / 111.0 + lng_delta_ex = expanded_r / (111.0 * max(math.cos(math.radians(user_lat)), 0.01)) + candidates_ex = qs.filter( + latitude__gte=user_lat - lat_delta_ex, + latitude__lte=user_lat + lat_delta_ex, + longitude__gte=user_lng - lng_delta_ex, + longitude__lte=user_lng + lng_delta_ex, + latitude__isnull=False, + longitude__isnull=False, + ) + nearby_ids = [] + for e in candidates_ex: + if e.latitude is not None and e.longitude is not None: + dist = _haversine_km(user_lat, user_lng, float(e.latitude), float(e.longitude)) + if dist <= expanded_r: + nearby_ids.append(e.id) + if len(nearby_ids) >= MIN_EVENTS_THRESHOLD: + radius_km = expanded_r + break + + if nearby_ids: + qs = qs.filter(id__in=nearby_ids) + used_radius = radius_km + + except (ValueError, TypeError): + pass # Invalid lat/lng — fall back to pincode + + # Priority 2: Pincode filtering (backward compatible fallback) + if used_radius is None and pincode and pincode != 'all': pincode_qs = qs.filter(pincode=pincode) - # Fallback to all events if pincode has too few if pincode_qs.count() >= MIN_EVENTS_THRESHOLD: qs = pincode_qs - # else: keep qs as Event.objects.all() if per_type > 0 and page == 1: - # Diverse mode: one bounded query per event type type_ids = list(qs.values_list('event_type_id', flat=True).distinct()) events_page = [] for tid in sorted(type_ids): @@ -99,19 +176,16 @@ class EventListAPI(APIView): total_count = qs.count() end = len(events_page) else: - # Standard pagination at DB level total_count = qs.count() qs = qs.order_by('-created_date') start = (page - 1) * page_size end = start + page_size events_page = list(qs[start:end]) - # Fetch images ONLY for the events we will return page_ids = [e.id for e in events_page] primary_images = EventImages.objects.filter(event_id__in=page_ids, is_primary=True) thumb_map = {img.event_id: img for img in primary_images} - # Serialize with direct attribute access (fast) event_list = [self._serialize_event(e, thumb_map) for e in events_page] return JsonResponse({ @@ -121,6 +195,7 @@ class EventListAPI(APIView): "page": page, "page_size": page_size, "has_next": end < total_count, + "radius_km": used_radius, }) except Exception as e: return JsonResponse({"status": "error", "message": str(e)})