import json from django.http import JsonResponse from rest_framework.views import APIView from rest_framework.authentication import TokenAuthentication from rest_framework.permissions import IsAuthenticated, AllowAny from events.models import Event, EventImages from master_data.models import EventType from django.forms.models import model_to_dict from django.utils.decorators import method_decorator 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 from accounts.models import User from eventify_logger.services import log def _resolve_contributor(identifier): """Resolve an eventifyId or email to a contributor dict. Returns None on miss.""" if not identifier: return None try: user = User.objects.filter( Q(eventify_id=identifier) | Q(email=identifier) ).first() if not user: return None # Count events this user contributed events_count = Event.objects.filter( Q(contributed_by=user.eventify_id) | Q(contributed_by=user.email) ).filter( event_status__in=['published', 'live', 'completed'] ).count() full_name = user.get_full_name() or user.username or '' avatar = '' if user.profile_picture and hasattr(user.profile_picture, 'url'): try: avatar = user.profile_picture.url except Exception: avatar = '' return { 'name': full_name, 'email': user.email, 'eventify_id': user.eventify_id or '', 'avatar': avatar, 'member_since': user.date_joined.strftime('%b %Y') if user.date_joined else '', 'events_contributed': events_count, 'location': ', '.join(filter(None, [user.place or '', user.district or '', user.state or ''])), } except Exception: return None def _serialize_event_for_contributor(event): """Lightweight event serializer for contributor profile listings.""" primary_img = '' try: img = EventImages.objects.filter(event=event, is_primary=True).first() if not img: img = EventImages.objects.filter(event=event).first() if img and img.event_image: primary_img = img.event_image.url except Exception: pass return { 'id': event.id, 'name': event.name or event.title or '', 'title': event.title or event.name or '', 'start_date': event.start_date.isoformat() if event.start_date else '', 'end_date': event.end_date.isoformat() if event.end_date else '', 'start_time': str(event.start_time or ''), 'end_time': str(event.end_time or ''), 'image': primary_img, 'venue_name': event.venue_name or '', 'place': event.place or '', 'district': event.district or '', 'state': event.state or '', 'pincode': event.pincode or '', 'latitude': str(event.latitude) if event.latitude else '', 'longitude': str(event.longitude) if event.longitude else '', 'event_type': event.event_type_id, 'event_status': event.event_status or '', 'source': event.source or '', } 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] def post(self, request): try: event_types_queryset = EventType.objects.all() event_types = [] for event_type in event_types_queryset: event_type_data = { "id": event_type.id, "event_type": event_type.event_type, "event_type_icon": event_type.event_type_icon.url if event_type.event_type_icon else None } event_types.append(event_type_data) return JsonResponse({"status": "success", "event_types": event_types}) except Exception as e: log("error", "EventTypeAPI exception", request=request, logger_data={"error": str(e)}) return JsonResponse({"status": "error", "message": "An unexpected server error occurred."}) class EventListAPI(APIView): permission_classes = [AllowAny] @staticmethod def _serialize_event(e, thumb_map): """Slim serialization for list views — only fields the Flutter app uses.""" img = thumb_map.get(e.id) lat = e.latitude lng = e.longitude desc = e.description or '' return { 'id': e.id, 'name': e.name or '', 'title': e.title or '', 'description': desc[:200] if len(desc) > 200 else desc, 'start_date': str(e.start_date) if e.start_date else '', 'end_date': str(e.end_date) if e.end_date else '', 'start_time': str(e.start_time) if e.start_time else '', 'end_time': str(e.end_time) if e.end_time else '', 'pincode': e.pincode or '', 'place': e.place or '', 'is_bookable': bool(e.is_bookable), 'event_type': e.event_type_id, 'event_status': e.event_status or '', 'venue_name': getattr(e, 'venue_name', '') or '', 'latitude': float(lat) if lat is not None else None, 'longitude': float(lng) if lng is not None else None, 'location_name': getattr(e, 'location_name', '') or '', 'thumb_img': img.event_image.url if img and img.event_image else '', 'is_eventify_event': bool(e.is_eventify_event), 'source': e.source or 'eventify', } def post(self, request): try: try: data = json.loads(request.body) if request.body else {} except Exception: data = {} pincode = data.get("pincode", "all") page = int(data.get("page", 1)) page_size = int(data.get("page_size", 50)) per_type = int(data.get("per_type", 0)) q = data.get("q", "").strip() # 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() 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) if pincode_qs.count() >= MIN_EVENTS_THRESHOLD: qs = pincode_qs # Priority 3: Full-text search on title / description if q: qs = qs.filter(Q(title__icontains=q) | Q(description__icontains=q)) if per_type > 0 and page == 1: type_ids = list(qs.values_list('event_type_id', flat=True).distinct()) events_page = [] for tid in sorted(type_ids): chunk = list(qs.filter(event_type_id=tid).order_by('-created_date')[:per_type]) events_page.extend(chunk) total_count = qs.count() end = len(events_page) else: 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]) 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} event_list = [self._serialize_event(e, thumb_map) for e in events_page] return JsonResponse({ "status": "success", "events": event_list, "total_count": total_count, "page": page, "page_size": page_size, "has_next": end < total_count, "radius_km": used_radius, }) except Exception as e: log("error", "EventListAPI exception", request=request, logger_data={"error": str(e)}) return JsonResponse({"status": "error", "message": "An unexpected server error occurred."}) class EventDetailAPI(APIView): permission_classes = [AllowAny] def post(self, request): try: try: data = json.loads(request.body) if request.body else {} except Exception: data = {} event_id = data.get("event_id") events = Event.objects.get(id=event_id) event_images = EventImages.objects.filter(event=event_id) event_data = model_to_dict(events) event_data["status"] = "success" event_images_list = [] for ei in event_images: event_img = {} event_img['is_primary'] = ei.is_primary event_img['image'] = ei.event_image.url event_images_list.append(event_img) event_data["images"] = event_images_list # Resolve contributor from contributed_by field contributed_by = getattr(events, 'contributed_by', None) if contributed_by: contributor = _resolve_contributor(contributed_by) if contributor: event_data["contributor"] = contributor return JsonResponse(event_data) except Exception as e: log("error", "EventDetailAPI exception", request=request, logger_data={"error": str(e)}) return JsonResponse({"status": "error", "message": "An unexpected server error occurred."}) class EventImagesListAPI(APIView): authentication_classes = [] permission_classes = [AllowAny] def post(self, request): try: user, token, data, error_response = validate_token_and_get_user(request) if error_response: return error_response event_id = data.get("event_id") event_images = EventImages.objects.filter(event=event_id) res_data = {} res_data["status"] = "success" event_images_list = [] for ei in event_images: event_images_list.append(ei.event_image.url) res_data["images"] = event_images_list print(res_data) return JsonResponse(res_data) except Exception as e: log("error", "EventImagesListAPI exception", request=request, logger_data={"error": str(e)}) return JsonResponse( {"status": "error", "message": "An unexpected server error occurred."}, ) @method_decorator(csrf_exempt, name='dispatch') class EventsByCategoryAPI(APIView): authentication_classes = [] permission_classes = [AllowAny] def post(self, request): try: user, token, data, error_response = validate_token_and_get_user(request) if error_response: return error_response category_id = data.get("category_id") if not category_id: return JsonResponse( {"status": "error", "message": "category_id is required"} ) events = Event.objects.filter(event_type=category_id) events_dict = [model_to_dict(obj) for obj in events] for event in events_dict: try: event['event_image'] = EventImages.objects.get(event=event['id'], is_primary=True).event_image.url except EventImages.DoesNotExist: event['event_image'] = '' # event['start_date'] = convert_date_to_dd_mm_yyyy(event['start_date']) print(events_dict) return JsonResponse({ "status": "success", "events": events_dict }) except Exception as e: log("error", "EventsByDateAPI exception", request=request, logger_data={"error": str(e)}) return JsonResponse( {"status": "error", "message": "An unexpected server error occurred."}, ) @method_decorator(csrf_exempt, name='dispatch') class EventsByMonthYearAPI(APIView): authentication_classes = [] permission_classes = [AllowAny] """ API to get events by month and year. Returns dates that have events, total count, and date-wise breakdown. """ def post(self, request): try: user, token, data, error_response = validate_token_and_get_user(request) if error_response: return error_response month_name = data.get("month") # e.g., "August", "august", "Aug" year = data.get("year") # e.g., 2025 if not month_name or not year: return JsonResponse( {"status": "error", "message": "month and year are required"} ) # Convert month name to month number month_name_lower = month_name.lower().capitalize() month_abbr = month_name_lower[:3] # Try full month name first, then abbreviation month_number = None for i in range(1, 13): if calendar.month_name[i].lower() == month_name_lower or calendar.month_abbr[i].lower() == month_abbr.lower(): month_number = i break if not month_number: return JsonResponse( {"status": "error", "message": f"Invalid month name: {month_name}"} ) # Convert year to integer try: year = int(year) except (ValueError, TypeError): return JsonResponse( {"status": "error", "message": "Invalid year format"} ) # Filter events where start_date or end_date falls in the given month/year # An event is included if any part of it (start_date to end_date) overlaps with the month # events = Event.objects.filter( # Q(start_date__year=year, start_date__month=month_number) | # Q(end_date__year=year, end_date__month=month_number) | # Q(start_date__lte=datetime(year, month_number, 1).date(), # end_date__gte=datetime(year, month_number, calendar.monthrange(year, month_number)[1]).date()) # ).distinct() events = Event.objects.filter(start_date__year=year, start_date__month=month_number).distinct() print('*' * 100) print(f'Total events: {events.count()}') print('*' * 100) unique_start_dates = events.values_list('start_date', flat=True).distinct() date_strings = [d.strftime('%Y-%m-%d') for d in unique_start_dates] print('*' * 100) print(f'Unique start dates: {date_strings}') print('*' * 100) # Group events by date date_events_dict = {} all_dates = set() # Calculate month boundaries month_start = datetime(year, month_number, 1).date() month_end = datetime(year, month_number, calendar.monthrange(year, month_number)[1]).date() for event in events: # Get all dates between start_date and end_date that fall in the target month current_date = max(event.start_date, month_start) end_date = min(event.end_date, month_end) # Iterate through each date in the event's date range that falls in the target month while current_date <= end_date: if current_date.year == year and current_date.month == month_number: date_str = current_date.strftime('%Y-%m-%d') all_dates.add(date_str) if date_str not in date_events_dict: date_events_dict[date_str] = 0 date_events_dict[date_str] += 1 # Move to next day current_date += timedelta(days=1) # Sort dates sorted_dates = sorted(all_dates) # Build date_events list date_events = [ { "date_of_event": date_str, "events_of_date": date_events_dict[date_str] } for date_str in sorted_dates ] # Calculate total number of events (unique events, not date occurrences) total_events = events.count() print(sorted_dates) print(total_events) print(date_events) return JsonResponse({ "status": "success", "dates": date_strings, "total_number_of_events": total_events, "date_events": date_events }) except Exception as e: log("error", "DateSheetAPI exception", request=request, logger_data={"error": str(e)}) return JsonResponse( {"status": "error", "message": "An unexpected server error occurred."}, ) @method_decorator(csrf_exempt, name='dispatch') class EventsByDateAPI(APIView): authentication_classes = [] permission_classes = [AllowAny] """ API to get events occurring on a specific date. Returns complete event information with primary images. """ def post(self, request): try: user, token, data, error_response = validate_token_and_get_user(request) if error_response: return error_response date_of_event = data.get("date_of_event") if not date_of_event: return JsonResponse( {"status": "error", "message": "date_of_event is required"} ) # Parse date_of_event in YYYY-MM-DD format try: event_date = datetime.strptime(date_of_event, "%Y-%m-%d").date() except ValueError: return JsonResponse( {"status": "error", "message": "Invalid date format. Expected YYYY-MM-DD"} ) # Filter events where the provided date falls between start_date and end_date (inclusive) events = Event.objects.filter( start_date=event_date ).order_by('start_date') event_list = [] for e in events: data_dict = model_to_dict(e) try: thumb_img = EventImages.objects.get(event=e.id, is_primary=True) data_dict['thumb_img'] = thumb_img.event_image.url except EventImages.DoesNotExist: data_dict['thumb_img'] = '' event_list.append(data_dict) return JsonResponse({ "status": "success", "events": event_list }) except Exception as e: log("error", "PincodeEventsAPI exception", request=request, logger_data={"error": str(e)}) return JsonResponse( {"status": "error", "message": "An unexpected server error occurred."}, ) @method_decorator(csrf_exempt, name='dispatch') class FeaturedEventsAPI(APIView): authentication_classes = [] permission_classes = [AllowAny] """Returns events where is_featured=True — used for the homepage hero carousel.""" def post(self, request): try: user, token, data, error_response = validate_token_and_get_user(request) if error_response: return error_response events = Event.objects.filter(is_featured=True).order_by('-created_date') event_list = [] for e in events: data_dict = model_to_dict(e) try: thumb = EventImages.objects.get(event=e.id, is_primary=True) data_dict['thumb_img'] = thumb.event_image.url except EventImages.DoesNotExist: data_dict['thumb_img'] = '' event_list.append(data_dict) return JsonResponse({"status": "success", "events": event_list}) except Exception as e: log("error", "FeaturedEventsAPI exception", request=request, logger_data={"error": str(e)}) return JsonResponse({"status": "error", "message": "An unexpected server error occurred."}) @method_decorator(csrf_exempt, name='dispatch') class TopEventsAPI(APIView): authentication_classes = [] permission_classes = [AllowAny] """Returns events where is_top_event=True — used for the Top Events section.""" def post(self, request): try: user, token, data, error_response = validate_token_and_get_user(request) if error_response: return error_response events = Event.objects.filter(is_top_event=True).order_by('-created_date') event_list = [] for e in events: data_dict = model_to_dict(e) try: thumb = EventImages.objects.get(event=e.id, is_primary=True) data_dict['thumb_img'] = thumb.event_image.url except EventImages.DoesNotExist: data_dict['thumb_img'] = '' event_list.append(data_dict) return JsonResponse({"status": "success", "events": event_list}) except Exception as e: log("error", "TopEventsAPI exception", request=request, logger_data={"error": str(e)}) return JsonResponse({"status": "error", "message": "An unexpected server error occurred."}) @method_decorator(csrf_exempt, name='dispatch') class ContributorProfileAPI(APIView): """ Public API to fetch a contributor's profile and their events. POST /api/events/contributor-profile/ Body: { "contributor_id": "EVT-XXXXXXXX" } (or email) """ authentication_classes = [] permission_classes = [AllowAny] def post(self, request): try: try: data = json.loads(request.body) if request.body else {} except Exception: data = {} contributor_id = data.get("contributor_id", "").strip() if not contributor_id: return JsonResponse( {"status": "error", "message": "contributor_id is required"}, status=400, ) # Resolve user contributor = _resolve_contributor(contributor_id) if not contributor: return JsonResponse( {"status": "error", "message": "Contributor not found"}, status=404, ) # Fetch this contributor's events user_identifiers = [v for v in [contributor['eventify_id'], contributor['email']] if v] events_qs = Event.objects.filter( contributed_by__in=user_identifiers, event_status__in=['published', 'live', 'completed'], ).order_by('-start_date', '-created_date') events_list = [_serialize_event_for_contributor(e) for e in events_qs] return JsonResponse({ "status": "success", "contributor": contributor, "events": events_list, }) except Exception as e: log("error", "ContributorProfileAPI exception", request=request, logger_data={"error": str(e)}) return JsonResponse({"status": "error", "message": "An unexpected server error occurred."})