Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions backend/app/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
from starlette.types import ASGIApp
from slowapi import _rate_limit_exceeded_handler
from slowapi.errors import RateLimitExceeded
from fastapi.responses import JSONResponse
from fastapi.responses import JSONResponse, ORJSONResponse
from datetime import datetime, timedelta, timezone
from contextlib import asynccontextmanager
import os
Expand Down Expand Up @@ -117,7 +117,8 @@ async def lifespan(app: FastAPI):
title="Divemap API",
description="Scuba diving site and center review platform",
version="1.0.0",
lifespan=lifespan
lifespan=lifespan,
default_response_class=ORJSONResponse
)

# Add rate limiter to app state
Expand Down
41 changes: 39 additions & 2 deletions backend/app/routers/dives/dives_crud.py
Original file line number Diff line number Diff line change
Expand Up @@ -295,7 +295,11 @@ def get_dives_count(
end_date: Optional[str] = Query(None, pattern=r"^\d{4}-\d{2}-\d{2}$"),
tag_ids: Optional[str] = Query(None), # Comma-separated tag IDs
buddy_id: Optional[int] = Query(None, description="Filter by buddy user ID"),
buddy_username: Optional[str] = Query(None, description="Filter by buddy username")
buddy_username: Optional[str] = Query(None, description="Filter by buddy username"),
north: Optional[float] = Query(None, ge=-90, le=90, description="North bound for viewport filtering"),
south: Optional[float] = Query(None, ge=-90, le=90, description="South bound for viewport filtering"),
east: Optional[float] = Query(None, ge=-180, le=180, description="East bound for viewport filtering"),
west: Optional[float] = Query(None, ge=-180, le=180, description="West bound for viewport filtering")
):
"""Get total count of dives matching the filters."""
query = db.query(Dive).join(User, Dive.user_id == User.id)
Expand Down Expand Up @@ -327,6 +331,16 @@ def get_dives_count(
# Apply filters
if dive_site_id:
query = query.filter(Dive.dive_site_id == dive_site_id)

# Apply bounds filtering if provided
if all(x is not None for x in [north, south, east, west]):
# Join DiveSite to access latitude and longitude if not already joined
if not dive_site_name and not search:
query = query.join(DiveSite, Dive.dive_site_id == DiveSite.id)
query = query.filter(
DiveSite.latitude.between(south, north),
DiveSite.longitude.between(west, east)
)

if dive_site_name:
# Join with DiveSite table to filter by dive site name
Expand Down Expand Up @@ -461,9 +475,22 @@ def get_dives(
sort_by: Optional[str] = Query(None, description="Sort field (dive_date, max_depth, duration, difficulty_level, visibility_rating, user_rating, created_at, updated_at). Admin users can also sort by view_count."),
sort_order: Optional[str] = Query("desc", description="Sort order (asc/desc)"),
page: int = Query(1, ge=1, description="Page number (1-based)"),
page_size: int = Query(25, description="Page size (25, 50, 100, or 1000)")
page_size: int = Query(25, description="Page size (25, 50, 100, or 1000)"),
north: Optional[float] = Query(None, ge=-90, le=90, description="North bound for viewport filtering"),
south: Optional[float] = Query(None, ge=-90, le=90, description="South bound for viewport filtering"),
east: Optional[float] = Query(None, ge=-180, le=180, description="East bound for viewport filtering"),
west: Optional[float] = Query(None, ge=-180, le=180, description="West bound for viewport filtering"),
detail_level: Optional[str] = Query('full', description="Data detail level: 'minimal' (id, name only), 'basic' (id, name), 'full' (all fields)")
):
"""Get dives with filtering options. Can view own dives and public dives from other users. Unauthenticated users can view public dives."""

# Validate bounds if provided
if all(x is not None for x in [north, south, east, west]):
if north <= south:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="north must be greater than south"
)
# Validate page_size
if page_size not in [1, 5, 25, 50, 100, 1000]:
raise HTTPException(
Expand Down Expand Up @@ -518,6 +545,16 @@ def get_dives(
# Apply filters
if dive_site_id:
query = query.filter(Dive.dive_site_id == dive_site_id)

# Apply bounds filtering if provided
if all(x is not None for x in [north, south, east, west]):
# Join DiveSite to access latitude and longitude if not already joined
if not dive_site_name and not search:
query = query.join(DiveSite, Dive.dive_site_id == DiveSite.id)
query = query.filter(
DiveSite.latitude.between(south, north),
DiveSite.longitude.between(west, east)
)

# Filter by username (partial match)
if username:
Expand Down
39 changes: 38 additions & 1 deletion backend/app/routers/diving_centers.py
Original file line number Diff line number Diff line change
Expand Up @@ -624,6 +624,10 @@ async def get_diving_centers(
sort_order: Optional[str] = Query("asc", description="Sort order (asc/desc)"),
page: int = Query(1, ge=1, description="Page number (1-based)"),
page_size: int = Query(25, description="Page size (25, 50, 100, or 1000)"),
north: Optional[float] = Query(None, ge=-90, le=90, description="North bound for viewport filtering"),
south: Optional[float] = Query(None, ge=-90, le=90, description="South bound for viewport filtering"),
east: Optional[float] = Query(None, ge=-180, le=180, description="East bound for viewport filtering"),
west: Optional[float] = Query(None, ge=-180, le=180, description="West bound for viewport filtering"),
detail_level: Optional[str] = Query('full', description="Data detail level: 'minimal' (id, name only), 'basic' (id, name, country, region, city), 'full' (all fields)"),
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user_optional)
Expand All @@ -641,6 +645,14 @@ async def get_diving_centers(
else:
detail_level = 'full'

# Validate bounds if provided
if all(x is not None for x in [north, south, east, west]):
if north <= south:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="north must be greater than south"
)

# Check if reviews are enabled
reviews_enabled = is_diving_center_reviews_enabled(db)

Expand All @@ -656,7 +668,16 @@ async def get_diving_centers(
detail="page_size must be one of: 25, 50, 100, 1000"
)

from sqlalchemy.orm import defer
query = db.query(DivingCenter)
if detail_level != 'full':
query = query.options(
defer(DivingCenter.description),
defer(DivingCenter.address),
defer(DivingCenter.email),
defer(DivingCenter.phone),
defer(DivingCenter.website)
)

# Apply filters
if name:
Expand All @@ -671,6 +692,13 @@ async def get_diving_centers(
if city:
query = query.filter(DivingCenter.city.ilike(f"%{city}%"))

# Apply bounds filtering if provided
if all(x is not None for x in [north, south, east, west]):
query = query.filter(
DivingCenter.latitude.between(south, north),
DivingCenter.longitude.between(west, east)
)

# Apply unified search across multiple fields (case-insensitive)
if search:
# Sanitize search input to prevent injection
Expand Down Expand Up @@ -921,14 +949,23 @@ async def get_diving_centers(
total_ratings = center_data[2]

if detail_level == 'minimal':
center_dict = {"id": center.id, "name": center.name}
center_dict = {
"id": center.id,
"name": center.name,
"latitude": float(center.latitude) if center.latitude else None,
"longitude": float(center.longitude) if center.longitude else None,
}
elif detail_level == 'basic':
center_dict = {
"id": center.id,
"name": center.name,
"latitude": float(center.latitude) if center.latitude else None,
"longitude": float(center.longitude) if center.longitude else None,
"country": center.country,
"region": center.region,
"city": center.city,
"average_rating": float(avg_rating) if reviews_enabled and avg_rating else None,
"total_ratings": (total_ratings or 0) if reviews_enabled else 0,
}
else:
owner_username = None
Expand Down
Loading
Loading