Skip to content

Commit eef7d5f

Browse files
Munger: OPA Incidents, improved detail formatting (#99)
* Add data munging * Documentation & comments * Rearrange partials, force column ordering * Improve long-form text layout * Only show a cross-street if we have one * Give the incident detail page more padding on the bottom * Add ellipses to cut off text * Only show street name if we have one * Formatting * Add "OPA Case" prefix to incident title * Only show unique internal identifier if it exists * Add args to just recipes, start stack after fresh start * Allow incident descriptions to have HTML * Improve OPA incident description formatting, add officer name * Remove OPA Case prefix when matching links * Formatting! * Remove redundant "Incident" in report number title * Add "number of known incidents" to general information section * Allow spaces in incident names (but no other special characters) * Add known incident count to officer list * Have acceptance tests be a separate step This should make it easier to run the regular tests, then the functional ones in dev while not having to run the regular ones twice * Fix the functional tests based on new description cutoff * Remove conditional for known incidents
1 parent 07f25b6 commit eef7d5f

18 files changed

+639
-337
lines changed

.github/workflows/pull_request.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,4 +25,5 @@ jobs:
2525
- name: Run tests
2626
run: |
2727
just build
28+
just test
2829
just test-acceptance

OpenOversight/app/main/forms.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -514,8 +514,8 @@ class IncidentForm(DateFieldForm):
514514
report_number = StringField(
515515
validators=[
516516
Regexp(
517-
r"^[a-zA-Z0-9-]*$",
518-
message="Report numbers can contain letters, numbers, and dashes",
517+
r"^[a-zA-Z0-9- ]*$",
518+
message="Report cannot contain special characters (dashes permitted)",
519519
)
520520
],
521521
description="Incident number for the organization tracking incidents",

OpenOversight/app/static/js/incidentDescription.js

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,17 @@
11
$(document).ready(function() {
2+
let overflow_length = 500;
23
$(".incident-description").each(function () {
34
let description = this;
45
let incidentId = $( this ).data("incident");
5-
if (description.innerText.length < 300) {
6+
if (description.innerHTML.length < overflow_length) {
67
$("#description-overflow-row_" + incidentId).hide();
78
}
8-
if(description.innerText.length > 300) {
9-
let originalDescription = description.innerText;
10-
description.innerText = description.innerText.substring(0, 300);
9+
if(description.innerHTML.length > overflow_length) {
10+
let originalDescription = description.innerHTML;
11+
description.innerHTML = description.innerHTML.substring(0, overflow_length) + "…";
1112
$(`#description-overflow-button_${incidentId}`).on('click', function(event) {
12-
description.innerText = originalDescription;
13+
event.stopImmediatePropagation();
14+
description.innerHTML = originalDescription;
1315
$("#description-overflow-row_" + incidentId).hide();
1416
})
1517
}

OpenOversight/app/templates/incident_detail.html

Lines changed: 40 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,13 @@
22
{% set incident = obj %}
33
{% block title %}{{ incident.department.name }} incident{% if incident.report_number %} {{incident.report_number}}{% endif %} - OpenOversight{% endblock %}
44
{% block meta %}
5-
{% if incident.description != None %}
6-
<meta name="description" content="{{ incident.description }}">
7-
{% else %}
8-
<meta name="description" content="View details for {{ incident.department.name }} incident{% if incident.report_number %} {{incident.report_number}}{% endif %}.">
9-
{% endif %}
10-
<!-- Google Breadcrumb https://developers.google.com/search/docs/data-types/breadcrumb -->
11-
<script type="application/ld+json">
5+
{% if incident.description != None %}
6+
<meta name="description" content="{{ incident.description }}">
7+
{% else %}
8+
<meta name="description" content="View details for {{ incident.department.name }} incident{% if incident.report_number %} {{incident.report_number}}{% endif %}.">
9+
{% endif %}
10+
<!-- Google Breadcrumb https://developers.google.com/search/docs/data-types/breadcrumb -->
11+
<script type="application/ld+json">
1212
{
1313
"@context": "https://schema.org",
1414
"@type": "BreadcrumbList",
@@ -31,34 +31,38 @@
3131
</script>
3232
{% endblock %}
3333
{% block content %}
34-
<main class='container pt-35' role='main'>
35-
<a href="{{ url_for('main.incident_api')}}">All Incidents</a>
36-
{% if incident.department %}
37-
<p>
38-
<a href="{{ url_for('main.incident_api', department_id=incident.department_id)}}">More incidents in the {{ incident.department.name }}</a>
39-
</p>
40-
{% endif %}
41-
<h1>Incident {% if incident.report_number %}{{incident.report_number}}{% endif %}</h1>
42-
<div class='row'>
43-
<div class="col-sm-12 col-md-6">
44-
{% with detail=True %}
45-
{% include 'partials/incident_fields.html' %}
46-
{% endwith %}
47-
</div>
48-
<div class="col-sm-12 col-md-6">
49-
<h5>Description</h5>
50-
<p>{{ incident.description }}</p>
51-
</div>
52-
</div>
53-
{% include 'partials/links_and_videos_row.html' %}
54-
{% if current_user.is_administrator
34+
<main class='container pt-35 pb-50' role='main'>
35+
<a href="{{ url_for('main.incident_api')}}">All Incidents</a>
36+
{% if incident.department %}
37+
<p>
38+
<a href="{{ url_for('main.incident_api', department_id=incident.department_id)}}">More incidents in the {{ incident.department.name }}</a>
39+
</p>
40+
{% endif %}
41+
<h1>{% if incident.report_number %}{{incident.report_number}}{% endif %}</h1>
42+
<div class='row'>
43+
<div class="col-sm-12 col-md-6">
44+
{% with detail=True %}
45+
{% include 'partials/incident_fields.html' %}
46+
{% endwith %}
47+
</div>
48+
<div class="col-sm-12 col-md-6">
49+
<h4>Description</h4>
50+
<br>
51+
{% for paragraph in incident.description.split('\n') %}
52+
{{ paragraph | safe }}<br/>
53+
{% endfor %}
54+
</p>
55+
</div>
56+
</div>
57+
{% include 'partials/links_and_videos_row.html' %}
58+
{% if current_user.is_administrator
5559
or (current_user.is_area_coordinator and current_user.ac_department_id == incident.department_id) %}
56-
<div class="row">
57-
<div class="col-sm-12 col-md-6">
58-
<a class="btn btn-primary" href="{{ '{}/edit'.format(url_for('main.incident_api', obj_id=incident.id))}}" role="button">Edit</a>
59-
<a class="btn btn-danger" href="{{ '{}/delete'.format(url_for('main.incident_api', obj_id=incident.id))}}" role="button">Delete</a>
60-
</div>
61-
</div>
62-
{% endif %}
63-
</main>
60+
<div class="row">
61+
<div class="col-sm-12 col-md-6">
62+
<a class="btn btn-primary" href="{{ '{}/edit'.format(url_for('main.incident_api', obj_id=incident.id))}}" role="button">Edit</a>
63+
<a class="btn btn-danger" href="{{ '{}/delete'.format(url_for('main.incident_api', obj_id=incident.id))}}" role="button">Delete</a>
64+
</div>
65+
</div>
66+
{% endif %}
67+
</main>
6468
{% endblock %}

OpenOversight/app/templates/list_officer.html

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -160,6 +160,8 @@ <h2>
160160
<dd>{{ officer.unit_descrip()|default('Unknown') }}</dd>
161161
<dt>Currently on the Force</dt>
162162
<dd>{{ officer.currently_on_force() }}</dd>
163+
<dt>Known incidents</dt>
164+
<dd>{{ officer.incidents | length }}</dd>
163165
</dl>
164166
</div>
165167
<div class="col-md-6 col-xs-6">

OpenOversight/app/templates/officer.html

Lines changed: 16 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -109,35 +109,27 @@ <h1>Officer Detail: <b>{{ officer.full_name() }}</b></h1>
109109
<div class="row">
110110
<div class="col-sm-12 col-md-6">
111111
{% include "partials/officer_assignment_history.html" %}
112+
{% if officer.descriptions or is_admin_or_coordinator %}
113+
{% include "partials/officer_descriptions.html" %}
114+
{% endif %}
115+
{# Notes are for internal use #}
116+
{% if is_admin_or_coordinator %}
117+
{% include "partials/officer_notes.html" %}
118+
{% endif %}
119+
{% with obj=officer %}
120+
{% include "partials/links_and_videos_row.html" %}
121+
{% endwith %}
112122
</div> {# end col #}
113123

114-
{% if officer.salaries or is_admin_or_coordinator %}
115-
<div class='col-sm-12 col-md-6'>
124+
<div class='col-sm-12 col-md-6'>
125+
{% if officer.salaries or is_admin_or_coordinator %}
116126
{% include "partials/officer_salary.html" %}
117-
</div>{# end col #}
118-
{% endif %}
119-
120-
{% if officer.incidents or is_admin_or_coordinator %}
121-
<div class='col-sm-12 col-md-6'>
127+
{% endif %}
128+
{% if officer.incidents or is_admin_or_coordinator %}
122129
{% include "partials/officer_incidents.html" %}
123-
</div>{# end col #}
124-
{% endif %}
125-
126-
{% if officer.descriptions or is_admin_or_coordinator %}
127-
<div class='col-sm-12 col-md-6'>
128-
{% include "partials/officer_descriptions.html" %}
129-
</div>{# end col #}
130-
{% endif %}
131-
132-
{# Notes are for internal use #}
133-
{% if is_admin_or_coordinator %}
134-
<div class='col-sm-12 col-md-6'>
135-
{% include "partials/officer_notes.html" %}
130+
{% endif %}
136131
</div>{# end col #}
137-
{% endif %}
132+
138133
</div> {# end row #}
139-
{% with obj=officer %}
140-
{% include "partials/links_and_videos_row.html" %}
141-
{% endwith %}
142134
</div> {# end container #}
143135
{% endblock %}
Lines changed: 91 additions & 88 deletions
Original file line numberDiff line numberDiff line change
@@ -1,93 +1,96 @@
11
<table class='table table-hover table-responsive'>
2-
<tbody>
3-
<tr>
4-
<td><strong>Date</strong></td>
5-
<td>{{ incident.date.strftime('%b %d, %Y') }}</td>
6-
</tr>
7-
<tr>
8-
<td><strong>Time</strong></td>
9-
<td>{% if incident.time %}{{ incident.time.strftime('%l:%M %p') }}{% endif %}</td>
10-
</tr>
11-
{% if incident.report_number %}
12-
<tr>
13-
<td><strong>Report #</strong></td>
14-
<td>{{ incident.report_number }}</td>
15-
</tr>
16-
{% endif %}
17-
<tr>
18-
<td><strong>Department</strong></td>
19-
<td>{{ incident.department.name }}</td>
20-
</tr>
21-
{% if incident.officers %}
22-
<tr>
23-
<td><strong>Officers</strong></td>
24-
<td>
25-
{% for officer in incident.officers %}
26-
<a href="{{url_for('main.officer_profile', officer_id=officer.id)}}">{{ officer.full_name()|title }}</a>{% if not loop.last %}, {% endif %}
27-
{% endfor %}
28-
</td>
29-
</tr>
30-
{% endif %}
31-
{% if detail and incident.license_plates %}
32-
<tr>
33-
<td><strong>License Plates</strong></td>
34-
<td>
35-
{% for plate in incident.license_plates %}
36-
{{ plate.state }} {{ plate.number }}{% if not loop.last %}<br/> {% endif %}
37-
{% endfor %}
38-
</td>
39-
</tr>
40-
{% endif %}
41-
{% if not detail %}
42-
<tr>
43-
<td><strong>Description</strong></td>
44-
<td class="incident-description" id="incident-description_{{incident.id}}" data-incident='{{incident.id | tojson}}'>
45-
{{ incident.description }}
46-
</td>
47-
</tr>
48-
<tr id="description-overflow-row_{{ incident.id }}" >
49-
<td style="border-top: none"></td>
50-
<td style="border-top: none" id="description-overflow-cell_{{ incident.id }}">
51-
<button id="description-overflow-button_{{ incident.id }}">
52-
Click to read more
53-
</button>
54-
</td>
55-
</tr>
56-
{% endif %}
57-
{% with address=incident.address %}
58-
{% if address %}
59-
<tr>
60-
<td><strong>Address</strong></td>
61-
<td>
62-
{{ address.street_name }}
63-
{% if address.cross_street1 and address.cross_street2 %}
64-
<em>between</em> {{ address.cross_street1 }} <em>and</em> {{ address.cross_street2 }}
65-
{% else %}
66-
<em>near</em> {{ address.cross_street1 }}
67-
{% endif %}
68-
<br/>
69-
{{ address.city }}, {{ address.state }} {% if address.zipcode %} {{ address.zip_code }} {% endif %}
70-
</td>
71-
</tr>
72-
{% endif %}
73-
{% endwith %}
74-
{% if detail and current_user.is_administrator %}
75-
{% if incident.creator %}
76-
<tr>
77-
<td><strong>Creator</strong></td>
78-
<td><a href="{{ url_for('main.profile', username=incident.creator.username)}}">{{ incident.creator.username }}</a></td>
79-
</tr>
80-
{% endif %}
81-
{% if incident.last_updated_by %}
82-
<tr>
83-
<td><strong>Last Edited By</strong></td>
84-
<td><a href="{{ url_for('main.profile', username=incident.last_updated_by.username)}}">{{ incident.last_updated_by.username }}</a></td>
85-
</tr>
86-
{% endif %}
87-
{% endif %}
88-
</tbody>
2+
<tbody>
3+
<tr>
4+
<td><strong>Date</strong></td>
5+
<td>{{ incident.date.strftime('%b %d, %Y') }}</td>
6+
</tr>
7+
<tr>
8+
<td><strong>Time</strong></td>
9+
<td>{% if incident.time %}{{ incident.time.strftime('%l:%M %p') }}{% endif %}</td>
10+
</tr>
11+
{% if incident.report_number %}
12+
<tr>
13+
<td><strong>Report #</strong></td>
14+
<td>{{ incident.report_number }}</td>
15+
</tr>
16+
{% endif %}
17+
<tr>
18+
<td><strong>Department</strong></td>
19+
<td>{{ incident.department.name }}</td>
20+
</tr>
21+
{% if incident.officers %}
22+
<tr>
23+
<td><strong>Officers</strong></td>
24+
<td>
25+
{% for officer in incident.officers %}
26+
<a href="{{url_for('main.officer_profile', officer_id=officer.id)}}">{{ officer.full_name()|title }}</a>{% if not loop.last %}, {% endif %}
27+
{% endfor %}
28+
</td>
29+
</tr>
30+
{% endif %}
31+
{% if detail and incident.license_plates %}
32+
<tr>
33+
<td><strong>License Plates</strong></td>
34+
<td>
35+
{% for plate in incident.license_plates %}
36+
{{ plate.state }} {{ plate.number }}{% if not loop.last %}<br/> {% endif %}
37+
{% endfor %}
38+
</td>
39+
</tr>
40+
{% endif %}
41+
{% if not detail %}
42+
<tr>
43+
<td><strong>Description</strong></td>
44+
<td class="incident-description" id="incident-description_{{incident.id}}" data-incident='{{incident.id | tojson}}'>
45+
{% for paragraph in incident.description.split('\n') %}
46+
{{ paragraph | safe }}<br/>
47+
{% endfor %}
48+
</td>
49+
</tr>
50+
<tr id="description-overflow-row_{{ incident.id }}" >
51+
<td style="border-top: none"></td>
52+
<td style="border-top: none" id="description-overflow-cell_{{ incident.id }}">
53+
<button id="description-overflow-button_{{ incident.id }}">
54+
Click to read more
55+
</button>
56+
</td>
57+
</tr>
58+
{% endif %}
59+
{% with address=incident.address %}
60+
{% if address %}
61+
<tr>
62+
<td><strong>Address</strong></td>
63+
<td>
64+
{% if address.street_name %}
65+
{{ address.street_name }}<br/>
66+
{% endif %}
67+
{% if address.cross_street1 and address.cross_street2 %}
68+
<em>between</em> {{ address.cross_street1 }} <em>and</em> {{ address.cross_street2 }}
69+
{% elif address.cross_street1 %}
70+
<em>near</em> {{ address.cross_street1 }}
71+
{% endif %}
72+
{{ address.city }}, {{ address.state }} {% if address.zipcode %} {{ address.zip_code }} {% endif %}
73+
</td>
74+
</tr>
75+
{% endif %}
76+
{% endwith %}
77+
{% if detail and current_user.is_administrator %}
78+
{% if incident.creator %}
79+
<tr>
80+
<td><strong>Creator</strong></td>
81+
<td><a href="{{ url_for('main.profile', username=incident.creator.username)}}">{{ incident.creator.username }}</a></td>
82+
</tr>
83+
{% endif %}
84+
{% if incident.last_updated_by %}
85+
<tr>
86+
<td><strong>Last Edited By</strong></td>
87+
<td><a href="{{ url_for('main.profile', username=incident.last_updated_by.username)}}">{{ incident.last_updated_by.username }}</a></td>
88+
</tr>
89+
{% endif %}
90+
{% endif %}
91+
</tbody>
8992
</table>
9093

9194
{% block js_footer %}
92-
<script src="{{ url_for('static', filename='js/incidentDescription.js') }}"></script>
95+
<script src="{{ url_for('static', filename='js/incidentDescription.js') }}"></script>
9396
{% endblock %}

0 commit comments

Comments
 (0)