Skip to content

Commit 30068f2

Browse files
Improve unit & rank search filters (#91)
* Make default arguments immutable * Make unit a multiple-select option * Add unit description to officer list, re-arrange items * Convert currently_on_force to property, add to officer list * Make really long form filters scrollable * Move uncertain currently_on_force logic out of template * Add comment describing behavior * Allow unit to be missing * Only scroll if the box overflows, set max height
1 parent e4e6f1b commit 30068f2

File tree

9 files changed

+84
-40
lines changed

9 files changed

+84
-40
lines changed

OpenOversight/app/__init__.py

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -107,13 +107,6 @@ class which will render the field accordion open.
107107
"""
108108
return " in " if form_data.get(field) else ""
109109

110-
@app.template_filter("currently_on_force")
111-
def officer_currently_on_force(assignments):
112-
if not assignments:
113-
return "Uncertain"
114-
most_recent = max(assignments, key=lambda x: x.star_date or datetime.date.min)
115-
return "Yes" if most_recent.resign_date is None else "No"
116-
117110
# Add commands
118111
Migrate(
119112
app, db, os.path.join(os.path.dirname(__file__), "..", "migrations")

OpenOversight/app/main/forms.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -563,6 +563,12 @@ class BrowseForm(Form):
563563
get_label="job_title",
564564
get_pk=lambda job: job.job_title,
565565
) # query set in view function
566+
unit = QuerySelectField(
567+
"unit",
568+
validators=[Optional()],
569+
get_label="descrip",
570+
get_pk=lambda unit: unit.descrip,
571+
) # query set in view function
566572
name = StringField("Last name")
567573
badge = StringField("Badge number")
568574
unique_internal_identifier = StringField("Unique ID")

OpenOversight/app/main/views.py

Lines changed: 16 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -613,9 +613,9 @@ def edit_department(department_id):
613613
def list_officer(
614614
department_id,
615615
page=1,
616-
race=[],
617-
gender=[],
618-
rank=[],
616+
race=None,
617+
gender=None,
618+
rank=None,
619619
min_age="16",
620620
max_age="100",
621621
name=None,
@@ -630,14 +630,14 @@ def list_officer(
630630
.all()
631631
)
632632
form_data = form.data
633-
form_data["race"] = race
634-
form_data["gender"] = gender
635-
form_data["rank"] = rank
633+
form_data["race"] = race or []
634+
form_data["gender"] = gender or []
635+
form_data["rank"] = rank or []
636636
form_data["min_age"] = min_age
637637
form_data["max_age"] = max_age
638638
form_data["name"] = name
639639
form_data["badge"] = badge
640-
form_data["unit"] = unit
640+
form_data["unit"] = unit or []
641641
form_data["unique_internal_identifier"] = unique_internal_identifier
642642

643643
OFFICERS_PER_PAGE = int(current_app.config["OFFICERS_PER_PAGE"])
@@ -658,8 +658,6 @@ def list_officer(
658658
form_data["name"] = name_arg
659659
if badge_arg := request.args.get("badge"):
660660
form_data["badge"] = badge_arg
661-
if (unit_arg := request.args.get("unit")) and unit_arg != "Not Sure":
662-
form_data["unit"] = int(unit_arg)
663661
if uid := request.args.get("unique_internal_identifier"):
664662
form_data["unique_internal_identifier"] = uid
665663
if (races := request.args.getlist("race")) and all(
@@ -674,18 +672,23 @@ def list_officer(
674672
form_data["gender"] = genders
675673

676674
unit_choices = [
677-
(unit.id, unit.descrip)
678-
for unit in Unit.query.filter_by(department_id=department_id)
675+
uc[0]
676+
for uc in db.session.query(Unit.descrip)
677+
.filter_by(department_id=department_id)
679678
.order_by(Unit.descrip.asc())
680679
.all()
681680
]
682681
rank_choices = [
683682
jc[0]
684683
for jc in db.session.query(Job.job_title, Job.order)
685684
.filter_by(department_id=department_id)
686-
.order_by(Job.order)
685+
.order_by(Job.job_title)
687686
.all()
688687
]
688+
if (units := request.args.getlist("unit")) and all(
689+
unit in unit_choices for unit in units
690+
):
691+
form_data["unit"] = units
689692
if (ranks := request.args.getlist("rank")) and all(
690693
rank in rank_choices for rank in ranks
691694
):
@@ -710,7 +713,7 @@ def list_officer(
710713
"race": RACE_CHOICES,
711714
"gender": GENDER_CHOICES,
712715
"rank": [(rc, rc) for rc in rank_choices],
713-
"unit": [("Not Sure", "Not Sure")] + unit_choices,
716+
"unit": [("Not Sure", "Not Sure")] + [(uc, uc) for uc in unit_choices],
714717
}
715718

716719
next_url = url_for(

OpenOversight/app/models.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -188,12 +188,27 @@ def job_title(self):
188188
self.assignments_lazy, key=lambda x: x.star_date or date.min
189189
).job.job_title
190190

191+
def unit_descrip(self):
192+
if self.assignments_lazy:
193+
unit = max(
194+
self.assignments_lazy, key=lambda x: x.star_date or date.min
195+
).unit
196+
return unit.descrip if unit else None
197+
191198
def badge_number(self):
192199
if self.assignments_lazy:
193200
return max(
194201
self.assignments_lazy, key=lambda x: x.star_date or date.min
195202
).star_no
196203

204+
def currently_on_force(self):
205+
if self.assignments_lazy:
206+
most_recent = max(
207+
self.assignments_lazy, key=lambda x: x.star_date or date.min
208+
)
209+
return "Yes" if most_recent.resign_date is None else "No"
210+
return "Uncertain"
211+
197212
def __repr__(self):
198213
if self.unique_internal_identifier:
199214
return "<Officer ID {}: {} {} {} {} ({})>".format(

OpenOversight/app/static/css/openoversight.css

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -484,6 +484,11 @@ tr:hover .row-actions {
484484
padding: 5px 0;
485485
}
486486

487+
.filter-sidebar .panel-body-long {
488+
max-height: 20vh;
489+
overflow-y: auto;
490+
}
491+
487492
.filter-sidebar .panel-heading .accordion-toggle:after {
488493
font-family: 'Glyphicons Halflings';
489494
content: "\e252";

OpenOversight/app/templates/list_officer.html

Lines changed: 16 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@ <h3 class="panel-title accordion-toggle">Gender</h3>
7070
<h3 class="panel-title accordion-toggle">Rank</h3>
7171
</div>
7272
<div class="collapse {{ form_data | field_in_query("rank") }}" id="filter-rank">
73-
<div class="panel-body">
73+
<div class="panel-body panel-body-long">
7474
<div class="form-group checkbox">
7575
{% for choice in choices['rank'] %}
7676
<label class="form-check">
@@ -86,13 +86,15 @@ <h3 class="panel-title accordion-toggle">Rank</h3>
8686
<h3 class="panel-title accordion-toggle">Unit</h3>
8787
</div>
8888
<div class="collapse {{ form_data | field_in_query("unit") }}" id="filter-unit">
89-
<div class="panel-body">
89+
<div class="panel-body panel-body-long">
9090
<div class="form-group">
91-
<select class="form-control" name="unit">
92-
{% for choice in choices['unit'] %}
93-
<option id="unit-{{ choice[0] }}" value="{{choice[0]}}" {% if choice[0] == form_data['unit'] %}selected='true'{% endif %}>{{choice[1]}}</option>
94-
{% endfor %}
95-
</select>
91+
<div class="form-group checkbox">
92+
{% for choice in choices['unit'] %}
93+
<label class="form-check">
94+
<input type="checkbox" class="form-check-input" id="unit-{{ choice[0] }}" name="unit" value="{{ choice[0] }}" {% if choice[0] in form_data['unit'] %}checked="checked" {% endif %}/>{{choice[1]}}
95+
</label>
96+
{% endfor %}
97+
</div>
9698
</div>
9799
</div>
98100
</div>
@@ -152,14 +154,18 @@ <h2>
152154
<div class="row">
153155
<div class="col-md-6 col-xs-6">
154156
<dl>
155-
<dt>Job Title</dt>
157+
<dt>Rank</dt>
156158
<dd>{{ officer.job_title()|default('Unknown') }}</dd>
157-
<dt>Race</dt>
158-
<dd>{{ officer.race_label()|default('Unknown')|lower|title }}</dd>
159+
<dt>Unit</dt>
160+
<dd>{{ officer.unit_descrip()|default('Unknown') }}</dd>
161+
<dt>Currently on the Force</dt>
162+
<dd>{{ officer.currently_on_force() }}</dd>
159163
</dl>
160164
</div>
161165
<div class="col-md-6 col-xs-6">
162166
<dl>
167+
<dt>Race</dt>
168+
<dd>{{ officer.race_label()|default('Unknown')|lower|title }}</dd>
163169
<dt>Gender</dt>
164170
<dd>{{ officer.gender_label()|default('Unknown') }}</dd>
165171
<dt>Number of Photos</dt>

OpenOversight/app/templates/partials/officer_general_information.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ <h3>
5050
</tr>
5151
<tr>
5252
<td><b>Currently on the force</b></td>
53-
<td>{{ assignments | currently_on_force }}</td>
53+
<td>{{ officer.currently_on_force() }}</td>
5454
</tr>
5555
</tbody>
5656
</table>

OpenOversight/app/utils.py

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -348,20 +348,30 @@ def filter_by_form(form_data, officer_query, department_id=None):
348348
.all()
349349
]
350350

351-
print(form_data)
352351
if "Not Sure" in form_data["rank"]:
353352
form_data["rank"].append(None)
354353

355-
if form_data.get("badge") or form_data.get("unit") or job_ids:
354+
unit_ids = []
355+
if form_data.get("unit"):
356+
unit_ids = [
357+
unit.id
358+
for unit in Unit.query.filter_by(department_id=department_id)
359+
.filter(Unit.descrip.in_(form_data.get("unit")))
360+
.all()
361+
]
362+
363+
if "Not Sure" in form_data["unit"]:
364+
# Convert "Not Sure" to None, so the resulting SQL query allows NULL values
365+
form_data["unit"].append(None)
366+
367+
if form_data.get("badge") or unit_ids or job_ids:
356368
officer_query = officer_query.join(Officer.assignments)
357369
if form_data.get("badge"):
358370
officer_query = officer_query.filter(
359371
Assignment.star_no.like("%%{}%%".format(form_data["badge"]))
360372
)
361-
if form_data.get("unit"):
362-
officer_query = officer_query.filter(
363-
Assignment.unit_id == form_data["unit"]
364-
)
373+
if unit_ids:
374+
officer_query = officer_query.filter(Assignment.unit_id.in_(unit_ids))
365375
if job_ids:
366376
officer_query = officer_query.filter(Assignment.job_id.in_(job_ids))
367377
officer_query = officer_query.options(selectinload(Officer.assignments_lazy))

OpenOversight/tests/routes/test_officer_and_department.py

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1654,6 +1654,7 @@ def test_browse_filtering_allows_good(client, mockdata, session):
16541654
]
16551655
officer = Officer.query.filter_by(department_id=AC_DEPT).first()
16561656
job = Job.query.filter_by(department_id=officer.department_id).first()
1657+
unit = Unit.query.filter_by(department_id=officer.department_id).first()
16571658
form = AddOfficerForm(
16581659
first_name="A",
16591660
last_name="A",
@@ -1662,6 +1663,7 @@ def test_browse_filtering_allows_good(client, mockdata, session):
16621663
gender="M",
16631664
star_no=666,
16641665
job_id=job.id,
1666+
unit=unit.id,
16651667
department=department_id,
16661668
birth_year=1990,
16671669
links=links,
@@ -1695,14 +1697,18 @@ def test_browse_filtering_allows_good(client, mockdata, session):
16951697
data=data,
16961698
follow_redirects=True,
16971699
)
1698-
filter_list = rv.data.decode("utf-8").split("<dt>Race</dt>")[1:]
1699-
assert any("<dd>White</dd>" in token for token in filter_list)
17001700

1701-
filter_list = rv.data.decode("utf-8").split("<dt>Job Title</dt>")[1:]
1701+
filter_list = rv.data.decode("utf-8").split("<dt>Rank</dt>")[1:]
17021702
assert any(
17031703
"<dd>{}</dd>".format(job.job_title) in token for token in filter_list
17041704
)
17051705

1706+
filter_list = rv.data.decode("utf-8").split("<dt>Unit</dt>")[1:]
1707+
assert any("<dd>{}</dd>".format(unit.descrip) in token for token in filter_list)
1708+
1709+
filter_list = rv.data.decode("utf-8").split("<dt>Race</dt>")[1:]
1710+
assert any("<dd>White</dd>" in token for token in filter_list)
1711+
17061712
filter_list = rv.data.decode("utf-8").split("<dt>Gender</dt>")[1:]
17071713
assert any("<dd>Male</dd>" in token for token in filter_list)
17081714

0 commit comments

Comments
 (0)