From 1126f3cbbefdb9cd8ec1de3e2e2cd5c6966af787 Mon Sep 17 00:00:00 2001 From: rizwanisready Date: Wed, 6 Mar 2024 22:40:53 +0530 Subject: [PATCH] filtering events --- accounts/api/serializers.py | 6 + goodtimes/services.py | 87 +++++++- manage_events/api/urls.py | 30 ++- manage_events/api/views.py | 192 ++++++++++++------ manage_events/migrations/0002_favorites.py | 49 +++++ ...entprincipalinteraction_unique_together.py | 19 ++ manage_events/models.py | 13 +- manage_events/utils.py | 38 ++++ manage_events/views.py | 35 ++-- templates/manage_events/event_list.html | 76 +++---- 10 files changed, 426 insertions(+), 119 deletions(-) create mode 100644 manage_events/migrations/0002_favorites.py create mode 100644 manage_events/migrations/0003_alter_eventprincipalinteraction_unique_together.py create mode 100644 manage_events/utils.py diff --git a/accounts/api/serializers.py b/accounts/api/serializers.py index 51bf2dc..44c7f0e 100644 --- a/accounts/api/serializers.py +++ b/accounts/api/serializers.py @@ -8,6 +8,7 @@ from accounts.models import ( IAmPrincipalType, # IAmPrincipalKYCDetails, ) +from manage_events.models import PrincipalPreference from manage_referrals.models import ( ReferralCode, ReferralRecord, @@ -140,6 +141,7 @@ class ProfileSerializer(serializers.ModelSerializer): invite_count = serializers.SerializerMethodField(read_only=True) principal_type_name = serializers.SerializerMethodField(read_only=True) has_active_subscription = serializers.SerializerMethodField(read_only=True) + has_preferences = serializers.SerializerMethodField(read_only=True) class Meta: model = IAmPrincipal @@ -153,6 +155,7 @@ class ProfileSerializer(serializers.ModelSerializer): "invite_count", "register_complete", "has_active_subscription", + "has_preferences", ] def update(self, instance, validated_data): @@ -171,6 +174,9 @@ class ProfileSerializer(serializers.ModelSerializer): def get_principal_type_name(self, obj): return obj.principal_type.name if obj.principal_type else None + def get_has_preferences(self, obj): + return PrincipalPreference.objects.filter(principal=obj).exists() + def get_image_url(self, obj, field_name, request): image_field = getattr(obj, field_name) if image_field: diff --git a/goodtimes/services.py b/goodtimes/services.py index 30925e6..2914557 100644 --- a/goodtimes/services.py +++ b/goodtimes/services.py @@ -3,6 +3,7 @@ from django.conf import settings from django.core.files.uploadedfile import UploadedFile from django.core.mail import EmailMessage 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 smtplib import SMTPException @@ -16,7 +17,12 @@ from manage_referrals.models import ( from manage_subscriptions.models import PrincipalSubscription, Subscription from manage_wallets.models import TransactionStatus, Wallet, Transaction from goodtimes.utils import CapacityError, RandomGenerator -from manage_events.models import Event, EventPrincipalInteraction +from manage_events.models import ( + Event, + EventPrincipalInteraction, + PrincipalPreference, + Venue, +) # from twilio.rest import Client from django.db.models import Q, Count @@ -373,3 +379,82 @@ class InteractionCalculator: interaction["interested"] = "Blue Flames" return interaction + + +class EventFilterService: + @staticmethod + def filter_events(filter_type, principal=None): + today = timezone.now().date() + events = Event.objects.none() + + current_and_future_events_query = Q(active=True, deleted=False) & ( + Q(start_date__lte=today, end_date__gte=today) | Q(start_date__gt=today) + ) + + if filter_type == "expensive": + events = Event.objects.filter(current_and_future_events_query).order_by( + "-entry_fee" + ) + elif filter_type == "cheap": + events = Event.objects.filter(current_and_future_events_query).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 + ) + events = Event.objects.filter( + category__in=preferred_categories_ids, end_date__gte=today + ).distinct() + + return events + + @staticmethod + def filter_events_by_category(category_id): + today = timezone.now().date() + + current_and_future_events_query = Q(active=True, deleted=False) & ( + Q(start_date__lte=today, end_date__gte=today) | Q(start_date__gt=today) + ) + + # Ensure the category_id is valid and within the specified range (1-8) + if 1 <= category_id <= 8: + events = Event.objects.filter( + current_and_future_events_query, category_id=category_id + ).distinct() + else: + events = ( + Event.objects.none() + ) # Return an empty queryset if the category_id is not valid + + return events + + @staticmethod + def filter_events_for_tomorrow(): + today = timezone.now().date() + tomorrow = today + timezone.timedelta(days=1) + + # Events that are starting tomorrow, ending tomorrow, or have an end date greater than tomorrow + events_query = ( + Q(start_date=tomorrow) + | Q(end_date=tomorrow) + | (Q(start_date__lte=tomorrow) & Q(end_date__gte=tomorrow)) + ) + events = Event.objects.filter( + events_query, active=True, deleted=False + ).distinct() + + return events + + @staticmethod + def filter_events_for_today(): + today = timezone.now().date() + print("Today: ", today) + + events = Event.objects.filter( + Q(active=True) & ~Q(deleted=True), + Q(start_date__lte=today) & Q(end_date__gte=today), + ) + + return events diff --git a/manage_events/api/urls.py b/manage_events/api/urls.py index 3975903..1823581 100644 --- a/manage_events/api/urls.py +++ b/manage_events/api/urls.py @@ -9,11 +9,7 @@ urlpatterns = [ views.CreateEventApi.as_view(), name="add_event", ), - path( - "get-events//", - views.EventsAPIView.as_view(), - name="get_events", - ), + path("get-events/", views.EventsAPIView.as_view(), name="events"), path( "event//", views.EventDetailAPIView.as_view(), @@ -29,7 +25,11 @@ urlpatterns = [ views.VenueListView.as_view(), name="get_venue", ), - path("event-master/search/", views.EventMasterSearchAPIView.as_view(), name="event_master_search"), + path( + "event-master/search/", + views.EventMasterSearchAPIView.as_view(), + name="event_master_search", + ), # Others path("geocode/", views.GeocodeAPIView.as_view(), name="geocode_api"), # All Preferences List @@ -56,4 +56,22 @@ urlpatterns = [ views.IAmPrincipalLocationAPIView.as_view(), name="add_location", ), + # Favorites + path( + "toggle-favorite//", + views.ToggleFavoriteView.as_view(), + name="toggle-favorite", + ), + # Going | Interested + path( + "event-status//", + views.EventStatusUpdateAPIView.as_view(), + name="event-status-update", + ), + # Events filtered by 10 KM + path( + "events/filter-by-location/", + views.EventFilterByLocationAPIView.as_view(), + name="filter-events-by-location", + ), ] diff --git a/manage_events/api/views.py b/manage_events/api/views.py index baac8e2..83648ee 100644 --- a/manage_events/api/views.py +++ b/manage_events/api/views.py @@ -1,5 +1,6 @@ from datetime import timedelta from django.db import transaction +from django.shortcuts import get_object_or_404 from django.utils import timezone from rest_framework import status, generics from rest_framework.views import APIView @@ -24,14 +25,19 @@ from manage_events.api.serializers import ( VenueSerializer, ) from manage_events.models import ( + EventInteractionType, EventMaster, Event, EventCategory, + EventPrincipalInteraction, + Favorites, PrincipalPreference, Venue, ) import requests +from manage_events.utils import filter_events_by_location + class CreateEventApi(APIView): authentication_classes = [JWTAuthentication] @@ -56,7 +62,7 @@ class CreateVenueApi(APIView): permission_classes = [IsAuthenticated] def post(self, request): - serializer = VenueSerializer(data=request.data, context={'request': request}) + serializer = VenueSerializer(data=request.data, context={"request": request}) serializer.is_valid(raise_exception=True) serializer.save(created_by=self.request.user, active=True) @@ -73,9 +79,10 @@ class EventsAPIView(APIView): authentication_classes = [JWTAuthentication] permission_classes = [IsAuthenticated] - def get(self, request, filter, *args, **kwargs): - today = timezone.now().date() - params = ["expensive", "cheap", "preference", "today", "tomorrow"] + def get(self, request, *args, **kwargs): + filter = request.query_params.get("filter", None) + category_id = request.query_params.get("category_id", None) + params = ["expensive", "cheap", "preference", "today", "tomorrow", "category"] if filter not in params: return ApiResponse.error( status=status.HTTP_400_BAD_REQUEST, @@ -84,61 +91,26 @@ class EventsAPIView(APIView): ) try: - - if filter == "expensive": - - # Constructing a complex query using Q objects - current_and_future_events_query = Q(active=True, deleted=False) & ( - Q(start_date__lte=today, end_date__gte=today) - | Q(start_date__gt=today) # Current events # Future events + if filter == "today": + events = services.EventFilterService.filter_events_for_today() + elif filter == "tomorrow": + events = services.EventFilterService.filter_events_for_tomorrow() + elif filter == "category" and category_id is not None: + events = services.EventFilterService.filter_events_by_category( + int(category_id) ) - - current_and_future_events = Event.objects.filter( - current_and_future_events_query - ).order_by("-entry_fee") - serializer = EventDetailSerializer( - current_and_future_events, context={"request": request}, many=True + else: + events = services.EventFilterService.filter_events( + filter, principal=request.user ) - return ApiResponse.success( - status=status.HTTP_200_OK, - message=constants.SUCCESS, - data=serializer.data, - ) - elif filter == "cheap": - current_and_future_events_query = Q(active=True, deleted=False) & ( - Q(start_date__lte=today, end_date__gte=today) - | Q(start_date__gt=today) # Current events # Future events - ) - - current_and_future_events = Event.objects.filter( - current_and_future_events_query - ).order_by("entry_fee") - serializer = EventDetailSerializer( - current_and_future_events, context={"request": request}, many=True - ) - return ApiResponse.success( - status=status.HTTP_200_OK, - message=constants.SUCCESS, - data=serializer.data, - ) - elif filter == "preference": - preferences = PrincipalPreference.objects.get(principal=request.user) - preferred_categories_ids = preferences.preferred_categories.values_list( - "id", flat=True - ) - # Filter events based on user preferences and that are upcoming or ongoing - preference_events = Event.objects.filter( - category__in=preferred_categories_ids, end_date__gte=today - ).distinct() - serializer = EventDetailSerializer( - preference_events, context={"request": request}, many=True - ) - return ApiResponse.success( - status=status.HTTP_200_OK, - message=constants.SUCCESS, - data=serializer.data, - ) - + serializer = EventDetailSerializer( + events, context={"request": request}, many=True + ) + return ApiResponse.success( + status=status.HTTP_200_OK, + message=constants.SUCCESS, + data=serializer.data, + ) except Exception as e: return ApiResponse.error( status=status.HTTP_400_BAD_REQUEST, @@ -315,6 +287,7 @@ class IAmPrincipalLocationAPIView(APIView): user_location = IAmPrincipalLocation.objects.filter( principal=request.user ).last() + print("user_location: ", user_location) serializer = IAmPrincipalLocationSerializer(user_location) return ApiResponse.success( status=status.HTTP_200_OK, @@ -332,6 +305,7 @@ class IAmPrincipalLocationAPIView(APIView): serializer = IAmPrincipalLocationSerializer( data=request.data, context={"request": request} ) + print("serializer: ", serializer) if serializer.is_valid(): serializer.save() return ApiResponse.success( @@ -381,6 +355,9 @@ class PrincipalPreferenceDetailView(generics.RetrieveAPIView): class EventMasterSearchAPIView(APIView): + authentication_classes = [JWTAuthentication] + permission_classes = [IsAuthenticated] + def post(self, request, *args, **kwargs): serializer = EventMasterSearchSerializer(data=request.data) if serializer.is_valid(): @@ -406,3 +383,104 @@ class EventMasterSearchAPIView(APIView): errors=serializer.errors, status=status.HTTP_400_BAD_REQUEST, ) + + +class ToggleFavoriteView(APIView): + authentication_classes = [JWTAuthentication] + permission_classes = [IsAuthenticated] + + def post(self, request, event_id, format=None): + principal = request.user + event = get_object_or_404(Event, pk=event_id) + + favorite, created = Favorites.objects.get_or_create( + principal=principal, event=event + ) + if not created: + favorite.delete() + return ApiResponse.success( + data="removed", + message=constants.SUCCESS, + status=status.HTTP_200_OK, + ) + + return ApiResponse.success( + data="added", + message=constants.SUCCESS, + status=status.HTTP_201_CREATED, + ) + + +class EventStatusUpdateAPIView(APIView): + authentication_classes = [JWTAuthentication] + permission_classes = [IsAuthenticated] + + def post(self, request, event_id): + principal = ( + request.user + ) # Assuming you're using some form of user authentication + event = get_object_or_404(Event, pk=event_id) + + status_requested = request.data.get("status") # 'going' or 'interested' + if status_requested not in EventInteractionType.values: + return ApiResponse.error( + message=constants.FAILURE, + errors="Invalid status provided.", + status=status.HTTP_400_BAD_REQUEST, + ) + + interaction, created = EventPrincipalInteraction.objects.update_or_create( + principal=principal, + event=event, + defaults={"status": status_requested}, + ) + + if created: + return ApiResponse.success( + data=status_requested, + message=f"Marked as {status_requested}.", + status=status.HTTP_201_CREATED, + ) + else: + return ApiResponse.success( + data=status_requested, + message=f"Updated status to {status_requested}.", + status=status.HTTP_200_OK, + ) + + +class EventFilterByLocationAPIView(APIView): + authentication_classes = [JWTAuthentication] + permission_classes = [IsAuthenticated] + + def get(self, request, *args, **kwargs): + principal = request.user + + # Fetching user location from IAmPrincipalLocation + try: + user_location = IAmPrincipalLocation.objects.filter(principal=principal).last() + user_lat = user_location.latitude + user_lon = user_location.longitude + except IAmPrincipalLocation.DoesNotExist: + return ApiResponse.error( + errors=constants.FAILURE, + message="User location not set.", + status=status.HTTP_400_BAD_REQUEST, + ) + + # Now filter events based on user location + events = filter_events_by_location(user_lat, user_lon) + print("events: ", events) + # Assuming you only want current and future events + today = timezone.now().date() + events = events.filter(Q(end_date__gte=today) | Q(start_date__gte=today)) + + # Serialize and return the filtered events + serializer = EventDetailSerializer( + events, context={"request": request}, many=True + ) + return ApiResponse.success( + data=serializer.data, + message=constants.SUCCESS, + status=status.HTTP_200_OK, + ) diff --git a/manage_events/migrations/0002_favorites.py b/manage_events/migrations/0002_favorites.py new file mode 100644 index 0000000..29d783e --- /dev/null +++ b/manage_events/migrations/0002_favorites.py @@ -0,0 +1,49 @@ +# Generated by Django 5.0.2 on 2024-03-06 16:07 + +import django.db.models.deletion +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("manage_events", "0001_initial"), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.CreateModel( + name="Favorites", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ( + "event", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="favorites", + to="manage_events.event", + ), + ), + ( + "principal", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="favorited_by", + to=settings.AUTH_USER_MODEL, + ), + ), + ], + options={ + "unique_together": {("principal", "event")}, + }, + ), + ] diff --git a/manage_events/migrations/0003_alter_eventprincipalinteraction_unique_together.py b/manage_events/migrations/0003_alter_eventprincipalinteraction_unique_together.py new file mode 100644 index 0000000..16a2a4d --- /dev/null +++ b/manage_events/migrations/0003_alter_eventprincipalinteraction_unique_together.py @@ -0,0 +1,19 @@ +# Generated by Django 5.0.2 on 2024-03-06 16:25 + +from django.conf import settings +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ("manage_events", "0002_favorites"), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.AlterUniqueTogether( + name="eventprincipalinteraction", + unique_together={("principal", "event")}, + ), + ] diff --git a/manage_events/models.py b/manage_events/models.py index e510756..3a29e52 100644 --- a/manage_events/models.py +++ b/manage_events/models.py @@ -106,7 +106,7 @@ class EventPrincipalInteraction(models.Model): ) class Meta: - unique_together = ("principal", "event", "status") + unique_together = ("principal", "event") class PrincipalPreference(BaseModel): @@ -120,3 +120,14 @@ class PrincipalPreference(BaseModel): class Meta: db_table = "user_preference" + + +class Favorites(models.Model): + principal = models.ForeignKey(IAmPrincipal, on_delete=models.CASCADE, related_name='favorited_by') + event = models.ForeignKey(Event, on_delete=models.CASCADE, related_name='favorites') + + class Meta: + unique_together = ('principal', 'event') + + def __str__(self): + return f"{self.principal}'s favorite: {self.event.title}" diff --git a/manage_events/utils.py b/manage_events/utils.py new file mode 100644 index 0000000..18ae3d2 --- /dev/null +++ b/manage_events/utils.py @@ -0,0 +1,38 @@ +import math +from manage_events.models import Event, Venue + + +def haversine(lon1, lat1, lon2, lat2): + """ + Calculate the great circle distance in kilometers between two points + on the earth (specified in decimal degrees) + """ + # convert decimal degrees to radians + lon1, lat1, lon2, lat2 = map(math.radians, [lon1, lat1, lon2, lat2]) + + # haversine formula + dlon = lon2 - lon1 + dlat = lat2 - lat1 + a = ( + math.sin(dlat / 2) ** 2 + + math.cos(lat1) * math.cos(lat2) * math.sin(dlon / 2) ** 2 + ) + c = 2 * math.asin(math.sqrt(a)) + r = 6371 # Radius of earth in kilometers. Use 3956 for miles + return c * r + + +def filter_events_by_location(user_lat, user_lon, radius_km=10): + venues_within_radius = [] + + # Check each venue to see if it's within the radius + for venue in Venue.objects.filter(deleted=False, active=True): + print("venue: ", venue) + distance = haversine(user_lon, user_lat, venue.longitude, venue.latitude) + print("distance: ", distance) + if distance <= radius_km: + venues_within_radius.append(venue.id) + print("venues_within_radius: ", venues_within_radius) + # Filter events based on the venues within the radius + events = Event.objects.filter(venue__id__in=venues_within_radius) + return events diff --git a/manage_events/views.py b/manage_events/views.py index f922f57..19f330e 100644 --- a/manage_events/views.py +++ b/manage_events/views.py @@ -299,25 +299,26 @@ class EventView(LoginRequiredMixin, generic.ListView): def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) context["page_name"] = self.page_name - - # Paginate the queryset - queryset = self.get_queryset() - page_obj = Paginator(queryset, self.paginate_by) - page_number = self.request.GET.get( - "page" - ) # Get current page from URL parameter - page_obj = page_obj.get_page(page_number) - - context.update( - { - "events": page_obj.object_list, - "paginator": page_obj, - "is_paginated": page_obj.has_other_pages(), # Check if there are more pages - } - ) - return context + # # Paginate the queryset + # queryset = self.get_queryset() + # page_obj = Paginator(queryset, self.paginate_by) + # page_number = self.request.GET.get( + # "page" + # ) # Get current page from URL parameter + # page_obj = page_obj.get_page(page_number) + + # context.update( + # { + # "events": page_obj.object_list, + # "paginator": page_obj, + # "is_paginated": page_obj.has_other_pages(), # Check if there are more pages + # } + # ) + + # return context + class EventDetailView(generic.DetailView): model = Event diff --git a/templates/manage_events/event_list.html b/templates/manage_events/event_list.html index 9ce2026..dd991eb 100644 --- a/templates/manage_events/event_list.html +++ b/templates/manage_events/event_list.html @@ -1,8 +1,8 @@ {% extends 'layout/base_template.html' %} {% load static %} {% block stylesheet %} - -{% include "cdn_through_html/datatable_cdn_css.html" %} + + {% include "cdn_through_html/datatable_cdn_css.html" %} {% endblock %} @@ -12,7 +12,7 @@
-

Manage Events

+

Manage Subscriptions

-{% include "cdn_through_html/datatable_cdn_js.html" %} - - -{% endblock %} \ No newline at end of file + + {% endblock content %} + + {% block javascript %} + + {% include "cdn_through_html/datatable_cdn_js.html" %} + + + {% endblock %} \ No newline at end of file