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
2 changes: 1 addition & 1 deletion backend/database/models/agency.py
Original file line number Diff line number Diff line change
Expand Up @@ -176,7 +176,7 @@ class Agency(StructuredNode, HasCitations, JsonSerializable, SearchableMixin):
# Relationships
city_node = RelationshipTo(
"backend.database.models.infra.locations.CityNode",
"LOCATED_IN", cardinality=One)
"LOCATED_IN", cardinality=ZeroOrOne)

@property
def units(self) -> RelQuery:
Expand Down
3 changes: 2 additions & 1 deletion backend/database/models/source.py
Original file line number Diff line number Diff line change
Expand Up @@ -184,7 +184,7 @@ class Source(StructuredNode, JsonSerializable):
organizations, government agencies, non-profits, or private firms.
"""
__property_order__ = [
"uid", "name", "url", "slug"
"uid", "name", "description", "url", "slug"
]
__hidden_properties__ = [
"invitations", "staged_invitations",
Expand All @@ -194,6 +194,7 @@ class Source(StructuredNode, JsonSerializable):

name = StringProperty(unique_index=True, required=True)
url = StringProperty()
description = StringProperty()

# Slug property for easy URL access
slug = StringProperty(unique_index=True)
Expand Down
40 changes: 39 additions & 1 deletion backend/dto/agency.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
from backend.database.models.agency import State, Jurisdiction
from backend.database.models.employment import (
EmploymentStatus, EmploymentType, Rank)
from backend.dto.common import PaginatedRequest
from backend.dto.common import PaginatedRequest, RequestDTO
from typing import List


Expand Down Expand Up @@ -72,6 +72,44 @@ def validate_include(cls, v):
return v


class CreateAgency(RequestDTO):
source_uid: str = Field(
...,
description="UID of the source making the creation",
)
name: Optional[str] = Field(None, description="Name of the agency")
hq_address: Optional[str] = Field(
None, description="Address of the agency")
hq_city: Optional[str] = Field(None, description="City of the agency")
hq_state: Optional[str] = Field(None, description="State of the agency")
hq_zip: Optional[str] = Field(None, description="Zip code of the agency")
jurisdiction: Optional[str] = Field(
None, description="Jurisdiction of the agency")
phone: Optional[str] = Field(None, description="Phone number of the agency")
email: Optional[str] = Field(None, description="Email of the agency")
website_url: Optional[str] = Field(
None, description="Website of the agency")


class UpdateAgency(RequestDTO):
source_uid: str = Field(
...,
description="UID of the source making the update",
)
name: Optional[str] = Field(None, description="Name of the agency")
hq_address: Optional[str] = Field(
None, description="Address of the agency")
hq_city: Optional[str] = Field(None, description="City of the agency")
hq_state: Optional[str] = Field(None, description="State of the agency")
hq_zip: Optional[str] = Field(None, description="Zip code of the agency")
jurisdiction: Optional[str] = Field(
None, description="Jurisdiction of the agency")
phone: Optional[str] = Field(None, description="Phone number of the agency")
email: Optional[str] = Field(None, description="Email of the agency")
website_url: Optional[str] = Field(
None, description="Website of the agency")


class GetAgencyOfficersParams(PaginatedRequest):
term: Optional[str] = Field(
None,
Expand Down
42 changes: 42 additions & 0 deletions backend/dto/officer.py
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,48 @@ def validate_include(cls, v):
return v


class CreateOfficer(RequestDTO):
source_uid: str = Field(
...,
description="UID of the source making the creation",
)
first_name: Optional[str] = Field(
None, description="First name of the officer")
middle_name: Optional[str] = Field(
None, description="Middle name of the officer")
last_name: Optional[str] = Field(
None, description="Last name of the officer")
suffix: Optional[str] = Field(
None, description="Suffix of the officer's name")
ethnicity: Optional[str] = Field(
None, description="The ethnicity of the officer")
gender: Optional[str] = Field(
None, description="The gender of the officer")
date_of_birth: Optional[str] = Field(
None, description="The date of birth of the officer")


class UpdateOfficer(RequestDTO):
source_uid: str = Field(
...,
description="UID of the source making the update",
)
first_name: Optional[str] = Field(
None, description="First name of the officer")
middle_name: Optional[str] = Field(
None, description="Middle name of the officer")
last_name: Optional[str] = Field(
None, description="Last name of the officer")
suffix: Optional[str] = Field(
None, description="Suffix of the officer's name")
ethnicity: Optional[str] = Field(
None, description="The ethnicity of the officer")
gender: Optional[str] = Field(
None, description="The gender of the officer")
date_of_birth: Optional[str] = Field(
None, description="The date of birth of the officer")


class GetOfficerMetricsParams(RequestDTO):
include: List[str]
start_date: Optional[str] = None
Expand Down
72 changes: 42 additions & 30 deletions backend/routes/agencies.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,16 @@
validate_request, add_pagination_wrapper, ordered_jsonify,
NodeConflictException)
from backend.mixpanel.mix import track_to_mp
from backend.database.models.user import UserRole
from backend.database.models.user import UserRole, User
from backend.database.models.agency import Agency
from backend.routes.search import (
fetch_details, build_agency_result)
from .tmp.pydantic.agencies import CreateAgency, UpdateAgency
from flask import Blueprint, abort, request, jsonify
from flask_jwt_extended import get_jwt
from flask_jwt_extended.view_decorators import jwt_required
from backend.dto.agency import (
AgencyQueryParams, GetAgencyParams, GetAgencyOfficersParams,
CreateAgency, UpdateAgency,
GetAgencyUnitsParams)
from backend.services.agency_service import AgencyService

Expand All @@ -28,36 +29,38 @@
@min_role_required(UserRole.CONTRIBUTOR)
@validate_request(CreateAgency)
def create_agency():
logger = logging.getLogger("create_agency")
"""Create an agency profile.
User must be a Contributor to create an agency.
Must include a name and jurisdiction.
"""
body: CreateAgency = request.validated_body
jwt_decoded = get_jwt()
current_user = User.get(jwt_decoded["sub"])
payload = body.model_dump(exclude={"source_uid"})

try:
agency = Agency.from_dict(body.model_dump())
response = agency_service.create_agency(
payload=payload,
source_uid=body.source_uid,
current_user=current_user,
)
track_to_mp(
request,
"create_agency",
{
"name": response.get("name")
},
)
return ordered_jsonify(response), 201
except NodeConflictException:
abort(409, description="Agency already exists")
except PermissionError as e:
abort(403, description=str(e))
except ValueError as e:
abort(400, description=str(e))
except Exception as e:
logger.error(f"Error, Agency.from_dict: {e}")
abort(400)

try:
Agency.link_location(agency, state=agency.hq_state, city=agency.hq_city)
except Exception as e:
logging.error(f"Error linking location {agency.name}: {e}")
print(f"Error linking location {agency.name}: {e}")
return

track_to_mp(
request,
"create_agency",
{
"name": agency.name
},
)
return agency.to_json()
logging.getLogger("create_agency").error(f"Error creating agency: {e}")
abort(400, description=str(e))


# Get agency profile
Expand Down Expand Up @@ -100,23 +103,32 @@ def get_agency(agency_uid: str):
def update_agency(agency_uid: str):
"""Update an agency profile.
"""
# logger = logging.getLogger("update_agency")
body: UpdateAgency = request.validated_body
agency = Agency.nodes.get_or_none(uid=agency_uid)
if agency is None:
abort(404, description="Agency not found")
jwt_decoded = get_jwt()
current_user = User.get(jwt_decoded["sub"])
payload = body.model_dump(exclude_unset=True, exclude={"source_uid"})

try:
agency = Agency.from_dict(body.model_dump(), agency_uid)
agency.refresh()
response = agency_service.update_agency(
agency_uid=agency_uid,
payload=payload,
source_uid=body.source_uid,
current_user=current_user,
)
track_to_mp(
request,
"update_agency",
{
"name": agency.name
"name": response.get("name")
}
)
return agency.to_json()
return ordered_jsonify(response)
except LookupError as e:
abort(404, description=str(e))
except PermissionError as e:
abort(403, description=str(e))
except ValueError as e:
abort(400, description=str(e))
except Exception as e:
abort(400, description=str(e))

Expand Down
67 changes: 43 additions & 24 deletions backend/routes/officers.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,12 @@
from backend.database.models.user import UserRole, User
from backend.database.models.officer import Officer
from backend.services.officer_service import OfficerService
from .tmp.pydantic.officers import CreateOfficer, UpdateOfficer
from flask import Blueprint, abort, request, jsonify
from flask_jwt_extended import get_jwt
from flask_jwt_extended.view_decorators import jwt_required
from backend.dto.officer import (
OfficerSearchParams, GetOfficerParams, GetOfficerMetricsParams)
OfficerSearchParams, GetOfficerParams, GetOfficerMetricsParams,
CreateOfficer, UpdateOfficer)


bp = Blueprint("officer_routes", __name__, url_prefix="/api/v1/officers")
Expand All @@ -25,25 +25,34 @@
def create_officer():
"""Create an officer profile.
"""
logger = logging.getLogger("create_officer")
body: CreateOfficer = request.validated_body
jwt_decoded = get_jwt()
current_user = User.get(jwt_decoded["sub"])
payload = body.model_dump(exclude={"source_uid"})

# try:
officer = Officer.from_dict(body.model_dump())
# except Exception as e:
# abort(400, description=str(e))

logger.info(f"Officer {officer.uid} created by User {current_user.uid}")
track_to_mp(
request,
"create_officer",
{
"officer_id": officer.uid
},
)
return officer.to_json()
try:
response = officer_service.create_officer(
payload=payload,
source_uid=body.source_uid,
current_user=current_user,
)
logging.getLogger("create_officer").info(
f"Officer {response.get('uid')} created by User {current_user.uid}"
)
track_to_mp(
request,
"create_officer",
{
"officer_id": response.get("uid")
},
)
return ordered_jsonify(response), 201
except PermissionError as e:
abort(403, description=str(e))
except ValueError as e:
abort(400, description=str(e))
except Exception as e:
abort(400, description=str(e))


# Get an officer profile
Expand Down Expand Up @@ -113,24 +122,34 @@ def update_officer(officer_uid: str):
"""Update an officer profile.
"""
body: UpdateOfficer = request.validated_body
o = Officer.nodes.get_or_none(uid=officer_uid)
if o is None:
abort(404, description="Officer not found")
jwt_decoded = get_jwt()
current_user = User.get(jwt_decoded["sub"])
payload = body.model_dump(exclude_unset=True, exclude={"source_uid"})

try:
o = Officer.from_dict(body.model_dump(), officer_uid)
o.refresh()
response = officer_service.update_officer(
officer_uid=officer_uid,
payload=payload,
source_uid=body.source_uid,
current_user=current_user,
)
except LookupError as e:
abort(404, description=str(e))
except PermissionError as e:
abort(403, description=str(e))
except ValueError as e:
abort(400, description=str(e))
except Exception as e:
abort(400, description=str(e))

track_to_mp(
request,
"update_officer",
{
"officer_id": o.uid
"officer_id": response.get("uid")
},
)
return o.to_json()
return ordered_jsonify(response)


# Delete an officer profile
Expand Down
3 changes: 3 additions & 0 deletions backend/schemas.py
Original file line number Diff line number Diff line change
Expand Up @@ -604,6 +604,7 @@ def link_location(cls: Type[T], item, state=None, city=None):
:param county: The county. Should be a FIPS code or county name.
:param city: The city. Should be a SimpleMaps ID or city name.
"""
linked = False
if state is not None:
state_node = StateNode.nodes.get_or_none(
abbreviation=state)
Expand All @@ -624,9 +625,11 @@ def link_location(cls: Type[T], item, state=None, city=None):
item.city_node.connect(city_node)
logging.info(
f"Linked {item.uid} to City {city_node.uid}")
linked = True

else:
logging.error(f"State not found: {state}")
return linked


class SearchableMixin:
Expand Down
Loading
Loading