diff --git a/OpenOversight/app/filters.py b/OpenOversight/app/filters.py index 31a88b1ad..88cebb2bb 100644 --- a/OpenOversight/app/filters.py +++ b/OpenOversight/app/filters.py @@ -8,7 +8,12 @@ from flask import Flask, current_app, session from markupsafe import Markup -from OpenOversight.app.utils.constants import FIELD_NOT_AVAILABLE, KEY_TIMEZONE +from OpenOversight.app.utils.constants import ( + FIELD_NOT_AVAILABLE, + KEY_TIMEZONE, + OO_DATE_FORMAT, + OO_TIME_FORMAT, +) from OpenOversight.app.utils.general import AVAILABLE_TIMEZONES @@ -47,21 +52,23 @@ def markdown(text: str) -> Markup: def display_date(value: datetime) -> str: """Convert UTC datetime.datetime into a localized date string.""" if value: - return value.strftime("%b %d, %Y") + return value.strftime(OO_DATE_FORMAT) return FIELD_NOT_AVAILABLE def local_date(value: datetime) -> str: """Convert UTC datetime.datetime into a localized date string.""" if value: - return value.astimezone(get_timezone()).strftime("%b %d, %Y") + return value.astimezone(get_timezone()).strftime(OO_DATE_FORMAT) return FIELD_NOT_AVAILABLE def local_date_time(value: datetime) -> str: """Convert UTC datetime.datetime into a localized date time string.""" if value: - return value.astimezone(get_timezone()).strftime("%I:%M %p (%Z) on %b %d, %Y") + return value.astimezone(get_timezone()).strftime( + f"{OO_TIME_FORMAT} (%Z) on {OO_DATE_FORMAT}" + ) return FIELD_NOT_AVAILABLE @@ -69,7 +76,7 @@ def display_time(value: datetime) -> str: """Convert UTC datetime.datetime into a localized date string.""" # This is used for incident times which are not currently tz-aware if value: - return value.strftime("%I:%M %p") + return value.strftime(OO_TIME_FORMAT) return FIELD_NOT_AVAILABLE diff --git a/OpenOversight/app/main/forms.py b/OpenOversight/app/main/forms.py index cc75d9164..7b40df98f 100644 --- a/OpenOversight/app/main/forms.py +++ b/OpenOversight/app/main/forms.py @@ -1,5 +1,5 @@ -import datetime import re +from datetime import datetime, time from flask_wtf import FlaskForm as Form from flask_wtf.file import FileAllowed, FileField, FileRequired @@ -175,7 +175,7 @@ class SalaryForm(Form): ) year = IntegerField( "Year", - default=datetime.datetime.now().year, + default=datetime.now().year, validators=[NumberRange(min=1900, max=2100)], ) is_fiscal_year = BooleanField("Is fiscal year?", default=False) @@ -434,7 +434,7 @@ class DateFieldForm(Form): time_field = TimeField("Time", validators=[Optional()]) def validate_time_field(self, field): - if not type(field.data) == datetime.time: + if not type(field.data) == time: raise ValidationError("Not a valid time.") def validate_date_field(self, field): diff --git a/OpenOversight/app/main/model_view.py b/OpenOversight/app/main/model_view.py index 61c207836..296e6613f 100644 --- a/OpenOversight/app/main/model_view.py +++ b/OpenOversight/app/main/model_view.py @@ -1,4 +1,4 @@ -import datetime +from datetime import datetime from http import HTTPMethod from typing import Callable, Union @@ -219,11 +219,11 @@ def populate_obj(self, form, obj): # if the object doesn't have a creator id set it to current user if hasattr(obj, "created_by") and not getattr(obj, "created_by"): - obj.created_by = current_user.get_id() + obj.created_by = current_user.id # if the object keeps track of who updated it last, set to current user if hasattr(obj, "last_updated_at"): - obj.last_updated_at = datetime.datetime.now() - obj.last_updated_by = current_user.get_id() + obj.last_updated_at = datetime.now() + obj.last_updated_by = current_user.id db.session.add(obj) db.session.commit() diff --git a/OpenOversight/app/main/views.py b/OpenOversight/app/main/views.py index 20d2513da..69a38331c 100644 --- a/OpenOversight/app/main/views.py +++ b/OpenOversight/app/main/views.py @@ -312,6 +312,7 @@ def officer_profile(officer_id: int): form = AssignmentForm() try: officer = Officer.query.filter_by(id=officer_id).one() + officer.incidents.query.order_by(Incident.date.desc(), Incident.time.desc()) except NoResultFound: abort(HTTPStatus.NOT_FOUND) except: # noqa: E722 @@ -1946,8 +1947,6 @@ def server_shutdown(): # pragma: no cover class IncidentApi(ModelView): model = Incident model_name = "incident" - order_by = "date" - descending = True form = IncidentForm create_function = create_incident department_check = True @@ -1986,7 +1985,7 @@ def get(self, obj_id: int): incidents = incidents.filter(self.model.date > after_date) incidents = incidents.order_by( - getattr(self.model, self.order_by).desc() + Incident.date.desc(), Incident.time.desc() ).paginate(page=page, per_page=self.per_page, error_out=False) url = f"main.{self.model_name}_api" diff --git a/OpenOversight/app/models/database_imports.py b/OpenOversight/app/models/database_imports.py index 7aeb57029..361b90829 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 @@ -36,13 +36,13 @@ def validate_choice( return None -def parse_date(date_str: Optional[str]) -> Optional[datetime.date]: +def parse_date(date_str: Optional[str]) -> Optional[date]: if date_str: return dateutil.parser.parse(date_str).date() return None -def parse_time(time_str: Optional[str]) -> Optional[datetime.time]: +def parse_time(time_str: Optional[str]) -> Optional[time]: if time_str: return dateutil.parser.parse(time_str).time() return None @@ -73,6 +73,8 @@ def parse_str(value: Optional[str], default: Optional[str] = "") -> Optional[str def create_officer_from_dict(data: Dict[str, Any], force_id: bool = False) -> Officer: + admin_user = User.query.filter_by(is_administrator=True).first() + officer = Officer( department_id=int(data["department_id"]), last_name=parse_str(data.get("last_name", "")), @@ -86,6 +88,8 @@ def create_officer_from_dict(data: Dict[str, Any], force_id: bool = False) -> Of unique_internal_identifier=parse_str( data.get("unique_internal_identifier"), None ), + created_by=admin_user.id, + last_updated_by=admin_user.id, ) if force_id and data.get("id"): officer.id = data["id"] @@ -118,6 +122,8 @@ def update_officer_from_dict(data: Dict[str, Any], officer: Officer) -> Officer: officer.unique_internal_identifier = parse_str( data.get("unique_internal_identifier"), None ) + officer.last_updated_at = datetime.now() + officer.last_updated_by = User.query.filter_by(is_administrator=True).first().id db.session.flush() return officer @@ -125,6 +131,8 @@ def update_officer_from_dict(data: Dict[str, Any], officer: Officer) -> Officer: def create_assignment_from_dict( data: Dict[str, Any], force_id: bool = False ) -> Assignment: + admin_user = User.query.filter_by(is_administrator=True).first() + assignment = Assignment( officer_id=int(data["officer_id"]), star_no=parse_str(data.get("star_no"), None), @@ -132,6 +140,8 @@ def create_assignment_from_dict( unit_id=parse_int(data.get("unit_id")), start_date=parse_date(data.get("start_date")), resign_date=parse_date(data.get("resign_date")), + created_by=admin_user.id, + last_updated_by=admin_user.id, ) if force_id and data.get("id"): assignment.id = data["id"] @@ -155,18 +165,24 @@ def update_assignment_from_dict( assignment.start_date = parse_date(data.get("start_date")) if "resign_date" in data.keys(): assignment.resign_date = parse_date(data.get("resign_date")) + assignment.last_updated_at = datetime.now() + assignment.last_updated_by = User.query.filter_by(is_administrator=True).first().id db.session.flush() return assignment def create_salary_from_dict(data: Dict[str, Any], force_id: bool = False) -> Salary: + admin_user = User.query.filter_by(is_administrator=True).first() + salary = Salary( officer_id=int(data["officer_id"]), salary=float(data["salary"]), overtime_pay=parse_float(data.get("overtime_pay")), year=int(data["year"]), is_fiscal_year=parse_bool(data.get("is_fiscal_year")), + created_by=admin_user.id, + last_updated_by=admin_user.id, ) if force_id and data.get("id"): salary.id = data["id"] @@ -186,19 +202,24 @@ def update_salary_from_dict(data: Dict[str, Any], salary: Salary) -> Salary: salary.year = int(data["year"]) if "is_fiscal_year" in data.keys(): salary.is_fiscal_year = parse_bool(data.get("is_fiscal_year")) + salary.last_updated_at = datetime.now() + salary.last_updated_by = User.query.filter_by(is_administrator=True).first().id db.session.flush() return salary def create_link_from_dict(data: Dict[str, Any], force_id: bool = False) -> Link: + admin_user = User.query.filter_by(is_administrator=True).first() + link = Link( title=data.get("title", ""), url=url_validator(data["url"]), link_type=validate_choice(data.get("link_type"), LINK_CHOICES), description=parse_str(data.get("description"), None), author=parse_str(data.get("author"), None), - created_by=parse_int(data.get("created_by")), + created_by=parse_int(data.get("created_by", admin_user.id)), + last_updated_by=parse_int(data.get("created_by", admin_user.id)), ) if force_id and data.get("id"): @@ -223,12 +244,12 @@ def update_link_from_dict(data: Dict[str, Any], link: Link) -> Link: link.description = parse_str(data.get("description"), None) if "author" in data: link.author = parse_str(data.get("author"), None) - if "created_by" in data: - link.created_by = parse_int(data.get("created_by")) if "officers" in data: link.officers = data.get("officers") or [] if "incidents" in data: link.incidents = data.get("incidents") or [] + link.last_updated_at = datetime.now() + link.last_updated_by = User.query.filter_by(is_administrator=True).first().id db.session.flush() return link @@ -275,6 +296,8 @@ def get_or_create_location_from_dict( def create_incident_from_dict(data: Dict[str, Any], force_id: bool = False) -> Incident: + admin_user = User.query.filter_by(is_administrator=True).first() + incident = Incident( date=parse_date(data.get("date")), time=parse_time(data.get("time")), @@ -282,9 +305,8 @@ def create_incident_from_dict(data: Dict[str, Any], force_id: bool = False) -> I 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(), + created_by=parse_int(data.get("created_by", admin_user.id)), + last_updated_by=parse_int(data.get("last_updated_by", admin_user.id)), ) incident.officers = data.get("officers", []) @@ -310,16 +332,18 @@ def update_incident_from_dict(data: Dict[str, Any], incident: Incident) -> Incid if "address_id" in data: incident.address_id = data.get("address_id") if "department_id" in data: - incident.department_id = parse_int(data.get("department_id")) - if "created_by" in data: - incident.created_by = parse_int(data.get("created_by")) + incident.department_id = parse_int( + data.get( + "department_id", User.query.filter_by(is_administrator=True).first().id + ) + ) if "last_updated_by" in data: incident.last_updated_by = parse_int(data.get("last_updated_by")) - incident.last_updated_at = datetime.datetime.now() if "officers" in data: incident.officers = data["officers"] or [] if "license_plate_objects" in data: incident.license_plates = data["license_plate_objects"] or [] + incident.last_updated_at = datetime.now() db.session.flush() return incident diff --git a/OpenOversight/app/templates/partials/officer_incidents.html b/OpenOversight/app/templates/partials/officer_incidents.html index f6a6f185f..33581e9cb 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 %} {% if not loop.first %} diff --git a/OpenOversight/app/utils/cloud.py b/OpenOversight/app/utils/cloud.py index 18a59aefe..1aed172cf 100644 --- a/OpenOversight/app/utils/cloud.py +++ b/OpenOversight/app/utils/cloud.py @@ -1,7 +1,7 @@ -import datetime import hashlib import os import sys +from datetime import datetime from io import BytesIO from traceback import format_exc from urllib.request import urlopen @@ -122,7 +122,7 @@ def save_image_to_s3_and_db(image_buf, user_id, department_id=None): date_taken = get_date_taken(pimage) if date_taken: - date_taken = datetime.datetime.strptime(date_taken, "%Y:%m:%d %H:%M:%S") + date_taken = datetime.strptime(date_taken, "%Y:%m:%d %H:%M:%S") pimage.getexif().clear() scrubbed_image_buf = BytesIO() pimage.save(scrubbed_image_buf, image_format) @@ -140,10 +140,10 @@ def save_image_to_s3_and_db(image_buf, user_id, department_id=None): new_image = Image( filepath=url, hash_img=hash_img, - created_at=datetime.datetime.now(), department_id=department_id, taken_at=date_taken, created_by=user_id, + last_updated_by=user_id, ) db.session.add(new_image) db.session.commit() diff --git a/OpenOversight/app/utils/constants.py b/OpenOversight/app/utils/constants.py index bb397c922..37a598c07 100644 --- a/OpenOversight/app/utils/constants.py +++ b/OpenOversight/app/utils/constants.py @@ -12,9 +12,6 @@ KEY_DEPT_TOTAL_INCIDENTS = "total_department_incidents" KEY_DEPT_TOTAL_OFFICERS = "total_department_officers" -# Database Key Constants -KEY_DB_CREATOR = "creator" - # Config Key Constants KEY_ALLOWED_EXTENSIONS = "ALLOWED_EXTENSIONS" KEY_DATABASE_URI = "SQLALCHEMY_DATABASE_URI" @@ -35,6 +32,13 @@ KEY_S3_BUCKET_NAME = "S3_BUCKET_NAME" KEY_TIMEZONE = "TIMEZONE" +# Database Key Constants +KEY_DB_CREATOR = "creator" + +# DateTime Constants +OO_DATE_FORMAT = "%b %d, %Y" +OO_TIME_FORMAT = "%I:%M %p" + # File Handling Constants ENCODING_UTF_8 = "utf-8" FILE_TYPE_HTML = "html" diff --git a/OpenOversight/app/utils/forms.py b/OpenOversight/app/utils/forms.py index c89d91507..c74c20be2 100644 --- a/OpenOversight/app/utils/forms.py +++ b/OpenOversight/app/utils/forms.py @@ -1,4 +1,4 @@ -import datetime +from datetime import datetime from typing import Union from sqlalchemy import or_ @@ -34,9 +34,7 @@ def if_exists_or_none(val: Union[str, None]) -> Union[str, None]: return val if val else None -def add_new_assignment( - officer_id: int, form: AssignmentForm, current_user: User -) -> None: +def add_new_assignment(officer_id: int, form: AssignmentForm, user: User) -> None: unit_id = form.unit.data.id if form.unit.data else None job = Job.query.filter_by( @@ -51,12 +49,14 @@ def add_new_assignment( unit_id=unit_id, start_date=form.start_date.data, resign_date=form.resign_date.data, + created_by=user.id, + last_updated_by=user.id, ) db.session.add(new_assignment) db.session.commit() -def add_officer_profile(form: AddOfficerForm, current_user: User) -> Officer: +def add_officer_profile(form: AddOfficerForm, user: User) -> Officer: officer = Officer( first_name=form.first_name.data, last_name=form.last_name.data, @@ -67,8 +67,8 @@ def add_officer_profile(form: AddOfficerForm, current_user: User) -> Officer: birth_year=form.birth_year.data, employment_date=form.employment_date.data, department_id=form.department.data.id, - created_by=current_user.id, - last_updated_by=current_user.id, + created_by=user.id, + last_updated_by=user.id, ) db.session.add(officer) db.session.commit() @@ -81,14 +81,14 @@ def add_officer_profile(form: AddOfficerForm, current_user: User) -> Officer: job_id=form.job_id.data, unit=officer_unit, start_date=form.employment_date.data, - created_by=current_user.id, - last_updated_by=current_user.id, + created_by=user.id, + last_updated_by=user.id, ) db.session.add(assignment) if form.links.data: for link in form.data["links"]: if link["url"]: - li = get_or_create_link_from_form(link, current_user) + li = get_or_create_link_from_form(link, user) officer.links.append(li) if form.notes.data: for note in form.data["notes"]: @@ -97,8 +97,8 @@ def add_officer_profile(form: AddOfficerForm, current_user: User) -> Officer: new_note = Note( text_contents=note["text_contents"], officer=officer, - created_by=current_user.id, - last_updated_by=current_user.id, + created_by=user.id, + last_updated_by=user.id, ) db.session.add(new_note) if form.descriptions.data: @@ -108,8 +108,8 @@ def add_officer_profile(form: AddOfficerForm, current_user: User) -> Officer: new_description = Description( text_contents=description["text_contents"], officer=officer, - created_by=current_user.id, - last_updated_by=current_user.id, + created_by=user.id, + last_updated_by=user.id, ) db.session.add(new_description) if form.salaries.data: @@ -122,8 +122,8 @@ def add_officer_profile(form: AddOfficerForm, current_user: User) -> Officer: overtime_pay=salary["overtime_pay"], year=salary["year"], is_fiscal_year=salary["is_fiscal_year"], - created_by=current_user.id, - last_updated_by=current_user.id, + created_by=user.id, + last_updated_by=user.id, ) db.session.add(new_salary) @@ -131,22 +131,20 @@ def add_officer_profile(form: AddOfficerForm, current_user: User) -> Officer: return officer -def create_description(self, form: TextForm, current_user: User) -> Description: +def create_description(self, form: TextForm, user: User) -> Description: return Description( text_contents=form.text_contents.data, officer_id=form.officer_id.data, + created_by=user.id, + last_updated_by=user.id, ) -def create_incident(self, form: IncidentForm, current_user: User) -> Incident: - fields = { - "date": form.date_field.data, - "time": form.time_field.data, - "officers": [], - "license_plates": [], - "links": [], - "address": "", - } +def create_incident(self, form: IncidentForm, user: User) -> Incident: + address_model = None + officers = [] + license_plates = [] + links = [] if "address" in form.data: address = form.data["address"] @@ -166,54 +164,61 @@ def create_incident(self, form: IncidentForm, current_user: User) -> Incident: state=if_exists_or_none(address["state"]), street_name=if_exists_or_none(address["street_name"]), zip_code=if_exists_or_none(address["zip_code"]), + created_by=user.id, + last_updated_by=user.id, ) - fields["address"] = location + db.session.add(location) + address_model = location if "officers" in form.data: for officer in form.data["officers"]: if officer["oo_id"]: of = Officer.query.filter_by(id=int(officer["oo_id"])).one() if of: - fields["officers"].append(of) + officers.append(of) if "license_plates" in form.data: for plate in form.data["license_plates"]: if plate["number"]: - pl = LicensePlate.query.filter_by( + lp = LicensePlate.query.filter_by( number=if_exists_or_none(plate["number"]), state=if_exists_or_none(plate["state"]), ).first() - if not pl: - pl = LicensePlate( + if not lp: + lp = LicensePlate( number=if_exists_or_none(plate["number"]), state=if_exists_or_none(plate["state"]), + created_by=user.id, + last_updated_by=user.id, ) - db.session.add(pl) - fields["license_plates"].append(pl) + db.session.add(lp) + license_plates.append(lp) if "links" in form.data: for link in form.data["links"]: if link["url"]: - li = get_or_create_link_from_form(link, current_user) - fields["links"].append(li) + li = get_or_create_link_from_form(link, user) + links.append(li) return Incident( - date=fields["date"], - time=fields["time"], - description=form.data["description"], + address=address_model, + date=form.date_field.data, department=form.data["department"], - address=fields["address"], - officers=fields["officers"], + description=form.data["description"], + license_plates=license_plates, + links=links, + officers=officers, report_number=form.data["report_number"], - license_plates=fields["license_plates"], - links=fields["links"], + time=form.time_field.data, ) -def create_note(self, form: TextForm, current_user: User) -> Note: +def create_note(self, form: TextForm, user: User) -> Note: return Note( text_contents=form.text_contents.data, officer_id=form.officer_id.data, + created_by=user.id, + last_updated_by=user.id, ) @@ -236,7 +241,7 @@ def edit_existing_assignment(assignment, form: AssignmentForm) -> Assignment: return assignment -def get_or_create_link_from_form(link_form, current_user: User) -> Union[Link, None]: +def get_or_create_link_from_form(link_form, user: User) -> Union[Link, None]: link = None if link_form["url"]: link = Link.query.filter_by( @@ -252,6 +257,8 @@ def get_or_create_link_from_form(link_form, current_user: User) -> Union[Link, N link_type=if_exists_or_none(link_form["link_type"]), title=if_exists_or_none(link_form["title"]), url=if_exists_or_none(link_form["url"]), + created_by=user.id, + last_updated_by=user.id, ) db.session.add(link) return link @@ -302,7 +309,7 @@ def filter_by_form(form_data: BrowseForm, officer_query, department_id=None): ) if form_data.get("min_age") and form_data.get("max_age"): - current_year = datetime.datetime.now().year + current_year = datetime.now().year min_birth_year = current_year - int(form_data["min_age"]) max_birth_year = current_year - int(form_data["max_age"]) officer_query = officer_query.filter( diff --git a/OpenOversight/app/utils/general.py b/OpenOversight/app/utils/general.py index 2d088e722..08b618fa9 100644 --- a/OpenOversight/app/utils/general.py +++ b/OpenOversight/app/utils/general.py @@ -1,12 +1,13 @@ import random import sys from distutils.util import strtobool -from typing import Optional +from typing import Optional, Union from urllib.parse import urlparse from zoneinfo import available_timezones from flask import current_app, url_for +from OpenOversight.app.models.database import Officer, User from OpenOversight.app.utils.constants import KEY_ALLOWED_EXTENSIONS @@ -15,13 +16,13 @@ AVAILABLE_TIMEZONES = available_timezones() -def ac_can_edit_officer(officer, ac): +def ac_can_edit_officer(officer: Officer, ac: User) -> bool: if officer.department_id == ac.ac_department_id: return True return False -def allowed_file(filename): +def allowed_file(filename: str) -> bool: return ( "." in filename and filename.rsplit(".", 1)[1].lower() @@ -83,7 +84,7 @@ def merge_dicts(*dict_args): return result -def normalize_gender(input_gender): +def normalize_gender(input_gender: str) -> Union[str, None]: if input_gender is None: return None normalized_genders = { @@ -141,17 +142,17 @@ def replace_list(items, obj, attr, model, db): setattr(obj, attr, new_list) -def serve_image(filepath): +def serve_image(filepath: str): if "http" in filepath: return filepath if "static" in filepath: return url_for("static", filename=filepath.replace("static/", "").lstrip("/")) -def str_is_true(str_): +def str_is_true(str_) -> bool: if str_ is None: return False - return strtobool(str_.lower()) + return bool(strtobool(str_.lower())) def validate_redirect_url(url: Optional[str]) -> Optional[str]: diff --git a/OpenOversight/tests/conftest.py b/OpenOversight/tests/conftest.py index 905bc9996..abfa3009d 100644 --- a/OpenOversight/tests/conftest.py +++ b/OpenOversight/tests/conftest.py @@ -1,14 +1,14 @@ import csv -import datetime import math import os import random import sys import threading -import time import uuid +from datetime import date, datetime, time, timedelta from io import BytesIO from pathlib import Path +from time import sleep from typing import List, Optional import pytest @@ -130,7 +130,7 @@ def pick_birth_date(): def pick_date( seed: bytes = b"", start_year: int = 2000, end_year: int = 2020 -) -> datetime.datetime: +) -> datetime: # source: https://stackoverflow.com/q/40351791 # Wanted to deterministically create a date from a seed string (e.g. the hash or # uuid on an officer object). @@ -143,7 +143,7 @@ def bytes_to_float(b): if seed == b"": seed = str(uuid.uuid4()).encode(ENCODING_UTF_8) - return datetime.datetime(start_year, 1, 1, 00, 00, 00) + datetime.timedelta( + return datetime(start_year, 1, 1, 00, 00, 00) + timedelta( days=365 * (end_year - start_year) * bytes_to_float(seed) ) @@ -199,7 +199,7 @@ def generate_officer( race=pick_race(), gender=pick_gender(), birth_year=year_born, - employment_date=datetime.datetime(year_born + 20, 4, 4, 1, 1, 1), + employment_date=datetime(year_born + 20, 4, 4, 1, 1, 1), department_id=department.id, created_by=user.id, ) @@ -230,28 +230,24 @@ def build_assignment( def build_note(officer: Officer, user: User, content=None) -> Note: - date = factory.date_time_this_year() if content is None: content = factory.text() return Note( text_contents=content, officer_id=officer.id, created_by=user.id, - created_at=date, - last_updated_at=date, + last_updated_by=user.id, ) def build_description(officer: Officer, user: User, content=None) -> Description: - date = factory.date_time_this_year() if content is None: content = factory.text() return Description( text_contents=content, officer_id=officer.id, created_by=user.id, - created_at=date, - last_updated_at=date, + last_updated_by=user.id, ) @@ -263,6 +259,7 @@ def build_salary(officer: Officer, user: User) -> Salary: year=random.randint(2000, 2019), is_fiscal_year=True if random.randint(0, 1) else False, created_by=user.id, + last_updated_by=user.id, ) @@ -275,6 +272,7 @@ def assign_faces(officer: Officer, images: Image, user: User): original_image_id=img_id, featured=False, created_by=user.id, + last_updated_by=user.id, ) else: return False @@ -426,6 +424,7 @@ def add_mockdata(session): state=SPRINGFIELD_PD.state, unique_internal_identifier_label=SPRINGFIELD_PD.uid_label, created_by=test_admin.id, + last_updated_by=test_admin.id, ) session.add(department) department2 = Department( @@ -433,6 +432,7 @@ def add_mockdata(session): short_name=OTHER_PD.short_name, state=OTHER_PD.state, created_by=test_admin.id, + last_updated_by=test_admin.id, ) session.add(department2) empty_department = Department( @@ -440,6 +440,7 @@ def add_mockdata(session): short_name=NO_OFFICER_PD.short_name, state=NO_OFFICER_PD.state, created_by=test_admin.id, + last_updated_by=test_admin.id, ) session.add(empty_department) session.commit() @@ -462,6 +463,7 @@ def add_mockdata(session): is_sworn_officer=True, department_id=department.id, created_by=test_admin.id, + last_updated_by=test_admin.id, ) ) session.add( @@ -471,6 +473,7 @@ def add_mockdata(session): is_sworn_officer=True, department_id=empty_department.id, created_by=test_admin.id, + last_updated_by=test_admin.id, ) ) @@ -482,6 +485,7 @@ def add_mockdata(session): is_sworn_officer=True, department_id=department2.id, created_by=test_admin.id, + last_updated_by=test_admin.id, ) ) session.commit() @@ -490,18 +494,35 @@ def add_mockdata(session): random.seed(current_app.config["SEED"]) test_units = [ - Unit(description="test", department_id=1, created_by=test_admin.id), - Unit(description="District 13", department_id=1, created_by=test_admin.id), - Unit(description="Donut Devourers", department_id=1, created_by=test_admin.id), + Unit( + description="test", + department_id=1, + created_by=test_admin.id, + last_updated_by=test_admin.id, + ), + Unit( + description="District 13", + department_id=1, + created_by=test_admin.id, + last_updated_by=test_admin.id, + ), + Unit( + description="Donut Devourers", + department_id=1, + created_by=test_admin.id, + last_updated_by=test_admin.id, + ), Unit( description="Bureau of Organized Crime", department_id=2, created_by=test_admin.id, + last_updated_by=test_admin.id, ), Unit( description="Porky's BBQ: Rub Division", department_id=2, created_by=test_admin.id, + last_updated_by=test_admin.id, ), ] session.add_all(test_units) @@ -513,6 +534,7 @@ def add_mockdata(session): filepath=f"/static/images/test_cop{x + 1}.png", department_id=department.id, created_by=test_admin.id, + last_updated_by=test_admin.id, ) for x in range(5) ] + [ @@ -520,6 +542,7 @@ def add_mockdata(session): filepath=f"/static/images/test_cop{x + 1}.png", department_id=department2.id, created_by=test_admin.id, + last_updated_by=test_admin.id, ) for x in range(5) ] @@ -532,6 +555,7 @@ def add_mockdata(session): title="OpenOversight", description="A public, searchable database of law enforcement officers.", created_by=test_admin.id, + last_updated_by=test_admin.id, ), Link( url="http://www.youtube.com/?v=help", @@ -539,6 +563,7 @@ def add_mockdata(session): title="Youtube", author="the internet", created_by=test_admin.id, + last_updated_by=test_admin.id, ), ] @@ -604,6 +629,7 @@ def add_mockdata(session): state="AZ", zip_code="23456", created_by=test_admin.id, + last_updated_by=test_admin.id, ), Location( street_name="Testing St", @@ -613,6 +639,7 @@ def add_mockdata(session): state="ME", zip_code="23456", created_by=test_admin.id, + last_updated_by=test_admin.id, ), ] @@ -633,12 +660,14 @@ def add_mockdata(session): link_type="link", creator=test_admin, created_by=test_admin.id, + last_updated_by=test_admin.id, ), Link( url="http://www.youtube.com/?v=help", link_type="video", creator=test_admin, created_by=test_admin.id, + last_updated_by=test_admin.id, ), ] @@ -647,8 +676,8 @@ def add_mockdata(session): test_incidents = [ Incident( - date=datetime.date(2016, 3, 16), - time=datetime.time(4, 20), + date=date(2016, 3, 16), + time=time(4, 20), report_number="42", description="### A thing happened\n **Markup** description", department_id=1, @@ -658,11 +687,10 @@ def add_mockdata(session): officers=[all_officers[o] for o in range(4)], created_by=test_admin.id, last_updated_by=test_admin.id, - last_updated_at=datetime.datetime.now(), ), Incident( - date=datetime.date(2017, 12, 11), - time=datetime.time(2, 40), + date=date(2017, 12, 11), + time=time(2, 40), report_number="38", description="A thing happened", department_id=2, @@ -672,10 +700,9 @@ def add_mockdata(session): officers=[all_officers[o] for o in range(3)], created_by=test_admin.id, last_updated_by=test_admin.id, - last_updated_at=datetime.datetime.now(), ), Incident( - date=datetime.datetime(2019, 1, 15), + date=date(2019, 1, 15), report_number="39", description=( Path(__file__).parent / "description_overflow.txt" @@ -687,7 +714,6 @@ def add_mockdata(session): officers=[all_officers[o] for o in range(1)], created_by=test_admin.id, last_updated_by=test_admin.id, - last_updated_at=datetime.datetime.now(), ), ] session.add_all(test_incidents) @@ -873,14 +899,14 @@ def browser(app, server_port): target=app.run, daemon=True, kwargs={"debug": False, "port": port} ).start() # give the server a few seconds to ensure it is up - time.sleep(10) + sleep(10) # start headless webdriver visual_display = Xvfb() visual_display.start() driver = webdriver.Firefox(service_log_path="/tmp/geckodriver.log") # wait for browser to start up - time.sleep(3) + sleep(3) yield driver # shutdown headless webdriver diff --git a/OpenOversight/tests/test_commands.py b/OpenOversight/tests/test_commands.py index 45bcdb3f5..d3704fadb 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, time import pandas as pd import pytest @@ -851,6 +851,7 @@ def test_advanced_csv_import__success(session, department, test_csv_dir): last_name="InDatabase", birth_year=1951, created_by=user.id, + last_updated_by=user.id, ) session.add(officer) @@ -858,9 +859,10 @@ 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, + last_updated_by=user.id, ) session.add(assignment) @@ -871,6 +873,7 @@ def test_advanced_csv_import__success(session, department, test_csv_dir): year=2018, is_fiscal_year=False, created_by=user.id, + last_updated_by=user.id, ) session.add(salary) @@ -879,8 +882,9 @@ 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), + time=time(23, 45, 16), created_by=user.id, + last_updated_by=user.id, ) incident.officers = [officer] session.add(incident) @@ -890,6 +894,7 @@ def test_advanced_csv_import__success(session, department, test_csv_dir): title="Existing Link", url="https://www.example.org", created_by=user.id, + last_updated_by=user.id, ) session.add(link) officer.links = [link] @@ -927,7 +932,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 @@ -943,8 +948,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 @@ -981,14 +986,15 @@ 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] assert incident.report_number == "CR-1234" + license_plates = {plate.state: plate.number for plate in incident.license_plates} assert license_plates["NY"] == "ABC123" assert license_plates["IL"] == "98UMC" @@ -1007,18 +1013,20 @@ 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.date == date(2020, 7, 26) + assert incident3.time is None + assert incident3.address is None + 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) assert link_new.title == "A Link" assert link_new.url == "https://www.example.com" assert {officer.id for officer in link_new.officers} == {cop1.id, cop4.id} + incident_link = incident2.links[0] assert incident_link.url == "https://www.example.com/incident" assert incident_link.title == "Another Link"