diff --git a/goodtimes/settings/base.py b/goodtimes/settings/base.py index f250c43..e50431d 100644 --- a/goodtimes/settings/base.py +++ b/goodtimes/settings/base.py @@ -68,6 +68,7 @@ LOCAL_APPS = [ ] THIRD_PARTY_APPS = [ + "gisserver", "rest_framework", "widget_tweaks", "rest_framework_simplejwt", diff --git a/manage_events/admin.py b/manage_events/admin.py index 0c0464b..60d5dbf 100644 --- a/manage_events/admin.py +++ b/manage_events/admin.py @@ -1,24 +1,84 @@ from django.contrib import admin -from .models import EventCategory, Venue, EventMaster +from .models import EventCategory, Venue, EventMaster, Event + + # Register your models here. class EventCategoryAdmin(admin.ModelAdmin): - list_display = ('title', 'description') - search_fields = ('title', 'description') - list_filter = ('title',) + list_display = ("title", "description") + search_fields = ("title", "description") + list_filter = ("title",) class VenueAdmin(admin.ModelAdmin): - list_display = ('title', 'address', 'latitude', 'longitude') - search_fields = ('title', 'address') - list_filter = ('title',) + list_display = ("id", "title", "address", "latitude", "longitude") + search_fields = ("title", "address") + list_filter = ("title",) class EventMasterAdmin(admin.ModelAdmin): - list_display = ('title', 'event_category', 'description') - search_fields = ('title', 'description') - list_filter = ('event_category', 'title') + list_display = ("title", "event_category", "description") + search_fields = ("title", "description") + list_filter = ("event_category", "title") +class EventAdmin(admin.ModelAdmin): + list_display = ( + "title", + "category", + "event_master", + "start_date", + "end_date", + "venue", + "status", + "draft", + ) + list_filter = ("category", "status", "draft", "start_date", "end_date", "venue") + search_fields = ( + "title", + "description", + "venue__title", + "category__name", + ) # Assuming Venue and EventCategory have a title and name fields respectively + date_hierarchy = "start_date" # Provides a quick date drill down functionality + ordering = ("start_date", "from_time") # Orders events in the list view + fieldsets = ( + (None, {"fields": ("title", "description", "image", "status")}), + ( + "Date & Time", + { + "fields": ("start_date", "end_date", "from_time", "to_time"), + }, + ), + ( + "Venue Details", + { + "fields": ("venue", "venue_capacity"), + }, + ), + ( + "Category Details", + { + "fields": ("category", ), + }, + ), + ( + "Entry", + { + "fields": ("entry_type", "entry_fee", "key_guest", "age_group"), + }, + ), + ( + "Miscellaneous", + { + "fields": ("video_url", "draft"), + }, + ), + ) + filter_horizontal = () # Use this if there are many-to-many fields + raw_id_fields = ("venue", "category", "event_master") + + +admin.site.register(Event, EventAdmin) admin.site.register(EventCategory, EventCategoryAdmin) admin.site.register(Venue, VenueAdmin) admin.site.register(EventMaster, EventMasterAdmin) diff --git a/manage_events/api/serializers.py b/manage_events/api/serializers.py index 5e89960..b826e2d 100644 --- a/manage_events/api/serializers.py +++ b/manage_events/api/serializers.py @@ -6,6 +6,7 @@ from manage_events.models import ( Event, EventCategory, EventImage, + Favorites, Venue, PrincipalPreference, ) @@ -42,6 +43,7 @@ class EventDetailSerializer(serializers.ModelSerializer): images = serializers.SerializerMethodField() venue = VenueSerializer(read_only=True) # Use VenueSerializer for the venue field category = EventCategorySerializer(read_only=True) + is_favorited = serializers.SerializerMethodField() class Meta: model = Event @@ -63,6 +65,7 @@ class EventDetailSerializer(serializers.ModelSerializer): "key_guest", "age_group", "images", + "is_favorited", ] def get_images(self, obj): @@ -71,6 +74,13 @@ class EventDetailSerializer(serializers.ModelSerializer): ) # Ensure this uses the correct related_name from your model return EventImageSerializer(images, many=True, context=self.context).data + def get_is_favorited(self, obj): + request = self.context.get("request") + if request and hasattr(request, "user"): + user = request.user + return Favorites.objects.filter(event=obj, principal=user).exists() + return False + class CreateEventSerializer(serializers.ModelSerializer): images = serializers.ListField( @@ -174,3 +184,8 @@ class EventMasterSerializer(serializers.ModelSerializer): class Meta: model = EventMaster fields = "__all__" + + +class EventDateRangeSerializer(serializers.Serializer): + start_date = serializers.DateField(format='%Y-%m-%d', input_formats=['%Y-%m-%d']) + end_date = serializers.DateField(format='%Y-%m-%d', input_formats=['%Y-%m-%d']) \ No newline at end of file diff --git a/manage_events/api/urls.py b/manage_events/api/urls.py index 1823581..5f1ae71 100644 --- a/manage_events/api/urls.py +++ b/manage_events/api/urls.py @@ -25,6 +25,11 @@ urlpatterns = [ views.VenueListView.as_view(), name="get_venue", ), + path( + "venue/delete//", + views.VenueDeleteAPIView.as_view(), + name="venue-delete", + ), path( "event-master/search/", views.EventMasterSearchAPIView.as_view(), @@ -74,4 +79,14 @@ urlpatterns = [ views.EventFilterByLocationAPIView.as_view(), name="filter-events-by-location", ), + # Events filtered by Princiapl's Favorites + path( + "favorites/events/", views.FavoriteEventsList.as_view(), name="favorite-events" + ), + # Events filtered by Date Range + path( + "events/date-range/", + views.EventDateRangeAPIView.as_view(), + name="event-date-range", + ), ] diff --git a/manage_events/api/views.py b/manage_events/api/views.py index 83648ee..601aec3 100644 --- a/manage_events/api/views.py +++ b/manage_events/api/views.py @@ -14,6 +14,7 @@ from goodtimes.utils import ApiResponse, CapacityError from rest_framework.permissions import IsAuthenticated from rest_framework_simplejwt.authentication import JWTAuthentication from manage_events.api.serializers import ( + EventDateRangeSerializer, EventMasterSearchSerializer, EventMasterSerializer, CreateEventSerializer, @@ -36,7 +37,7 @@ from manage_events.models import ( ) import requests -from manage_events.utils import filter_events_by_location +from manage_events.utils import filter_events_by_location, haversine_one class CreateEventApi(APIView): @@ -75,6 +76,30 @@ class CreateVenueApi(APIView): ) +class VenueDeleteAPIView(APIView): + """ + API view to set a venue's 'deleted' to True and 'active' to False. + """ + + def patch(self, request, pk): + try: + venue = Venue.objects.get(pk=pk) + venue.deleted = True + venue.active = False + venue.save() + return ApiResponse.success( + status=status.HTTP_200_OK, + message=constants.SUCCESS, + data="Venue deleted successfully", + ) + except Venue.DoesNotExist: + return ApiResponse.error( + status=status.HTTP_400_BAD_REQUEST, + message=constants.FAILURE, + errors="Venue not found", + ) + + class EventsAPIView(APIView): authentication_classes = [JWTAuthentication] permission_classes = [IsAuthenticated] @@ -449,16 +474,19 @@ class EventStatusUpdateAPIView(APIView): ) +# Filter Events within 10 KMs class EventFilterByLocationAPIView(APIView): authentication_classes = [JWTAuthentication] permission_classes = [IsAuthenticated] def get(self, request, *args, **kwargs): principal = request.user - + today = timezone.now().date() # Fetching user location from IAmPrincipalLocation try: - user_location = IAmPrincipalLocation.objects.filter(principal=principal).last() + user_location = IAmPrincipalLocation.objects.filter( + principal=principal + ).last() user_lat = user_location.latitude user_lon = user_location.longitude except IAmPrincipalLocation.DoesNotExist: @@ -468,12 +496,31 @@ class EventFilterByLocationAPIView(APIView): 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)) + max_distance_km = 10 # Set your desired maximum distance + current_and_future_events_query = Q(active=True, deleted=False, draft=False) & ( + Q(start_date__lte=today, end_date__gte=today) | Q(start_date__gt=today) + ) + + # Get the queryset based on the filter conditions + events_queryset = Event.objects.filter(current_and_future_events_query) + + venue_ids = events_queryset.values_list("venue", flat=True).distinct() + venues = Venue.objects.filter(id__in=venue_ids) + + venues_within_range = [] + for venue in venues: + distance = haversine_one( + user_lon, user_lat, venue.longitude, venue.latitude + ) + print("distance: ", distance) + if distance <= max_distance_km: + print( + venue + ) # This will print the venue object, or whatever __str__ returns for the venue model + venues_within_range.append(venue.id) + print("venues_within_range: ", venues_within_range) + # venues_data = [venue_to_dict(venue) for venue in venues_within_range] + events = Event.objects.filter(venue__id__in=venues_within_range) # Serialize and return the filtered events serializer = EventDetailSerializer( @@ -484,3 +531,62 @@ class EventFilterByLocationAPIView(APIView): message=constants.SUCCESS, status=status.HTTP_200_OK, ) + + +# Filter Principal's Favorites Events +class FavoriteEventsList(APIView): + authentication_classes = [JWTAuthentication] + permission_classes = [IsAuthenticated] + + def get(self, request, *args, **kwargs): + principal = request.user + events = Event.objects.filter( + id__in=Favorites.objects.filter(principal=principal) + .select_related("event") + .values_list("event_id", flat=True), + deleted=False, + active=True, + draft=False, + ) + serializer = EventDetailSerializer( + events, context={"request": request}, many=True + ) + return ApiResponse.success( + data=serializer.data, + message=constants.SUCCESS, + status=status.HTTP_200_OK, + ) + + +class EventDateRangeAPIView(APIView): + authentication_classes = [JWTAuthentication] + permission_classes = [IsAuthenticated] + + def get(self, request, *args, **kwargs): + serializer = EventDateRangeSerializer(data=request.data) + if not serializer.is_valid(): + return ApiResponse.error( + errors=serializer.errors, + message=constants.FAILURE, + status=status.HTTP_400_BAD_REQUEST, + ) + + start_date = serializer.validated_data["start_date"] + end_date = serializer.validated_data["end_date"] + + events = Event.objects.filter( + Q(end_date__gte=start_date) + & Q(start_date__lte=end_date) + & Q(deleted=False) + & Q(active=True) + & Q(draft=False) + ) + + 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/models.py b/manage_events/models.py index 3a29e52..0cf16f8 100644 --- a/manage_events/models.py +++ b/manage_events/models.py @@ -1,8 +1,10 @@ from django.db import models from accounts.models import BaseModel, IAmPrincipal from django.db import transaction + # from django.contrib.gis.db import models as gis_models + # Create your models here. class EventCategory(BaseModel): title = models.CharField(max_length=255) @@ -50,7 +52,9 @@ class EventMaster(BaseModel): class Event(BaseModel): title = models.CharField(max_length=255) category = models.ForeignKey(EventCategory, on_delete=models.CASCADE) - event_master = models.ForeignKey(EventMaster, on_delete=models.SET_NULL, null=True, blank=True) + event_master = models.ForeignKey( + EventMaster, on_delete=models.SET_NULL, null=True, blank=True + ) description = models.TextField(blank=True, null=True) image = models.ImageField(upload_to="event", null=True, blank=True) status = models.CharField( @@ -123,11 +127,13 @@ class PrincipalPreference(BaseModel): 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') + 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') + 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 index 18ae3d2..d7edf46 100644 --- a/manage_events/utils.py +++ b/manage_events/utils.py @@ -1,4 +1,5 @@ import math +from accounts.models import IAmPrincipalLocation from manage_events.models import Event, Venue @@ -36,3 +37,36 @@ def filter_events_by_location(user_lat, user_lon, radius_km=10): # Filter events based on the venues within the radius events = Event.objects.filter(venue__id__in=venues_within_radius) return events + + +from math import sin, cos, radians, sqrt, atan2 + + +def haversine_one(lon1, lat1, lon2, lat2): + """ + Calculates the distance between two points on a sphere using the Haversine formula. + + Args: + lon1 (float): Longitude of the first point. + lat1 (float): Latitude of the first point. + lon2 (float): Longitude of the second point. + lat2 (float): Latitude of the second point. + + Returns: + float: Distance in kilometers between the two points. + """ + R = 6371 # Earth's radius in kilometers + + dlon = radians(lon2 - lon1) + dlat = radians(lat2 - lat1) + + a = sin(dlat / 2) * sin(dlat / 2) + cos(radians(lat1)) * cos(radians(lat2)) * sin( + dlon / 2 + ) * sin(dlon / 2) + c = 2 * atan2(sqrt(a), sqrt(1 - a)) + + return R * c + + +# Example usage: +