Skip to content

Commit a9bc847

Browse files
authored
Adjust the age and date range when searching on 'Find a Stop'. (#349)
* Update age and date range search on 'Find a Stop'. * Show messages about the adjusted ranges on the frontend. * Minor comment change.
1 parent 5f082ee commit a9bc847

File tree

5 files changed

+155
-4
lines changed

5 files changed

+155
-4
lines changed

frontend/src/Components/FindAStopResults/FindAStopResults.js

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,9 @@ function FindAStopResults() {
5656
const [stops, setStops] = useState([]);
5757
const [loading, setLoading] = useState(false);
5858
const [lastReportedStop, setLastReportedStop] = useState(null);
59+
const [age, setAge] = useState(null);
60+
const [startDate, setStartDate] = useState(null);
61+
const [endDate, setEndDate] = useState(null);
5962

6063
useEffect(() => {
6164
async function _fetchStops() {
@@ -75,6 +78,39 @@ function FindAStopResults() {
7578
} else {
7679
setLastReportedStop(null);
7780
}
81+
if (data.start_date) {
82+
setStartDate({
83+
entered: new Intl.DateTimeFormat('en-US', {
84+
year: 'numeric',
85+
month: 'short',
86+
day: 'numeric',
87+
}).format(new Date(data.start_date.entered)),
88+
adjusted: new Intl.DateTimeFormat('en-US', {
89+
year: 'numeric',
90+
month: 'short',
91+
day: 'numeric',
92+
}).format(new Date(data.start_date.adjusted)),
93+
});
94+
} else {
95+
setStartDate(null);
96+
}
97+
if (data.end_date) {
98+
setEndDate({
99+
entered: new Intl.DateTimeFormat('en-US', {
100+
year: 'numeric',
101+
month: 'short',
102+
day: 'numeric',
103+
}).format(new Date(data.end_date.entered)),
104+
adjusted: new Intl.DateTimeFormat('en-US', {
105+
year: 'numeric',
106+
month: 'short',
107+
day: 'numeric',
108+
}).format(new Date(data.end_date.adjusted)),
109+
});
110+
} else {
111+
setEndDate(null);
112+
}
113+
setAge(data.age);
78114
} catch (e) {
79115
// eslint-disable-next-line no-console
80116
console.warn(e);
@@ -109,6 +145,22 @@ function FindAStopResults() {
109145
: `${stops.length} results found`}
110146
</P>
111147
)}
148+
{!loading && age && (
149+
<P size={SIZES[0]} color={COLORS[0]}>
150+
You entered {age.entered} for Age but we used {age.adjusted[0]} - {age.adjusted[1]}{' '}
151+
instead.
152+
</P>
153+
)}
154+
{!loading && startDate && (
155+
<P size={SIZES[0]} color={COLORS[0]}>
156+
You entered {startDate.entered} for Start Date but we used {startDate.adjusted} instead.
157+
</P>
158+
)}
159+
{!loading && endDate && (
160+
<P size={SIZES[0]} color={COLORS[0]}>
161+
You entered {endDate.entered} for End Date but we used {endDate.adjusted} instead.
162+
</P>
163+
)}
112164
</S.Heading>
113165
<S.TableContainer>
114166
{loading && <TableSkeleton />}

nc/filters.py

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
from datetime import timedelta
2+
13
from django.db.models import Q
24
from django_filters import rest_framework as filters
35

@@ -22,6 +24,7 @@ class DriverStopsFilter(filters.FilterSet):
2224
stop_action = filters.MultipleChoiceFilter(
2325
label="Stop action", choices=models.ACTION_CHOICES, method="filter_stop_action"
2426
)
27+
age = filters.NumberFilter(method="filter_age")
2528

2629
def filter_agency(self, queryset, name, value):
2730
return queryset.filter(stop__agency_id=value)
@@ -31,9 +34,17 @@ def filter_stop_date(self, queryset, name, value):
3134
end_date = value.stop
3235
query = Q()
3336
if start_date:
34-
query &= Q(stop__date__gte=start_date)
37+
# Adjust it to 2 days earlier
38+
adjusted_start_date = start_date - timedelta(2)
39+
query &= Q(stop__date__gte=adjusted_start_date)
40+
if self.request:
41+
self.request.adjusted_start_date = start_date.date(), adjusted_start_date.date()
3542
if end_date:
36-
query &= Q(stop__date__lte=end_date)
43+
# Adjust it to 2 days later
44+
adjusted_end_date = end_date + timedelta(2)
45+
query &= Q(stop__date__lte=adjusted_end_date)
46+
if self.request:
47+
self.request.adjusted_end_date = end_date.date(), adjusted_end_date.date()
3748
return queryset.filter(query)
3849

3950
def filter_officer(self, queryset, name, value):
@@ -45,6 +56,14 @@ def filter_stop_purpose(self, queryset, name, value):
4556
def filter_stop_action(self, queryset, name, value):
4657
return queryset.filter(stop__action__in=value)
4758

59+
def filter_age(self, queryset, name, value):
60+
# Instead of searching for the exact age specified, search for age +/- 2 years
61+
value = int(value)
62+
age_range = max(value - 2, 0), value + 2
63+
if self.request:
64+
self.request.adjusted_age = value, age_range
65+
return queryset.filter(age__gte=age_range[0], age__lte=age_range[1])
66+
4867
class Meta:
4968
model = models.Person
5069
fields = (

nc/tests/api/test_basic_search.py

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
from datetime import timedelta
2+
13
import faker
24
import pytest
35

@@ -44,7 +46,14 @@ def test_response_person_fields(client, search_url, durham):
4446
"stop_action": person.stop.get_action_display(),
4547
}
4648
assert result == expected
49+
# 'last_reported_stop' should only be included if no matching stops were found
4750
assert "last_reported_stop" not in response.data
51+
# 'age' should only be included if the user entered an age
52+
assert "age" not in response.data
53+
# 'start_date' should only be included if the user entered a start date
54+
assert "start_date" not in response.data
55+
# 'end_date' should only be included if the user entered an end date
56+
assert "end_date" not in response.data
4857

4958

5059
@pytest.mark.django_db(databases=["traffic_stops_nc"])
@@ -70,3 +79,63 @@ def test_no_stops_found(client, search_url):
7079
assert response.status_code == status.HTTP_200_OK
7180
assert response.data.get("results") == []
7281
assert response.data.get("last_reported_stop") == agency.last_reported_stop
82+
83+
84+
@pytest.mark.django_db(databases=["traffic_stops_nc"])
85+
def test_age_adjusted(client, search_url, durham):
86+
"""Ensure people aged + or - 2 years of the age the user entered are included
87+
in search results.
88+
"""
89+
age = 18
90+
# Create 5 stops with people within the expected age range
91+
people = [factories.PersonFactory(stop__agency=durham, age=i) for i in range(age - 2, age + 3)]
92+
# Create 2 stops with people outside the expected age range. These should not
93+
# be included in the search results
94+
factories.PersonFactory(stop__agency=durham, age=age - 3)
95+
factories.PersonFactory(stop__agency=durham, age=age + 3)
96+
97+
response = client.get(search_url, data={"agency": durham.pk, "age": age}, format="json")
98+
99+
assert len(response.data["results"]) == len(people)
100+
stop_ids = {stop["stop_id"] for stop in response.data["results"]}
101+
assert {p.stop.stop_id for p in people} == stop_ids
102+
# 'age' should be included in the response data, with the entered age and
103+
# the adjusted age range
104+
assert response.data["age"] == {"entered": age, "adjusted": (age - 2, age + 2)}
105+
106+
107+
@pytest.mark.django_db(databases=["traffic_stops_nc"])
108+
def test_stop_date_range_adjusted(client, search_url, durham):
109+
"""Ensure the date range entered by the user is adjusted such that the start_date
110+
is 2 days earlier and end_date is 2 days later.
111+
"""
112+
start_date = fake.past_date()
113+
end_date = fake.past_date(start_date=start_date)
114+
# Create some stops within the expected date range
115+
dates = (
116+
[start_date, end_date]
117+
+ [start_date - timedelta(d) for d in [1, 2]]
118+
+ [end_date + timedelta(d) for d in [1, 2]]
119+
)
120+
people = [factories.PersonFactory(stop__agency=durham, stop__date=d) for d in dates]
121+
# Create 2 stops outside the expected date range. These should not be included
122+
# in the search results
123+
factories.PersonFactory(stop__agency=durham, stop__date=start_date - timedelta(3))
124+
factories.PersonFactory(stop__agency=durham, stop__date=end_date + timedelta(3))
125+
126+
response = client.get(
127+
search_url,
128+
data={"agency": durham.pk, "stop_date_after": start_date, "stop_date_before": end_date},
129+
format="json",
130+
)
131+
132+
assert len(response.data["results"]) == len(people)
133+
stop_ids = {stop["stop_id"] for stop in response.data["results"]}
134+
assert {p.stop.stop_id for p in people} == stop_ids
135+
# 'start_date' and 'end_date' should be included in the response data, with
136+
# the entered and adjusted date for each
137+
assert response.data["start_date"] == {
138+
"entered": start_date,
139+
"adjusted": start_date - timedelta(2),
140+
}
141+
assert response.data["end_date"] == {"entered": end_date, "adjusted": end_date + timedelta(2)}

nc/tests/api/test_timezones.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313

1414
@pytest.fixture
1515
def july_person(durham):
16-
stop_date = nc_timezone.localize(dt.datetime(2020, 7, 31, 20, 26))
16+
stop_date = nc_timezone.localize(dt.datetime(2020, 7, 29, 20, 26))
1717
return factories.PersonFactory(stop__agency=durham, stop__date=stop_date)
1818

1919

nc/views/main.py

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -273,7 +273,18 @@ class DriverStopsViewSet(viewsets.ReadOnlyModelViewSet):
273273

274274
def list(self, request, *args, **kwargs):
275275
response = super().list(request, *args, **kwargs)
276-
if not response.data["results"]:
276+
if response.data["results"]:
277+
# If the user entered an age or date range, add the values entered
278+
# and the adjusted values to the response, so they can be included
279+
# in a message on the frontend
280+
for field in ("age", "start_date", "end_date"):
281+
adjusted = getattr(request, f"adjusted_{field}", None)
282+
if adjusted:
283+
response.data[field] = {
284+
"entered": adjusted[0],
285+
"adjusted": adjusted[1],
286+
}
287+
else:
277288
# No stops were found. Add the agency's last_reported_stop to the
278289
# response data
279290
try:

0 commit comments

Comments
 (0)