Added a "Published in the last X days" filter, applied by the backend

GET /api/opportunities takes released_from, an RFC 3339 time, and lists
only the opportunities whose latest release is at or after it. It works
with paging and with changed_after, which keeps the same batch either
way; an invalid value is 400.

The frontend's filter defaults to 7 days, from midnight UK time, and
sends it with every load and catch-up, so a year of opportunities isn't
downloaded to show a week's. Changing the days loads the list again;
Clear filters goes back to 7.
This commit is contained in:
bwilliams committed 2026-10-19 10:05:45 +01:00
1 parent b8244da2e5
commit 746fa0a9c1
19 files changed
+333 -64

No files matched your search

+52 -3
View File
@@ -104,7 +104,8 @@ func uuids(page OpportunityPage) []string {
return ids
}
// Opportunities are listed newest first, a page at a time.
// Opportunities are listed newest first, a page at a time, from
// released_from if given (ocds-N is released 2026-10-0N at 12:00 UTC).
func TestListOpportunitiesPaging(t *testing.T) {
url := opportunitiesServer(t, 5)
all := []string{"ocds-5", "ocds-4", "ocds-3", "ocds-2", "ocds-1"}
@@ -120,6 +121,12 @@ func TestListOpportunitiesPaging(t *testing.T) {
{"?limit=5", all, false},
{"?offset=10", nil, false},
{"", all, false},
{"?released_from=", all, false},
{"?released_from=2026-10-03T12:00:00Z", []string{"ocds-5", "ocds-4", "ocds-3"}, false},
{"?released_from=2026-10-03T13:00:00%2B01:00", []string{"ocds-5", "ocds-4", "ocds-3"}, false},
{"?released_from=2026-10-03T12:00:01Z", []string{"ocds-5", "ocds-4"}, false},
{"?released_from=2026-10-02T00:00:00Z&limit=1", []string{"ocds-5"}, true},
{"?released_from=2026-10-02T00:00:00Z&limit=2&offset=2", []string{"ocds-3", "ocds-2"}, false},
}
for _, tt := range tests {
var page OpportunityPage
@@ -161,9 +168,19 @@ func TestListOpportunitiesEmpty(t *testing.T) {
}
}
// Bad paging or released_from is 400. An unencoded "+" in released_from
// arrives as a space, so it is refused too.
func TestListOpportunitiesBadPaging(t *testing.T) {
url := opportunitiesServer(t, 1)
queries := []string{"?limit=0", "?limit=101", "?limit=x", "?offset=-1"}
queries := []string{
"?limit=0",
"?limit=101",
"?limit=x",
"?offset=-1",
"?released_from=x",
"?released_from=2026-10-03",
"?released_from=2026-10-03T13:00:00+01:00",
}
for _, query := range queries {
status := request(t, "GET", url+"/opportunities"+query, nil)
if status != 400 {
@@ -211,6 +228,36 @@ func TestListChangedOpportunities(t *testing.T) {
)
}
// released_from leaves out ocds-2 (released on the 2nd), not the batch.
var bounded OpportunityPage
status = request(
t,
"GET",
url+"/opportunities?released_from=2026-10-03T00:00:00Z"+
"&changed_after="+page.Batch,
&bounded,
)
if status != 200 || len(bounded.Opportunities) != 0 ||
bounded.Batch != changed.Batch {
t.Errorf(
"released_from the 3rd: status %d, %+v; want [] and batch %q",
status, bounded, changed.Batch,
)
}
status = request(
t,
"GET",
url+"/opportunities?changed_after="+page.Batch+
"&released_from=2026-10-02T12:00:00Z",
&bounded,
)
if status != 200 || strings.Join(uuids(bounded), ",") != "ocds-2" {
t.Errorf(
"released_from the 2nd: status %d, %+v; want ocds-2",
status, bounded,
)
}
var latest OpportunityPage
request(t, "GET", url+"/opportunities?limit=1", &latest)
if latest.Batch != changed.Batch {
@@ -218,7 +265,8 @@ func TestListChangedOpportunities(t *testing.T) {
}
}
// An unknown batch is 410 Gone, and paging can't be used with changed_after.
// An unknown batch is 410 Gone, and paging can't be used with changed_after;
// a bad released_from is 400 there too.
func TestListChangedOpportunitiesRefused(t *testing.T) {
url := opportunitiesServer(t, 1)
var page OpportunityPage
@@ -232,6 +280,7 @@ func TestListChangedOpportunitiesRefused(t *testing.T) {
{"?changed_after=", http.StatusGone},
{"?changed_after=" + page.Batch + "&limit=5", http.StatusBadRequest},
{"?offset=0&changed_after=" + page.Batch, http.StatusBadRequest},
{"?changed_after=" + page.Batch + "&released_from=x", http.StatusBadRequest},
}
for _, tt := range tests {
status := request(t, "GET", url+"/opportunities"+tt.query, nil)