Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Create incidents endpoint and tests #355

Open
wants to merge 21 commits into
base: main
Choose a base branch
from
Open
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 .github/workflows/python-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ jobs:
runs-on: ubuntu-latest
services:
postgres:
image: postgres:12-alpine
image: postgres:16-alpine
env:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
Expand All @@ -23,10 +23,10 @@ jobs:
- 5432:5432
steps:
- uses: actions/checkout@v4
- name: Python 3.8 Setup
- name: Python 3.12 Setup
uses: actions/setup-python@v5
with:
python-version: 3.8
python-version: 3.12
- name: Install dependencies
run: |
sudo apt-get update
Expand Down
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ MIXPANEL_TOKEN=your_mixpanel_token
> **Note**
> When running locally, you may need to update one of the ports in the `.env` file if it conflicts with another application on your machine.

3. Build and run the project with `docker-compose build; docker-compose up -d; docker-compose logs -f app`
3. Build and run the project with `docker-compose build && docker-compose up -d && docker-compose logs -f`

## Installation (Frontend Only)

Expand All @@ -67,7 +67,7 @@ You'll need to replace `police-data-trust-api-1` with the name of the container
docker container ls
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
c0cf******** police-data-trust-api "/bin/sh -c '/wait &…" About a minute ago Up About a minute 0.0.0.0:5001->5001/tcp police-data-trust-api-1
5e6f******** postgres:13.2 "docker-entrypoint.s…" 3 days ago Up About a minute 0.0.0.0:5432->5432/tcp police-data-trust-db-1
5e6f******** postgres:16.1 "docker-entrypoint.s…" 3 days ago Up About a minute 0.0.0.0:5432->5432/tcp police-data-trust-db-1
dacd******** police-data-trust-web "docker-entrypoint.s…" 3 days ago Up About a minute 0.0.0.0:3000->3000/tcp police-data-trust-web-1
```

Expand Down
4 changes: 2 additions & 2 deletions backend/Dockerfile
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
FROM python:3.9.5-slim-buster AS base
FROM python:3-slim-buster AS base

RUN apt-get update && apt-get install curl -y && apt-get install g++ libpq-dev gcc -y

ADD https://github.com/ufoscout/docker-compose-wait/releases/download/2.7.3/wait /wait
ADD https://github.com/ufoscout/docker-compose-wait/releases/download/2.12.1/wait /wait
RUN chmod +x /wait


Expand Down
2 changes: 1 addition & 1 deletion backend/Dockerfile.cloud
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# docker build command:
# docker build -t police-data-trust-backend-dev -f backend/Dockerfile.cloud .
FROM python:3.8-slim-buster
FROM python:3-slim-buster

WORKDIR /app/

Expand Down
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
3 changes: 2 additions & 1 deletion backend/database/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@

import click
import pandas as pd
import psycopg
import psycopg2.errors
from flask import abort, current_app
from flask.cli import AppGroup, with_appcontext
Expand Down Expand Up @@ -122,7 +123,7 @@ def create_database(

try:
cursor.execute(f"CREATE DATABASE {database};")
except psycopg2.errors.lookup("42P04"):
except (psycopg2.errors.lookup("42P04"), psycopg.errors.DuplicateDatabase):
click.echo(f"Database {database!r} already exists.")
else:
click.echo(f"Created database {database!r}.")
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)
1 change: 0 additions & 1 deletion backend/database/models/attorney.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,4 @@

class Attorney(db.Model):
id = db.Column(db.Integer, primary_key=True)
legal_case_id = db.Column(db.Integer, db.ForeignKey("legal_case.id"))
text_contents = db.Column(db.String)
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}>"
4 changes: 3 additions & 1 deletion backend/database/models/incident.py
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,9 @@ def __init__(self, **kwargs):
criminal_case_brought = db.Column(db.Boolean)
case_id = db.Column(db.Integer) # TODO: foreign key of some sort?
victims = db.relationship("Victim", backref="incident")
perpetrators = db.relationship("Perpetrator", backref="incident")
perpetrators = db.relationship(
"Perpetrator",
backref="incident")
# descriptions = db.relationship("Description", backref="incident")
tags = db.relationship("Tag", secondary=incident_tag, backref="incidents")
agencies_present = db.relationship(
Expand Down
10 changes: 2 additions & 8 deletions backend/database/models/legal_case.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@


class LegalCaseType(str, enum.Enum):
# TODO: Do we want string enumerations to be all caps? i.e. CIVIL = "CIVIL"
CIVIL = "CIVIL"
CRIMINAL = "CRIMINAL"

Expand All @@ -15,15 +14,10 @@ class LegalCase(db.Model):
jurisdiction = db.Column(db.String)
judge = db.Column(db.String)
docket_number = db.Column(db.String)
# TODO: Foreign key to officer/victim?
defendant = db.Column(db.String)
defendant_council = db.relationship(
"Attorney", backref="legal_case_defendant", uselist=False
)
defendant_council = db.Column(db.String)
plaintiff = db.Column(db.String)
plaintiff_council = db.relationship(
"Attorney", backref="legal_case_plaintiff", uselist=False
)
plaintiff_council = db.Column(db.String)
start_date = db.Column(db.DateTime)
end_date = db.Column(db.DateTime)
outcome = db.Column(db.String)
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}>"
7 changes: 6 additions & 1 deletion backend/database/models/user.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ class CI_String(TypeDecorator):
"""Case-insensitive String subclass definition"""

impl = String
cache_ok = True

def __init__(self, length, **kwargs):
if kwargs.get("collate"):
Expand Down Expand Up @@ -69,7 +70,8 @@ class User(db.Model, UserMixin, CrudMixin):
# User authentication information. The collation="NOCASE" is required
# to search case insensitively when USER_IFIND_MODE is "nocase_collation".
email = db.Column(
CI_String(255, collate="NOCASE"), nullable=False, unique=True
CI_String(255, collate="NOCASE"),
nullable=False, unique=True
)
email_confirmed_at = db.Column(db.DateTime())
password = db.Column(db.String(255), nullable=False, server_default="")
Expand All @@ -91,6 +93,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
2 changes: 1 addition & 1 deletion backend/routes/healthcheck.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,4 +31,4 @@ class Resp(BaseModel):
def healthcheck():
"""Verifies service health and returns the api version"""
check_db()
return ({"apiVersion": spec.config.VERSION}, 200)
return {"apiVersion": spec.config.version}, 200
Loading