Merge pull request #62 from WDI-Ideas/development
feat(module_2_filter):Home page advance filter api
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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",
|
||||
|
||||
34
manage_events/api/filters.py
Normal file
34
manage_events/api/filters.py
Normal file
@@ -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)
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
),
|
||||
]
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user