diff --git a/OpenOversight/app/templates/officer.html b/OpenOversight/app/templates/officer.html
index 06f228f8d..ef5053a3e 100644
--- a/OpenOversight/app/templates/officer.html
+++ b/OpenOversight/app/templates/officer.html
@@ -109,34 +109,27 @@
Officer Detail: {{ officer.full_name() }}
{% include "partials/officer_assignment_history.html" %}
+ {% if officer.descriptions or is_admin_or_coordinator %}
+ {% include "partials/officer_descriptions.html" %}
+ {% endif %}
+ {# Notes are for internal use #}
+ {% if is_admin_or_coordinator %}
+ {% include "partials/officer_notes.html" %}
+ {% endif %}
+ {% with obj=officer %}
+ {% include "partials/links_and_videos_row.html" %}
+ {% endwith %}
{# end col #}
- {% if officer.salaries or is_admin_or_coordinator %}
-
+
+ {% if officer.salaries or is_admin_or_coordinator %}
{% include "partials/officer_salary.html" %}
-
{# end col #}
- {% endif %}
-
- {% if officer.incidents or is_admin_or_coordinator %}
-
+ {% endif %}
+ {% if officer.incidents or is_admin_or_coordinator %}
{% include "partials/officer_incidents.html" %}
-
{# end col #}
- {% endif %}
-
- {% if officer.descriptions or is_admin_or_coordinator %}
-
- {% include "partials/officer_descriptions.html" %}
-
{# end col #}
- {% endif %}
-
- {% if is_admin_or_coordinator %}
-
- {% include "partials/officer_notes.html" %}
+ {% endif %}
{# end col #}
- {% endif %}
+
{# end row #}
- {% with obj=officer %}
- {% include "partials/links_and_videos_row.html" %}
- {% endwith %}
{# end container #}
{% endblock %}
diff --git a/OpenOversight/app/templates/partials/incident_fields.html b/OpenOversight/app/templates/partials/incident_fields.html
index eacd4d1b8..2fd9c1a73 100644
--- a/OpenOversight/app/templates/partials/incident_fields.html
+++ b/OpenOversight/app/templates/partials/incident_fields.html
@@ -94,5 +94,5 @@
{% block js_footer %}
-
+
{% endblock %}
diff --git a/OpenOversight/app/templates/partials/links_and_videos_row.html b/OpenOversight/app/templates/partials/links_and_videos_row.html
index b29bc3de9..b18353015 100644
--- a/OpenOversight/app/templates/partials/links_and_videos_row.html
+++ b/OpenOversight/app/templates/partials/links_and_videos_row.html
@@ -1,125 +1,117 @@
{% if obj.links|length > 0 or is_admin_or_coordinator %}
-
-
-
Links
- {% for type, list in obj.links|groupby('link_type') %}
- {% if type == 'link' %}
-
- {% endif %}
- {% endfor %}
- {% if officer and (current_user.is_administrator
+
Delete
+
+
+ {% endif %}
+ {% if link.description or link.author %}
+
+ {% if link.description %}
+ {{ link.description }}
+ {% endif %}
+ {% if link.author %}
+ {% if link.description %}- {% endif %}{{ link.author }}
+ {% endif %}
+
+ {% endif %}
+
+ {% endfor %}
+
+ {% endif %}
+ {% endfor %}
+ {% if officer and (current_user.is_administrator
or (current_user.is_area_coordinator and current_user.ac_department_id == officer.department_id)) %}
-
New Link/Video
- {% endif %}
-
- {% for type, list in obj.links|groupby('link_type') %}
- {% if type == 'video' %}
-
-
Videos
-
- {% for link in list %}
- {% with link_url = link.url.split('v=')[1] %}
- -
- {% if link.title %}
-
{{ link.title }}
- {% endif %}
- {% if officer and (current_user.is_administrator
+ New Link/Video
+ {% endif %}
+ {% for type, list in obj.links|groupby('link_type') %}
+ {% if type == 'video' %}
+ Videos
+
-
- {% endif %}
- {% if type == 'other_video' %}
-
-
Other videos
-
+ {% endif %}
+ {% if type == 'other_video' %}
+
Other videos
+
-
- {% endif %}
- {% endfor %}
-
{# end row #}
+
Delete
+
+
+ {% endif %}
+ {% if link.description or link.author %}
+
+ {% if link.description %}
+ {{ link.description }}
+ {% endif %}
+ {% if link.author %}
+ {% if link.description %}- {% endif %}{{ link.author }}
+ {% endif %}
+
+ {% endif %}
+
+ {% endfor %}
+
+ {% endif %}
+ {% endfor %}
{% endif %}
diff --git a/OpenOversight/app/templates/partials/officer_general_information.html b/OpenOversight/app/templates/partials/officer_general_information.html
index d7a7e6fef..ed4dc39c3 100644
--- a/OpenOversight/app/templates/partials/officer_general_information.html
+++ b/OpenOversight/app/templates/partials/officer_general_information.html
@@ -18,10 +18,12 @@
OpenOversight ID |
{{ officer.id }} |
+ {% if officer.unique_internal_identifier %}
Unique Internal Identifier |
{{ officer.unique_internal_identifier }} |
+ {% endif %}
Department |
{{ officer.department.name }} |
@@ -42,6 +44,10 @@
First Employment Date |
{{ officer.employment_date }} |
+
+ Number of known incidents |
+ {{ officer.incidents | length }} |
+
Currently on the force |
{{ officer.currently_on_force() }} |
diff --git a/OpenOversight/tests/conftest.py b/OpenOversight/tests/conftest.py
index e07316d74..03564d7a9 100644
--- a/OpenOversight/tests/conftest.py
+++ b/OpenOversight/tests/conftest.py
@@ -7,6 +7,7 @@
import time
import uuid
from io import BytesIO
+from pathlib import Path
from typing import List
import pytest
@@ -486,7 +487,9 @@ def add_mockdata(session):
models.Incident(
date=datetime.datetime(2019, 1, 15),
report_number="39",
- description="A test description that has over 300 chars. The purpose is to see how to display a larger descrption. Descriptions can get lengthy. So lengthy. It is a description with a lot to say. Descriptions can get lengthy. So lengthy. It is a description with a lot to say. Descriptions can get lengthy. So lengthy. It is a description with a lot to say. Lengthy lengthy lengthy.",
+ description=(
+ Path(__file__).parent / "description_overflow.txt"
+ ).read_text(),
department_id=2,
address=test_addresses[1],
license_plates=[test_license_plates[0]],
diff --git a/OpenOversight/tests/description_overflow.txt b/OpenOversight/tests/description_overflow.txt
new file mode 100644
index 000000000..6aa6bc19f
--- /dev/null
+++ b/OpenOversight/tests/description_overflow.txt
@@ -0,0 +1,6 @@
+A test description that has over 500 chars.
+Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nullam gravida ante sed dui venenatis sollicitudin. Fusce id diam aliquam, tincidunt dolor sit amet, fermentum justo. In facilisis faucibus tempus. Ut in fermentum tellus, vitae consectetur risus. Vestibulum sodales lorem at augue congue, ac finibus magna consequat. Suspendisse odio nibh, sollicitudin ac nunc a, ultrices laoreet urna. Nulla hendrerit non enim eu viverra. Donec non est sagittis purus facilisis sodales. Quisque rutrum, tortor ut volutpat blandit, ipsum urna tempus mi, ut aliquam dui mauris a arcu. Mauris lectus tortor, volutpat a venenatis sit amet, scelerisque a eros. Fusce nulla elit, tempus et viverra in, interdum sit amet nisi. Sed dictum dolor nec lorem rhoncus, vel rhoncus tellus egestas.
+Aenean sed arcu finibus, auctor lectus non, pharetra odio. Cras non hendrerit nisi. Integer auctor tortor lectus, in placerat massa scelerisque ut. Phasellus vestibulum tortor eget nisi ultrices, id aliquam nisl ultricies. Aliquam finibus, tellus facilisis hendrerit maximus, tellus tortor rhoncus turpis, ut fermentum felis felis vitae lectus. Nam condimentum, justo id feugiat porttitor, lorem erat pharetra massa, quis mattis sem orci sed sem. Quisque tempor viverra enim a scelerisque. Nam cursus facilisis orci, et accumsan eros dictum nec. Donec eleifend dui elit, vel laoreet neque sagittis et. Praesent at ante id turpis vulputate aliquet vel nec tortor. Proin at ante viverra, rhoncus elit at, efficitur mauris. Maecenas placerat neque nisi, volutpat dictum orci venenatis quis. Vestibulum rhoncus purus et libero ornare dapibus.
+Aenean consequat libero vel metus ornare, eu fermentum mauris sagittis. Sed congue semper nulla, id hendrerit augue aliquam a. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nunc rhoncus risus in nibh tempor blandit. Donec consectetur accumsan purus id laoreet. Mauris elit libero, sodales sit amet dignissim eu, ultricies ut leo. Integer venenatis ligula id interdum vehicula. Nulla et enim hendrerit, molestie justo ut, condimentum nunc. Aenean vitae congue orci. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Fusce felis elit, tristique eu fermentum sed, blandit a arcu. Nullam efficitur tellus ac eros molestie congue. Mauris in nisl est. Sed mi tortor, elementum vitae vehicula sit amet, ullamcorper eget ante. Integer laoreet vehicula ligula quis venenatis.
+Duis eget semper libero, id aliquet ligula. Sed blandit faucibus sapien. Morbi placerat ultricies metus, non iaculis magna rutrum nec. Etiam id vestibulum mauris, et congue enim. Ut tincidunt malesuada viverra. Cras turpis nibh, faucibus a tellus non, consequat volutpat nibh. Praesent nunc dui, auctor in lacus nec, laoreet scelerisque sem. Quisque eleifend mattis massa, dictum lobortis risus faucibus ac. Pellentesque fermentum vel felis a eleifend. Pellentesque molestie consectetur augue, non tristique lectus gravida ac. Integer condimentum ligula non malesuada fringilla. Proin eget bibendum turpis. Mauris placerat condimentum cursus. Nunc sed consectetur nisi, vel feugiat lorem. Suspendisse vulputate fermentum odio, eget fermentum tellus tempor vel. Ut at fringilla sem.
+Fusce tempus odio nec lectus mollis pulvinar. Mauris nec faucibus leo. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia curae; Sed malesuada tristique risus, et imperdiet odio scelerisque sit amet. Nam sapien urna, euismod nec elementum a, efficitur quis mi. Vestibulum mollis efficitur sagittis. Vestibulum sit amet accumsan tellus. Donec tempor elit ut orci facilisis, id mollis arcu lacinia. Donec tempor, purus eget facilisis vulputate, augue nisl euismod dui, vel ullamcorper metus turpis sagittis orci. Fusce sagittis, nibh ultrices hendrerit tempor, magna nulla efficitur sapien, vitae viverra magna ante a metus. Vivamus venenatis imperdiet hendrerit. Aenean ut lobortis magna.
diff --git a/OpenOversight/tests/routes/test_incidents.py b/OpenOversight/tests/routes/test_incidents.py
index 0f646ad61..f97cb4b7b 100644
--- a/OpenOversight/tests/routes/test_incidents.py
+++ b/OpenOversight/tests/routes/test_incidents.py
@@ -44,11 +44,19 @@ def test_route_admin_or_required(route, client, mockdata):
assert rv.status_code == 403
-def test_admins_can_create_basic_incidents(mockdata, client, session):
+@pytest.mark.parametrize(
+ "report_number",
+ [
+ # Ensure different report number formats are accepted
+ "42",
+ "My-Special-Case",
+ "PPP Case 92",
+ ],
+)
+def test_admins_can_create_basic_incidents(report_number, mockdata, client, session):
with current_app.test_request_context():
login_admin(client)
date = datetime(2000, 5, 25, 1, 45)
- report_number = "42"
address_form = LocationForm(
street_name="AAAAA",
@@ -83,6 +91,45 @@ def test_admins_can_create_basic_incidents(mockdata, client, session):
assert inc is not None
+def test_admins_cannot_create_incident_with_invalid_report_number(
+ mockdata, client, session
+):
+ with current_app.test_request_context():
+ login_admin(client)
+ date = datetime(2000, 5, 25, 1, 45)
+ report_number = "Will Not Work! #45"
+
+ address_form = LocationForm(
+ street_name="AAAAA",
+ cross_street1="BBBBB",
+ city="FFFFF",
+ state="IA",
+ zip_code="03435",
+ )
+ # These have to have a dropdown selected because if not, an empty Unicode string is sent, which does not mach the '' selector.
+ link_form = LinkForm(link_type="video")
+ license_plates_form = LicensePlateForm(state="AZ")
+ form = IncidentForm(
+ date_field=str(date.date()),
+ time_field=str(date.time()),
+ report_number=report_number,
+ description="Something happened",
+ department="1",
+ address=address_form.data,
+ links=[link_form.data],
+ license_plates=[license_plates_form.data],
+ officers=[],
+ )
+ data = process_form_data(form.data)
+
+ rv = client.post(
+ url_for("main.incident_api") + "new", data=data, follow_redirects=True
+ )
+
+ assert rv.status_code == 200
+ assert "Report cannot contain special characters" in rv.data.decode("utf-8")
+
+
def test_admins_can_edit_incident_date_and_address(mockdata, client, session):
with current_app.test_request_context():
login_admin(client)
diff --git a/OpenOversight/tests/test_functional.py b/OpenOversight/tests/test_functional.py
index 1cf266c44..7cb10a753 100644
--- a/OpenOversight/tests/test_functional.py
+++ b/OpenOversight/tests/test_functional.py
@@ -14,6 +14,9 @@
from OpenOversight.app.models import Department, Incident, Officer, Unit, db
+DESCRIPTION_CUTOFF = 500
+
+
@contextmanager
def wait_for_page_load(browser, timeout=10):
old_page = browser.find_element_by_tag_name("html")
@@ -150,14 +153,14 @@ def test_find_officer_cannot_see_uii_question_for_depts_without_uiis(mockdata, b
assert len(results) == 0
-def test_incident_detail_display_read_more_button_for_descriptions_over_300_chars(
+def test_incident_detail_display_read_more_button_for_descriptions_over_cutoff(
mockdata, browser
):
# Navigate to profile page for officer with short and long incident descriptions
browser.get("http://localhost:5000/officer/1")
incident_long_descrip = Incident.query.filter(
- func.length(Incident.description) > 300
+ func.length(Incident.description) > DESCRIPTION_CUTOFF
).one_or_none()
incident_id = str(incident_long_descrip.id)
@@ -165,13 +168,13 @@ def test_incident_detail_display_read_more_button_for_descriptions_over_300_char
assert result.is_displayed()
-def test_incident_detail_do_not_display_read_more_button_for_descriptions_under_300_chars(
+def test_incident_detail_do_not_display_read_more_button_for_descriptions_under_cutoff(
mockdata, browser
):
# Navigate to profile page for officer with short and long incident descriptions
browser.get("http://localhost:5000/officer/1")
- # Select incident for officer that has description under 300 chars
+ # Select incident for officer that has description under cuttoff chars
result = browser.find_element_by_id("description-overflow-row_1")
assert not result.is_displayed()
@@ -181,9 +184,9 @@ def test_click_to_read_more_displays_full_description(mockdata, browser):
browser.get("http://localhost:5000/officer/1")
incident_long_descrip = Incident.query.filter(
- func.length(Incident.description) > 300
+ func.length(Incident.description) > DESCRIPTION_CUTOFF
).one_or_none()
- orig_descrip = incident_long_descrip.description
+ orig_descrip = incident_long_descrip.description.strip()
incident_id = str(incident_long_descrip.id)
button = browser.find_element_by_id("description-overflow-button_" + incident_id)
@@ -191,7 +194,7 @@ def test_click_to_read_more_displays_full_description(mockdata, browser):
description_text = browser.find_element_by_id(
"incident-description_" + incident_id
- ).text
+ ).text.strip()
assert len(description_text) == len(orig_descrip)
assert description_text == orig_descrip
@@ -201,7 +204,7 @@ def test_click_to_read_more_hides_the_read_more_button(mockdata, browser):
browser.get("http://localhost:5000/officer/1")
incident_long_descrip = Incident.query.filter(
- func.length(Incident.description) > 300
+ func.length(Incident.description) > DESCRIPTION_CUTOFF
).one_or_none()
incident_id = str(incident_long_descrip.id)