Skip to content

Commit

Permalink
feat: Enhance home endpoint with dynamic uptime and detailed API over…
Browse files Browse the repository at this point in the history
…view; update API title and description
  • Loading branch information
AhmedNassar7 committed Dec 20, 2024
1 parent 411d0fa commit 3c9e4ed
Show file tree
Hide file tree
Showing 4 changed files with 161 additions and 79 deletions.
105 changes: 48 additions & 57 deletions apps/stations/views.py
Original file line number Diff line number Diff line change
@@ -1,22 +1,28 @@
# apps/stations/views.py

import logging
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 rest_framework.exceptions import APIException # Import APIException for custom exceptions
from django.db import DatabaseError # Import DatabaseError for database exceptions
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 django.shortcuts import get_object_or_404 # Import get_object_or_404 for error handling
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

logger = logging.getLogger(__name__)


# Create your views here.
class StationListView(generics.ListAPIView):
Expand All @@ -29,21 +35,35 @@ 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)
try:
# Start with the base queryset
queryset = Station.objects.all()

# Get the search term from the query params and ensure it's a string
search_term = self.request.query_params.get("search", "").strip()

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

queryset = queryset.prefetch_related(
"lines"
) # Optimize querying of related lines
return queryset # Return the filtered queryset
# Prefetch related lines to optimize database access
queryset = queryset.prefetch_related("lines")

# Return the filtered queryset
return queryset

except DatabaseError as e:
# Handle database-specific errors gracefully
logger.error(f"Database error: {str(e)}")
raise APIException(f"Database error occurred: {str(e)}")

except Exception as e:
# Handle any other unexpected errors
logger.error(f"Unexpected error: {str(e)}")
raise APIException(f"An unexpected error occurred: {str(e)}")


class TripDetailsView(APIView):
Expand All @@ -55,9 +75,9 @@ class TripDetailsView(APIView):

def get(self, request, start_station_id, end_station_id):
try:
# Get stations
start_station = Station.objects.get(id=start_station_id)
end_station = Station.objects.get(id=end_station_id)
# Get stations with proper error handling using get_object_or_404
start_station = get_object_or_404(Station, id=start_station_id)
end_station = get_object_or_404(Station, id=end_station_id)

# Calculate ticket price using the service
ticket_price = calculate_ticket_price(start_station, end_station)
Expand All @@ -84,8 +104,10 @@ def get(self, request, start_station_id, end_station_id):
}
return Response(data)

except Station.DoesNotExist:
return Response({"error": "Station not found."}, status=status.HTTP_404_NOT_FOUND)
except Exception as e:
return Response({"error": str(e)}, status=400)
return Response({"error": str(e)}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)


class NearestStationView(APIView):
Expand All @@ -95,50 +117,19 @@ class NearestStationView(APIView):

permission_classes = [AllowAny] # Public access

def get(self, request):
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 nearest_station is None:
return Response(
{"error": "No stations available."},
status=status.HTTP_404_NOT_FOUND,
)
permission_classes = [AllowAny] # Public access

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 get(self, request):
return self._get_nearest_station(request)

def post(self, request):
"""
Handle POST request for nearest station based on latitude and longitude.
"""
return self._get_nearest_station(request)

def _get_nearest_station(self, request):
try:
# Extract latitude and longitude from the request body (JSON)
latitude = request.data.get("latitude")
longitude = request.data.get("longitude")
# Extract latitude and longitude from query params (GET) or request body (POST)
latitude = request.query_params.get("latitude") or request.data.get("latitude")
longitude = request.query_params.get("longitude") or request.data.get("longitude")

if not latitude or not longitude:
return Response(
Expand Down
40 changes: 29 additions & 11 deletions egypt_metro/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,16 +16,24 @@
from datetime import timedelta # Time delta for JWT tokens
from corsheaders.defaults import default_headers # Default headers for CORS
from decouple import config
from datetime import datetime

# Build paths inside the project like this: BASE_DIR / 'subdir'.
BASE_DIR = Path(__file__).resolve().parent.parent # Base directory for the project

# Load the appropriate .env file based on an environment variable
ENVIRONMENT = os.getenv("ENVIRONMENT", "dev") # Default to dev
dotenv_path = BASE_DIR / f"env/.env.{ENVIRONMENT}"
load_dotenv(dotenv_path)

# SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = os.getenv("SECRET_KEY") # Secret key for Django

# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = os.getenv("DEBUG", "False") == "True" # Default to False
ALLOWED_HOSTS = config("ALLOWED_HOSTS", default="").split(",")

# Define the API's start time globally using an environment variable or default to `now`
API_START_TIME = os.getenv("API_START_TIME", datetime.now().isoformat())

# Application definition

INSTALLED_APPS = [
Expand All @@ -44,6 +52,7 @@
"rest_framework", # REST framework
"rest_framework_simplejwt", # JWT authentication
"corsheaders", # CORS headers
'drf_yasg', # Swagger
# "debug_toolbar", # Debug toolbar

# Custom apps
Expand All @@ -66,21 +75,33 @@
]

ROOT_URLCONF = "egypt_metro.urls" # Root URL configuration
WSGI_APPLICATION = "egypt_metro.wsgi.application" # WSGI application

# CORS settings
CORS_ALLOW_ALL_ORIGINS = (
os.getenv("CORS_ALLOW_ALL_ORIGINS", "False") == "True"
) # Default to False

if not CORS_ALLOW_ALL_ORIGINS:
CORS_ALLOWED_ORIGINS = [
"https://backend-54v5.onrender.com",
"http://localhost:8000",
]

CORS_ALLOW_HEADERS = list(default_headers) + [ # Default headers + custom headers
"Authorization", # Authorization header
"Content-Type", # Content type header
]

CORS_ALLOW_CREDENTIALS = True # Allow credentials

if ENVIRONMENT == "dev":
CORS_ALLOW_ALL_ORIGINS = True

TEMPLATES = [
{
"BACKEND": "django.template.backends.django.DjangoTemplates",
"DIRS": [], # Add template directories here
"DIRS": [os.path.join(BASE_DIR, 'templates')], # Add template directories here
"APP_DIRS": True, # Enable app templates
"OPTIONS": {
"context_processors": [
Expand All @@ -94,24 +115,17 @@
},
]

WSGI_APPLICATION = "egypt_metro.wsgi.application" # WSGI application

# Database
# https://docs.djangoproject.com/en/5.1/ref/settings/#databases

# Custom User Model
AUTH_USER_MODEL = "users.User"

# Load the appropriate .env file based on an environment variable
ENVIRONMENT = os.getenv("ENVIRONMENT", "dev") # Default to dev
dotenv_path = BASE_DIR / f"env/.env.{ENVIRONMENT}"
load_dotenv(dotenv_path)

# Load secret file if in production
# if ENVIRONMENT == "prod":
# load_dotenv("/etc/secrets/env.prod") # Load production secrets

# General settings
DEBUG = os.getenv("DEBUG", "False") == "True" # Default to False
SECRET_KEY = os.getenv("SECRET_KEY") # Secret key for Django
BASE_URL = os.getenv("BASE_URL") # Base URL for the project
JWT_SECRET = os.getenv("JWT_SECRET") # Secret key for JWT tokens
Expand Down Expand Up @@ -303,6 +317,10 @@
"whitenoise.storage.CompressedManifestStaticFilesStorage" # Static files storage
)

STATICFILES_DIRS = [
os.path.join(BASE_DIR, 'static'),
]

# Media files (optional, if your project uses media uploads)
MEDIA_URL = "/media/"
MEDIA_ROOT = BASE_DIR / "mediafiles" # Folder where media files will be uploaded
Expand Down
19 changes: 12 additions & 7 deletions egypt_metro/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,12 @@
# OpenAPI schema view
schema_view = get_schema_view(
openapi.Info(
title="Your API",
title="Metro API",
default_version="v1",
description="API documentation for the Flutter app",
description="API documentation for Metro application",
terms_of_service="https://www.google.com/policies/terms/",
# contact=openapi.Contact(email="[email protected]"),
# license=openapi.License(name="MIT License"),
),
public=True,
permission_classes=(AllowAny,),
Expand All @@ -53,11 +56,13 @@
path("health/", health_check, name="health_check"), # Health check

# Documentation
# path(
# "swagger/",
# schema_view.with_ui("swagger", cache_timeout=0),
# name="schema-swagger-ui",
# ), # Swagger UI
path(
"swagger/",
schema_view.with_ui("swagger", cache_timeout=0),
name="schema-swagger-ui",
), # Swagger UI

# path("redoc/", schema_view.with_ui("redoc", cache_timeout=0), name="schema-redoc"),
]

# Debug Toolbar (only for development)
Expand Down
76 changes: 72 additions & 4 deletions egypt_metro/views.py
Original file line number Diff line number Diff line change
@@ -1,18 +1,86 @@
import logging
from django.http import JsonResponse
from django.db import connection
from django.utils.timezone import now
from datetime import timedelta
from django.http import HttpResponse

from django.views.decorators.csrf import csrf_exempt

logger = logging.getLogger(__name__)

# Define the API's start time globally (when the server starts)
API_START_TIME = now()


@csrf_exempt
def home(request):
"""
Home endpoint that provides an overview of the API.
Includes links to key features like admin panel, documentation, health checks, and API routes.
"""

# Calculate uptime dynamically
current_time = now()
uptime_delta = current_time - API_START_TIME
uptime = str(timedelta(seconds=uptime_delta.total_seconds()))

# Data to return as JSON response
data = {
"message": "Welcome to Egypt Metro Backend",
"admin_panel": "/admin/",
"api_documentation": "/docs/",
"health_check": "/health/",
"status": "OK", # Status indicating the API is operational
"admin_panel": "/admin/", # Link to Django admin panel
"api_documentation": "/docs/", # Link to API documentation
"health_check": "/health/", # Health check endpoint
"swagger": "/swagger/", # Swagger API documentation
"redoc": "/redoc/", # Redoc API documentation
"version": "1.0.0", # Backend version
"uptime": uptime, # Dynamically calculated uptime
"api_routes": {
"users": "/api/users/", # User-related routes
"register": "/api/users/register/", # User registration
"login": "/api/users/login/", # User login
"profile": "/api/users/profile/", # User profile
"update_profile": "/api/users/profile/update/", # Update profile
"token_refresh": "/api/users/token/refresh/", # Refresh token
"stations": "/api/stations/", # Stations-related routes
"stations_list": "/api/stations/list/", # List stations
"trip_details": "/api/stations/trip/<start_station_id>/<end_station_id>/", # Trip details
"nearest_station": "/api/stations/nearest/", # Nearest station
},
}
return JsonResponse(data)

# Check if browser or API client
if "text/html" in request.META.get("HTTP_ACCEPT", ""):
html_content = f"""
<html>
<head>
<title>Egypt Metro API</title>
</head>
<body>
<h1>Welcome to Egypt Metro Backend</h1>
<p>Status: {data['status']}</p>
<p>Version: {data['version']}</p>
<p>Uptime: {data['uptime']}</p>
<h2>Quick Links</h2>
<ul>
<li><a href="{data['admin_panel']}">Admin Panel</a></li>
<li><a href="{data['api_documentation']}">API Documentation</a></li>
<li><a href="{data['health_check']}">Health Check</a></li>
<li><a href="{data['swagger']}">Swagger API Documentation</a></li>
<li><a href="{data['redoc']}">Redoc API Documentation</a></li>
</ul>
<h2>API Routes</h2>
<ul>
"""
for name, path in data["api_routes"].items():
html_content += f"<li><a href='{path}'>{name}</a></li>"
html_content += "</ul></body></html>"

return HttpResponse(html_content)

# Return the JSON response with status code 200
return JsonResponse(data, status=200)


def health_check(request):
Expand Down

0 comments on commit 3c9e4ed

Please sign in to comment.