Skip to content

Commit 3480163

Browse files
authored
Update Search Components (#544)
* Lock the map component to the center point. This may need to be updated when we have more usage of the map component accross the app. * Update GET `unit/{uid}` response - Include agency information - Include location information Also updates the Agency DTO * Update Unit Detail page - Breaks out search result components and styles into separate files - Adds new search result components for use on search results page and unit detail page * Fix formatting * Fix tests * Fix FE linting warnings * Remove agancy page for now
1 parent 244110a commit 3480163

24 files changed

+516
-245
lines changed

backend/dto/agency.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
from pydantic import Field, BaseModel, validator, field_validator
1+
from pydantic import Field, BaseModel, field_validator
22
from typing import Optional
33
from backend.database.models.agency import State, Jurisdiction
44
from backend.dto.common import PaginatedRequest
@@ -31,13 +31,13 @@ class AgencyQueryParams(PaginatedRequest):
3131
# per_page: int = Field(default=20, ge=1)
3232
searchResult: bool = Field(default=False)
3333

34-
@validator("hq_state")
34+
@field_validator("hq_state")
3535
def validate_state(cls, v):
3636
if v and v not in State.choices():
3737
raise ValueError(f"Invalid state: {v}")
3838
return v
3939

40-
@validator("jurisdiction")
40+
@field_validator("jurisdiction")
4141
def validate_jurisdiction(cls, v):
4242
if v and v not in Jurisdiction.choices():
4343
raise ValueError(f"Invalid jurisdiction: {v}")

backend/routes/units.py

Lines changed: 40 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,8 @@
44
add_pagination_wrapper, ordered_jsonify)
55
from backend.database.models.user import UserRole
66
from backend.database.models.agency import Unit
7-
from backend.routes.search import fetch_details, build_unit_result
7+
from backend.routes.search import (
8+
fetch_details, build_unit_result, build_officer_result)
89
from flask import Blueprint, abort, request, jsonify
910
from flask_jwt_extended.view_decorators import jwt_required
1011
from backend.dto.unit import UnitQueryParams, GetUnitParams
@@ -23,31 +24,27 @@
2324

2425
LOCATION_CYPHER = """
2526
CALL (u) {
26-
MATCH (u)-[]-(:Agency)-[]-(city:CityNode)
27-
RETURN city.coordinates AS location
27+
MATCH (u)-[]-(:Agency)-[]-(city:CityNode)-[]-(:CountyNode)
28+
-[]-(state:StateNode)
29+
RETURN {
30+
coords: city.coordinates,
31+
city: city.name,
32+
state: state.name
33+
} AS location
2834
}
2935
"""
3036

3137
MOST_REPORTED_OFFICER_CYPHER = """
3238
CALL (u) {
33-
MATCH (u)<-[]-(e:Employment)-[]->(o:Officer)
39+
MATCH (u)<-[]-(:Employment)-[]->(o:Officer)
3440
-[:ACCUSED_OF]->(a:Allegation)-[:ALLEGED]-(c:Complaint)
3541
WITH
3642
o,
37-
e,
3843
count(DISTINCT c) AS complaint_count,
3944
count(DISTINCT a) AS allegation_count
40-
ORDER BY complaint_count DESC
45+
ORDER BY complaint_count DESC, allegation_count DESC
4146
LIMIT 3
42-
RETURN collect({
43-
officer_uid: o.uid,
44-
name: o.first_name + " " + o.last_name,
45-
gender: o.gender,
46-
ethnicity: o.ethnicity,
47-
rank: e.rank,
48-
complaint_count: complaint_count,
49-
allegation_count: allegation_count
50-
}) AS most_reported_officers
47+
RETURN collect(o) AS most_reported_officers
5148
}
5249
"""
5350

@@ -63,11 +60,14 @@
6360
CALL (u) {
6461
OPTIONAL MATCH (u)<-[]-(:Employment)-[]->(:Officer)
6562
-[:ACCUSED_OF]->(a:Allegation)-[:ALLEGED]-(c:Complaint)
66-
WITH count(DISTINCT c) AS total_complaints, count(DISTINCT a) AS total_allegations
63+
WITH
64+
count(DISTINCT c) AS total_complaints,
65+
count(DISTINCT a) AS total_allegations
6766
RETURN total_complaints, total_allegations
6867
}
6968
"""
7069

70+
7171
@bp.route("", methods=["GET"])
7272
@jwt_required()
7373
@min_role_required(UserRole.PUBLIC)
@@ -147,8 +147,8 @@ def get_unit(uid: str):
147147
except Exception as e:
148148
logging.warning(f"Invalid query params: {e}")
149149
abort(400, description=str(e))
150-
match_clause = "MATCH (u:Unit {uid: $uid})"
151-
return_clause = "RETURN u"
150+
match_clause = "MATCH (u:Unit {uid: $uid})-[]-(a:Agency) "
151+
return_clause = "RETURN u, a"
152152
subqueries = ""
153153
if params.include:
154154
if "reported_officers" in params.include:
@@ -170,10 +170,22 @@ def get_unit(uid: str):
170170
abort(404, description="Unit not found")
171171
row = rows[0]
172172
unit_data = row[0]._properties
173+
unit_data["agency"] = row[1]._properties if row[1] else None
173174
if params.include:
174-
idx = 1
175+
idx = 2
175176
if "reported_officers" in params.include:
176-
unit_data["most_reported_officers"] = row[idx]
177+
details = fetch_details(
178+
[o.get("uid") for o in row[idx]], "Officer")
179+
officers = [build_officer_result(
180+
o, details.get(o.get("uid"), {})) for o in row[idx]]
181+
item_dump = [
182+
item.model_dump() for item in officers if item
183+
]
184+
for item in item_dump:
185+
item["last_updated"] = item[
186+
"last_updated"].isoformat() if item.get(
187+
"last_updated", None) else None
188+
unit_data["most_reported_officers"] = item_dump
177189
idx += 1
178190
if "total_officers" in params.include:
179191
unit_data["total_officers"] = row[idx]
@@ -183,6 +195,11 @@ def get_unit(uid: str):
183195
unit_data["total_allegations"] = row[idx + 1]
184196
idx += 2
185197
if "location" in params.include:
186-
coords = row[idx]
187-
unit_data["location"] = {"latitude": coords.y, "longitude": coords.x} if coords else None
188-
return ordered_jsonify(unit_data), 200
198+
loc = row[idx]
199+
unit_data["location"] = {
200+
"latitude": loc["coords"].y,
201+
"longitude": loc["coords"].x,
202+
"city": loc["city"],
203+
"state": loc["state"]
204+
} if loc and loc.get("coords", None) else None
205+
return ordered_jsonify(unit_data), 200

backend/tests/test_units.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,7 @@ def test_get_all_units(client, example_units, access_token):
7575

7676
# Test that we can get all units
7777
res = client.get(
78-
"/api/v1/units/",
78+
"/api/v1/units",
7979
headers={"Authorization": "Bearer {0}".format(access_token)}
8080
)
8181
assert res.status_code == 200
@@ -87,7 +87,7 @@ def test_get_search_result(client, example_unit, access_token):
8787
name__icontains='Precinct 1'
8888
).__len__()
8989
res = client.get(
90-
"/api/v1/units/?name=Precinct 1&searchResult=true",
90+
"/api/v1/units?name=Precinct 1&searchResult=true",
9191
headers={"Authorization": "Bearer {0}".format(access_token)},
9292
)
9393
assert res.status_code == 200
@@ -96,7 +96,7 @@ def test_get_search_result(client, example_unit, access_token):
9696

9797
def test_bad_query_param(client, access_token):
9898
res = client.get(
99-
"/api/v1/units/?abc=123",
99+
"/api/v1/units?abc=123",
100100
headers={"Authorization": "Bearer {0}".format(access_token)},
101101
)
102102

@@ -110,7 +110,7 @@ def test_unit_pagination(client, example_units, access_token):
110110
expected_total_pages = math.ceil(total_units//per_page)
111111
for page in range(1, expected_total_pages + 1):
112112
res = client.get(
113-
f"/api/v1/units/?per_page={per_page}&page={page}",
113+
f"/api/v1/units?per_page={per_page}&page={page}",
114114
headers={"Authorization": "Bearer {0}".format(access_token)},
115115
)
116116

@@ -123,7 +123,7 @@ def test_unit_pagination(client, example_units, access_token):
123123

124124
res = client.get(
125125
(
126-
f"/api/v1/units/?per_page={per_page}"
126+
f"/api/v1/units?per_page={per_page}"
127127
f"&page={expected_total_pages + 1}"
128128
),
129129
headers={"Authorization": "Bearer {0}".format(access_token)},

frontend/app/search/SearchResults.tsx

Lines changed: 0 additions & 152 deletions
This file was deleted.

frontend/app/search/page.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
import { useCallback } from "react"
33
import SearchBar from "@/components/SearchBar"
44
import styles from "./page.module.css"
5-
import SearchResults from "./SearchResults"
5+
import SearchResults from "@/components/search/SearchResults"
66
import Pagination from "./Pagination"
77
import Filter from "./Filter"
88
import { useSearch } from "@/providers/SearchProvider"

frontend/components/Details/ContentDetails/UnitContentDetails.tsx

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,16 +6,14 @@ type UnitContentDetailsProps = {
66
}
77

88
export default function UnitContentDetails({ unit }: UnitContentDetailsProps) {
9-
const totalComplaints =
10-
unit.total_complaints || 0
9+
const totalComplaints = unit.total_complaints || 0
1110

1211
const totalAllegations = unit.total_allegations || 0
1312

1413
const totalOfficers = unit.total_officers || 0
1514

1615
const dataSources =
17-
unit.sources?.map((source) => source.name).filter((name): name is string => Boolean(name)) ||
18-
[]
16+
unit.sources?.map((source) => source.name).filter((name): name is string => Boolean(name)) || []
1917

2018
return (
2119
<ContentDetails

0 commit comments

Comments
 (0)