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
11 changes: 6 additions & 5 deletions backend/database/models/agency.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,8 +41,8 @@ class Unit(StructuredNode, HasCitations, JsonSerializable, SearchableMixin):
__property_order__ = [
"uid", "name", "website_url", "phone",
"email", "description", "address",
"city", "state", "zip", "agency_url",
"officers_url", "date_established"
"hq_address", "hq_city", "hq_state", "hq_zip",
"agency", "date_established"
]
__hidden_properties__ = ["citations", "city_node"]

Expand Down Expand Up @@ -151,9 +151,10 @@ def search(cls, query: str = None, filters: dict = None,

class Agency(StructuredNode, HasCitations, JsonSerializable, SearchableMixin):
__property_order__ = [
"uid", "name", "website_url", "hq_address",
"hq_city", "hq_state", "hq_zip", "phone",
"email", "description", "jurisdiction"
"uid", "name", "website_url", "phone",
"email", "description", "address",
"hq_address", "hq_city", "hq_state", "hq_zip",
"jurisdiction", "date_established"
]
__hidden_properties__ = ["citations", "state_node",
"county_node", "city_node"]
Expand Down
60 changes: 47 additions & 13 deletions backend/database/models/employment.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,24 +16,57 @@

# Enums - Not yet used for validation, but could be in the future
class EmploymentType(str, PropertyEnum):
LAW_ENFORCEMENT = "LAW_ENFORCEMENT"
CORRECTIONS = "CORRECTIONS"
LAW_ENFORCEMENT = "Law Enforcement"
CORRECTIONS = "Corrections"


class EmploymentStatus(PropertyEnum):
FULL_TIME = "FULL_TIME"
PART_TIME = "PART_TIME"
PROVISIONAL = "PROVISIONAL"
TEMPORARY = "TEMPORARY"
VOLUNTEER = "VOLUNTEER"
FULL_TIME = "Full-Time"
PART_TIME = "Part-Time"
PROVISIONAL = "Provisional"
TEMPORARY = "Temporary"
VOLUNTEER = "Volunteer"


class EmploymentChange(PropertyEnum):
PROMOTION = "PROMOTION"
DEMOTION = "DEMOTION"
TRANSFER = "TRANSFER"
TERMINATION = "TERMINATION"
RESIGNATION = "RESIGNATION"
PROMOTION = "Promotion"
DEMOTION = "Demotion"
TRANSFER = "Transfer"
TERMINATION = "Termination"
RESIGNATION = "Resignation"


class Rank(str, PropertyEnum):
POLICE_OFFICER = "Police Officer"
DETECTIVE = "Detective"
SERGEANT = "Sergeant"
LIEUTENANT = "Lieutenant"
CAPTAIN = "Captain"
MAJOR = "Major"
COLONEL = "Colonel"
COMMANDER = "Commander"
CHIEF = "Chief"

def get_value(self):
if self == Rank.POLICE_OFFICER:
return 10
elif self == Rank.DETECTIVE:
return 20
elif self == Rank.SERGEANT:
return 30
elif self == Rank.LIEUTENANT:
return 40
elif self == Rank.CAPTAIN:
return 50
elif self == Rank.MAJOR:
return 60
elif self == Rank.COLONEL:
return 70
elif self == Rank.COMMANDER:
return 80
elif self == Rank.CHIEF:
return 90
return 0


class Employment(StructuredNode, HasCitations, JsonSerializable):
Expand All @@ -43,7 +76,8 @@ class Employment(StructuredNode, HasCitations, JsonSerializable):
earliest_date = DateNeo4jFormatProperty(index=True)
latest_date = DateNeo4jFormatProperty(index=True)
badge_number = StringProperty(index=True)
highest_rank = StringProperty()
highest_rank = StringProperty(choices=Rank.choices())
rank_label = StringProperty()
salary = IntegerProperty()
status = StringProperty()
change = StringProperty()
Expand Down
10 changes: 5 additions & 5 deletions backend/database/models/officer.py
Original file line number Diff line number Diff line change
Expand Up @@ -259,18 +259,18 @@ def search(

if count:
cypher_query += "\nRETURN count(*) as c"
logging.warning("Cypher count query:\n%s", cypher_query)
logging.warning("Params: %s", params)
logging.warning("Query: %s", cypher_query)
logging.debug("Cypher count query:\n%s", cypher_query)
logging.debug("Params: %s", params)
logging.debug("Query: %s", cypher_query)
count_results, _ = db.cypher_query(cypher_query, params)
return count_results[0][0] if count_results else 0
else:
cypher_query += f"""
RETURN o SKIP {skip} LIMIT {limit}
"""

logging.warning("Cypher query:\n%s", cypher_query)
logging.warning("Params: %s", params)
logging.debug("Cypher query:\n%s", cypher_query)
logging.debug("Params: %s", params)

rows, _ = db.cypher_query(cypher_query, params,
resolve_objects=inflate)
Expand Down
70 changes: 70 additions & 0 deletions backend/dto/agency.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
from pydantic import Field, BaseModel, field_validator
from typing import Optional
from backend.database.models.agency import State, Jurisdiction
from backend.database.models.employment import (
EmploymentStatus, EmploymentType, Rank)
from backend.dto.common import PaginatedRequest
from typing import List

Expand Down Expand Up @@ -68,10 +70,59 @@ def validate_include(cls, v):


class GetAgencyOfficersParams(PaginatedRequest):
term: Optional[str] = Field(
None,
description="Search term to filter officers by name or badge number."
)
type: Optional[List[str]] = Field(
None,
description="Filter officers by employment type "
"(e.g., 'law_enforcement', 'corrections')."
)
status: Optional[List[str]] = Field(
None,
description="Filter officers by employment status "
"(e.g., 'full-time', 'part-time')."
)
rank: Optional[List[str]] = Field(
None,
description="Filter officers by rank "
"(e.g., 'Sergeant', 'Lieutenant')."
)
include: Optional[List[str]] = Field(
None, description="Related data to include in the response."
)

@field_validator("status")
def validate_status(cls, v):
allowed_statuses = EmploymentStatus.choices()
if v:
invalid = set(v) - set(allowed_statuses)
if invalid:
raise ValueError(
f"Invalid status parameters: {', '.join(invalid)}")
return v

@field_validator("type")
def validate_type(cls, v):
allowed_types = EmploymentType.choices()
if v:
invalid = set(v) - set(allowed_types)
if invalid:
raise ValueError(
f"Invalid type parameters: {', '.join(invalid)}")
return v

@field_validator("rank")
def validate_rank(cls, v):
allowed_ranks = Rank.choices()
if v:
invalid = set(v) - set(allowed_ranks)
if invalid:
raise ValueError(
f"Invalid rank parameters: {', '.join(invalid)}")
return v

@field_validator("include")
def validate_include(cls, v):
allowed_includes = {
Expand All @@ -83,3 +134,22 @@ def validate_include(cls, v):
raise ValueError(
f"Invalid include parameters: {', '.join(invalid)}")
return v


class GetAgencyUnitsParams(PaginatedRequest):
include: Optional[List[str]] = Field(
None, description="Related data to include in the response."
)

@field_validator("include")
def validate_include(cls, v):
allowed_includes = {
"officers",
"complaints",
}
if v:
invalid = set(v) - allowed_includes
if invalid:
raise ValueError(
f"Invalid include parameters: {', '.join(invalid)}")
return v
56 changes: 54 additions & 2 deletions backend/dto/unit.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
from pydantic import Field, field_validator
from typing import Optional, List
from backend.database.models.agency import State
from backend.database.models.employment import (
EmploymentStatus, EmploymentType, Rank)
from backend.dto.common import PaginatedRequest, RequestDTO


Expand All @@ -25,8 +27,9 @@ class GetUnitParams(RequestDTO):
@field_validator("include")
def validate_include(cls, v):
allowed_includes = {
"total_officers",
"total_complaints",
"officers",
"complaints",
"allegations",
"reported_officers",
"leadership",
"location",
Expand All @@ -40,10 +43,59 @@ def validate_include(cls, v):


class GetUnitOfficersParams(PaginatedRequest):
term: Optional[str] = Field(
None,
description="Search term to filter officers by name or badge number."
)
type: Optional[List[str]] = Field(
None,
description="Filter officers by employment type "
"(e.g., 'law_enforcement', 'corrections')."
)
status: Optional[List[str]] = Field(
None,
description="Filter officers by employment status "
"(e.g., 'full-time', 'part-time')."
)
rank: Optional[List[str]] = Field(
None,
description="Filter officers by rank "
"(e.g., 'Sergeant', 'Lieutenant')."
)
include: Optional[List[str]] = Field(
None, description="Related data to include in the response."
)

@field_validator("status")
def validate_status(cls, v):
allowed_statuses = EmploymentStatus.choices()
if v:
invalid = set(v) - set(allowed_statuses)
if invalid:
raise ValueError(
f"Invalid status parameters: {', '.join(invalid)}")
return v

@field_validator("type")
def validate_type(cls, v):
allowed_types = EmploymentType.choices()
if v:
invalid = set(v) - set(allowed_types)
if invalid:
raise ValueError(
f"Invalid type parameters: {', '.join(invalid)}")
return v

@field_validator("rank")
def validate_rank(cls, v):
allowed_ranks = Rank.choices()
if v:
invalid = set(v) - set(allowed_ranks)
if invalid:
raise ValueError(
f"Invalid rank parameters: {', '.join(invalid)}")
return v

@field_validator("include")
def validate_include(cls, v):
allowed_includes = {
Expand Down
Loading
Loading