diff --git a/goodtimes/services.py b/goodtimes/services.py index cc22c0e..e2faeb2 100644 --- a/goodtimes/services.py +++ b/goodtimes/services.py @@ -1,4 +1,5 @@ import random +import googlemaps from django.conf import settings from django.core.files.uploadedfile import UploadedFile from django.core.mail import EmailMessage @@ -6,7 +7,9 @@ from django.utils.html import strip_tags import math from django.template.loader import render_to_string from django.shortcuts import get_object_or_404 +from django.db.models import Case, When from smtplib import SMTPException + from accounts.models import IAmPrincipal, IAmPrincipalOtp, IAmPrincipalType from manage_referrals.models import ( GoodTimeCoins, @@ -495,7 +498,9 @@ class EventFilterService: events = events.order_by("entry_fee") elif filter_type == "preference" and principal is not None: preferences = PrincipalPreference.objects.get(principal=principal) - preferred_categories_ids = preferences.preferred_categories.values_list("id", flat=True) + preferred_categories_ids = preferences.preferred_categories.values_list( + "id", flat=True + ) events = events.filter(category__in=preferred_categories_ids) return events.distinct() @@ -508,7 +513,9 @@ class EventFilterService: if 1 <= category_id <= 10: events = events.filter(category_id=category_id) else: - events = Event.objects.none() # Return an empty queryset if the category_id is not valid + events = ( + Event.objects.none() + ) # Return an empty queryset if the category_id is not valid return events.distinct() @@ -680,3 +687,133 @@ class MyEventFilterService: ) return events + + +class GoogleMapsservice: + def __init__(self, api_key=None): + self.api_key = api_key or settings.GOOGLE_MAPS_API_KEY + self.client = googlemaps.Client(key=self.api_key) + + def get_distance_matrix(self, origin: list, destination: list): + """ + Get the distance matrix from Google Maps API for the given origins and destinations. + + Args: + origins (list): List of origin coordinates (latitude, longitude). + destinations (list): List of destination coordinates (latitude, longitude). + + Returns: + dict: Distance matrix response from Google Maps API. + """ + return self.client.distance_matrix(origin, destination) + + def search_address(self, address): + """ + Search for a list of addresses matching the given address string. + + :param address: Address string to search for + :return: List of matching addresses + """ + geocode_result = self.client.geocode(address) + return geocode_result + + def get_coordinates_from_address(self, address): + """ + Get the coordinates (latitude and longitude) of the given address. + + :param address: Address string to get coordinates for + :return: Coordinates as a tuple (latitude, longitude) + """ + geocode_result = self.client.geocode(address) + if geocode_result: + location = geocode_result[0]['geometry']['location'] + return location['lat'], location['lng'] + return None + + def get_place_id_from_address(self, address): + """ + Get the place ID of the given address. + + :param address: Address string to get the place ID for + :return: Place ID + """ + geocode_result = self.client.geocode(address) + if geocode_result: + return geocode_result[0]['place_id'] + return None + + def get_place_id_from_coordinates(self, latitude, longitude): + """ + Get the place ID of the given coordinates. + + :param latitude: Latitude of the location + :param longitude: Longitude of the location + :return: Place ID + """ + reverse_geocode_result = self.client.reverse_geocode((latitude, longitude)) + if reverse_geocode_result: + return reverse_geocode_result[0]['place_id'] + return None + + def search_addresses_containing(self, keyword): + """ + Search for a list of addresses containing the given keyword. + + :param keyword: Keyword to search for in addresses + :return: List of matching addresses containing the keyword + """ + geocode_result = self.client.geocode(keyword) + matching_addresses = [result['formatted_address'] for result in geocode_result if keyword.lower() in result['formatted_address'].lower()] + return matching_addresses + + def get_nearest_events(self, queryset, latitude, longitude, radius_km=10): + """ + Filter and sort events by their distance to the given latitude and longitude. + + Args: + queryset (QuerySet): The queryset of events to filter and sort. + latitude (float): The latitude of the origin point. + longitude (float): The longitude of the origin point. + radius_km (int): The radius in kilometers within which to filter events. + + Returns: + QuerySet: The filtered and sorted queryset of events. + """ + + # Set the origin to the provided latitude and longitude + origins = [(latitude, longitude)] + + # Create a list of destination coordinates for all events with valid venues + destinations = [ + (event.venue.latitude, event.venue.longitude) + for event in queryset + if event.venue.latitude and event.venue.longitude + ] + + # If there is no destination coordinates + if not destinations: + return queryset + + # Get the distance matrix from the Google Maps API + matrix = self.get_distance_matrix(origins, destinations) + + # Create a dictionary of event IDs and their corresponding distances + distances = { + event.id: element["distance"]["value"] + for event, element in zip(queryset, matrix["rows"][0]["elements"]) + if element["status"] == "OK" and element["distance"]["value"] <= radius_km * 1000 # Convert km to meters + } + + # Filter the queryset to include only events within the specified radius + queryset = queryset.filter(id__in=distances.keys()) + + # # Sort the event IDs by their distances in ascending order + # event_ids_by_distance = sorted(distances, key=distances.get) + + # # Create a Case/When expression to preserve the order of events by distance + # preserved_order = Case(*[When(pk=pk, then=pos) for pos, pk in enumerate(event_ids_by_distance)]) + # print(f"preserved_order is {preserved_order}") + # # Order the queryset based on the preserved order + # queryset = queryset.order_by(preserved_order) + + return queryset diff --git a/goodtimes/settings/base.py b/goodtimes/settings/base.py index 7a4febd..5476155 100644 --- a/goodtimes/settings/base.py +++ b/goodtimes/settings/base.py @@ -81,6 +81,7 @@ THIRD_PARTY_APPS = [ "allauth.socialaccount", "allauth.socialaccount.providers.apple", "allauth.socialaccount.providers.google", + "django_filters", # "django_crontab", # "django_celery_results", # "django_celery_beat", diff --git a/manage_events/api/filters.py b/manage_events/api/filters.py new file mode 100644 index 0000000..aeb164c --- /dev/null +++ b/manage_events/api/filters.py @@ -0,0 +1,34 @@ +from django_filters import rest_framework as filters +from django.db.models import Count, Q +from ..models import Event, EventInteractionType + + +class EventFilter(filters.FilterSet): + """ + FilterSet for Event model. + """ + title = filters.CharFilter(field_name="title", lookup_expr="icontains") + location = filters.CharFilter(field_name="venue__address", lookup_expr="icontains") + category = filters.CharFilter(method="filter_category") + start_date = filters.DateFilter(field_name="start_date", lookup_expr="gte") + end_date = filters.DateFilter(field_name="end_date", lookup_expr="lte") + price_from = filters.DateFilter(field_name="entry_fee", lookup_expr="gte") + price_to = filters.DateFilter(field_name="entry_fee", lookup_expr="lte") + age_group = filters.CharFilter(field_name="age_group", lookup_expr="icontains") + + class Meta: + model = Event + fields = [ + 'title', + 'location', + 'category', + 'start_date', + 'end_date', + 'price_from', + 'price_to', + 'age_group', + ] + + def filter_category(self, queryset, name, value): + category = value.split(',') + return queryset.filter(category__title__in=category) \ No newline at end of file diff --git a/manage_events/api/serializers.py b/manage_events/api/serializers.py index 9dd9c25..b198381 100644 --- a/manage_events/api/serializers.py +++ b/manage_events/api/serializers.py @@ -51,7 +51,7 @@ class EventCategorySerializer(serializers.ModelSerializer): class EventListSerializer(serializers.ModelSerializer): - # category = EventCategorySerializer(read_only=True) + category = EventCategorySerializer(read_only=True) # venue = VenueSerializer(read_only=True) # draft = serializers.BooleanField(read_only=True) # tags = TagSerializer(many=True, read_only=True) @@ -64,9 +64,9 @@ class EventListSerializer(serializers.ModelSerializer): "description", "start_date", "end_date", - # "from_time", - # "to_time", - # "category", + "from_time", + "to_time", + "category", # "venue", # "venue_capacity", "image", @@ -74,7 +74,7 @@ class EventListSerializer(serializers.ModelSerializer): # "entry_type", "entry_fee", "key_guest", - # "age_group", + "age_group", # "images", # "is_favorited", # "reviews", diff --git a/manage_events/api/urls.py b/manage_events/api/urls.py index 5b63eb7..1f657d3 100644 --- a/manage_events/api/urls.py +++ b/manage_events/api/urls.py @@ -56,6 +56,11 @@ urlpatterns = [ views.PrincipalPreferenceDetailView.as_view(), name="principal-preferences", ), + path( + "preferences/", + views.EventPreferencesView.as_view(), + name="preferences", + ), # Principal Location path( "add-location/", @@ -123,4 +128,10 @@ urlpatterns = [ views.EventShareView.as_view(), name="capture_event_share", ), + + path( + "events/", + views.EventListView.as_view(), + name="event_filter", + ), ] diff --git a/manage_events/api/views.py b/manage_events/api/views.py index b5c2a79..d15be1a 100644 --- a/manage_events/api/views.py +++ b/manage_events/api/views.py @@ -1,14 +1,18 @@ +import datetime from django.shortcuts import get_object_or_404 from django.utils import timezone +from django_filters.rest_framework import DjangoFilterBackend +import googlemaps from rest_framework import status, generics, mixins from rest_framework.views import APIView from django.conf import settings from accounts.models import IAmPrincipalLocation from goodtimes import constants -from django.db.models import Q +from django.db.models import Q, Count from taggit.models import Tag from django.utils.dateparse import parse_date from goodtimes import services +from goodtimes.services import GoogleMapsservice from goodtimes.utils import ApiResponse, CapacityError from rest_framework.permissions import IsAuthenticated from rest_framework_simplejwt.authentication import JWTAuthentication @@ -43,6 +47,7 @@ from manage_events.models import ( import requests from manage_events.utils import haversine_one, update_principal_location +from .filters import EventFilter class CreateEventApi(APIView): @@ -122,7 +127,9 @@ class CreateVenueApi(APIView): serializer = VenueSerializer(data=data, context={"request": request}) serializer.is_valid(raise_exception=True) - serializer.save(created_by=self.request.user, principal=self.request.user, active=True) + serializer.save( + created_by=self.request.user, principal=self.request.user, active=True + ) # Add additional logic for handling other relationships (e.g., Venue) return ApiResponse.success( @@ -568,6 +575,21 @@ class PrincipalPreferenceDetailView(generics.RetrieveAPIView): ) +class EventPreferencesView(APIView): + authentication_classes = [JWTAuthentication] + permission_classes = [IsAuthenticated] + model = EventCategory + serializer_class = EventCategorySerializer + + def get(self, request, *args, **kwargs): + """Get all event categories for the authenticated user.""" + obj = self.model.objects.filter(active=True, deleted=False) + serializer = self.serializer_class(obj, many=True) + return ApiResponse.success( + data=serializer.data, message=constants.SUCCESS, status=status.HTTP_200_OK + ) + + class EventMasterSearchAPIView(APIView): authentication_classes = [JWTAuthentication] permission_classes = [IsAuthenticated] @@ -688,9 +710,12 @@ class EventFilterByLocationAPIView(APIView): ) max_distance_km = 10 # Set your desired maximum distance - current_and_future_events_query = Q(active=True, deleted=False, draft=False, created_by__is_active=True,) & ( - Q(end_date__gte=today) - ) + current_and_future_events_query = Q( + active=True, + deleted=False, + draft=False, + created_by__is_active=True, + ) & (Q(end_date__gte=today)) # Get the queryset based on the filter conditions events_queryset = Event.objects.filter(current_and_future_events_query) @@ -961,3 +986,60 @@ class EventShareView(APIView): data="Event shared successfully.", status=status.HTTP_200_OK, ) + +class EventListView(generics.ListAPIView): + authentication_classes = [JWTAuthentication] + permission_classes = [IsAuthenticated] + queryset = Event.objects.filter(active=True, draft=False, deleted=False) + serializer_class = EventListSerializer + filter_backends = [DjangoFilterBackend] + filterset_class = EventFilter + + def apply_popularity_latest(self, queryset, ordering): + # Split the ordering fields and remove any leading '-' characters + ordering_fields = [field.lstrip("-") for field in ordering.split(",")] + + # Check if 'nearest' is in the ordering fields and remove it as nearest work only for lat and longitude + if "nearest" in ordering_fields: + ordering.replace("nearest", "") + ordering_fields.remove("nearest") + + # Annotate the queryset with popularity if 'popularity' is in the ordering fields + if "popularity" in ordering_fields: + queryset = queryset.annotate(popularity=Count("interaction_event")) + + # Replace 'latest' with '-created_on' in the ordering fields + ordering = ",".join( + "-created_on" if field == "latest" else f"-{field}" + for field in ordering_fields + ) + # Apply the ordering to the queryset + return queryset.order_by(*ordering.split(",")) + + def get_queryset(self): + queryset = super().get_queryset() + + # Sort by nearest location if latitude and longitude are provided + latitude = self.request.query_params.get("latitude") + longitude = self.request.query_params.get("longitude") + + if latitude and longitude: + print("latitude fucntion called") + gmaps_service = GoogleMapsservice() + queryset = gmaps_service.get_nearest_events(queryset, float(latitude), float(longitude)) + + # Apply popularity annotation and ordering if requested + ordering = self.request.query_params.get("ordering") + if ordering: + queryset = self.apply_popularity_latest(queryset, ordering) + return queryset + + def get(self, request, *args, **kwargs): + queryset = self.filter_queryset(self.get_queryset()) + page = self.paginate_queryset(queryset) + if page is not None: + serializer = self.get_serializer(page, many=True) + data = self.get_paginated_response(serializer.data) + return ApiResponse.success(message=constants.SUCCESS, data=data) + serializer = self.get_serializer(queryset, many=True) + return ApiResponse.success(message=constants.SUCCESS, data=serializer.data)