Skip to content

Commit

Permalink
feat: Implement particles.js background, optimize image loading, and …
Browse files Browse the repository at this point in the history
…add admin customization styles
  • Loading branch information
AhmedNassar7 committed Feb 14, 2025
1 parent 152f3b3 commit 1ec4006
Show file tree
Hide file tree
Showing 16 changed files with 680 additions and 530 deletions.
15 changes: 15 additions & 0 deletions .hintrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
{
"extends": [
"development"
],
"hints": {
"compat-api/css": [
"default",
{
"ignore": [
"-webkit-backdrop-filter"
]
}
]
}
}
144 changes: 91 additions & 53 deletions apps/stations/admin.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
# apps/stations/admin.py

from venv import logger
from django.contrib import admin
from django.utils.html import format_html
from django.db.models import Count
import markupsafe

from egypt_metro import settings
from .models import Line, Station, LineStation
from apps.stations.management.commands.populate_metro_data import (
Command as MetroDataCommand,
)
from apps.stations.management.commands.populate_metro_data import Command as MetroDataCommand

# Get constants from MetroDataCommand
LINE_OPERATIONS = MetroDataCommand.LINE_OPERATIONS
Expand Down Expand Up @@ -90,7 +92,7 @@ class StationAdmin(admin.ModelAdmin):
list_display = [
"name",
"get_lines_display",
"is_interchange_display",
"is_interchange_display", # Use the modified method
"get_coordinates",
"get_connections",
]
Expand All @@ -106,54 +108,83 @@ class StationAdmin(admin.ModelAdmin):
("Location", {"fields": ("latitude", "longitude"), "classes": ("collapse",)}),
)

def is_interchange_display(self, obj):
"""Display interchange status"""
return obj.lines.count() > 1
is_interchange_display.boolean = True
is_interchange_display.short_description = "Interchange"

def get_lines_display(self, obj):
"""Display lines with their colors"""
return format_html(
" ".join(
f'<span style="background-color: {line.color_code}; '
f'padding: 3px 7px; border-radius: 3px; margin: 0 2px;">'
f"{line.name}</span>"
for line in obj.lines.all()
)
)
"""Display lines without colors"""
return ", ".join([line.name for line in obj.lines.all()])

get_lines_display.short_description = "Lines"

def is_interchange_display(self, obj):
"""Display interchange status with icon"""
is_interchange = obj.lines.count() > 1
icon = "✓" if is_interchange else "✗"
color = "green" if is_interchange else "red"
return format_html(f'<span style="color: {color};">{icon}</span>')
def is_interchange_status(self, obj):
"""
Custom method to display interchange status with a more robust approach
"""
try:
# Determine interchange status
is_interchange = obj.lines.count() > 1

# Use Django's built-in boolean icon method
return markupsafe(
'<img src="{}" alt="{}" style="width:16px; height:16px;">'.format(
f'/static/admin/img/icon-{"yes" if is_interchange else "no"}.svg',
'Yes' if is_interchange else 'No'
)
)
except Exception as e:
if settings.DEBUG:
print(f"Error in is_interchange_status: {e}")
return markupsafe('<span style="color:red;">Error</span>')

is_interchange_display.short_description = "Interchange"
is_interchange_display.boolean = True
is_interchange_status.short_description = "Interchange"
is_interchange_status.boolean = True

def get_coordinates(self, obj):
"""Display coordinates with link to map"""
if obj.latitude and obj.longitude:
return format_html(
'<a href="https://www.google.com/maps?q={},{}" target="_blank">'
"{:.6f}, {:.6f}</a>",
obj.latitude,
obj.longitude,
obj.latitude,
obj.longitude,
)
return "N/A"
try:
# Check if coordinates exist and are valid
if (obj.latitude is not None and obj.longitude is not None and isinstance(obj.latitude, (int, float)) and isinstance(obj.longitude, (int, float))):

# Safely convert to float and round
lat = round(float(obj.latitude), 6)
lon = round(float(obj.longitude), 6)

# Validate coordinate ranges
if -90 <= lat <= 90 and -180 <= lon <= 180:
return format_html(
'<a href="https://www.google.com/maps?q={},{}" target="_blank">{}, {}</a>',
lat, lon, lat, lon
)

logger.warning(f"Invalid coordinates for station {obj.name}: Lat {obj.latitude}, Lon {obj.longitude}")
return "N/A"
except Exception as e:
# Log the full error
logger.error(f"Coordinate error for station {obj.name}: {e}", exc_info=True)
return "Invalid Coordinates"

get_coordinates.short_description = "Location"

def get_connections(self, obj):
"""Display connecting stations"""
if not obj.is_interchange():
return "-"
connections = []
for conn in CONNECTING_STATIONS:
if conn["name"] == obj.name:
connections.extend(conn["lines"])
return " ↔ ".join(connections) if connections else "-"

try:
if not obj.is_interchange():
return "-"

connections = []
for conn in CONNECTING_STATIONS:
if conn["name"] == obj.name:
connections.extend(conn["lines"])

return " ↔ ".join(connections) if connections else "-"
except Exception as e:
if settings.DEBUG:
print(f"Error in get_connections: {e}")
return "Error"
get_connections.short_description = "Connections"


Expand All @@ -174,21 +205,28 @@ class LineStationAdmin(admin.ModelAdmin):

def get_next_station(self, obj):
"""Display next station in sequence"""
next_station = LineStation.objects.filter(
line=obj.line, order=obj.order + 1
).first()
return next_station.station.name if next_station else "-"

get_next_station.short_description = "Next Station"
try:
next_station = LineStation.objects.filter(
line=obj.line, order=obj.order + 1
).first()
return next_station.station.name if next_station else "-"
except Exception as e:
if settings.DEBUG:
print(f"Error in get_next_station: {e}")
return "Error"

def get_distance_to_next(self, obj):
"""Display distance to next station"""
next_station = LineStation.objects.filter(
line=obj.line, order=obj.order + 1
).first()
if next_station:
distance = obj.station.distance_to(next_station.station)
return f"{distance / 1000:.2f} km"
return "-"

try:
next_station = LineStation.objects.filter(
line=obj.line, order=obj.order + 1
).first()
if next_station:
distance = obj.station.distance_to(next_station.station)
return f"{distance / 1000:.2f} km"
return "-"
except Exception as e:
if settings.DEBUG:
print(f"Error in get_distance_to_next: {e}")
return "Error"
get_distance_to_next.short_description = "Distance to Next"
6 changes: 6 additions & 0 deletions apps/stations/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,12 @@ def is_interchange(self) -> bool:
"""Checks if this is an interchange station."""
return self.lines.count() > 1

def is_interchange_display(self, obj):
"""Display interchange status"""
return obj.is_interchange
is_interchange_display.boolean = True
is_interchange_display.short_description = "Interchange"

def get_nearest_interchange(
self, target_line: Line
) -> Optional[Tuple["Station", float]]:
Expand Down
140 changes: 108 additions & 32 deletions egypt_metro/views.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,13 @@
# egypt_metro/views.py

import logging
from django.http import JsonResponse
from django.shortcuts import render
import os
from django.http import HttpResponse, JsonResponse
from django.db import connection
# from django.utils.timezone import now
from datetime import datetime
from django.views.decorators.csrf import csrf_exempt

from egypt_metro import settings

logger = logging.getLogger(__name__)

# Define the API's start time globally (when the server starts)
Expand All @@ -26,35 +24,113 @@ def log_session_data(request):

@csrf_exempt
def home(request):
context = {
'version': '1.0.0',
'environment': settings.ENVIRONMENT,
'current_time': datetime.now().strftime('%d/%m/%Y %I:%M:%S %p'),
'metro_lines': [
{
'name': 'First Line',
'color': '#FF0000',
'stations': 35,
'start': 'Helwan',
'end': 'New El-Marg'
},
{
'name': 'Second Line',
'color': '#008000',
'stations': 20,
'start': 'El-Mounib',
'end': 'Shubra El-Kheima'
},
{
'name': 'Third Line',
'color': '#0000FF',
'stations': 34,
'start': 'Adly Mansour',
'end': 'Rod al-Farag Axis'
}
]
"""
Home endpoint that provides an overview of the API.
Includes links to key features like admin panel, documentation, health checks, and API routes.
"""

# Get the current time
current_time = datetime.utcnow()

# Format the current time with minutes and seconds
current_date_time = current_time.strftime("%d/%m/%Y %I:%M:%S %p")

# Calculate uptime dynamically
uptime_delta = current_time - API_START_TIME
days, remainder = divmod(uptime_delta.total_seconds(), 86400) # 86400 seconds in a day
hours, remainder = divmod(remainder, 3600) # 3600 seconds in an hour
minutes, seconds = divmod(remainder, 60) # 60 seconds in a minute

# Get the current environment (dev or prod)
environment = os.getenv("ENVIRONMENT", "dev") # Default to dev if not set
logger.debug(f"Current environment: {environment}")

# Data to return as JSON response
data = {
"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
"current_date_time": current_date_time, # Current date and time with minutes and seconds
"environment": environment, # Current environment (dev or prod)
"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
"precomputed_route": "/api/routes/route/<start_station_id>/<end_station_id>/", # Precomputed route
"shortest_route": "/api/routes/shortest/", # Shortest route
},
}
return render(request, 'home.html', context)

# 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>
<style>
body {{
font-family: Arial, sans-serif;
margin: 20px;
padding: 0;
background-color: #f4f4f4;
color: #333;
}}
h1 {{
color: #2c3e50;
}}
h2 {{
color: #34495e;
}}
ul {{
list-style-type: none;
padding: 0;
}}
li {{
margin: 10px 0;
}}
a {{
color: #3498db;
text-decoration: none;
}}
a:hover {{
text-decoration: underline;
}}
</style>
</head>
<body>
<h1>Welcome to Egypt Metro Backend</h1>
<p>Version: {data['version']}</p>
<p>Date & Time: {data['current_date_time']}</p>
<p>Environment: {data['environment']}</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
Loading

0 comments on commit 1ec4006

Please sign in to comment.