Skip to content

Commit f2da13a

Browse files
notvasubnaasanov
andauthored
Implemented Radius Search Route (#70)
* Implemented Radius Search Route * fixed tests to use mock * hopefully fixed mocks * Fised broken tests from merge * Added tests for get_parties_by_radius_and_date_range(), changed config to 0.25 * Add untracked files before merge * 🙏 * Fixed tests * fixes * Fixed tests * fix: reverted student tests --------- Co-authored-by: Vasu <[email protected]> Co-authored-by: Nick A <[email protected]>
1 parent 1a06fb9 commit f2da13a

File tree

6 files changed

+1024
-239
lines changed

6 files changed

+1024
-239
lines changed

backend/src/core/config.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ class Config(BaseSettings):
2121
POSTGRES_HOST: str = "db"
2222
POSTGRES_PORT: int = 5432
2323
HOST: str = "localhost"
24-
PARTY_SEARCH_RADIUS_MILES: float = 3.0
24+
PARTY_SEARCH_RADIUS_MILES: float = 0.25
2525
GOOGLE_MAPS_API_KEY: str
2626

2727

backend/src/modules/party/party_router.py

Lines changed: 63 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,13 @@
55
from src.core.authentication import (
66
authenticate_admin,
77
authenticate_by_role,
8+
authenticate_police_or_admin,
89
authenticate_staff_or_admin,
910
authenticate_user,
1011
)
11-
from src.core.exceptions import ForbiddenException
12+
from src.core.exceptions import BadRequestException, ForbiddenException
1213
from src.modules.account.account_model import Account, AccountRole
14+
from src.modules.location.location_service import LocationService
1315

1416
from .party_model import (
1517
AdminCreatePartyDTO,
@@ -115,6 +117,59 @@ async def list_parties(
115117
)
116118

117119

120+
@party_router.get("/nearby")
121+
async def get_parties_nearby(
122+
place_id: str = Query(..., description="Google Maps place ID"),
123+
start_date: str = Query(..., description="Start date (YYYY-MM-DD format)"),
124+
end_date: str = Query(..., description="End date (YYYY-MM-DD format)"),
125+
party_service: PartyService = Depends(),
126+
location_service: LocationService = Depends(),
127+
_=Depends(authenticate_police_or_admin),
128+
) -> list[Party]:
129+
"""
130+
Returns parties within a radius of a location specified by Google Maps place ID,
131+
filtered by date range.
132+
133+
Query Parameters:
134+
- place_id: Google Maps place ID from autocomplete selection
135+
- start_date: Start date for the search range (YYYY-MM-DD format)
136+
- end_date: End date for the search range (YYYY-MM-DD format)
137+
138+
Returns:
139+
- List of parties within the search radius and date range
140+
141+
Raises:
142+
- 400: If place ID is invalid or dates are in wrong format
143+
- 404: If place ID is not found
144+
- 403: If user is not a police officer or admin
145+
"""
146+
# Parse date strings to datetime objects
147+
try:
148+
start_datetime = datetime.strptime(start_date, "%Y-%m-%d")
149+
end_datetime = datetime.strptime(end_date, "%Y-%m-%d")
150+
# Set end_datetime to end of day (23:59:59)
151+
end_datetime = end_datetime.replace(hour=23, minute=59, second=59)
152+
except ValueError as e:
153+
raise BadRequestException(f"Invalid date format. Expected YYYY-MM-DD: {str(e)}")
154+
155+
# Validate that start_date is not greater than end_date
156+
if start_datetime > end_datetime:
157+
raise BadRequestException("Start date must be less than or equal to end date")
158+
159+
# Get location coordinates from place ID
160+
location_data = await location_service.get_place_details(place_id)
161+
162+
# Perform proximity search with date range
163+
parties = await party_service.get_parties_by_radius_and_date_range(
164+
latitude=location_data.latitude,
165+
longitude=location_data.longitude,
166+
start_date=start_datetime,
167+
end_date=end_datetime,
168+
)
169+
170+
return parties
171+
172+
118173
@party_router.get("/csv")
119174
async def get_parties_csv(
120175
start_date: str = Query(..., description="Start date in YYYY-MM-DD format"),
@@ -148,6 +203,13 @@ async def get_parties_csv(
148203
detail="Invalid date format. Use YYYY-MM-DD format for dates.",
149204
)
150205

206+
# Validate that start_date is not greater than end_date
207+
if start_datetime > end_datetime:
208+
raise HTTPException(
209+
status_code=400,
210+
detail="Start date must be less than or equal to end date",
211+
)
212+
151213
parties = await party_service.get_parties_by_date_range(
152214
start_datetime, end_datetime
153215
)

backend/src/modules/party/party_service.py

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -450,6 +450,57 @@ async def get_parties_by_radius(
450450

451451
return [party.to_model() for party in parties_within_radius]
452452

453+
async def get_parties_by_radius_and_date_range(
454+
self,
455+
latitude: float,
456+
longitude: float,
457+
start_date: datetime,
458+
end_date: datetime,
459+
) -> List[Party]:
460+
"""
461+
Get parties within a radius of a location within a specified date range.
462+
463+
Args:
464+
latitude: Latitude of the search center
465+
longitude: Longitude of the search center
466+
start_date: Start of the date range (inclusive)
467+
end_date: End of the date range (inclusive)
468+
469+
Returns:
470+
List of parties within the radius and date range
471+
"""
472+
result = await self.session.execute(
473+
select(PartyEntity)
474+
.options(
475+
selectinload(PartyEntity.location),
476+
selectinload(PartyEntity.contact_one).selectinload(
477+
StudentEntity.account
478+
),
479+
)
480+
.where(
481+
PartyEntity.party_datetime >= start_date,
482+
PartyEntity.party_datetime <= end_date,
483+
)
484+
)
485+
parties = result.scalars().all()
486+
487+
parties_within_radius = []
488+
for party in parties:
489+
if party.location is None:
490+
continue
491+
492+
distance = self._calculate_haversine_distance(
493+
latitude,
494+
longitude,
495+
float(party.location.latitude),
496+
float(party.location.longitude),
497+
)
498+
499+
if distance <= env.PARTY_SEARCH_RADIUS_MILES:
500+
parties_within_radius.append(party)
501+
502+
return [party.to_model() for party in parties_within_radius]
503+
453504
def _calculate_haversine_distance(
454505
self, lat1: float, lon1: float, lat2: float, lon2: float
455506
) -> float:

backend/test/modules/account/account_service_test.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import pytest
2+
import pytest_asyncio
23
from sqlalchemy.ext.asyncio import AsyncSession
34
from src.modules.account.account_model import AccountData, AccountRole
45
from src.modules.account.account_service import (
@@ -14,7 +15,7 @@ def account_service(test_async_session: AsyncSession) -> AccountService:
1415
return AccountService(session=test_async_session)
1516

1617

17-
@pytest.fixture()
18+
@pytest_asyncio.fixture()
1819
async def accounts_by_roles_fixture(account_service: AccountService) -> None:
1920
await account_service.create_account(
2021
AccountData(

0 commit comments

Comments
 (0)