Skip to content

Commit

Permalink
feat: Implement serializers, pagination, and views for stations; add …
Browse files Browse the repository at this point in the history
…search functionality and enhance nearest station retrieval
  • Loading branch information
AhmedNassar7 committed Dec 12, 2024
1 parent c6f7c2b commit a25c5b8
Show file tree
Hide file tree
Showing 8 changed files with 238 additions and 114 deletions.
4 changes: 2 additions & 2 deletions apps/stations/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ class LineAdmin(admin.ModelAdmin):
"""
Admin configuration for the Line model.
"""
list_display = ("name", "color_code", "total_stations") # fields to display
list_display = ("name", "total_stations") # fields to display
search_fields = ("name",) # fields to search
inlines = [LineStationInline] # inline to display

Expand All @@ -28,7 +28,7 @@ class StationAdmin(admin.ModelAdmin):
"""
Admin configuration for the Station model.
"""
list_display = ("name", "latitude", "longitude", "is_interchange") # fields to display
list_display = ("name", "is_interchange") # fields to display
search_fields = ("name",) # fields to search
inlines = [LineStationInline] # inline to display
fieldsets = (
Expand Down
8 changes: 8 additions & 0 deletions apps/stations/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,14 @@ class Station(models.Model):
latitude = models.FloatField(null=True, blank=True) # GPS latitude
longitude = models.FloatField(null=True, blank=True) # GPS longitude
lines = models.ManyToManyField(Line, through="LineStation", related_name="stations")

# Optimized method to get stations by line or name
@staticmethod
def get_stations_by_query(query):
return Station.objects.filter(
models.Q(name__icontains=query) |
models.Q(lines__name__icontains=query)
).distinct()

def __str__(self):
return self.name
Expand Down
21 changes: 21 additions & 0 deletions apps/stations/pagination.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# apps/stations/pagination.py

from rest_framework.pagination import PageNumberPagination
from rest_framework.response import Response


class StandardResultsSetPagination(PageNumberPagination):
page_size = 10
page_size_query_param = 'page_size'
max_page_size = 100

def get_paginated_response(self, data):
try:
return Response({
'count': self.page.paginator.count,
'total_pages': self.page.paginator.num_pages,
'current_page': self.page.number,
'results': data
})
except Exception as e:
return Response({"error": str(e)}, status=500)
14 changes: 14 additions & 0 deletions apps/stations/serializers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
from rest_framework import serializers
from .models import Station, Line

class LineSerializer(serializers.ModelSerializer):
class Meta:
model = Line
fields = ['id', 'name']

class StationSerializer(serializers.ModelSerializer):
lines = LineSerializer(many=True) # Serialize associated lines

class Meta:
model = Station
fields = ['id', 'name', 'lines']
3 changes: 2 additions & 1 deletion apps/stations/urls.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
# apps/stations/urls.py

from django.urls import path
from .views import TripDetailsView, NearestStationView
from .views import TripDetailsView, NearestStationView, StationListView

urlpatterns = [
path('stations-list/', StationListView.as_view(), name='stations-list'),
path("trip-details/<int:start_station_id>/<int:end_station_id>/", TripDetailsView.as_view(), name="trip-details"),
path('nearest-station/', NearestStationView.as_view(), name='nearest-station'),
]
11 changes: 6 additions & 5 deletions apps/stations/utils/location_helpers.py
Original file line number Diff line number Diff line change
@@ -1,18 +1,19 @@
from geopy.distance import geodesic # type: ignore
# apps/stations/utils/location_helpers.py

from geopy.distance import geodesic # type: ignore
from apps.stations.models import Station


def find_nearest_station(latitude, longitude):
"""Find the nearest station to the user's location."""
user_location = (latitude, longitude)
stations = Station.objects.all()
stations = Station.objects.exclude(latitude__isnull=True, longitude__isnull=True)

if not stations.exists():
return None, None

nearest_station = min(
stations,
key=lambda station: geodesic(user_location, (station.latitude, station.longitude)).meters,
)

distance = geodesic(user_location, (nearest_station.latitude, nearest_station.longitude)).meters
return nearest_station, distance
106 changes: 89 additions & 17 deletions apps/stations/views.py
Original file line number Diff line number Diff line change
@@ -1,17 +1,47 @@
# apps/stations/views.py

from django.shortcuts import render
from rest_framework.views import APIView
from rest_framework.response import Response
from django.http import JsonResponse
from apps.stations.models import Station
from .services.route_service import RouteService
from apps.stations.services.ticket_service import calculate_ticket_price
from apps.stations.utils.location_helpers import find_nearest_station
from rest_framework import generics, status # Import generics for ListAPIView
from rest_framework.permissions import AllowAny # Import AllowAny for public access
from rest_framework.views import APIView # Import APIView for creating API views
from rest_framework.response import Response # Import Response for sending JSON responses
from django.db.models import Q # Import Q for complex queries
from apps.stations.models import Station # Import the Station model
from .serializers import StationSerializer # Import the StationSerializer
from .pagination import StandardResultsSetPagination # Import the pagination class
from apps.stations.services.ticket_service import calculate_ticket_price # Import the ticket price calculation service
from apps.stations.utils.location_helpers import find_nearest_station # Import the find_nearest_station function
from geopy.distance import geodesic # type: ignore

# Create your views here.
class StationListView(generics.ListAPIView):
queryset = Station.objects.all() # Get all stations
serializer_class = StationSerializer # Use the StationSerializer
pagination_class = StandardResultsSetPagination # Apply pagination
permission_classes = [AllowAny] # Allow access

def get_queryset(self):
"""
Retrieves a paginated list of stations, with optional search filtering (name or line name).
"""
queryset = Station.objects.all()
search_term = self.request.query_params.get('search', None)

if search_term:
queryset = queryset.filter( # Filter stations by name or line name
Q(name__icontains=search_term) | # Case-insensitive search by station name
Q(lines__name__icontains=search_term) # Case-insensitive search by line name
).distinct() # Remove duplicates

queryset = queryset.prefetch_related('lines') # Optimize querying of related lines
return queryset # Return the filtered queryset


class TripDetailsView(APIView):
"""
Provides trip details between two stations, including ticket price, travel time, and distance.
"""
permission_classes = [AllowAny] # Public access

def get(self, request, start_station_id, end_station_id):
try:
# Get stations
Expand Down Expand Up @@ -43,15 +73,57 @@ def get(self, request, start_station_id, end_station_id):


class NearestStationView(APIView):
"""
Finds the nearest station to the user's location.
"""
permission_classes = [AllowAny] # Public access

def get(self, request):
latitude = float(request.query_params.get("latitude"))
longitude = float(request.query_params.get("longitude"))
try:
latitude = request.query_params.get("latitude")
longitude = request.query_params.get("longitude")

if not latitude or not longitude:
return Response({"error": "Latitude and Longitude are required."}, status=status.HTTP_400_BAD_REQUEST)

latitude, longitude = float(latitude), float(longitude)
nearest_station, distance = find_nearest_station(latitude, longitude)

if not latitude or not longitude:
return Response({"error": "Latitude and Longitude are required"}, status=400)
if nearest_station is None:
return Response({"error": "No stations available."}, status=status.HTTP_404_NOT_FOUND)

nearest_station, distance = find_nearest_station(latitude, longitude)
return Response({
"nearest_station": nearest_station.name,
"distance": round(distance, 2),
})
return Response({
"nearest_station": nearest_station.name,
"distance": round(distance, 2),
})
except ValueError:
return Response({"error": "Invalid Latitude or Longitude."}, status=status.HTTP_400_BAD_REQUEST)
except Exception as e:
return Response({"error": str(e)}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)

def post(self, request):
"""
Handle POST request for nearest station based on latitude and longitude.
"""
try:
# Extract latitude and longitude from the request body (JSON)
latitude = request.data.get("latitude")
longitude = request.data.get("longitude")

if not latitude or not longitude:
return Response({"error": "Latitude and Longitude are required."}, status=status.HTTP_400_BAD_REQUEST)

latitude, longitude = float(latitude), float(longitude)
nearest_station, distance = find_nearest_station(latitude, longitude)

if nearest_station is None:
return Response({"error": "No stations available."}, status=status.HTTP_404_NOT_FOUND)

return Response({
"nearest_station": nearest_station.name,
"distance": round(distance, 2),
})
except ValueError:
return Response({"error": "Invalid Latitude or Longitude."}, status=status.HTTP_400_BAD_REQUEST)
except Exception as e:
return Response({"error": str(e)}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
Loading

0 comments on commit a25c5b8

Please sign in to comment.