Skip to content

Commit 85fdc78

Browse files
VidurShahMasonMines2006naasanovnotvasub
authored
Police Page + Date Filter #80 (#97)
* Added basic embedded map in newly created components folder under EmbeddedMap.tsx. this is using the google maps API, using a personal API key that I would love to change. * Implemented basic embed map * created react map * Created changed to * Fixed Embedded Map to include the react integration library ( vis.gl/react-google-map). Created example map of Syndey with map pointers to different locations. * Created Party Demo test page * First mockup of party list props, and page created to be displayed in party demo * Added popup text box and color to map * Removed package and package lock in root directory, updated .env.template * Created functioning dropdown for party list * Implemented Radius Search Route * fixed tests to use mock * hopefully fixed mocks * Modified styling to be accurate with the Lo-Fi wireframe * Made changes as requested in ticket appraisal * further main merging * Fised broken tests from merge * Update frontend/src/components/PartyList.tsx Minor change to parties Co-authored-by: Nicolas Asanov <[email protected]> * pulled from mason branch, started work on police page. needs lots of work * wip changed a bunch of things, built police page, added small changes to related components for compatibility and neccesary funcitonality * changed a little margins * fix: corrected auth in location tests --------- Co-authored-by: MasonMines2006 <[email protected]> Co-authored-by: Nicolas Asanov <[email protected]> Co-authored-by: MasonMines2006 <[email protected]> Co-authored-by: Vasu <[email protected]> Co-authored-by: Nick A <[email protected]>
1 parent f2da13a commit 85fdc78

File tree

14 files changed

+817
-83
lines changed

14 files changed

+817
-83
lines changed

backend/src/core/config.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,4 +25,4 @@ class Config(BaseSettings):
2525
GOOGLE_MAPS_API_KEY: str
2626

2727

28-
env = Config() # pyright: ignore[reportCallIssue]
28+
env = Config() # pyright: ignore[reportCallIssue]

backend/src/modules/location/location_router.py

Lines changed: 38 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,19 @@
11
from fastapi import APIRouter, Depends, HTTPException, status
22
from src.core.authentication import (
33
authenticate_admin,
4+
authenticate_by_role,
45
authenticate_staff_or_admin,
5-
authenticate_user,
66
)
77
from src.modules.account.account_model import Account
88
from src.modules.location.location_model import (
9+
AddressData,
910
Location,
1011
LocationCreate,
1112
LocationData,
1213
PaginatedLocationResponse,
1314
)
1415
from src.modules.location.location_service import LocationService
16+
from src.modules.police.police_model import PoliceAccount
1517

1618
from .location_model import AutocompleteInput, AutocompleteResult
1719

@@ -28,7 +30,9 @@
2830
async def autocomplete_address(
2931
input_data: AutocompleteInput,
3032
location_service: LocationService = Depends(),
31-
user: Account = Depends(authenticate_user),
33+
user: Account | PoliceAccount = Depends(
34+
authenticate_by_role("police", "student", "admin", "staff")
35+
),
3236
) -> list[AutocompleteResult]:
3337
"""
3438
Autocomplete address search endpoint.
@@ -50,6 +54,38 @@ async def autocomplete_address(
5054
)
5155

5256

57+
@location_router.get(
58+
"/place-details/{place_id}",
59+
response_model=AddressData,
60+
status_code=status.HTTP_200_OK,
61+
summary="Get place details from Google Maps place ID",
62+
description="Returns address details including coordinates for a given place ID.",
63+
)
64+
async def get_place_details(
65+
place_id: str,
66+
location_service: LocationService = Depends(),
67+
user: Account | PoliceAccount = Depends(
68+
authenticate_by_role("police", "student", "admin", "staff")
69+
),
70+
) -> AddressData:
71+
"""
72+
Get place details endpoint.
73+
"""
74+
try:
75+
address_data = await location_service.get_place_details(place_id)
76+
return address_data
77+
except ValueError as e:
78+
raise HTTPException(
79+
status_code=status.HTTP_400_BAD_REQUEST,
80+
detail=str(e),
81+
)
82+
except Exception:
83+
raise HTTPException(
84+
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
85+
detail="Failed to fetch place details. Please try again later.",
86+
)
87+
88+
5389
@location_router.get("/", response_model=PaginatedLocationResponse)
5490
async def get_locations(
5591
location_service: LocationService = Depends(),

backend/test/modules/location/location_router_test.py

Lines changed: 49 additions & 71 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,7 @@
44
import pytest
55
import pytest_asyncio
66
from httpx import ASGITransport, AsyncClient
7-
from src.core.authentication import authenticate_user
87
from src.main import app
9-
from src.modules.account.account_model import Account, AccountRole
108
from src.modules.location.location_model import AutocompleteResult
119
from src.modules.location.location_service import LocationService
1210

@@ -21,20 +19,9 @@ async def mock_location_service():
2119
async def override_dependencies(mock_location_service: AsyncMock):
2220
"""Override dependencies to provide mock service and auth"""
2321

24-
async def _fake_user():
25-
return Account(
26-
id=1,
27-
28-
first_name="Test",
29-
last_name="User",
30-
pid="123456789",
31-
role=AccountRole.STUDENT,
32-
)
33-
3422
def _get_mock_location_service():
3523
return mock_location_service
3624

37-
app.dependency_overrides[authenticate_user] = _fake_user
3825
app.dependency_overrides[LocationService] = _get_mock_location_service
3926
yield
4027
app.dependency_overrides.clear()
@@ -52,9 +39,20 @@ def _get_mock_location_service():
5239
app.dependency_overrides.clear()
5340

5441

42+
@pytest_asyncio.fixture
43+
async def admin_client(override_dependencies: Any):
44+
"""Create an authenticated admin client"""
45+
async with AsyncClient(
46+
transport=ASGITransport(app=app),
47+
base_url="http://test",
48+
headers={"Authorization": "Bearer admin"},
49+
) as client:
50+
yield client
51+
52+
5553
@pytest.mark.asyncio
5654
async def test_autocomplete_success(
57-
override_dependencies: Any, mock_location_service: AsyncMock
55+
admin_client: AsyncClient, mock_location_service: AsyncMock
5856
):
5957
"""Test that the endpoint returns multiple address suggestions successfully"""
6058
mock_results = [
@@ -69,67 +67,53 @@ async def test_autocomplete_success(
6967
]
7068
mock_location_service.autocomplete_address.return_value = mock_results
7169

72-
async with AsyncClient(
73-
transport=ASGITransport(app=app), base_url="http://test"
74-
) as client:
75-
response = await client.post(
76-
"/api/locations/autocomplete", json={"address": "123 Main St"}
77-
)
70+
response = await admin_client.post(
71+
"/api/locations/autocomplete", json={"address": "123 Main St"}
72+
)
7873

79-
assert response.status_code == 200
80-
data = response.json()
81-
assert len(data) == 2
82-
assert data[0]["formatted_address"] == "123 Main St, Chapel Hill, NC 27514, USA"
83-
assert data[0]["place_id"] == "ChIJTest123"
84-
mock_location_service.autocomplete_address.assert_called_once_with(
85-
"123 Main St"
86-
)
74+
assert response.status_code == 200
75+
data = response.json()
76+
assert len(data) == 2
77+
assert data[0]["formatted_address"] == "123 Main St, Chapel Hill, NC 27514, USA"
78+
assert data[0]["place_id"] == "ChIJTest123"
79+
mock_location_service.autocomplete_address.assert_called_once_with("123 Main St")
8780

8881

8982
@pytest.mark.asyncio
9083
async def test_autocomplete_empty_results(
91-
override_dependencies: Any, mock_location_service: AsyncMock
84+
admin_client: AsyncClient, mock_location_service: AsyncMock
9285
):
9386
"""Test that the endpoint returns an empty list when no addresses match"""
9487
mock_location_service.autocomplete_address.return_value = []
9588

96-
async with AsyncClient(
97-
transport=ASGITransport(app=app), base_url="http://test"
98-
) as client:
99-
response = await client.post(
100-
"/api/locations/autocomplete",
101-
json={"address": "nonexistentaddress12345xyz"},
102-
)
89+
response = await admin_client.post(
90+
"/api/locations/autocomplete",
91+
json={"address": "nonexistentaddress12345xyz"},
92+
)
10393

104-
assert response.status_code == 200
105-
assert response.json() == []
94+
assert response.status_code == 200
95+
assert response.json() == []
10696

10797

10898
@pytest.mark.asyncio
109-
async def test_autocomplete_missing_address(override_dependencies: Any):
99+
async def test_autocomplete_missing_address(admin_client: AsyncClient):
110100
"""Test that the endpoint returns 422 when address field is missing"""
111-
async with AsyncClient(
112-
transport=ASGITransport(app=app), base_url="http://test"
113-
) as client:
114-
response = await client.post("/api/locations/autocomplete", json={})
115-
assert response.status_code == 422
101+
response = await admin_client.post("/api/locations/autocomplete", json={})
102+
assert response.status_code == 422
116103

117104

118105
@pytest.mark.asyncio
119106
async def test_autocomplete_empty_string(
120-
override_dependencies: Any, mock_location_service: AsyncMock
107+
admin_client: AsyncClient, mock_location_service: AsyncMock
121108
):
122109
"""Test that the endpoint handles empty string gracefully"""
123110
mock_location_service.autocomplete_address.return_value = []
124111

125-
async with AsyncClient(
126-
transport=ASGITransport(app=app), base_url="http://test"
127-
) as client:
128-
response = await client.post(
129-
"/api/locations/autocomplete", json={"address": ""}
130-
)
131-
assert response.status_code == 200
132-
assert response.json() == []
112+
response = await admin_client.post(
113+
"/api/locations/autocomplete", json={"address": ""}
114+
)
115+
assert response.status_code == 200
116+
assert response.json() == []
133117

134118

135119
@pytest.mark.asyncio
@@ -146,39 +130,33 @@ async def test_autocomplete_unauthenticated(override_dependencies_no_auth: Any):
146130

147131
@pytest.mark.asyncio
148132
async def test_autocomplete_service_exception(
149-
override_dependencies: Any, mock_location_service: AsyncMock
133+
admin_client: AsyncClient, mock_location_service: AsyncMock
150134
):
151135
"""Test that service exceptions are handled correctly"""
152136
mock_location_service.autocomplete_address.side_effect = Exception(
153137
"Service temporarily unavailable"
154138
)
155139

156-
async with AsyncClient(
157-
transport=ASGITransport(app=app), base_url="http://test"
158-
) as client:
159-
response = await client.post(
160-
"/api/locations/autocomplete",
161-
json={"address": "123 Test St"},
162-
)
140+
response = await admin_client.post(
141+
"/api/locations/autocomplete",
142+
json={"address": "123 Test St"},
143+
)
163144

164-
assert response.status_code == 500
145+
assert response.status_code == 500
165146

166147

167148
@pytest.mark.asyncio
168149
async def test_autocomplete_value_error(
169-
override_dependencies: Any, mock_location_service: AsyncMock
150+
admin_client: AsyncClient, mock_location_service: AsyncMock
170151
):
171152
"""Test that ValueError from API is handled correctly"""
172153
mock_location_service.autocomplete_address.side_effect = ValueError(
173154
"Invalid API key provided"
174155
)
175156

176-
async with AsyncClient(
177-
transport=ASGITransport(app=app), base_url="http://test"
178-
) as client:
179-
response = await client.post(
180-
"/api/locations/autocomplete",
181-
json={"address": "123 Test St"},
182-
)
157+
response = await admin_client.post(
158+
"/api/locations/autocomplete",
159+
json={"address": "123 Test St"},
160+
)
183161

184-
assert response.status_code == 400
162+
assert response.status_code == 400

frontend/.env.template

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,4 @@
1-
NEXT_PUBLIC_API_BASE_URL=http://localhost:4000
2-
NEXTAUTH_SECRET=REPLACE_ME
1+
NEXT_PUBLIC_API_BASE_URL=http://localhost:8000/api
2+
NEXT_PUBLIC_GOOGLE_MAPS_API_KEY=REPLACE_ME
3+
NEXT_PUBLIC_GOOGLE_MAP_ID=REPLACE_ME
4+
NEXTAUTH_SECRET=REPLACE_ME

frontend/package-lock.json

Lines changed: 21 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

frontend/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
"lint": "eslint"
1010
},
1111
"dependencies": {
12+
"@vis.gl/react-google-maps": "^1.6.1",
1213
"@hookform/resolvers": "^5.2.2",
1314
"@radix-ui/react-checkbox": "^1.3.3",
1415
"@radix-ui/react-dropdown-menu": "^2.1.16",

frontend/src/app/layout.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ export default function RootLayout({
2626
return (
2727
<html lang="en">
2828
<body
29-
className={`${geistSans.variable} ${geistMono.variable} antialiased`}
29+
className={`${geistSans.variable} ${geistMono.variable} antialiased bg-white`}
3030
>
3131
<Providers>{children}</Providers>
3232
</body>

frontend/src/app/page.tsx

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,25 @@
1-
21
export default function Home() {
32
return (
43
<div className="font-sans min-h-screen flex flex-col items-center justify-center p-8 sm:p-20 max-w-2xl mx-auto">
5-
<h1 className="text-3xl font-bold mb-6 text-center">Party Registration System</h1>
6-
<p className="mb-8 text-lg text-center">A CS+SG project for the Office of Off-Campus Student Life at UNC.</p>
7-
<h2 className="text-xl font-semibold mb-4 text-center">About the Office of Off-Campus Student Life</h2>
4+
<h1 className="text-3xl font-bold mb-6 text-center">
5+
Party Registration System
6+
</h1>
7+
<p className="mb-8 text-lg text-center">
8+
A CS+SG project for the Office of Off-Campus Student Life at UNC.
9+
</p>
10+
<h2 className="text-xl font-semibold mb-4 text-center">
11+
About the Office of Off-Campus Student Life
12+
</h2>
813
<p className="text-base text-center">
9-
The Office of Off-Campus Student Life serves as a vital resource for students living off-campus, providing support, programs, and services to enhance the off-campus living experience. This office works to connect off-campus students with campus resources, facilitate community building, and ensure student safety and well-being in off-campus environments. Through various initiatives and programs, the office aims to bridge the gap between on-campus and off-campus student experiences, fostering a sense of belonging and engagement for all students regardless of their housing situation.
14+
The Office of Off-Campus Student Life serves as a vital resource for
15+
students living off-campus, providing support, programs, and services to
16+
enhance the off-campus living experience. This office works to connect
17+
off-campus students with campus resources, facilitate community
18+
building, and ensure student safety and well-being in off-campus
19+
environments. Through various initiatives and programs, the office aims
20+
to bridge the gap between on-campus and off-campus student experiences,
21+
fostering a sense of belonging and engagement for all students
22+
regardless of their housing situation.
1023
</p>
1124
</div>
1225
);

0 commit comments

Comments
 (0)