From 3c9e4edeb60b1d0e9df87ea366ba403e2ef6b218 Mon Sep 17 00:00:00 2001 From: Ahmed Nassar Date: Fri, 20 Dec 2024 14:51:52 +0200 Subject: [PATCH] feat: Enhance home endpoint with dynamic uptime and detailed API overview; update API title and description --- apps/stations/views.py | 105 ++++++++++++++++++---------------------- egypt_metro/settings.py | 40 ++++++++++----- egypt_metro/urls.py | 19 +++++--- egypt_metro/views.py | 76 +++++++++++++++++++++++++++-- 4 files changed, 161 insertions(+), 79 deletions(-) diff --git a/apps/stations/views.py b/apps/stations/views.py index 96dee73..33c5b15 100644 --- a/apps/stations/views.py +++ b/apps/stations/views.py @@ -1,15 +1,19 @@ # 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 @@ -17,6 +21,8 @@ find_nearest_station, ) # Import the find_nearest_station function +logger = logging.getLogger(__name__) + # Create your views here. class StationListView(generics.ListAPIView): @@ -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): @@ -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) @@ -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): @@ -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( diff --git a/egypt_metro/settings.py b/egypt_metro/settings.py index e3ce51a..af95276 100644 --- a/egypt_metro/settings.py +++ b/egypt_metro/settings.py @@ -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 = [ @@ -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 @@ -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": [ @@ -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 @@ -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 diff --git a/egypt_metro/urls.py b/egypt_metro/urls.py index c51093a..b813efe 100644 --- a/egypt_metro/urls.py +++ b/egypt_metro/urls.py @@ -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="support@egyptmetro.com"), + # license=openapi.License(name="MIT License"), ), public=True, permission_classes=(AllowAny,), @@ -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) diff --git a/egypt_metro/views.py b/egypt_metro/views.py index 82f101f..4a69f4a 100644 --- a/egypt_metro/views.py +++ b/egypt_metro/views.py @@ -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///", # 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""" + + + Egypt Metro API + + +

Welcome to Egypt Metro Backend

+

Status: {data['status']}

+

Version: {data['version']}

+

Uptime: {data['uptime']}

+

Quick Links

+ +

API Routes

+
    + """ + for name, path in data["api_routes"].items(): + html_content += f"
  • {name}
  • " + html_content += "
" + + return HttpResponse(html_content) + + # Return the JSON response with status code 200 + return JsonResponse(data, status=200) def health_check(request):