mirror of
https://github.com/pacnpal/thrillwiki_django_no_react.git
synced 2026-02-05 11:25:19 -05:00
feat: Implement a new notifications application, add admin API views for dashboard metrics, introduce scheduled tasks, and update API routing and project configurations.
This commit is contained in:
@@ -30,4 +30,7 @@ urlpatterns = [
|
||||
views.MapCacheAPIView.as_view(),
|
||||
name="map_cache_invalidate",
|
||||
),
|
||||
# Location detection and enrichment
|
||||
path("detect-location/", views.DetectLocationView.as_view(), name="detect_location"),
|
||||
path("enrich-location/", views.EnrichLocationView.as_view(), name="enrich_location"),
|
||||
]
|
||||
|
||||
@@ -999,3 +999,245 @@ MapSearchView = MapSearchAPIView
|
||||
MapBoundsView = MapBoundsAPIView
|
||||
MapStatsView = MapStatsAPIView
|
||||
MapCacheView = MapCacheAPIView
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Location Detection / Enrichment Endpoints
|
||||
# =============================================================================
|
||||
|
||||
|
||||
@extend_schema_view(
|
||||
post=extend_schema(
|
||||
summary="Detect user location from IP",
|
||||
description="Detect the user's approximate location based on their IP address.",
|
||||
request={
|
||||
"application/json": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"ip_address": {
|
||||
"type": "string",
|
||||
"description": "IP address to geolocate. If not provided, uses request IP.",
|
||||
}
|
||||
},
|
||||
}
|
||||
},
|
||||
responses={
|
||||
200: {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"latitude": {"type": "number"},
|
||||
"longitude": {"type": "number"},
|
||||
"city": {"type": "string"},
|
||||
"region": {"type": "string"},
|
||||
"country": {"type": "string"},
|
||||
"timezone": {"type": "string"},
|
||||
},
|
||||
}
|
||||
},
|
||||
tags=["Maps"],
|
||||
),
|
||||
)
|
||||
class DetectLocationView(APIView):
|
||||
"""
|
||||
POST /maps/detect-location/
|
||||
Detect user's location based on IP address using a geolocation service.
|
||||
"""
|
||||
|
||||
permission_classes = [AllowAny]
|
||||
|
||||
def post(self, request):
|
||||
try:
|
||||
# Get IP address from request or payload
|
||||
ip_address = request.data.get("ip_address")
|
||||
if not ip_address:
|
||||
# Get client IP from request
|
||||
x_forwarded_for = request.META.get("HTTP_X_FORWARDED_FOR")
|
||||
if x_forwarded_for:
|
||||
ip_address = x_forwarded_for.split(",")[0].strip()
|
||||
else:
|
||||
ip_address = request.META.get("REMOTE_ADDR", "")
|
||||
|
||||
# For localhost/development, return a default location
|
||||
if ip_address in ("127.0.0.1", "::1", "localhost") or ip_address.startswith("192.168."):
|
||||
return Response(
|
||||
{
|
||||
"latitude": 40.7128,
|
||||
"longitude": -74.006,
|
||||
"city": "New York",
|
||||
"region": "New York",
|
||||
"country": "US",
|
||||
"country_name": "United States",
|
||||
"timezone": "America/New_York",
|
||||
"detected": False,
|
||||
"reason": "localhost_fallback",
|
||||
}
|
||||
)
|
||||
|
||||
# Use IP geolocation service (ipapi.co, ipinfo.io, etc.)
|
||||
import httpx
|
||||
|
||||
try:
|
||||
response = httpx.get(
|
||||
f"https://ipapi.co/{ip_address}/json/",
|
||||
timeout=5.0,
|
||||
headers={"User-Agent": "ThrillWiki/1.0"},
|
||||
)
|
||||
if response.status_code == 200:
|
||||
data = response.json()
|
||||
return Response(
|
||||
{
|
||||
"latitude": data.get("latitude"),
|
||||
"longitude": data.get("longitude"),
|
||||
"city": data.get("city", ""),
|
||||
"region": data.get("region", ""),
|
||||
"country": data.get("country_code", ""),
|
||||
"country_name": data.get("country_name", ""),
|
||||
"timezone": data.get("timezone", ""),
|
||||
"detected": True,
|
||||
}
|
||||
)
|
||||
except httpx.HTTPError as e:
|
||||
logger.warning(f"IP geolocation failed: {e}")
|
||||
|
||||
# Fallback response
|
||||
return Response(
|
||||
{
|
||||
"latitude": None,
|
||||
"longitude": None,
|
||||
"city": "",
|
||||
"region": "",
|
||||
"country": "",
|
||||
"country_name": "",
|
||||
"timezone": "",
|
||||
"detected": False,
|
||||
"reason": "geolocation_failed",
|
||||
}
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
capture_and_log(e, "Detect location from IP", source="api")
|
||||
return Response(
|
||||
{"detail": str(e)},
|
||||
status=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
)
|
||||
|
||||
|
||||
@extend_schema_view(
|
||||
post=extend_schema(
|
||||
summary="Enrich location with geocoding",
|
||||
description="Enrich location data with reverse geocoding (coordinates to address).",
|
||||
request={
|
||||
"application/json": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"latitude": {"type": "number", "required": True},
|
||||
"longitude": {"type": "number", "required": True},
|
||||
},
|
||||
}
|
||||
},
|
||||
responses={
|
||||
200: {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"formatted_address": {"type": "string"},
|
||||
"street_address": {"type": "string"},
|
||||
"city": {"type": "string"},
|
||||
"state": {"type": "string"},
|
||||
"postal_code": {"type": "string"},
|
||||
"country": {"type": "string"},
|
||||
},
|
||||
}
|
||||
},
|
||||
tags=["Maps"],
|
||||
),
|
||||
)
|
||||
class EnrichLocationView(APIView):
|
||||
"""
|
||||
POST /maps/enrich-location/
|
||||
Enrich location with reverse geocoding (coordinates to address).
|
||||
"""
|
||||
|
||||
permission_classes = [AllowAny]
|
||||
|
||||
def post(self, request):
|
||||
try:
|
||||
latitude = request.data.get("latitude")
|
||||
longitude = request.data.get("longitude")
|
||||
|
||||
if latitude is None or longitude is None:
|
||||
return Response(
|
||||
{"detail": "latitude and longitude are required"},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
try:
|
||||
lat = float(latitude)
|
||||
lng = float(longitude)
|
||||
except (TypeError, ValueError):
|
||||
return Response(
|
||||
{"detail": "Invalid latitude or longitude"},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
# Use reverse geocoding service
|
||||
import httpx
|
||||
|
||||
try:
|
||||
# Using Nominatim (OpenStreetMap) - free, no API key required
|
||||
response = httpx.get(
|
||||
"https://nominatim.openstreetmap.org/reverse",
|
||||
params={
|
||||
"lat": lat,
|
||||
"lon": lng,
|
||||
"format": "json",
|
||||
"addressdetails": 1,
|
||||
},
|
||||
timeout=5.0,
|
||||
headers={"User-Agent": "ThrillWiki/1.0"},
|
||||
)
|
||||
if response.status_code == 200:
|
||||
data = response.json()
|
||||
address = data.get("address", {})
|
||||
return Response(
|
||||
{
|
||||
"formatted_address": data.get("display_name", ""),
|
||||
"street_address": address.get("road", ""),
|
||||
"house_number": address.get("house_number", ""),
|
||||
"city": (
|
||||
address.get("city")
|
||||
or address.get("town")
|
||||
or address.get("village")
|
||||
or ""
|
||||
),
|
||||
"state": address.get("state", ""),
|
||||
"postal_code": address.get("postcode", ""),
|
||||
"country": address.get("country", ""),
|
||||
"country_code": address.get("country_code", "").upper(),
|
||||
"enriched": True,
|
||||
}
|
||||
)
|
||||
except httpx.HTTPError as e:
|
||||
logger.warning(f"Reverse geocoding failed: {e}")
|
||||
|
||||
# Fallback response
|
||||
return Response(
|
||||
{
|
||||
"formatted_address": "",
|
||||
"street_address": "",
|
||||
"city": "",
|
||||
"state": "",
|
||||
"postal_code": "",
|
||||
"country": "",
|
||||
"country_code": "",
|
||||
"enriched": False,
|
||||
"reason": "geocoding_failed",
|
||||
}
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
capture_and_log(e, "Enrich location", source="api")
|
||||
return Response(
|
||||
{"detail": str(e)},
|
||||
status=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user