Skip to content

Commit

Permalink
Officer Table Overhaul (#358)
Browse files Browse the repository at this point in the history
* Officer Table Overhaul (In Progress)
- Adding tests for Officer API endpoints
- Creating association objects for:
    - Accusation (Officer/Perpetrator)
    - Employment (Officer/Agency)

* Officer Model Updates

* Add additional officer endpoints and tests
- Get officer by id
- Get all officers
- Create officer

* Style Corrections

* Officer search error fix
  • Loading branch information
DMalone87 authored Mar 13, 2024
1 parent 61b444e commit f547efe
Show file tree
Hide file tree
Showing 13 changed files with 963 additions and 65 deletions.
2 changes: 2 additions & 0 deletions backend/database/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@
from .models.attachment import *
from .models.perpetrator import *
from .models.officer import *
from .models.employment import *
from .models.accusation import *
from .models.participant import *
from .models.tag import *
from .models.result_of_stop import *
Expand Down
23 changes: 0 additions & 23 deletions backend/database/models/_assoc_tables.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
from .. import db
from backend.database.models.officer import Rank


incident_agency = db.Table(
Expand All @@ -17,25 +16,3 @@
primary_key=True),
db.Column('tag_id', db.Integer, db.ForeignKey('tag.id'), primary_key=True)
)

agency_officer = db.Table(
'agency_officer',
db.Column('agency_id', db.Integer, db.ForeignKey('agency.id'),
primary_key=True),
db.Column('officer_id', db.Integer, db.ForeignKey('officer.id'),
primary_key=True),
db.Column('earliest_employment', db.Text),
db.Column('latest_employment', db.Text),
db.Column('badge_number', db.Text),
db.Column('unit', db.Text),
db.Column('highest_rank', db.Enum(Rank)),
db.Column('currently_employed', db.Boolean)
)

perpetrator_officer = db.Table(
'perpetrator_officer',
db.Column('perpetrator_id', db.Integer, db.ForeignKey('perpetrator.id'),
primary_key=True),
db.Column('officer_id', db.Integer, db.ForeignKey('officer.id'),
primary_key=True)
)
17 changes: 17 additions & 0 deletions backend/database/models/accusation.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
from .. import db


class Accusation(db.Model):
id = db.Column(db.Integer, primary_key=True)
perpetrator_id = db.Column(db.Integer, db.ForeignKey("perpetrator.id"))
officer_id = db.Column(db.Integer, db.ForeignKey("officer.id"))
user_id = db.Column(db.Integer, db.ForeignKey("user.id"))
date_created = db.Column(db.Text)
basis = db.Column(db.Text)

attachments = db.relationship("Attachment", backref="accusation")
perpetrator = db.relationship("Perpetrator", back_populates="suspects")
officer = db.relationship("Officer", back_populates="accusations")

def __repr__(self):
return f"<Employment {self.id}>"
5 changes: 2 additions & 3 deletions backend/database/models/agency.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import enum
from backend.database.models._assoc_tables import agency_officer
from .. import db


Expand All @@ -20,8 +19,8 @@ class Agency(db.Model):
hq_city = db.Column(db.Text)
hq_zip = db.Column(db.Text)
jurisdiction = db.Column(db.Enum(JURISDICTION))
known_officers = db.relationship(
"Officer", secondary=agency_officer, backref="known_employers")

known_officers = db.relationship("Employment", back_populates="agency")

def __repr__(self):
return f"<Agency {self.name}>"
3 changes: 2 additions & 1 deletion backend/database/models/attachment.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@
class Attachment(db.Model):
id = db.Column(db.Integer, primary_key=True)
incident_id = db.Column(db.Integer, db.ForeignKey("incident.id"))
accusation_id = db.Column(db.Integer, db.ForeignKey("accusation.id"))
title = db.Column(db.Text)
hash = db.Column(db.Text)
location = db.Column(db.Text)
url = db.Column(db.Text)
filetype = db.Column(db.Text)
34 changes: 34 additions & 0 deletions backend/database/models/employment.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import enum
from .. import db


class Rank(str, enum.Enum):
# TODO: Is this comprehensive?
TECHNICIAN = "TECHNICIAN"
OFFICER = "OFFICER"
DETECTIVE = "DETECTIVE"
CORPORAL = "CORPORAL"
SERGEANT = "SERGEANT"
LIEUTENANT = "LIEUTENANT"
CAPTAIN = "CAPTAIN"
DEPUTY = "DEPUTY"
CHIEF = "CHIEF"
COMMISSIONER = "COMMISSIONER"


class Employment(db.Model):
id = db.Column(db.Integer, primary_key=True)
officer_id = db.Column(db.Integer, db.ForeignKey("officer.id"))
agency_id = db.Column(db.Integer, db.ForeignKey("agency.id"))
earliest_employment = db.Column(db.Text)
latest_employment = db.Column(db.Text)
badge_number = db.Column(db.Text)
unit = db.Column(db.Text)
highest_rank = db.Column(db.Enum(Rank))
currently_employed = db.Column(db.Boolean)

officer = db.relationship("Officer", back_populates="known_employers")
agency = db.relationship("Agency", back_populates="known_officers")

def __repr__(self):
return f"<Employment {self.id}>"
27 changes: 9 additions & 18 deletions backend/database/models/officer.py
Original file line number Diff line number Diff line change
@@ -1,19 +1,6 @@
import enum

from .. import db


class Rank(str, enum.Enum):
# TODO: Is this comprehensive?
TECHNICIAN = "TECHNICIAN"
OFFICER = "OFFICER"
DETECTIVE = "DETECTIVE"
CORPORAL = "CORPORAL"
SERGEANT = "SERGEANT"
LIEUTENANT = "LIEUTENANT"
CAPTAIN = "CAPTAIN"
DEPUTY = "DEPUTY"
CHIEF = "CHIEF"
from ..core import db, CrudMixin


class State(str, enum.Enum):
Expand Down Expand Up @@ -72,8 +59,8 @@ class State(str, enum.Enum):
class StateID(db.Model):
"""
Represents a Statewide ID that follows an offcier even as they move between
law enforcement agencies. for an officer. For example, in New York, this
would be the Tax ID Number.
law enforcement agencies. For example, in New York, this would be
the Tax ID Number.
"""
id = db.Column(db.Integer, primary_key=True)
officer_id = db.Column(
Expand All @@ -83,17 +70,21 @@ class StateID(db.Model):
value = db.Column(db.Text) # e.g. "958938"

def __repr__(self):
return f"<StateID {self.id}>"
return f"<StateID: Officer {self.officer_id}, {self.state}>"


class Officer(db.Model):
class Officer(db.Model, CrudMixin):
id = db.Column(db.Integer, primary_key=True) # officer id
first_name = db.Column(db.Text)
middle_name = db.Column(db.Text)
last_name = db.Column(db.Text)
race = db.Column(db.Text)
ethnicity = db.Column(db.Text)
gender = db.Column(db.Text)
date_of_birth = db.Column(db.Date)
known_employers = db.relationship("Employment", back_populates="officer")
accusations = db.relationship("Accusation", back_populates="officer")
state_ids = db.relationship("StateID", backref="officer")

def __repr__(self):
return f"<Officer {self.id}>"
6 changes: 3 additions & 3 deletions backend/database/models/perpetrator.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
from backend.database.models._assoc_tables import perpetrator_officer
from backend.database.models.officer import Rank, State
from backend.database.models.officer import State
from backend.database.models.employment import Rank
from .. import db


Expand All @@ -20,7 +20,7 @@ class Perpetrator(db.Model):
state_id_name = db.Column(db.Text)
role = db.Column(db.Text)
suspects = db.relationship(
"Officer", secondary=perpetrator_officer, backref="accusations")
"Accusation", back_populates="perpetrator")

def __repr__(self):
return f"<Perpetrator {self.id}>"
3 changes: 3 additions & 0 deletions backend/database/models/user.py
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,9 @@ class User(db.Model, UserMixin, CrudMixin):
"PartnerMember", back_populates="user", lazy="select")
member_of = association_proxy("partner_association", "partner")

# Officer Accusations
accusations = db.relationship("Accusation", backref="user")

def verify_password(self, pw):
return bcrypt.checkpw(pw.encode("utf8"), self.password.encode("utf8"))

Expand Down
110 changes: 99 additions & 11 deletions backend/routes/officers.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,16 @@
from backend.mixpanel.mix import track_to_mp
from mixpanel import MixpanelException
from backend.database.models.user import UserRole
from backend.database.models.employment import Employment
from flask import Blueprint, abort, request
from flask_jwt_extended.view_decorators import jwt_required
from pydantic import BaseModel

from ..database import Officer, db, agency_officer
from ..database import Officer, db
from ..schemas import (
CreateOfficerSchema,
officer_orm_to_json,
officer_to_orm,
validate,
)

Expand All @@ -21,9 +24,10 @@


class SearchOfficerSchema(BaseModel):
officerName: Optional[str] = None
location: Optional[str] = None
name: Optional[str] = None
agency: Optional[str] = None
badgeNumber: Optional[str] = None
location: Optional[str] = None
page: Optional[int] = 1
perPage: Optional[int] = 20

Expand Down Expand Up @@ -51,19 +55,35 @@ def search_officer():
logger = logging.getLogger("officers")

try:
if body.officerName:
if body.name:
names = body.officerName.split()
first_name = names[0] if len(names) > 0 else ''
last_name = names[1] if len(names) > 1 else ''
query = Officer.query.filter(or_(
Officer.first_name.ilike(f"%{first_name}%"),
Officer.last_name.ilike(f"%{last_name}%")
))
if len(names) == 1:
query = Officer.query.filter(
or_(
Officer.first_name.ilike(f"%{body.officerName}%"),
Officer.last_name.ilike(f"%{body.officerName}%")
)
)
elif len(names) == 2:
query = Officer.query.filter(
or_(
Officer.first_name.ilike(f"%{names[0]}%"),
Officer.last_name.ilike(f"%{names[1]}%")
)
)
else:
query = Officer.query.filter(
or_(
Officer.first_name.ilike(f"%{names[0]}%"),
Officer.middle_name.ilike(f"%{names[1]}%"),
Officer.last_name.ilike(f"%{names[2]}%")
)
)

if body.badgeNumber:
officer_ids = [
result.officer_id for result in db.session.query(
agency_officer
Employment
).filter_by(badge_number=body.badgeNumber).all()
]
query = Officer.query.filter(Officer.id.in_(officer_ids)).all()
Expand Down Expand Up @@ -93,3 +113,71 @@ def search_officer():
}
except Exception as e:
abort(500, description=str(e))


@bp.route("/create", methods=["POST"])
@jwt_required()
@min_role_required(UserRole.CONTRIBUTOR)
@validate(json=CreateOfficerSchema)
def create_officer():
"""Create an officer profile.
"""

try:
officer = officer_to_orm(request.context.json)
except Exception:
abort(400)

created = officer.create()

track_to_mp(
request,
"create_officer",
{
"first_name": officer.first_name,
"middle_name": officer.middle_name,
"last_name": officer.last_name
},
)
return officer_orm_to_json(created)


@bp.route("/<int:officer_id>", methods=["GET"])
@jwt_required()
@min_role_required(UserRole.PUBLIC)
@validate()
def get_officer(officer_id: int):
"""Get an officer profile.
"""
officer = db.session.query(Officer).get(officer_id)
if officer is None:
abort(404, description="Officer not found")
return officer_orm_to_json(officer)


@bp.route("/", methods=["GET"])
@jwt_required()
@min_role_required(UserRole.PUBLIC)
@validate()
def get_all_officers():
"""Get all officers.
Accepts Query Parameters for pagination:
per_page: number of results per page
page: page number
"""
args = request.args
q_page = args.get("page", 1, type=int)
q_per_page = args.get("per_page", 20, type=int)

all_officers = db.session.query(Officer)
pagination = all_officers.paginate(
page=q_page, per_page=q_per_page, max_per_page=100
)

return {
"results": [
officer_orm_to_json(officer) for officer in pagination.items],
"page": pagination.page,
"totalPages": pagination.pages,
"totalResults": pagination.total,
}
Loading

0 comments on commit f547efe

Please sign in to comment.