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

Add occurred_at column #992

Closed
wants to merge 69 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
69 commits
Select commit Hold shift + click to select a range
5713521
Update database.py
michplunkett Jul 27, 2023
71bdb39
Create 2023-07-27-1848_9fa948bcea25_change_incident_date_and_time_to_.py
michplunkett Jul 27, 2023
d8b8b6f
Update 2023-07-27-1848_9fa948bcea25_change_incident_date_and_time_to_.py
michplunkett Jul 27, 2023
ac78495
Update views.py
michplunkett Jul 27, 2023
f65b4ec
Update incident_fields.html
michplunkett Jul 27, 2023
50e43f5
Update officer_incidents.html
michplunkett Jul 27, 2023
30fd480
Update incident_detail.html
michplunkett Jul 27, 2023
d513069
Update database_imports.py
michplunkett Jul 27, 2023
928d813
Update conftest.py
michplunkett Jul 27, 2023
242ffe4
Update forms.py
michplunkett Jul 27, 2023
0885c30
Update test_commands.py
michplunkett Jul 27, 2023
1d6d776
Update test_commands.py
michplunkett Jul 27, 2023
e06cf85
Update test_incidents.py
michplunkett Jul 27, 2023
81fa0ab
Update views.py
michplunkett Jul 27, 2023
285a254
Update database_imports.py
michplunkett Jul 27, 2023
1a24998
Update views.py
michplunkett Jul 27, 2023
a4ad73c
Update views.py
michplunkett Jul 27, 2023
5f5710c
Update views.py
michplunkett Jul 27, 2023
ed1c1f9
Update test_incidents.py
michplunkett Jul 27, 2023
de0936c
Update test_commands.py
michplunkett Jul 27, 2023
2424c0d
Update forms.py
michplunkett Jul 27, 2023
9c061dc
Update downloads.py
michplunkett Jul 27, 2023
55d949c
Update views.py
michplunkett Jul 27, 2023
893fb36
Update database_imports.py
michplunkett Jul 27, 2023
7856910
Update database_imports.py
michplunkett Jul 28, 2023
d7247ec
Update database_imports.py
michplunkett Jul 28, 2023
8c1ee4a
Update test_commands.py
michplunkett Jul 28, 2023
4c42df1
Update csv_imports.py
michplunkett Jul 28, 2023
213f94f
Update incidents.csv
michplunkett Jul 28, 2023
26bec47
Update database_imports.py
michplunkett Jul 28, 2023
23cebb5
Update route_helpers.py
michplunkett Jul 28, 2023
63024a5
Update advanced_csv_import.rst
michplunkett Jul 28, 2023
e606346
Update test_commands.py
michplunkett Jul 28, 2023
eeb9e56
Update test_incidents.py
michplunkett Jul 28, 2023
f1ec469
Merge branch 'develop' into incidents_to_timestamptz
michplunkett Jul 29, 2023
259dd0c
Merge branch 'develop' into incidents_to_timestamptz
michplunkett Jul 30, 2023
6179c89
Merge branch 'develop' into incidents_to_timestamptz
michplunkett Aug 1, 2023
7620a2b
Merge branch 'develop' into incidents_to_timestamptz
michplunkett Aug 1, 2023
91537e7
Merge branch 'develop' into incidents_to_timestamptz
michplunkett Aug 6, 2023
e2ef3d1
Update test_incidents.py
michplunkett Aug 6, 2023
9fb83a5
Update 2023-08-06-2316_9fa948bcea25_change_incident_date_and_time_to_.py
michplunkett Aug 7, 2023
4e5588a
Update general.py
michplunkett Aug 7, 2023
8d68b8f
Update views.py
michplunkett Aug 7, 2023
d12759d
Update test_commands.py
michplunkett Aug 7, 2023
d138618
Update database_imports.py
michplunkett Aug 7, 2023
1f84170
Update forms.py
michplunkett Aug 7, 2023
4502e28
Update test_commands.py
michplunkett Aug 7, 2023
b2c8b9d
Update general.py
michplunkett Aug 7, 2023
50deaaf
Update test_utils.py
michplunkett Aug 7, 2023
81b0cc1
Merge branch 'develop' into incidents_to_timestamptz
michplunkett Aug 7, 2023
39229d4
Delete 2023-08-06-2316_9fa948bcea25_change_incident_date_and_time_to_.py
michplunkett Aug 7, 2023
0d8f0bb
Update database.py
michplunkett Aug 7, 2023
593b0b9
Create 2023-08-07-2114_2b99be2696a9_add_occurred_at_to_incidents.py
michplunkett Aug 7, 2023
bb95a43
Update 2023-08-07-2114_2b99be2696a9_add_occurred_at_to_incidents.py
michplunkett Aug 7, 2023
47c8f3c
Update database.py
michplunkett Aug 7, 2023
662b36b
Update test_utils.py
michplunkett Aug 7, 2023
1b474b5
Merge branch 'develop' into incidents_to_timestamptz
michplunkett Aug 7, 2023
0be501e
Merge branch 'develop' into incidents_to_timestamptz
michplunkett Aug 7, 2023
5a2b0f2
Merge branch 'develop' into incidents_to_timestamptz
michplunkett Aug 8, 2023
db81b99
Update incident_detail.html
michplunkett Aug 8, 2023
fd516d0
Merge branch 'develop' into incidents_to_timestamptz
michplunkett Aug 12, 2023
8392601
Merge branch 'develop' into incidents_to_timestamptz
michplunkett Aug 12, 2023
ff6e82f
Merge branch 'develop' into incidents_to_timestamptz
michplunkett Aug 14, 2023
c68554f
Update 2023-08-07-2114_2b99be2696a9_add_occurred_at_to_incidents.py
michplunkett Aug 16, 2023
c59cca8
Update general.py
michplunkett Aug 16, 2023
592e37b
Update database_imports.py
michplunkett Aug 16, 2023
3f0550c
Merge branch 'develop' into incidents_to_timestamptz
michplunkett Aug 16, 2023
88b7d05
Merge branch 'develop' into incidents_to_timestamptz
michplunkett Aug 16, 2023
157bb3a
Update database.py
michplunkett Aug 16, 2023
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
3 changes: 1 addition & 2 deletions OpenOversight/app/csv_imports.py
Original file line number Diff line number Diff line change
Expand Up @@ -364,9 +364,8 @@ def _handle_incidents_csv(
with _csv_reader(incidents_csv) as csv_reader:
_check_provided_fields(
csv_reader,
required_fields=["id", "department_name", "department_state"],
required_fields=["id", "department_name", "department_state", "date"],
optional_fields=[
"date",
"time",
"report_number",
"description",
Expand Down
3 changes: 1 addition & 2 deletions OpenOversight/app/main/downloads.py
Original file line number Diff line number Diff line change
Expand Up @@ -132,8 +132,7 @@ def incidents_record_maker(incident: Incident) -> _Record:
return {
"id": incident.id,
"report_num": incident.report_number,
"date": incident.date,
"time": incident.time,
"occurred_at": incident.occurred_at,
"description": incident.description,
"location": incident.address,
"licenses": " ".join(map(str, incident.license_plates)),
Expand Down
27 changes: 15 additions & 12 deletions OpenOversight/app/main/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,7 @@
allowed_file,
get_or_create,
get_random_image,
get_utc_datetime,
replace_list,
serve_image,
validate_redirect_url,
Expand Down Expand Up @@ -1664,8 +1665,7 @@ def download_incidents_csv(department_id: int):
field_names = [
"id",
"report_num",
"date",
"time",
"occurred_at",
"description",
"location",
"licenses",
Expand Down Expand Up @@ -1938,7 +1938,7 @@ def server_shutdown(): # pragma: no cover
class IncidentApi(ModelView):
model = Incident
model_name = "incident"
order_by = "date"
order_by = "occurred_at"
descending = True
form = IncidentForm
create_function = create_incident
Expand Down Expand Up @@ -1970,12 +1970,12 @@ def get(self, obj_id: int):
if occurred_before := request.args.get("occurred_before"):
before_date = datetime.datetime.strptime(occurred_before, "%Y-%m-%d").date()
form.occurred_before.data = before_date
incidents = incidents.filter(self.model.date < before_date)
incidents = incidents.filter(self.model.occurred_at < before_date)

if occurred_after := request.args.get("occurred_after"):
after_date = datetime.datetime.strptime(occurred_after, "%Y-%m-%d").date()
form.occurred_after.data = after_date
incidents = incidents.filter(self.model.date > after_date)
incidents = incidents.filter(self.model.occurred_at > after_date)

incidents = incidents.order_by(
getattr(self.model, self.order_by).desc()
Expand Down Expand Up @@ -2037,10 +2037,10 @@ def get_edit_form(self, obj: Incident):
form.license_plates.min_entries = no_license_plates
form.links.min_entries = no_links
form.officers.min_entries = no_officers
if not form.date_field.data and obj.date:
form.date_field.data = obj.date
if not form.time_field.data and obj.time:
form.time_field.data = obj.time
if not form.date_field.data and obj.occurred_at:
form.date_field.data = obj.occurred_at.date()
if not form.time_field.data and obj.occurred_at:
form.time_field.data = obj.occurred_at.time()
return form

def populate_obj(self, form: FlaskForm, obj: Incident):
Expand Down Expand Up @@ -2078,11 +2078,14 @@ def populate_obj(self, form: FlaskForm, obj: Incident):
if license_plates and license_plates[0]["number"]:
replace_list(license_plates, obj, "license_plates", LicensePlate, db)

obj.date = form.date_field.data
if form.time_field.raw_data and form.time_field.raw_data != [""]:
obj.time = form.time_field.data
obj.occurred_at = get_utc_datetime(
datetime.datetime.combine(form.date_field.data, form.time_field.data)
)
else:
obj.time = None
obj.occurred_at = get_utc_datetime(
datetime.datetime.combine(form.date_field.data, datetime.time(0, 0))
)
super(IncidentApi, self).populate_obj(form, obj)


Expand Down
3 changes: 3 additions & 0 deletions OpenOversight/app/models/database.py
Original file line number Diff line number Diff line change
Expand Up @@ -637,6 +637,9 @@ class Incident(BaseModel):
id = db.Column(db.Integer, primary_key=True)
date = db.Column(db.Date, unique=False, index=True)
time = db.Column(db.Time, unique=False, index=True)
occurred_at = db.Column(
db.DateTime(timezone=True), unique=False, nullable=True, index=True
)
report_number = db.Column(db.String(50), index=True)
description = db.Column(db.Text(), nullable=True)
address_id = db.Column(db.Integer, db.ForeignKey("locations.id"))
Expand Down
47 changes: 34 additions & 13 deletions OpenOversight/app/models/database_imports.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import datetime
from datetime import date, datetime, time
from typing import Any, Dict, Optional, Sequence, Tuple, Union

import dateutil.parser
Expand All @@ -21,7 +21,7 @@
RACE_CHOICES,
SUFFIX_CHOICES,
)
from OpenOversight.app.utils.general import get_or_create, str_is_true
from OpenOversight.app.utils.general import get_or_create, get_utc_datetime, str_is_true
from OpenOversight.app.validators import state_validator, url_validator


Expand All @@ -36,16 +36,22 @@ def validate_choice(
return None


def parse_date(date_str: Optional[str]) -> Optional[datetime.date]:
def parse_date(date_str: Optional[str]) -> date:
if date_str:
return dateutil.parser.parse(date_str).date()
return None
return datetime.now().date()


def parse_date_time(date_time_str: str) -> datetime:
if date_time_str:
return datetime.combine(parse_date(date_time_str), parse_time(date_time_str))
return datetime.now()


def parse_time(time_str: Optional[str]) -> Optional[datetime.time]:
def parse_time(time_str: str) -> time:
if time_str:
return dateutil.parser.parse(time_str).time()
return None
return datetime.now().time()


def parse_int(value: Optional[Union[str, int]]) -> Optional[int]:
Expand Down Expand Up @@ -276,15 +282,23 @@ def get_or_create_location_from_dict(

def create_incident_from_dict(data: Dict[str, Any], force_id: bool = False) -> Incident:
incident = Incident(
date=parse_date(data.get("date")),
time=parse_time(data.get("time")),
occurred_at=get_utc_datetime(
parse_date_time(
" ".join(
[
data.get("date", datetime.now().date().strftime("%x")),
data.get("time", "00:00"),
]
)
)
),
report_number=parse_str(data.get("report_number"), None),
description=parse_str(data.get("description"), None),
address_id=data.get("address_id"),
department_id=parse_int(data.get("department_id")),
created_by=parse_int(data.get("created_by")),
last_updated_by=parse_int(data.get("last_updated_by")),
last_updated_at=datetime.datetime.now(),
last_updated_at=datetime.now(),
)

incident.officers = data.get("officers", [])
Expand All @@ -300,9 +314,16 @@ def create_incident_from_dict(data: Dict[str, Any], force_id: bool = False) -> I

def update_incident_from_dict(data: Dict[str, Any], incident: Incident) -> Incident:
if "date" in data:
incident.date = parse_date(data.get("date"))
if "time" in data:
incident.time = parse_time(data.get("time"))
incident.occurred_at = get_utc_datetime(
parse_date_time(
" ".join(
[
data.get("date", datetime.now().date().strftime("%x")),
data.get("time", "00:00"),
]
)
)
)
if "report_number" in data:
incident.report_number = parse_str(data.get("report_number"), None)
if "description" in data:
Expand All @@ -315,7 +336,7 @@ def update_incident_from_dict(data: Dict[str, Any], incident: Incident) -> Incid
incident.created_by = parse_int(data.get("created_by"))
if "last_updated_by" in data:
incident.last_updated_by = parse_int(data.get("last_updated_by"))
incident.last_updated_at = datetime.datetime.now()
incident.last_updated_at = datetime.now()
if "officers" in data:
incident.officers = data["officers"] or []
if "license_plate_objects" in data:
Expand Down
16 changes: 7 additions & 9 deletions OpenOversight/app/templates/partials/incident_fields.html
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,14 @@
<td>
<strong>Date</strong>
</td>
<td>{{ incident.date.strftime("%b %d, %Y") }}</td>
<td>{{ incident.occurred_at | local_date }}</td>
</tr>
<tr>
<td>
<strong>Time</strong>
</td>
<td>{{ incident.occurred_at | local_time }}</td>
</tr>
{% if incident.time %}
<tr>
<td>
<strong>Time</strong>
</td>
<td>{{ incident.time.strftime("%l:%M %p") }}</td>
</tr>
{% endif %}
{% if incident.report_number %}
<tr>
<td>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ <h3>Incidents</h3>
{% if officer.incidents %}
<table class="table table-hover table-responsive">
<tbody>
{% for incident in officer.incidents | sort(attribute='date') | reverse %}
{% for incident in officer.incidents | sort(attribute='occurred_at') | reverse %}
{% if not loop.first %}
<tr class="border:none">
<td colspan="2">&nbsp;</td>
Expand Down
10 changes: 5 additions & 5 deletions OpenOversight/app/utils/forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
db,
)
from OpenOversight.app.utils.choices import GENDER_CHOICES, RACE_CHOICES
from OpenOversight.app.utils.general import get_or_create
from OpenOversight.app.utils.general import get_or_create, get_utc_datetime


def add_new_assignment(officer_id, form):
Expand Down Expand Up @@ -134,8 +134,9 @@ def create_description(self, form):

def create_incident(self, form):
fields = {
"date": form.date_field.data,
"time": form.time_field.data,
"occurred_at": get_utc_datetime(
datetime.datetime.combine(form.date_field.data, form.time_field.data)
),
"officers": [],
"license_plates": [],
"links": [],
Expand Down Expand Up @@ -171,8 +172,7 @@ def create_incident(self, form):
fields["links"].append(li)

return Incident(
date=fields["date"],
time=fields["time"],
occurred_at=fields["occurred_at"],
description=form.data["description"],
department=form.data["department"],
address=fields["address"],
Expand Down
14 changes: 10 additions & 4 deletions OpenOversight/app/utils/general.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import random
import sys
from datetime import datetime, timezone
from distutils.util import strtobool
from typing import Optional
from urllib.parse import urlparse
Expand All @@ -15,7 +16,7 @@ def ac_can_edit_officer(officer, ac):
return False


def allowed_file(filename):
def allowed_file(filename: str):
return (
"." in filename
and filename.rsplit(".", 1)[1].lower()
Expand Down Expand Up @@ -66,6 +67,11 @@ def get_random_image(image_query):
return None


def get_utc_datetime(dt: datetime) -> datetime:
"""Return the current datetime in UTC or the converted given datetime to UTC."""
return dt.replace(tzinfo=timezone.utc)


def merge_dicts(*dict_args):
"""
Given any number of dicts, shallow copy and merge into a new dict,
Expand All @@ -77,7 +83,7 @@ def merge_dicts(*dict_args):
return result


def normalize_gender(input_gender):
def normalize_gender(input_gender: str):
if input_gender is None:
return None
normalized_genders = {
Expand Down Expand Up @@ -142,8 +148,8 @@ def serve_image(filepath):
return url_for("static", filename=filepath.replace("static/", "").lstrip("/"))


def str_is_true(str_):
return strtobool(str_.lower())
def str_is_true(string: str):
return strtobool(string.lower())


def validate_redirect_url(url: Optional[str]) -> Optional[str]:
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
"""add occurred_at to incidents

Revision ID: 2b99be2696a9
Revises: b38c133bed3c
Create Date: 2023-08-07 21:14:31.711553

"""
import os

import sqlalchemy as sa
from alembic import op


# revision identifiers, used by Alembic.
revision = "2b99be2696a9"
down_revision = "b38c133bed3c"
branch_labels = None
depends_on = None


TIMEZONE = os.getenv("TIMEZONE", "America/Chicago")


def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table("incidents", schema=None) as batch_op:
batch_op.add_column(
sa.Column("occurred_at", sa.DateTime(timezone=True), nullable=True)
)
batch_op.create_index(
batch_op.f("ix_incidents_occurred_at"), ["occurred_at"], unique=False
)

op.execute(
f"""
UPDATE incidents
SET
occurred_at = (date::date || ' ' || time::timetz)::timestamp AT TIME ZONE '{TIMEZONE}',
time = NULL,
date = NULL
WHERE occurred_at IS NULL
AND time IS NOT NULL
AND date IS NOT NULL
"""
)

# ### end Alembic commands ###


def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table("incidents", schema=None) as batch_op:
batch_op.drop_index(batch_op.f("ix_incidents_occurred_at"))

op.execute(
f"""
UPDATE incidents
SET (date, time) = (occurred_at::date, (occurred_at::timestamptz AT TIME ZONE '{TIMEZONE}')::time)
WHERE occurred_at IS NOT NULL
"""
)

with op.batch_alter_table("incidents", schema=None) as batch_op:
batch_op.drop_column("occurred_at")

# ### end Alembic commands ###
Loading
Loading