Skip to content

Commit e041f7f

Browse files
authored
Add incident search (#122)
* Add incident search * Update report number query to strip input and do inclusive search
1 parent 097ad91 commit e041f7f

File tree

4 files changed

+171
-51
lines changed

4 files changed

+171
-51
lines changed

OpenOversight/app/main/forms.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -597,3 +597,11 @@ class BrowseForm(Form):
597597
validators=[AnyOf(allowed_values(AGE_CHOICES))],
598598
)
599599
submit = SubmitField(label="Submit")
600+
601+
602+
class IncidentListForm(Form):
603+
department_id = HiddenField("Department Id")
604+
report_number = StringField("Report Number")
605+
occurred_before = DateField("Occurred Before")
606+
occurred_after = DateField("Occurred After")
607+
submit = SubmitField(label="Submit")

OpenOversight/app/main/views.py

Lines changed: 62 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import datetime
12
import os
23
import re
34
import sys
@@ -79,6 +80,7 @@
7980
FindOfficerForm,
8081
FindOfficerIDForm,
8182
IncidentForm,
83+
IncidentListForm,
8284
OfficerLinkForm,
8385
SalaryForm,
8486
TextForm,
@@ -1368,26 +1370,72 @@ class IncidentApi(ModelView):
13681370
department_check = True
13691371

13701372
def get(self, obj_id):
1373+
if obj_id:
1374+
# Single-item view
1375+
return super(IncidentApi, self).get(obj_id)
1376+
1377+
# List view
13711378
if request.args.get("page"):
13721379
page = int(request.args.get("page"))
13731380
else:
13741381
page = 1
1375-
if request.args.get("department_id"):
1376-
department_id = request.args.get("department_id")
1382+
1383+
form = IncidentListForm()
1384+
incidents = self.model.query
1385+
1386+
dept = None
1387+
if department_id := request.args.get("department_id"):
13771388
dept = Department.query.get_or_404(department_id)
1378-
obj = (
1379-
self.model.query.filter_by(department_id=department_id)
1380-
.order_by(getattr(self.model, self.order_by).desc())
1381-
.paginate(page, self.per_page, False)
1382-
)
1383-
return render_template(
1384-
"{}_list.html".format(self.model_name),
1385-
objects=obj,
1386-
url="main.{}_api".format(self.model_name),
1387-
department=dept,
1389+
form.department_id.data = department_id
1390+
incidents = incidents.filter_by(department_id=department_id)
1391+
1392+
if report_number := request.args.get("report_number"):
1393+
form.report_number.data = report_number
1394+
incidents = incidents.filter(
1395+
Incident.report_number.contains(report_number.strip())
13881396
)
1389-
else:
1390-
return super(IncidentApi, self).get(obj_id)
1397+
1398+
if occurred_before := request.args.get("occurred_before"):
1399+
before_date = datetime.datetime.strptime(occurred_before, "%Y-%m-%d").date()
1400+
form.occurred_before.data = before_date
1401+
incidents = incidents.filter(self.model.date < before_date)
1402+
1403+
if occurred_after := request.args.get("occurred_after"):
1404+
after_date = datetime.datetime.strptime(occurred_after, "%Y-%m-%d").date()
1405+
form.occurred_after.data = after_date
1406+
incidents = incidents.filter(self.model.date > after_date)
1407+
1408+
incidents = incidents.order_by(
1409+
getattr(self.model, self.order_by).desc()
1410+
).paginate(page, self.per_page, False)
1411+
1412+
url = "main.{}_api".format(self.model_name)
1413+
next_url = url_for(
1414+
url,
1415+
page=incidents.next_num,
1416+
department_id=department_id,
1417+
report_number=report_number,
1418+
occurred_after=occurred_after,
1419+
occurred_before=occurred_before,
1420+
)
1421+
prev_url = url_for(
1422+
url,
1423+
page=incidents.prev_num,
1424+
department_id=department_id,
1425+
report_number=report_number,
1426+
occurred_after=occurred_after,
1427+
occurred_before=occurred_before,
1428+
)
1429+
1430+
return render_template(
1431+
"{}_list.html".format(self.model_name),
1432+
form=form,
1433+
incidents=incidents,
1434+
url=url,
1435+
next_url=next_url,
1436+
prev_url=prev_url,
1437+
department=dept,
1438+
)
13911439

13921440
def get_new_form(self):
13931441
form = self.form()
Lines changed: 73 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -1,43 +1,79 @@
1-
{% extends "list.html" %}
1+
{% extends "base.html" %}
2+
{% import "bootstrap/wtf.html" as wtf %}
23
{% block title %}View incidents - OpenOversight{% endblock %}
34
{% block meta %}
4-
{% if objects.items|length > 0 %}
5-
<meta name="description" content="View all incidents for {{ department.name if department else 'OpenOversight' }}.">
6-
{% else %}
7-
<meta name="description" content="No incidents found.">
8-
{% endif %}
5+
{% if incidents.items|length > 0 %}
6+
<meta name="description" content="View all incidents for {{ department.name if department else 'OpenOversight' }}.">
7+
{% else %}
8+
<meta name="description" content="No incidents found.">
9+
{% endif %}
910
{% endblock %}
10-
{% block list %}
11-
<h1>Incidents</h1>
12-
{% if department %}
13-
<h2><small>{{ department.name }}</small></h2>
14-
{% endif %}
15-
<ul class="list-group">
16-
{% if objects.items %}
17-
{% for incident in objects.items %}
18-
<li class="list-group-item">
19-
<h3>
20-
<a href="{{ url_for('main.incident_api', obj_id=incident.id)}}">
21-
Incident
22-
{% if incident.report_number %}
23-
{{ incident.report_number }}
24-
{% endif %}
25-
</a>
26-
</h3>
27-
{% include 'partials/incident_fields.html' %}
28-
</li>
29-
{% endfor %}
30-
{% else %}
31-
<p>There are no incidents.</p>
32-
{% endif %}
33-
</ul>
34-
{% if current_user.is_administrator or current_user.is_area_coordinator %}
35-
<a href="{{ url_for('main.incident_api')+ 'new' }}" class="btn btn-primary" role="button">
36-
<span class="glyphicon glyphicon-plus" aria-hidden="true"></span>
37-
Add New Incident
38-
</a>
39-
{% endif %}
40-
{% endblock list %}
11+
{% block content %}
12+
<div class="container" role="main">
13+
<h1>Incidents</h1>
14+
{% if department %}
15+
<h2><small>{{ department.name }}</small></h2>
16+
{% endif %}
17+
<div class="row">
18+
<div class="filter-sidebar col-sm-3">
19+
<h3 class="sidebar-title">Filter incidents</h3>
20+
<form class="form" method="get" role="form">
21+
{% if department %}
22+
<div class="hidden">
23+
{{ wtf.form_field(form.department_id) }}
24+
</div>
25+
{% endif %}
26+
<div class="panel">
27+
{{ wtf.form_field(form.report_number) }}
28+
</div>
29+
<div class="panel">
30+
{{ wtf.form_field(form.occurred_before) }}
31+
</div>
32+
<div class="panel">
33+
{{ wtf.form_field(form.occurred_after) }}
34+
</div>
35+
<div class="panel">
36+
{{ wtf.form_field(form.submit, id="submit", button_map={'submit':'primary'}) }}
37+
</div>
38+
</form>
39+
</div>
40+
<div class="search-results col-sm-9">
41+
{% with paginate=incidents, location='top' %}
42+
{% include "partials/paginate_nav.html" %}
43+
{% endwith %}
44+
45+
<ul class="list-group">
46+
{% if incidents.items %}
47+
{% for incident in incidents.items %}
48+
<li class="list-group-item">
49+
<h3>
50+
<a href="{{ url_for('main.incident_api', obj_id=incident.id)}}">
51+
Incident
52+
{% if incident.report_number %}
53+
{{ incident.report_number }}
54+
{% endif %}
55+
</a>
56+
</h3>
57+
{% include 'partials/incident_fields.html' %}
58+
</li>
59+
{% endfor %}
60+
{% else %}
61+
<p>There are no incidents.</p>
62+
{% endif %}
63+
</ul>
64+
{% if current_user.is_administrator or current_user.is_area_coordinator %}
65+
<a href="{{ url_for('main.incident_api')+ 'new' }}" class="btn btn-primary" role="button">
66+
<span class="glyphicon glyphicon-plus" aria-hidden="true"></span>
67+
Add New Incident
68+
</a>
69+
{% endif %}
70+
{% with paginate=incidents, location='bottom' %}
71+
{% include "partials/paginate_nav.html" %}
72+
{% endwith %}
73+
</div>
74+
</div>
75+
</div>
76+
{% endblock content %}
4177
{% block js_footer %}
4278
<script src="{{ url_for('static', filename='js/incidentDescription.js') }}"></script>
4379
{% endblock %}

OpenOversight/tests/routes/test_incidents.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -689,3 +689,31 @@ def test_form_with_officer_id_prepopulates(mockdata, client, session):
689689
url_for("main.incident_api") + "new?officer_id={}".format(officer_id)
690690
)
691691
assert officer_id in rv.data.decode("utf-8")
692+
693+
694+
@pytest.mark.parametrize(
695+
"params,included_report_nums,excluded_report_nums",
696+
[
697+
({"department_id": "1"}, ["42"], ["38", "39"]),
698+
({"occurred_before": "2017-12-12"}, ["38", "42"], ["39"]),
699+
(
700+
{"occurred_after": "2017-12-10", "occurred_before": "2019-01-01"},
701+
["38"],
702+
["39", "42"],
703+
),
704+
({"report_number": "38"}, ["38"], ["42", "39"]), # Base case
705+
({"report_number": "3"}, ["38", "39"], ["42"]), # Test inclusive match
706+
({"report_number": "38 "}, ["38"], ["42", "39"]), # Test trim
707+
],
708+
)
709+
def test_users_can_search_incidents(
710+
params, included_report_nums, excluded_report_nums, mockdata, client, session
711+
):
712+
with current_app.test_request_context():
713+
rv = client.get(url_for("main.incident_api", **params))
714+
715+
for report_num in included_report_nums:
716+
assert "<td>{}</td>".format(report_num) in rv.data.decode("utf-8")
717+
718+
for report_num in excluded_report_nums:
719+
assert "<td>{}</td>".format(report_num) not in rv.data.decode("utf-8")

0 commit comments

Comments
 (0)