diff --git a/OpenOversight/app/csv_imports.py b/OpenOversight/app/csv_imports.py index 8b365dcc1..2cff70213 100644 --- a/OpenOversight/app/csv_imports.py +++ b/OpenOversight/app/csv_imports.py @@ -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", diff --git a/OpenOversight/app/main/downloads.py b/OpenOversight/app/main/downloads.py index 8aead13ce..76e85c8e4 100644 --- a/OpenOversight/app/main/downloads.py +++ b/OpenOversight/app/main/downloads.py @@ -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)), diff --git a/OpenOversight/app/main/views.py b/OpenOversight/app/main/views.py index 97f49999c..c0b6589c5 100644 --- a/OpenOversight/app/main/views.py +++ b/OpenOversight/app/main/views.py @@ -116,6 +116,7 @@ allowed_file, get_or_create, get_random_image, + get_utc_datetime, replace_list, serve_image, validate_redirect_url, @@ -1664,8 +1665,7 @@ def download_incidents_csv(department_id: int): field_names = [ "id", "report_num", - "date", - "time", + "occurred_at", "description", "location", "licenses", @@ -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 @@ -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() @@ -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): @@ -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) diff --git a/OpenOversight/app/models/database.py b/OpenOversight/app/models/database.py index 084bdafcb..885d9eeb4 100644 --- a/OpenOversight/app/models/database.py +++ b/OpenOversight/app/models/database.py @@ -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")) diff --git a/OpenOversight/app/models/database_imports.py b/OpenOversight/app/models/database_imports.py index 7aeb57029..e18f8a2ef 100644 --- a/OpenOversight/app/models/database_imports.py +++ b/OpenOversight/app/models/database_imports.py @@ -1,4 +1,4 @@ -import datetime +from datetime import date, datetime, time from typing import Any, Dict, Optional, Sequence, Tuple, Union import dateutil.parser @@ -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 @@ -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]: @@ -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", []) @@ -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: @@ -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: diff --git a/OpenOversight/app/templates/partials/incident_fields.html b/OpenOversight/app/templates/partials/incident_fields.html index 9444f9000..152b099b8 100644 --- a/OpenOversight/app/templates/partials/incident_fields.html +++ b/OpenOversight/app/templates/partials/incident_fields.html @@ -2,16 +2,14 @@ Date - {{ incident.date.strftime("%b %d, %Y") }} + {{ incident.occurred_at | local_date }} + + + + Time + + {{ incident.occurred_at | local_time }} -{% if incident.time %} - - - Time - - {{ incident.time.strftime("%l:%M %p") }} - -{% endif %} {% if incident.report_number %} diff --git a/OpenOversight/app/templates/partials/officer_incidents.html b/OpenOversight/app/templates/partials/officer_incidents.html index f6a6f185f..4a79b5c6e 100644 --- a/OpenOversight/app/templates/partials/officer_incidents.html +++ b/OpenOversight/app/templates/partials/officer_incidents.html @@ -2,7 +2,7 @@

Incidents

{% if officer.incidents %} - {% for incident in officer.incidents | sort(attribute='date') | reverse %} + {% for incident in officer.incidents | sort(attribute='occurred_at') | reverse %} {% if not loop.first %} diff --git a/OpenOversight/app/utils/forms.py b/OpenOversight/app/utils/forms.py index 5f86226e8..d5dfc3ffe 100644 --- a/OpenOversight/app/utils/forms.py +++ b/OpenOversight/app/utils/forms.py @@ -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): @@ -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": [], @@ -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"], diff --git a/OpenOversight/app/utils/general.py b/OpenOversight/app/utils/general.py index 84ddcf44f..fb1d93f57 100644 --- a/OpenOversight/app/utils/general.py +++ b/OpenOversight/app/utils/general.py @@ -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 @@ -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() @@ -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, @@ -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 = { @@ -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]: diff --git a/OpenOversight/migrations/versions/2023-08-07-2114_2b99be2696a9_add_occurred_at_to_incidents.py b/OpenOversight/migrations/versions/2023-08-07-2114_2b99be2696a9_add_occurred_at_to_incidents.py new file mode 100644 index 000000000..f4241c23a --- /dev/null +++ b/OpenOversight/migrations/versions/2023-08-07-2114_2b99be2696a9_add_occurred_at_to_incidents.py @@ -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 ### diff --git a/OpenOversight/tests/conftest.py b/OpenOversight/tests/conftest.py index 948cb5b73..8d0bca51d 100644 --- a/OpenOversight/tests/conftest.py +++ b/OpenOversight/tests/conftest.py @@ -647,8 +647,7 @@ def add_mockdata(session): test_incidents = [ Incident( - date=datetime.date(2016, 3, 16), - time=datetime.time(4, 20), + occurred_at=datetime.datetime(2016, 3, 16, 4, 20), report_number="42", description="### A thing happened\n **Markup** description", department_id=1, @@ -661,8 +660,7 @@ def add_mockdata(session): last_updated_at=datetime.datetime.now(), ), Incident( - date=datetime.date(2017, 12, 11), - time=datetime.time(2, 40), + occurred_at=datetime.datetime(2017, 12, 11, 2, 40), report_number="38", description="A thing happened", department_id=2, @@ -675,7 +673,7 @@ def add_mockdata(session): last_updated_at=datetime.datetime.now(), ), Incident( - date=datetime.datetime(2019, 1, 15), + occurred_at=datetime.datetime(2019, 1, 15), report_number="39", description=( Path(__file__).parent / "description_overflow.txt" diff --git a/OpenOversight/tests/routes/test_incidents.py b/OpenOversight/tests/routes/test_incidents.py index cb587b7a5..147fc67a3 100644 --- a/OpenOversight/tests/routes/test_incidents.py +++ b/OpenOversight/tests/routes/test_incidents.py @@ -99,7 +99,7 @@ def test_admins_can_create_basic_incidents(report_number, mockdata, client, sess assert rv.status_code == HTTPStatus.OK assert "created" in rv.data.decode(ENCODING_UTF_8) - inc = Incident.query.filter_by(date=test_date.date()).first() + inc = Incident.query.filter_by(occurred_at=test_date).first() assert inc is not None @@ -200,8 +200,7 @@ def test_admins_can_edit_incident_date_and_address(mockdata, client, session): assert rv.status_code == HTTPStatus.OK assert "successfully updated" in rv.data.decode(ENCODING_UTF_8) updated = Incident.query.get(inc_id) - assert updated.date == test_date - assert updated.time == test_time + assert updated.occurred_at == datetime.combine(test_date, test_time) assert updated.address.street_name == street_name @@ -239,8 +238,8 @@ def test_admins_can_edit_incident_links_and_licenses(mockdata, client, session, ooid_forms = [OOIdForm(ooid=officer.id) for officer in inc.officers] form = IncidentForm( - date_field=str(inc.date), - time_field=str(inc.time), + date_field=str(inc.occurred_at.date()), + time_field=str(inc.occurred_at.time()), report_number=inc.report_number, description=inc.description, department="1", @@ -292,7 +291,7 @@ def test_admins_cannot_make_ancient_incidents(mockdata, client, session): form = IncidentForm( date_field=date(1899, 12, 5), - time_field=str(inc.time), + time_field=str(inc.occurred_at.time()), report_number=inc.report_number, description=inc.description, department="1", @@ -438,8 +437,8 @@ def test_admins_can_edit_incident_officers(mockdata, client, session): new_ooid_form = OOIdForm(oo_id=new_officer.id) form = IncidentForm( - date_field=str(inc.date), - time_field=str(inc.time), + date_field=str(inc.occurred_at.date()), + time_field=str(inc.occurred_at.time()), report_number=inc.report_number, description=inc.description, department="1", @@ -498,8 +497,8 @@ def test_admins_cannot_edit_nonexisting_officers(mockdata, client, session): new_ooid_form = OOIdForm(oo_id="99999999999999999") form = IncidentForm( - date_field=str(inc.date), - time_field=str(inc.time), + date_field=str(inc.occurred_at.date()), + time_field=str(inc.occurred_at.time()), report_number=inc.report_number, description=inc.description, department="1", @@ -567,8 +566,7 @@ def test_ac_can_edit_incidents_in_their_department(mockdata, client, session): ) assert rv.status_code == HTTPStatus.OK assert "successfully updated" in rv.data.decode(ENCODING_UTF_8) - assert inc.date == test_date.date() - assert inc.time == test_date.time() + assert inc.occurred_at == test_date assert inc.address.street_name == street_name @@ -735,7 +733,7 @@ def test_users_cannot_see_who_created_incidents(mockdata, client, session): assert "Creator" not in rv.data.decode(ENCODING_UTF_8) -def test_form_with_officer_id_prepopulates(mockdata, client, session): +def test_form_with_officer_id_pre_populate(mockdata, client, session): with current_app.test_request_context(): login_admin(client) officer_id = "1234" @@ -784,8 +782,8 @@ def test_admins_cannot_inject_unsafe_html(mockdata, client, session): ooid_forms = [OOIdForm(oo_id=the_id) for the_id in old_officer_ids] form = IncidentForm( - date_field=str(inc.date), - time_field=str(inc.time), + date_field=str(inc.occurred_at.date()), + time_field=str(inc.occurred_at.time()), report_number=inc.report_number, description="", department="1", diff --git a/OpenOversight/tests/test_commands.py b/OpenOversight/tests/test_commands.py index 8ad876368..2058d5a78 100644 --- a/OpenOversight/tests/test_commands.py +++ b/OpenOversight/tests/test_commands.py @@ -1,10 +1,10 @@ import csv -import datetime import operator import os import random import traceback import uuid +from datetime import date, datetime import pandas as pd import pytest @@ -856,7 +856,7 @@ def test_advanced_csv_import__success(session, department, test_csv_dir): id=77021, officer_id=officer.id, star_no="4567", - start_date=datetime.date(2020, 1, 1), + start_date=date(2020, 1, 1), job_id=department.jobs[0].id, created_by=user.id, ) @@ -877,7 +877,7 @@ def test_advanced_csv_import__success(session, department, test_csv_dir): report_number="Old_Report_Number", department_id=1, description="description", - time=datetime.time(23, 45, 16), + occurred_at=datetime(2020, 7, 26, 23, 45, 16), created_by=user.id, ) incident.officers = [officer] @@ -925,7 +925,7 @@ def test_advanced_csv_import__success(session, department, test_csv_dir): assert cop1.last_name == "Smith" assert cop1.gender == "M" assert cop1.race == "WHITE" - assert cop1.employment_date == datetime.date(2019, 7, 12) + assert cop1.employment_date == date(2019, 7, 12) assert cop1.birth_year == 1984 assert cop1.middle_initial == "O" assert cop1.suffix is None @@ -941,8 +941,8 @@ def test_advanced_csv_import__success(session, department, test_csv_dir): cop1.assignments, key=operator.attrgetter("start_date") ) assert assignment_po.star_no == "1234" - assert assignment_po.start_date == datetime.date(2019, 7, 12) - assert assignment_po.resign_date == datetime.date(2020, 1, 1) + assert assignment_po.start_date == date(2019, 7, 12) + assert assignment_po.resign_date == date(2020, 1, 1) assert assignment_po.job.job_title == "Police Officer" assert assignment_po.unit_id is None @@ -979,10 +979,10 @@ def test_advanced_csv_import__success(session, department, test_csv_dir): cop4.assignments, key=operator.attrgetter("start_date") ) assert updated_assignment.job.job_title == "Police Officer" - assert updated_assignment.resign_date == datetime.date(2020, 7, 10) + assert updated_assignment.resign_date == date(2020, 7, 10) assert updated_assignment.star_no == "4567" assert new_assignment.job.job_title == "Captain" - assert new_assignment.start_date == datetime.date(2020, 7, 10) + assert new_assignment.start_date == date(2020, 7, 10) assert new_assignment.star_no == "54321" incident = cop4.incidents[0] @@ -1005,12 +1005,11 @@ def test_advanced_csv_import__success(session, department, test_csv_dir): assert incident3.report_number == "CR-39283" assert incident3.description == "Don't know where it happened" assert incident3.officers == [cop1] - assert incident3.date == datetime.date(2020, 7, 26) + assert incident3.occurred_at == datetime(2020, 7, 26, 0, 0) lp = incident3.license_plates[0] assert lp.number == "XYZ11" assert lp.state is None assert incident3.address is None - assert incident3.time is None link_new = cop4.links[0] assert [link_new] == list(cop1.links) @@ -1104,6 +1103,7 @@ def test_advanced_csv_import__force_create(session, department, tmp_path): incidents_data = [ { "id": 66001, + "date": "2021-08-12", "officer_ids": "99002|99001", "department_name": department.name, "department_state": department.state, diff --git a/OpenOversight/tests/test_csvs/incidents.csv b/OpenOversight/tests/test_csvs/incidents.csv index 87b2eef78..75aef66a3 100644 --- a/OpenOversight/tests/test_csvs/incidents.csv +++ b/OpenOversight/tests/test_csvs/incidents.csv @@ -1,4 +1,4 @@ id,department name,department state,date,time,report number,description,street name,cross street1,cross street2,city,state,zip code,license plates,officer_ids,created by,last updated by ,Springfield Police Department,IL,2020-07-20,06:30,CR-1234,Something happened,,East Ave,Main St,Chicago,IL,60603,ABC123_NY|98UMC_IL,#1|49483,, -#I1,Springfield Police Department,IL,,,CR-9912,Something happened,Fake Street,Main Street,,Chicago,IL,60603,,#1,, +#I1,Springfield Police Department,IL,2019-08-12,,CR-9912,Something happened,Fake Street,Main Street,,Chicago,IL,60603,,#1,, 123456,Springfield Police Department,IL,2020-07-26,,CR-39283,Don't know where it happened,,,,,,,XYZ11,#1,, diff --git a/OpenOversight/tests/test_utils.py b/OpenOversight/tests/test_utils.py index decfeb3bf..67476eacc 100644 --- a/OpenOversight/tests/test_utils.py +++ b/OpenOversight/tests/test_utils.py @@ -1,6 +1,8 @@ +from datetime import datetime, timedelta, timezone from io import BytesIO import pytest +import pytz from flask import current_app from flask_login import current_user from mock import MagicMock, Mock, patch @@ -12,9 +14,14 @@ save_image_to_s3_and_db, upload_file_to_s3, ) +from OpenOversight.app.utils.constants import KEY_TIMEZONE from OpenOversight.app.utils.db import unit_choices from OpenOversight.app.utils.forms import filter_by_form, grab_officers -from OpenOversight.app.utils.general import allowed_file, validate_redirect_url +from OpenOversight.app.utils.general import ( + allowed_file, + get_utc_datetime, + validate_redirect_url, +) from OpenOversight.tests.routes.route_helpers import login_user @@ -74,6 +81,23 @@ def test_gender_filter_include_all_genders_if_not_sure(mockdata): assert results.count() == len(department.officers) +def test_get_utc_datetime(): + utc_now = datetime.utcnow() + test_utc_now = get_utc_datetime() + assert (test_utc_now - utc_now).total_seconds() < 0.5 + + with current_app.test_request_context(): + server_timezone = pytz.timezone(current_app.config[KEY_TIMEZONE]) + local = server_timezone.localize(datetime.now() + timedelta(days=10, hours=3)) + test_local_to_utc = get_utc_datetime(local) + test = (local - test_local_to_utc).total_seconds() + correct = ( + datetime.now(tz=timezone.utc) + - datetime.now().astimezone(tz=server_timezone) + ).total_seconds() + assert test == correct + + def test_rank_filter_select_all_commanders(mockdata): department = Department.query.first() results = grab_officers({"rank": ["Commander"], "dept": department})