diff --git a/alembic/dev_seeds.py b/alembic/dev_seeds.py
index 472feb5a..e1baa7ec 100644
--- a/alembic/dev_seeds.py
+++ b/alembic/dev_seeds.py
@@ -2,12 +2,11 @@
from backend.database import User, UserRole
from backend.auth import user_manager
from backend.database.models.incident import Incident
-from backend.database.models.officer import Officer
+from backend.database.models.perpetrator import Perpetrator
from backend.database.models.source import Source
from backend.database.models.use_of_force import UseOfForce
-
def create_user(user):
user_exists = (
db.session.query(User).filter_by(email=user.email).first() is not None
@@ -61,6 +60,7 @@ def create_user(user):
)
)
+
def create_source(source):
source_exists = (
db.session.query(Source).filter_by(id=source.id).first() is not None
@@ -69,13 +69,13 @@ def create_source(source):
if not source_exists:
source.create()
+
create_source(
Source(
- id="mpv",
- publication_name="Mapping Police Violence",
- publication_date="01/01/2015",
- author="Samuel Sinyangwe",
- URL="https://mappingpoliceviolence.us",
+ id=10000000,
+ name="Mapping Police Violence",
+ url="https://mappingpoliceviolence.us",
+ contact_email="info@campaignzero.org"
)
)
@@ -83,22 +83,23 @@ def create_source(source):
def create_incident(key=1, date="10-01-2019", lon=84, lat=34):
base_id = 10000000
id = base_id + key
+ mpv = db.session.query(Source).filter_by(
+ name="Mapping Police Violence").first()
incident = Incident(
id=id,
+ source=mpv,
location=f"Test location {key}",
longitude=lon,
latitude=lat,
description=f"Test description {key}",
- department=f"Small Police Department {key}",
time_of_incident=f"{date} 00:00:00",
- officers=[
- Officer(
+ perpetrators=[
+ Perpetrator(
first_name=f"TestFirstName {key}",
last_name=f"TestLastName {key}",
)
],
- use_of_force=[UseOfForce(item=f"gunshot {key}")],
- source="mpv",
+ use_of_force=[UseOfForce(item=f"gunshot {key}")]
)
exists = db.session.query(Incident).filter_by(id=id).first() is not None
diff --git a/backend/database/__init__.py b/backend/database/__init__.py
index f535b124..6c93fb2c 100644
--- a/backend/database/__init__.py
+++ b/backend/database/__init__.py
@@ -16,7 +16,8 @@
from .models.incident import *
from .models.investigation import *
from .models.legal_case import *
-from .models.multimedia import *
+from .models.attachment import *
+from .models.perpetrator import *
from .models.officer import *
from .models.participant import *
from .models.tag import *
@@ -25,5 +26,4 @@
from .models.use_of_force import *
from .models.user import *
from .models.victim import *
-from .models.accusation import *
from .models.source import *
diff --git a/backend/database/core.py b/backend/database/core.py
index 8bbfb3d6..5f303b1c 100644
--- a/backend/database/core.py
+++ b/backend/database/core.py
@@ -15,9 +15,7 @@
from flask_sqlalchemy import SQLAlchemy
from psycopg2 import connect
from psycopg2.extensions import connection
-from sqlalchemy import ForeignKey
from sqlalchemy.exc import ResourceClosedError
-from sqlalchemy.ext.declarative import declared_attr
from werkzeug.utils import secure_filename
from ..config import TestingConfig
@@ -50,34 +48,6 @@ def get(cls, id: Any, abort_if_null: bool = True):
return obj
-class SourceMixin:
- """Adds support for unique, external source ID's"""
-
- # Identifies the source dataset or organization
- @declared_attr
- def source(self):
- return db.Column(db.Text, ForeignKey("source.id"))
-
- # Identifies the unique primary key in the source
- source_id = db.Column(db.Text)
-
- def __init_subclass__(cls, **kwargs):
- # Require that source ID's be unique within each source. Postgres does
- # not enforce uniqueness if either value is null.
- # https://www.postgresql.org/docs/9.0/ddl-constraints.html#AEN2445
- uc = db.UniqueConstraint(
- "source", "source_id", name=f"{cls.__name__.lower()}_source_uc"
- )
-
- cls.__table_args__ = tuple(
- a
- for a in (uc, getattr(cls, "__table_args__", None))
- if a is not None
- )
-
- super().__init_subclass__(**kwargs)
-
-
QUERIES_DIR = os.path.abspath(
os.path.join(os.path.dirname(__file__), "queries")
)
diff --git a/backend/database/models/_assoc_tables.py b/backend/database/models/_assoc_tables.py
new file mode 100644
index 00000000..9a10d4c2
--- /dev/null
+++ b/backend/database/models/_assoc_tables.py
@@ -0,0 +1,40 @@
+from .. import db
+from backend.database.models.officer import Rank
+
+incident_agency = db.Table(
+ 'incident_agency',
+ db.Column('incident_id', db.Integer, db.ForeignKey('incident.id'),
+ primary_key=True),
+ db.Column('agency_id', db.Integer, db.ForeignKey('agency.id'),
+ primary_key=True),
+ db.Column('officers_present', db.Integer)
+)
+
+incident_tag = db.Table(
+ 'incident_tag',
+ db.Column('incident_id', db.Integer, db.ForeignKey('incident.id'),
+ 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)
+)
diff --git a/backend/database/models/accusation.py b/backend/database/models/accusation.py
deleted file mode 100644
index fc67a52c..00000000
--- a/backend/database/models/accusation.py
+++ /dev/null
@@ -1,17 +0,0 @@
-from .. import db
-
-
-class Accusation(db.Model):
- """Models CPDP `complaints-accused` table"""
-
- id = db.Column(db.Integer, primary_key=True)
- incident_id = db.Column(db.Integer, db.ForeignKey("incident.id"))
- officer_id = db.Column(db.Integer, db.ForeignKey("officer.id"))
- category = db.Column(db.Text)
- category_code = db.Column(db.Text)
- finding = db.Column(db.Text)
- outcome = db.Column(db.Text)
-
- __table_args__ = (
- db.UniqueConstraint("incident_id", "officer_id", name="accusation_uc"),
- )
diff --git a/backend/database/models/agency.py b/backend/database/models/agency.py
index fd579b70..9f3744aa 100644
--- a/backend/database/models/agency.py
+++ b/backend/database/models/agency.py
@@ -1,7 +1,27 @@
+import enum
+from backend.database.models._assoc_tables import agency_officer
from .. import db
+class JURISDICTION(enum.Enum):
+ FEDERAL = 1
+ STATE = 2
+ COUNTY = 3
+ MUNICIPAL = 4
+ PRIVATE = 5
+ OTHER = 6
+
+
class Agency(db.Model):
id = db.Column(db.Integer, primary_key=True)
- incident_id = db.Column(db.Integer, db.ForeignKey("incident.id"))
name = db.Column(db.Text)
+ website_url = db.Column(db.Text)
+ hq_address = db.Column(db.Text)
+ 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")
+
+ def __repr__(self):
+ return f""
diff --git a/backend/database/models/attachment.py b/backend/database/models/attachment.py
new file mode 100644
index 00000000..9d0259e0
--- /dev/null
+++ b/backend/database/models/attachment.py
@@ -0,0 +1,10 @@
+from .. import db
+
+
+class Attachment(db.Model):
+ id = db.Column(db.Integer, primary_key=True)
+ incident_id = db.Column(db.Integer, db.ForeignKey("incident.id"))
+ title = db.Column(db.Text)
+ hash = db.Column(db.Text)
+ location = db.Column(db.Text)
+ filetype = db.Column(db.Text)
diff --git a/backend/database/models/incident.py b/backend/database/models/incident.py
index 6515ee03..6745c30b 100644
--- a/backend/database/models/incident.py
+++ b/backend/database/models/incident.py
@@ -1,10 +1,15 @@
"""Define the SQL classes for Users."""
import enum
+from ..core import CrudMixin, db
+from backend.database.models._assoc_tables import incident_agency, incident_tag
-from ..core import CrudMixin, SourceMixin, db
-# Question: Should we be doing string enums?
+class RecordType(enum.Enum):
+ NEWS_REPORT = 1
+ GOVERNMENT_RECORD = 2
+ LEGAL_ACTION = 3
+ PERSONAL_ACCOUNT = 4
class InitialEncounter(enum.Enum):
@@ -50,31 +55,26 @@ class VictimStatus(enum.Enum):
DECEASED = 5
-# TODO: This file's a bit of a mess (my fault!)
-# There are a lot of association tables in here, and the incidents table is
-# not clearly either a facts table or component table.
-# We need to get a better idea of the relationships we want and then we should
-# implement them accordingly.
-
-
-class Incident(db.Model, CrudMixin, SourceMixin):
+class Incident(db.Model, CrudMixin):
"""The incident table is the fact table."""
id = db.Column(db.Integer, primary_key=True, autoincrement=True)
+ source_id = db.Column(
+ db.String, db.ForeignKey("source.id"))
+ source_details = db.relationship(
+ "SourceDetails", backref="incident", uselist=False)
time_of_incident = db.Column(db.DateTime)
+ time_confidence = db.Column(db.Integer)
complaint_date = db.Column(db.Date)
closed_date = db.Column(db.Date)
location = db.Column(db.Text) # TODO: location object
# Float is double precision (8 bytes) by default in Postgres
longitude = db.Column(db.Float)
latitude = db.Column(db.Float)
- # TODO: neighborhood seems like a weird identifier that may not always
- # apply in consistent ways across municipalities.
- neighborhood = db.Column(db.Text)
description = db.Column(db.Text)
stop_type = db.Column(db.Text) # TODO: enum
call_type = db.Column(db.Text) # TODO: enum
- has_multimedia = db.Column(db.Boolean)
+ has_attachments = db.Column(db.Boolean)
from_report = db.Column(db.Boolean)
# These may require an additional table. Also can dox a victim
was_victim_arrested = db.Column(db.Boolean)
@@ -83,31 +83,38 @@ class Incident(db.Model, CrudMixin, SourceMixin):
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")
- # TODO: Remove this. incident-officer relationship is many-many using
- # accusation as the join table.
- officers = db.relationship("Officer", backref="incident")
- department = db.Column(db.Text)
+ perpetrators = db.relationship("Perpetrator", backref="incident")
# descriptions = db.relationship("Description", backref="incident")
- tags = db.relationship("Tag", backref="incident")
+ tags = db.relationship("Tag", secondary=incident_tag, backref="incidents")
+ agencies_present = db.relationship(
+ "Agency", secondary=incident_agency, backref="recorded_incidents")
participants = db.relationship("Participant", backref="incident")
- multimedias = db.relationship("Multimedia", backref="incident")
+ attachments = db.relationship("Attachment", backref="incident")
investigations = db.relationship("Investigation", backref="incident")
results_of_stop = db.relationship("ResultOfStop", backref="incident")
actions = db.relationship("Action", backref="incident")
use_of_force = db.relationship("UseOfForce", backref="incident")
legal_case = db.relationship("LegalCase", backref="incident")
- accusations = db.relationship("Accusation", backref="incident")
-
-class Description(db.Model):
- id = db.Column(db.Integer, primary_key=True) # description id
- incident_id = db.Column(
- db.Integer, db.ForeignKey("incident.id"), nullable=False
- )
- text = db.Column(db.Text)
- type = db.Column(db.Text) # TODO: enum
+ def __repr__(self):
+ """Represent instance as a unique string."""
+ return f""
+
+
+# On the Description object:
+# Seems like this is based on the WITNESS standard. It also appears that the
+# original intention of that standard is to allow multiple descriptions to be
+# applied to a single incident. I recomend we handle this as part of a
+# larger epic when we add the annotation system, which is related.
+# class Description(db.Model):
+# id = db.Column(db.Integer, primary_key=True) # description id
+# incident_id = db.Column(
+# db.Integer, db.ForeignKey("incident.id"), nullable=False
+# )
+# text = db.Column(db.Text)
+# type = db.Column(db.Text) # TODO: enum
# TODO: are there rules for this column other than text?
- source = db.Column(db.Text)
+ # source = db.Column(db.Text)
# location = db.Column(db.Text) # TODO: location object
# # TODO: neighborhood seems like a weird identifier that may not always
# # apply in consistent ways across municipalities.
@@ -122,3 +129,27 @@ class Description(db.Model):
# # Does an existing warrant count here?
# criminal_case_brought = db.Column(db.Boolean)
# case_id = db.Column(db.Integer) # TODO: foreign key of some sort?
+
+
+class SourceDetails(db.Model):
+ id = db.Column(db.Integer, primary_key=True) # source details id
+ incident_id = db.Column(
+ db.Integer, db.ForeignKey("incident.id"), nullable=False
+ )
+ record_type = db.Column(db.Enum(RecordType))
+ # For Journalistic Publications
+ publication_name = db.Column(db.Text)
+ publication_date = db.Column(db.Date)
+ publication_url = db.Column(db.Text)
+ author = db.Column(db.Text)
+ author_url = db.Column(db.Text)
+ author_email = db.Column(db.Text)
+ # For Government Records
+ reporting_organization = db.Column(db.Text)
+ reporting_organization_url = db.Column(db.Text)
+ reporting_organization_email = db.Column(db.Text)
+ # For Legal Records
+ court = db.Column(db.Text)
+ judge = db.Column(db.Text)
+ docket_number = db.Column(db.Text)
+ date_of_action = db.Column(db.Date)
diff --git a/backend/database/models/multimedia.py b/backend/database/models/multimedia.py
deleted file mode 100644
index 3042a34a..00000000
--- a/backend/database/models/multimedia.py
+++ /dev/null
@@ -1,6 +0,0 @@
-from .. import db
-
-
-class Multimedia(db.Model):
- id = db.Column(db.Integer, primary_key=True)
- incident_id = db.Column(db.Integer, db.ForeignKey("incident.id"))
diff --git a/backend/database/models/officer.py b/backend/database/models/officer.py
index 87f1f5f9..1d11334e 100644
--- a/backend/database/models/officer.py
+++ b/backend/database/models/officer.py
@@ -1,7 +1,5 @@
import enum
-from backend.database.core import SourceMixin
-
from .. import db
@@ -18,18 +16,17 @@ class Rank(str, enum.Enum):
CHIEF = "CHIEF"
-class Officer(db.Model, SourceMixin):
+class Officer(db.Model):
id = db.Column(db.Integer, primary_key=True) # officer id
- incident_id = db.Column(db.Integer, db.ForeignKey("incident.id"))
first_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)
- appointed_date = db.Column(db.DateTime)
- badge = db.Column(db.Text)
- unit = db.Column(db.Text) # type?
# Note: rank at time of incident
- rank = db.Column(db.Text) # type?
+ rank = db.Column(db.Enum(Rank))
star = db.Column(db.Text) # type?
date_of_birth = db.Column(db.Date)
- accusations = db.relationship("Accusation", backref="officer")
+
+ def __repr__(self):
+ return f""
diff --git a/backend/database/models/perpetrator.py b/backend/database/models/perpetrator.py
new file mode 100644
index 00000000..be44f075
--- /dev/null
+++ b/backend/database/models/perpetrator.py
@@ -0,0 +1,24 @@
+from backend.database.models._assoc_tables import perpetrator_officer
+from backend.database.models.officer import Rank
+from .. import db
+
+
+class Perpetrator(db.Model):
+ id = db.Column(db.Integer, primary_key=True, autoincrement=True)
+ incident_id = db.Column(db.Integer, db.ForeignKey("incident.id"))
+ first_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)
+ badge = db.Column(db.Text)
+ unit = db.Column(db.Text) # type?
+ # Note: rank at time of incident
+ rank = db.Column(db.Enum(Rank))
+ star = db.Column(db.Text) # type?
+ role = db.Column(db.Text)
+ suspects = db.relationship(
+ "Officer", secondary=perpetrator_officer, backref="accusations")
+
+ def __repr__(self):
+ return f""
diff --git a/backend/database/models/source.py b/backend/database/models/source.py
index ffb4b87a..4c5641cf 100644
--- a/backend/database/models/source.py
+++ b/backend/database/models/source.py
@@ -2,8 +2,9 @@
class Source(db.Model, CrudMixin):
- id = db.Column(db.Text, primary_key=True)
- publication_name = db.Column(db.Text)
- publication_date = db.Column(db.Date)
- author = db.Column(db.Text)
- URL = db.Column(db.Text)
+ id = db.Column(db.String, primary_key=True)
+ name = db.Column(db.Text)
+ url = db.Column(db.Text)
+ contact_email = db.Column(db.Text)
+ reported_incidents = db.relationship(
+ 'Incident', backref='source', lazy="select")
diff --git a/backend/database/models/tag.py b/backend/database/models/tag.py
index ed0f5d48..dc71438c 100644
--- a/backend/database/models/tag.py
+++ b/backend/database/models/tag.py
@@ -3,7 +3,7 @@
class Tag(db.Model):
id = db.Column(db.Integer, primary_key=True)
- incident_id = db.Column(
- db.Integer, db.ForeignKey("incident.id"), nullable=False
- )
term = db.Column(db.Text)
+
+ def __repr__(self):
+ return f""
diff --git a/backend/database/models/victim.py b/backend/database/models/victim.py
index 8e4a3ba3..3f7ae785 100644
--- a/backend/database/models/victim.py
+++ b/backend/database/models/victim.py
@@ -6,8 +6,9 @@ class Victim(db.Model):
incident_id = db.Column(db.Integer, db.ForeignKey("incident.id"))
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) # TODO: add "estimated"?
+ age = db.Column(db.Integer)
manner_of_injury = db.Column(db.Text) # TODO: is an enum
injury_description = db.Column(db.Text)
injury_condition = db.Column(db.Text)
diff --git a/backend/schemas.py b/backend/schemas.py
index aaa62d68..190cc53e 100644
--- a/backend/schemas.py
+++ b/backend/schemas.py
@@ -10,11 +10,14 @@
from .database import User
from .database.models.action import Action
-from .database.models.incident import Description, Incident
+from .database.models.source import Source
+from .database.models.incident import Incident, SourceDetails
+from .database.models.agency import Agency
+from .database.models.officer import Officer
from .database.models.investigation import Investigation
from .database.models.legal_case import LegalCase
-from .database.models.multimedia import Multimedia
-from .database.models.officer import Officer
+from .database.models.attachment import Attachment
+from .database.models.perpetrator import Perpetrator
from .database.models.participant import Participant
from .database.models.result_of_stop import ResultOfStop
from .database.models.tag import Tag
@@ -95,11 +98,10 @@ def validate(auth=True, **kwargs):
_incident_list_attrs = [
"victims",
- "officers",
- "descriptions",
+ "perpetrators",
"tags",
"participants",
- "multimedias",
+ "attachments",
"investigations",
"results_of_stop",
"actions",
@@ -127,13 +129,16 @@ def schema_create(model_type: DeclarativeMeta, **kwargs) -> ModelMetaclass:
return sqlalchemy_to_pydantic(model_type, exclude="id", **kwargs)
+CreateSourceSchema = schema_create(Source)
_BaseCreateIncidentSchema = schema_create(Incident)
-CreateVictimSchema = schema_create(Victim)
CreateOfficerSchema = schema_create(Officer)
-CreateDescriptionSchema = schema_create(Description)
+CreateAgencySchema = schema_create(Agency)
+CreateVictimSchema = schema_create(Victim)
+CreatePerpetratorSchema = schema_create(Perpetrator)
+CreateSourceDetailsSchema = schema_create(SourceDetails)
CreateTagSchema = schema_create(Tag)
CreateParticipantSchema = schema_create(Participant)
-CreateMultimediaSchema = schema_create(Multimedia)
+CreateAttachmentSchema = schema_create(Attachment)
CreateInvestigationSchema = schema_create(Investigation)
CreateResultOfStopSchema = schema_create(ResultOfStop)
CreateActionSchema = schema_create(Action)
@@ -143,10 +148,10 @@ def schema_create(model_type: DeclarativeMeta, **kwargs) -> ModelMetaclass:
class CreateIncidentSchema(_BaseCreateIncidentSchema, _IncidentMixin):
victims: Optional[List[CreateVictimSchema]]
- officers: Optional[List[CreateOfficerSchema]]
+ perpetrators: Optional[List[CreatePerpetratorSchema]]
tags: Optional[List[CreateTagSchema]]
participants: Optional[List[CreateParticipantSchema]]
- multimedias: Optional[List[CreateMultimediaSchema]]
+ attachments: Optional[List[CreateAttachmentSchema]]
investigations: Optional[List[CreateInvestigationSchema]]
results_of_stop: Optional[List[CreateResultOfStopSchema]]
actions: Optional[List[CreateActionSchema]]
@@ -160,11 +165,10 @@ def schema_get(model_type: DeclarativeMeta, **kwargs) -> ModelMetaclass:
_BaseIncidentSchema = schema_get(Incident)
VictimSchema = schema_get(Victim)
-OfficerSchema = schema_get(Officer)
-DescriptionSchema = schema_get(Description)
+PerpetratorSchema = schema_get(Perpetrator)
TagSchema = schema_get(Tag)
ParticipantSchema = schema_get(Participant)
-MultimediaSchema = schema_get(Multimedia)
+AttachmentSchema = schema_get(Attachment)
InvestigationSchema = schema_get(Investigation)
ResultOfStopSchema = schema_get(ResultOfStop)
ActionSchema = schema_get(Action)
@@ -174,10 +178,10 @@ def schema_get(model_type: DeclarativeMeta, **kwargs) -> ModelMetaclass:
class IncidentSchema(_BaseIncidentSchema, _IncidentMixin):
victims: List[VictimSchema]
- officers: List[OfficerSchema]
+ perpetrators: List[PerpetratorSchema]
tags: List[TagSchema]
participants: List[ParticipantSchema]
- multimedias: List[MultimediaSchema]
+ attachments: List[AttachmentSchema]
investigations: List[InvestigationSchema]
results_of_stop: List[ResultOfStopSchema]
actions: List[ActionSchema]
@@ -198,7 +202,7 @@ def incident_to_orm(incident: CreateIncidentSchema) -> Incident:
associated with a schema instance.
"""
- converters = {"officers": Officer, "use_of_force": UseOfForce}
+ converters = {"perpetrators": Perpetrator, "use_of_force": UseOfForce}
orm_attrs = incident.dict()
for k, v in orm_attrs.items():
is_dict = isinstance(v, dict)
@@ -217,7 +221,6 @@ def incident_orm_to_json(incident: Incident) -> dict:
exclude={
"actions",
"investigations",
- "multimedias",
"legal_case",
"participants",
"results_of_stop",
diff --git a/backend/scraper/configs.yaml b/backend/scraper/configs.yaml
index 2050a464..2f6d3b99 100644
--- a/backend/scraper/configs.yaml
+++ b/backend/scraper/configs.yaml
@@ -45,7 +45,6 @@ tables:
- death_location_state
- description
- perpetrator
- - department_present
optional:
- incident_type
victim:
diff --git a/backend/scraper/data_scrapers/load_full_database.py b/backend/scraper/data_scrapers/load_full_database.py
index bfa9d937..4f9a8d76 100644
--- a/backend/scraper/data_scrapers/load_full_database.py
+++ b/backend/scraper/data_scrapers/load_full_database.py
@@ -4,7 +4,7 @@
from dateutil import parser
from ...api import db
-from ...database import UseOfForce, Incident, Officer
+from ...database import UseOfForce, Incident, Perpetrator
def isnan(x):
@@ -69,8 +69,8 @@ def row_to_orm(row):
if row.department_present:
incident.department = row.department_present
if row.perpetrator:
- incident.officers = [
- Officer(
+ incident.perpetrators = [
+ Perpetrator(
last_name=row.perpetrator,
)
]
diff --git a/backend/tests/test_incidents.py b/backend/tests/test_incidents.py
index 49825b9a..60b789ab 100644
--- a/backend/tests/test_incidents.py
+++ b/backend/tests/test_incidents.py
@@ -3,52 +3,52 @@
import pytest
from backend.database import Incident, Source
+mock_sources = {
+ "cpdp": {"name": "Citizens Police Data Project"},
+ "mpv": {"name": "Mapping Police Violence"},
+}
+
mock_incidents = {
"domestic": {
"time_of_incident": "2021-03-14 01:05:09",
"description": "Domestic disturbance",
- "officers": [
+ "perpetrators": [
{"first_name": "Susie", "last_name": "Suserson"},
{"first_name": "Lisa", "last_name": "Wong"},
],
"use_of_force": [{"item": "Injurious restraint"}],
- "source": "cpdp",
+ "source": "Citizens Police Data Project",
"location": "123 Right St Chicago, IL",
},
"traffic": {
"time_of_incident": "2021-10-01 00:00:00",
"description": "Traffic stop",
- "officers": [
+ "perpetrators": [
{"first_name": "Ronda", "last_name": "Sousa"},
],
"use_of_force": [{"item": "verbalization"}],
- "source": "mpv",
+ "source": "Mapping Police Violence",
"location": "Park St and Boylston Boston",
},
"firearm": {
"time_of_incident": "2021-10-05 00:00:00",
"description": "Robbery",
- "officers": [
+ "perpetrators": [
{"first_name": "Dale", "last_name": "Green"},
],
"use_of_force": [{"item": "indirect firearm"}],
- "source": "cpdp",
+ "source": "Citizens Police Data Project",
"location": "CHICAGO ILLINOIS",
},
"missing_fields": {
"description": "Robbery",
- "officers": [
+ "perpetrators": [
{"first_name": "Dale", "last_name": "Green"},
],
- "source": "cpdp",
+ "source": "Citizens Police Data Project",
},
}
-mock_sources = {
- "cpdp": {"publication_name": "chicago police data project"},
- "mpv": {"publication_name": "Mapping Police Violence"},
-}
-
@pytest.fixture
def example_incidents(db_session, client, access_token):
@@ -69,7 +69,7 @@ def example_incidents(db_session, client, access_token):
def test_create_incident(db_session, example_incidents):
- expected = mock_incidents["domestic"]
+ # expected = mock_incidents["domestic"]
created = example_incidents["domestic"]
incident_obj = (
@@ -78,9 +78,10 @@ def test_create_incident(db_session, example_incidents):
assert incident_obj.time_of_incident == datetime(2021, 3, 14, 1, 5, 9)
for i in [0, 1]:
- assert incident_obj.officers[i].id == created["officers"][i]["id"]
+ assert incident_obj.perpetrators[i].id == \
+ created["perpetrators"][i]["id"]
assert incident_obj.use_of_force[0].id == created["use_of_force"][0]["id"]
- assert incident_obj.source == expected["source"]
+ # assert incident_obj.source == expected["source"]
def test_get_incident(app, client, db_session, access_token):
diff --git a/frontend/compositions/incident-view/incident-view-body/incident-data/index.tsx b/frontend/compositions/incident-view/incident-view-body/incident-data/index.tsx
index b8d96a42..9771f73b 100644
--- a/frontend/compositions/incident-view/incident-view-body/incident-data/index.tsx
+++ b/frontend/compositions/incident-view/incident-view-body/incident-data/index.tsx
@@ -13,7 +13,7 @@ export default function IncidentData(incident: Incident) {
{description}
-
Departments Involved:
+
Agencies Involved:
{/* TODO: Display badge numbers and departments once api model is updated.
{incident.officers.map((officer) => (
diff --git a/frontend/compositions/officer-view/officer-affiliations/index.tsx b/frontend/compositions/officer-view/officer-affiliations/index.tsx
index c1b00acc..46df2295 100644
--- a/frontend/compositions/officer-view/officer-affiliations/index.tsx
+++ b/frontend/compositions/officer-view/officer-affiliations/index.tsx
@@ -5,12 +5,9 @@ export default function OfficerAffiliations(officer: OfficerRecordType) {
const { affiliations } = officer
const { wrapper } = styles
- const affiliationsString = affiliations.join(", ")
-
return (
Professional Affiliations:
-
{affiliationsString}
)
}
diff --git a/frontend/compositions/officer-view/officer-view-header/index.tsx b/frontend/compositions/officer-view/officer-view-header/index.tsx
index 765dcd67..92dbf4fa 100644
--- a/frontend/compositions/officer-view/officer-view-header/index.tsx
+++ b/frontend/compositions/officer-view/officer-view-header/index.tsx
@@ -2,7 +2,7 @@ import { OfficerRecordType } from "../../../models/officer"
import styles from "./officer-view-header.module.css"
export default function OfficerHeader(officer: OfficerRecordType) {
- const { firstName, lastName, badgeNo, status, department } = officer
+ const { firstName, lastName, knownEmployers } = officer
const { category, name, titleAndName, otherData, viewWrapper } = styles
return (
@@ -15,16 +15,7 @@ export default function OfficerHeader(officer: OfficerRecordType) {
-
Badge Number
-
{badgeNo}
-
-
-
Officer Status
-
{status}
-
-
-
Department
-
{department}
+
Known Employers
diff --git a/frontend/compositions/officer-view/officer-work-history/work-history-instance/index.tsx b/frontend/compositions/officer-view/officer-work-history/work-history-instance/index.tsx
index 6103525f..aef63730 100644
--- a/frontend/compositions/officer-view/officer-work-history/work-history-instance/index.tsx
+++ b/frontend/compositions/officer-view/officer-work-history/work-history-instance/index.tsx
@@ -3,16 +3,16 @@ import styles from "./work-history-instance.module.css"
import Image from "next/image"
export default function WorkHistoryInstance(pastWorkplace: EmploymentType) {
- const { department, status, startDate, endDate } = pastWorkplace
- const { departmentName, deptImage, deptAddress, webAddress } = department
- const startDateString = new Date(startDate).toLocaleDateString().split(",")[0]
- const endDateString = new Date(endDate).toLocaleDateString().split(",")[0]
+ const { agency, currentlyEmployed, earliestEmployment, latestEmployment } = pastWorkplace
+ const { agencyName, agencyImage, agencyHqAddress, websiteUrl } = agency
+ const startDateString = new Date(earliestEmployment).toLocaleDateString().split(",")[0]
+ const endDateString = new Date(latestEmployment).toLocaleDateString().split(",")[0]
const { patch, wrapper, dates } = styles
return (
-
+
{status}
@@ -20,9 +20,9 @@ export default function WorkHistoryInstance(pastWorkplace: EmploymentType) {
{startDateString} - {endDateString}
-
{departmentName}
+
{agencyName}
{/*TODO: Get Phone number from officer data, mock data currently does not have*/}
-
(123) 456-7890 * {deptAddress}
+
(123) 456-7890 * {agencyHqAddress}
)
diff --git a/frontend/compositions/officer-view/optional-officer-info/index.tsx b/frontend/compositions/officer-view/optional-officer-info/index.tsx
index c63d2668..26fa1e8c 100644
--- a/frontend/compositions/officer-view/optional-officer-info/index.tsx
+++ b/frontend/compositions/officer-view/optional-officer-info/index.tsx
@@ -2,10 +2,10 @@ import { OfficerRecordType } from "../../../models/officer"
import styles from "./optional-officer-info.module.css"
export default function OptionalOfficerInfo(officer: OfficerRecordType) {
- const { birthDate, gender, race, ethnicity, incomeBracket } = officer
+ const { dateOfBirth, gender, race } = officer
const { category, data, viewWrapper } = styles
- const dateString: string = new Date(birthDate).toLocaleDateString().split(",")[0]
+ const dateString: string = new Date(dateOfBirth).toLocaleDateString().split(",")[0]
return (
@@ -21,14 +21,6 @@ export default function OptionalOfficerInfo(officer: OfficerRecordType) {
Race
{race}
-
-
Ethnicity
-
{ethnicity}
-
-
-
Income Bracket
-
{incomeBracket}
-
)
diff --git a/frontend/compositions/profile-saved-tables/saved-results.tsx b/frontend/compositions/profile-saved-tables/saved-results.tsx
index 752a8528..ccd47fb1 100644
--- a/frontend/compositions/profile-saved-tables/saved-results.tsx
+++ b/frontend/compositions/profile-saved-tables/saved-results.tsx
@@ -1,19 +1,19 @@
import React from "react"
import { Column } from "react-table"
import { useSearch } from "../../helpers"
-import { Officer } from "../../helpers/api"
+import { Perpetrator } from "../../helpers/api"
import { DataTable } from "../../shared-components/data-table/data-table"
export const savedResultsColumns: Column[] = [
{
- Header: "Officer(s)",
+ Header: "Perpetrator(s)",
accessor: (row: any) =>
- row["officers"].map((names: Officer) => Object.values(names).join(", ")).join(", "),
- id: "officers"
+ row["perpetrators"].map((names: Perpetrator) => Object.values(names).join(", ")).join(", "),
+ id: "perpetrators"
},
{
- Header: "Department",
- accessor: "department"
+ Header: "Agency",
+ accessor: "agency"
},
{
Header: "Use of Force",
diff --git a/frontend/compositions/profile-saved-tables/saved-searches.tsx b/frontend/compositions/profile-saved-tables/saved-searches.tsx
index ec0cb381..1bced6cf 100644
--- a/frontend/compositions/profile-saved-tables/saved-searches.tsx
+++ b/frontend/compositions/profile-saved-tables/saved-searches.tsx
@@ -1,7 +1,7 @@
import React from "react"
import { Column } from "react-table"
import { useSearch } from "../../helpers"
-import { Officer } from "../../helpers/api"
+import { Perpetrator } from "../../helpers/api"
import { formatDate } from "../../helpers/syntax-helper"
import { TooltipIcons, TooltipTypes } from "../../models"
import { InfoTooltip } from "../../shared-components"
@@ -21,14 +21,14 @@ export const searchesColumns: Column[] = [
id: "time_of_incident"
},
{
- Header: "Officer(s)",
+ Header: "Perpetrator(s)",
accessor: (row: any) =>
- row["officers"].map((names: Officer) => Object.values(names).join(", ")).join(", "),
- id: "officers"
+ row["perpetrators"].map((names: Perpetrator) => Object.values(names).join(", ")).join(", "),
+ id: "perpetrators"
},
{
- Header: "Department",
- accessor: "department"
+ Header: "Agency",
+ accessor: "agency"
},
{
Header: () => (
diff --git a/frontend/compositions/search-results/search-results.tsx b/frontend/compositions/search-results/search-results.tsx
index 389143a7..9e9d2f69 100644
--- a/frontend/compositions/search-results/search-results.tsx
+++ b/frontend/compositions/search-results/search-results.tsx
@@ -3,7 +3,7 @@ import { faSlidersH } from "@fortawesome/free-solid-svg-icons"
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"
import { useSearch } from "../../helpers"
-import { Officer } from "../../helpers/api"
+import { Perpetrator } from "../../helpers/api"
import { formatDate } from "../../helpers/syntax-helper"
import { TooltipIcons, TooltipTypes } from "../../models"
import { InfoTooltip } from "../../shared-components"
@@ -38,11 +38,11 @@ export const resultsColumns: Column[] = [
id: "searchDate"
},
{
- Header: "Officer(s) Involved",
+ Header: "Perpetrator(s) Involved",
accessor: (row: any) =>
- row["officers"].map((names: Officer) => Object.values(names).join(", ")).join(", "),
+ row["perpetrators"].map((names: Perpetrator) => Object.values(names).join(", ")).join(", "),
filter: "text",
- id: "officers"
+ id: "perpetrators"
},
{
Header: () => (
diff --git a/frontend/helpers/api/api.ts b/frontend/helpers/api/api.ts
index 081022c7..b32f5faa 100644
--- a/frontend/helpers/api/api.ts
+++ b/frontend/helpers/api/api.ts
@@ -38,6 +38,18 @@ export interface ResetPasswordResponse {
message: string
}
+export interface Source {
+ name?: string
+ id?: number
+ url?: string
+ contact_email?: string
+}
+
+export interface Perpetrator {
+ first_name?: string
+ last_name?: string
+}
+
export interface Officer {
first_name?: string
last_name?: string
@@ -48,16 +60,17 @@ export interface UseOfForce {
}
export interface Incident {
id: number
+ source?: Source
+ source_id?: number
location?: string
locationLonLat?: [number, number] //TODO: Backend data does not return locationLonLat attribute. Remove this and refactor frontend
latitude?: number
longitude?: number
time_of_incident?: string
department?: string
- officers: Officer[]
+ perpetrators: Perpetrator[]
description?: string
use_of_force?: UseOfForce[]
- source?: string
}
interface AuthenticatedRequest {
diff --git a/frontend/helpers/api/mocks/data.ts b/frontend/helpers/api/mocks/data.ts
index 7c4fe908..f40ec38d 100644
--- a/frontend/helpers/api/mocks/data.ts
+++ b/frontend/helpers/api/mocks/data.ts
@@ -40,13 +40,12 @@ function createTestIncident(
description: `Test description ${key}`,
location: `Test location ${key}`,
locationLonLat: [lonlat[0], lonlat[1]],
- officers: [
+ perpetrators: [
{
first_name: `TestFirstName ${key}`,
last_name: `TestLastName ${key}`
}
],
- source: "mpv",
time_of_incident: new Date(date).toUTCString(),
use_of_force: [
{
diff --git a/frontend/helpers/mock-to-officer-type.ts b/frontend/helpers/mock-to-officer-type.ts
index fcb88ed3..cd7dc0b0 100644
--- a/frontend/helpers/mock-to-officer-type.ts
+++ b/frontend/helpers/mock-to-officer-type.ts
@@ -1,4 +1,5 @@
import { OfficerRecordType, EmploymentType } from "../models/officer"
+import { PerpetratorRecordType } from "../models/perpetrator"
import { Incident, Officer, UseOfForce } from "../helpers/api"
import officers from "../models/mock-data/officer.json"
@@ -8,71 +9,47 @@ export function getOfficerFromMockData(officerId: number) {
}
}
-export function mockToOfficerType(officer: typeof officers[0]): OfficerRecordType {
+export function getPerpetratorFromMockData(perpetratorId: number) {
+ if (perpetratorId >= 0 && perpetratorId < 100) {
+ return mockToPerpetratorType(officers[perpetratorId])
+ }
+}
+
+export function mockToOfficerType(officer: (typeof officers)[0]): OfficerRecordType {
function mockToWorkHistoryType(workHistory: typeof officer.workHistory): EmploymentType[] {
const converted: EmploymentType[] = workHistory.map((item) => {
return {
- department: {
- departmentName: item.deptName,
- deptImage: item.deptImage.replace("./frontend/models/mock-data/dept-images", ""),
- deptAddress: item.deptAddress,
- webAddress: "https://www.google.com/search?q=police+department"
+ agency: {
+ agencyName: item.deptName,
+ agencyImage: item.deptImage.replace("./frontend/models/mock-data/dept-images", ""),
+ agencyHqAddress: item.deptAddress,
+ websiteUrl: "https://www.google.com/search?q=police+department"
},
- status: item.status,
- startDate: new Date(item.dates.split("-")[0].trim()),
- endDate: new Date(item.dates.split("-")[1].trim())
+ currentlyEmployed: true,
+ earliestEmployment: new Date(item.dates.split("-")[0].trim()),
+ latestEmployment: new Date(item.dates.split("-")[1].trim())
}
})
return converted
}
- function mockToIncidentType(incidents: typeof officer.incidents): Incident[] {
- const officerNames = incidents[0].officers
- function mockToOfficerNameType(names: typeof officerNames): Officer[] {
- const converted: Officer[] = names.map((item) => {
- return {
- first_name: item.split(".")[0] + ".",
- last_name: item.split(".")[1]
- }
- })
- return converted
- }
-
- const usesOfForce = incidents[0].useOfForce
- function mockToForceType(forces: typeof usesOfForce): UseOfForce[] {
- const converted: UseOfForce[] = forces.map((force) => {
- return {
- item: force
- }
- })
- return converted
- }
-
- const converted: Incident[] = incidents.map((incident) => {
- return {
- ...incident,
- id: incident.id,
- officers: mockToOfficerNameType(incident.officers),
- use_of_force: mockToForceType(incident.useOfForce)
- }
- })
- return converted
+ return {
+ recordId: officer.id,
+ firstName: officer.firstName,
+ lastName: officer.lastName,
+ dateOfBirth: new Date(officer.birthDate),
+ gender: officer.gender,
+ race: officer.race,
+ workHistory: mockToWorkHistoryType(officer.workHistory)
}
+}
+export function mockToPerpetratorType(officer: (typeof officers)[0]): PerpetratorRecordType {
return {
recordId: officer.id,
firstName: officer.firstName,
lastName: officer.lastName,
- badgeNo: officer.badgeNo,
- status: officer.status,
- department: officer.department,
- birthDate: new Date(officer.birthDate),
gender: officer.gender,
- race: officer.race,
- ethnicity: officer.ethnicity,
- incomeBracket: officer.incomeBracket,
- workHistory: mockToWorkHistoryType(officer.workHistory),
- affiliations: officer.affiliations,
- incidents: mockToIncidentType(officer.incidents)
+ race: officer.race
}
}
diff --git a/frontend/models/officer.ts b/frontend/models/officer.ts
index bf86c7ba..aa2bde14 100644
--- a/frontend/models/officer.ts
+++ b/frontend/models/officer.ts
@@ -1,51 +1,49 @@
-import { Incident } from "../helpers/api"
+import { Incident, Perpetrator } from "../helpers/api"
-export interface DepartmentType {
- departmentName: string
- deptImage: string
- deptAddress: string
- webAddress: string
+export interface AgencyType {
+ agencyName: string
+ agencyImage: string
+ agencyHqAddress: string
+ websiteUrl: string
}
-export const departmentColumns = [
+export const agencyColumns = [
{
- Header: "Department",
- accessor: "dept"
+ Header: "agency",
+ accessor: "agency"
},
{
Header: "Address",
- accessor: "deptAddress"
+ accessor: "agencyHqAddress"
},
{
Header: "Website",
- accessor: "webAddress"
+ accessor: "websiteUrl"
},
{
- Header: "Dept Address",
- accessor: "deptAddress"
+ Header: "Agency Address",
+ accessor: "agencyHqAddress"
}
]
export interface EmploymentType {
- department: DepartmentType
- status: string
- startDate: Date
- endDate: Date
+ agency: AgencyType
+ currentlyEmployed: boolean
+ earliestEmployment: Date
+ latestEmployment: Date
+ badgeNumber?: string
}
export interface OfficerRecordType {
recordId: number
firstName: string
lastName: string
- badgeNo: string
- status: string
- department: string
- birthDate?: Date
+ dateOfBirth?: Date
gender?: string
race?: string
ethnicity?: string
- incomeBracket?: string
- workHistory: EmploymentType[]
- affiliations?: string[]
- incidents?: Incident[]
+ knownEmployers?: AgencyType[]
+ workHistory?: EmploymentType[]
+ accusations?: Perpetrator[]
+ affiliations?: OfficerRecordType[]
}
diff --git a/frontend/models/perpetrator.ts b/frontend/models/perpetrator.ts
new file mode 100644
index 00000000..431856b9
--- /dev/null
+++ b/frontend/models/perpetrator.ts
@@ -0,0 +1,39 @@
+import { Incident } from "../helpers/api"
+
+export interface AgencyType {
+ agencyName: string
+ agencyImage: string
+ agencyHqAddress: string
+ websiteUrl: string
+}
+
+export const departmentColumns = [
+ {
+ Header: "Department",
+ accessor: "dept"
+ },
+ {
+ Header: "Address",
+ accessor: "deptAddress"
+ },
+ {
+ Header: "Website",
+ accessor: "webAddress"
+ },
+ {
+ Header: "Dept Address",
+ accessor: "deptAddress"
+ }
+]
+
+export interface PerpetratorRecordType {
+ recordId: number
+ firstName?: string
+ lastName?: string
+ badge?: string
+ rank?: string
+ gender?: string
+ race?: string
+ ethnicity?: string
+ incident?: Incident
+}
diff --git a/frontend/tests/snapshots/__snapshots__/incident.test.tsx.snap b/frontend/tests/snapshots/__snapshots__/incident.test.tsx.snap
index df5b5cbb..0878f619 100644
--- a/frontend/tests/snapshots/__snapshots__/incident.test.tsx.snap
+++ b/frontend/tests/snapshots/__snapshots__/incident.test.tsx.snap
@@ -68,7 +68,7 @@ Object {
- Departments Involved:
+ Agencies Involved:
- Departments Involved:
+ Agencies Involved:
- Badge Number
-
-
- 30420452
-
-
-
-
- Officer Status
-
-
- Chief
-
-
-
-
- Department
-
-
- Houston Police Department
+ Known Employers
@@ -102,30 +75,6 @@ Object {
Black or African American
-
-
- Ethnicity
-
-
- Hispanic or Latino
-
-
-
-
- Income Bracket
-
-
- Over $523,600
-
-
@@ -150,7 +99,7 @@ Object {
/>
- Captain
+
@@ -180,7 +129,7 @@ Object {
/>
- Sergeant
+
@@ -208,9 +157,6 @@ Object {
Professional Affiliations:
-
- Fraternal Order of Police, International Union of Police Associations, National Association of Police Organizations
-