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
6 changes: 3 additions & 3 deletions backend/dto/agency.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from pydantic import Field, BaseModel, validator, field_validator
from pydantic import Field, BaseModel, field_validator
from typing import Optional
from backend.database.models.agency import State, Jurisdiction
from backend.dto.common import PaginatedRequest
Expand Down Expand Up @@ -31,13 +31,13 @@ class AgencyQueryParams(PaginatedRequest):
# per_page: int = Field(default=20, ge=1)
searchResult: bool = Field(default=False)

@validator("hq_state")
@field_validator("hq_state")
def validate_state(cls, v):
if v and v not in State.choices():
raise ValueError(f"Invalid state: {v}")
return v

@validator("jurisdiction")
@field_validator("jurisdiction")
def validate_jurisdiction(cls, v):
if v and v not in Jurisdiction.choices():
raise ValueError(f"Invalid jurisdiction: {v}")
Expand Down
63 changes: 40 additions & 23 deletions backend/routes/units.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@
add_pagination_wrapper, ordered_jsonify)
from backend.database.models.user import UserRole
from backend.database.models.agency import Unit
from backend.routes.search import fetch_details, build_unit_result
from backend.routes.search import (
fetch_details, build_unit_result, build_officer_result)
from flask import Blueprint, abort, request, jsonify
from flask_jwt_extended.view_decorators import jwt_required
from backend.dto.unit import UnitQueryParams, GetUnitParams
Expand All @@ -23,31 +24,27 @@

LOCATION_CYPHER = """
CALL (u) {
MATCH (u)-[]-(:Agency)-[]-(city:CityNode)
RETURN city.coordinates AS location
MATCH (u)-[]-(:Agency)-[]-(city:CityNode)-[]-(:CountyNode)
-[]-(state:StateNode)
RETURN {
coords: city.coordinates,
city: city.name,
state: state.name
} AS location
}
"""

MOST_REPORTED_OFFICER_CYPHER = """
CALL (u) {
MATCH (u)<-[]-(e:Employment)-[]->(o:Officer)
MATCH (u)<-[]-(:Employment)-[]->(o:Officer)
-[:ACCUSED_OF]->(a:Allegation)-[:ALLEGED]-(c:Complaint)
WITH
o,
e,
count(DISTINCT c) AS complaint_count,
count(DISTINCT a) AS allegation_count
ORDER BY complaint_count DESC
ORDER BY complaint_count DESC, allegation_count DESC
LIMIT 3
RETURN collect({
officer_uid: o.uid,
name: o.first_name + " " + o.last_name,
gender: o.gender,
ethnicity: o.ethnicity,
rank: e.rank,
complaint_count: complaint_count,
allegation_count: allegation_count
}) AS most_reported_officers
RETURN collect(o) AS most_reported_officers
}
"""

Expand All @@ -63,11 +60,14 @@
CALL (u) {
OPTIONAL MATCH (u)<-[]-(:Employment)-[]->(:Officer)
-[:ACCUSED_OF]->(a:Allegation)-[:ALLEGED]-(c:Complaint)
WITH count(DISTINCT c) AS total_complaints, count(DISTINCT a) AS total_allegations
WITH
count(DISTINCT c) AS total_complaints,
count(DISTINCT a) AS total_allegations
RETURN total_complaints, total_allegations
}
"""


@bp.route("", methods=["GET"])
@jwt_required()
@min_role_required(UserRole.PUBLIC)
Expand Down Expand Up @@ -147,8 +147,8 @@ def get_unit(uid: str):
except Exception as e:
logging.warning(f"Invalid query params: {e}")
abort(400, description=str(e))
match_clause = "MATCH (u:Unit {uid: $uid})"
return_clause = "RETURN u"
match_clause = "MATCH (u:Unit {uid: $uid})-[]-(a:Agency) "
return_clause = "RETURN u, a"
subqueries = ""
if params.include:
if "reported_officers" in params.include:
Expand All @@ -170,10 +170,22 @@ def get_unit(uid: str):
abort(404, description="Unit not found")
row = rows[0]
unit_data = row[0]._properties
unit_data["agency"] = row[1]._properties if row[1] else None
if params.include:
idx = 1
idx = 2
if "reported_officers" in params.include:
unit_data["most_reported_officers"] = row[idx]
details = fetch_details(
[o.get("uid") for o in row[idx]], "Officer")
officers = [build_officer_result(
o, details.get(o.get("uid"), {})) for o in row[idx]]
item_dump = [
item.model_dump() for item in officers if item
]
for item in item_dump:
item["last_updated"] = item[
"last_updated"].isoformat() if item.get(
"last_updated", None) else None
unit_data["most_reported_officers"] = item_dump
idx += 1
if "total_officers" in params.include:
unit_data["total_officers"] = row[idx]
Expand All @@ -183,6 +195,11 @@ def get_unit(uid: str):
unit_data["total_allegations"] = row[idx + 1]
idx += 2
if "location" in params.include:
coords = row[idx]
unit_data["location"] = {"latitude": coords.y, "longitude": coords.x} if coords else None
return ordered_jsonify(unit_data), 200
loc = row[idx]
unit_data["location"] = {
"latitude": loc["coords"].y,
"longitude": loc["coords"].x,
"city": loc["city"],
"state": loc["state"]
} if loc and loc.get("coords", None) else None
return ordered_jsonify(unit_data), 200
10 changes: 5 additions & 5 deletions backend/tests/test_units.py
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ def test_get_all_units(client, example_units, access_token):

# Test that we can get all units
res = client.get(
"/api/v1/units/",
"/api/v1/units",
headers={"Authorization": "Bearer {0}".format(access_token)}
)
assert res.status_code == 200
Expand All @@ -87,7 +87,7 @@ def test_get_search_result(client, example_unit, access_token):
name__icontains='Precinct 1'
).__len__()
res = client.get(
"/api/v1/units/?name=Precinct 1&searchResult=true",
"/api/v1/units?name=Precinct 1&searchResult=true",
headers={"Authorization": "Bearer {0}".format(access_token)},
)
assert res.status_code == 200
Expand All @@ -96,7 +96,7 @@ def test_get_search_result(client, example_unit, access_token):

def test_bad_query_param(client, access_token):
res = client.get(
"/api/v1/units/?abc=123",
"/api/v1/units?abc=123",
headers={"Authorization": "Bearer {0}".format(access_token)},
)

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

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

res = client.get(
(
f"/api/v1/units/?per_page={per_page}"
f"/api/v1/units?per_page={per_page}"
f"&page={expected_total_pages + 1}"
),
headers={"Authorization": "Bearer {0}".format(access_token)},
Expand Down
152 changes: 0 additions & 152 deletions frontend/app/search/SearchResults.tsx

This file was deleted.

2 changes: 1 addition & 1 deletion frontend/app/search/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
import { useCallback } from "react"
import SearchBar from "@/components/SearchBar"
import styles from "./page.module.css"
import SearchResults from "./SearchResults"
import SearchResults from "@/components/search/SearchResults"
import Pagination from "./Pagination"
import Filter from "./Filter"
import { useSearch } from "@/providers/SearchProvider"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,16 +6,14 @@ type UnitContentDetailsProps = {
}

export default function UnitContentDetails({ unit }: UnitContentDetailsProps) {
const totalComplaints =
unit.total_complaints || 0
const totalComplaints = unit.total_complaints || 0

const totalAllegations = unit.total_allegations || 0

const totalOfficers = unit.total_officers || 0

const dataSources =
unit.sources?.map((source) => source.name).filter((name): name is string => Boolean(name)) ||
[]
unit.sources?.map((source) => source.name).filter((name): name is string => Boolean(name)) || []

return (
<ContentDetails
Expand Down
Loading
Loading