""" Selectors for core functionality including map services and analytics. Following Django styleguide pattern for separating data access from business logic. """ from typing import Optional, Dict, Any, List from django.db.models import QuerySet, Q, Count from django.contrib.gis.geos import Point, Polygon from django.contrib.gis.measure import Distance from django.utils import timezone from datetime import timedelta from .analytics import PageView from parks.models import Park from rides.models import Ride def unified_locations_for_map( *, bounds: Optional[Polygon] = None, location_types: Optional[List[str]] = None, filters: Optional[Dict[str, Any]] = None, ) -> Dict[str, QuerySet]: """ Get unified location data for map display across all location types. Args: bounds: Geographic boundary polygon location_types: List of location types to include ('park', 'ride') filters: Additional filter parameters Returns: Dictionary containing querysets for each location type """ results = {} # Default to all location types if none specified if not location_types: location_types = ["park", "ride"] # Parks if "park" in location_types: park_queryset = ( Park.objects.select_related("operator") .prefetch_related("location") .annotate(ride_count_calculated=Count("rides")) ) if bounds: park_queryset = park_queryset.filter(location__coordinates__within=bounds) if filters: if "status" in filters: park_queryset = park_queryset.filter(status=filters["status"]) if "operator" in filters: park_queryset = park_queryset.filter(operator=filters["operator"]) results["parks"] = park_queryset.order_by("name") # Rides if "ride" in location_types: ride_queryset = Ride.objects.select_related( "park", "manufacturer" ).prefetch_related("park__location", "location") if bounds: ride_queryset = ride_queryset.filter( Q(location__coordinates__within=bounds) | Q(park__location__coordinates__within=bounds) ) if filters: if "category" in filters: ride_queryset = ride_queryset.filter(category=filters["category"]) if "manufacturer" in filters: ride_queryset = ride_queryset.filter( manufacturer=filters["manufacturer"] ) if "park" in filters: ride_queryset = ride_queryset.filter(park=filters["park"]) results["rides"] = ride_queryset.order_by("park__name", "name") return results def locations_near_point( *, point: Point, distance_km: float = 50, location_types: Optional[List[str]] = None, limit: int = 20, ) -> Dict[str, QuerySet]: """ Get locations near a specific geographic point across all types. Args: point: Geographic point (longitude, latitude) distance_km: Maximum distance in kilometers location_types: List of location types to include limit: Maximum number of results per type Returns: Dictionary containing nearby locations by type """ results = {} if not location_types: location_types = ["park", "ride"] # Parks near point if "park" in location_types: results["parks"] = ( Park.objects.filter( location__coordinates__distance_lte=( point, Distance(km=distance_km), ) ) .select_related("operator") .prefetch_related("location") .distance(point) .order_by("distance")[:limit] ) # Rides near point if "ride" in location_types: results["rides"] = ( Ride.objects.filter( Q( location__coordinates__distance_lte=( point, Distance(km=distance_km), ) ) | Q( park__location__coordinates__distance_lte=( point, Distance(km=distance_km), ) ) ) .select_related("park", "manufacturer") .prefetch_related("park__location") .distance(point) .order_by("distance")[:limit] ) return results def search_all_locations(*, query: str, limit: int = 20) -> Dict[str, QuerySet]: """ Search across all location types for a query string. Args: query: Search string limit: Maximum results per type Returns: Dictionary containing search results by type """ results = {} # Search parks results["parks"] = ( Park.objects.filter( Q(name__icontains=query) | Q(description__icontains=query) | Q(location__city__icontains=query) | Q(location__region__icontains=query) ) .select_related("operator") .prefetch_related("location") .order_by("name")[:limit] ) # Search rides results["rides"] = ( Ride.objects.filter( Q(name__icontains=query) | Q(description__icontains=query) | Q(park__name__icontains=query) | Q(manufacturer__name__icontains=query) ) .select_related("park", "manufacturer") .prefetch_related("park__location") .order_by("park__name", "name")[:limit] ) return results def page_views_for_analytics( *, start_date: Optional[timezone.datetime] = None, end_date: Optional[timezone.datetime] = None, path_pattern: Optional[str] = None, ) -> QuerySet[PageView]: """ Get page views for analytics with optional filtering. Args: start_date: Start date for filtering end_date: End date for filtering path_pattern: URL path pattern to filter by Returns: QuerySet of page views """ queryset = PageView.objects.all() if start_date: queryset = queryset.filter(timestamp__gte=start_date) if end_date: queryset = queryset.filter(timestamp__lte=end_date) if path_pattern: queryset = queryset.filter(path__icontains=path_pattern) return queryset.order_by("-timestamp") def popular_pages_summary(*, days: int = 30) -> Dict[str, Any]: """ Get summary of most popular pages in the last N days. Args: days: Number of days to analyze Returns: Dictionary containing popular pages statistics """ cutoff_date = timezone.now() - timedelta(days=days) # Most viewed pages popular_pages = ( PageView.objects.filter(timestamp__gte=cutoff_date) .values("path") .annotate(view_count=Count("id")) .order_by("-view_count")[:10] ) # Total page views total_views = PageView.objects.filter(timestamp__gte=cutoff_date).count() # Unique visitors (based on IP) unique_visitors = ( PageView.objects.filter(timestamp__gte=cutoff_date) .values("ip_address") .distinct() .count() ) return { "popular_pages": list(popular_pages), "total_views": total_views, "unique_visitors": unique_visitors, "period_days": days, } def geographic_distribution_summary() -> Dict[str, Any]: """ Get geographic distribution statistics for all locations. Returns: Dictionary containing geographic statistics """ # Parks by country parks_by_country = ( Park.objects.filter(location__country__isnull=False) .values("location__country") .annotate(count=Count("id")) .order_by("-count") ) # Rides by country (through park location) rides_by_country = ( Ride.objects.filter(park__location__country__isnull=False) .values("park__location__country") .annotate(count=Count("id")) .order_by("-count") ) return { "parks_by_country": list(parks_by_country), "rides_by_country": list(rides_by_country), } def system_health_metrics() -> Dict[str, Any]: """ Get system health and activity metrics. Returns: Dictionary containing system health statistics """ now = timezone.now() last_24h = now - timedelta(hours=24) last_7d = now - timedelta(days=7) return { "total_parks": Park.objects.count(), "operating_parks": Park.objects.filter(status="OPERATING").count(), "total_rides": Ride.objects.count(), "page_views_24h": PageView.objects.filter(timestamp__gte=last_24h).count(), "page_views_7d": PageView.objects.filter(timestamp__gte=last_7d).count(), "data_freshness": { "latest_park_update": ( Park.objects.order_by("-updated_at").first().updated_at if Park.objects.exists() else None ), "latest_ride_update": ( Ride.objects.order_by("-updated_at").first().updated_at if Ride.objects.exists() else None ), }, }