fix(web): detail row lookup walks all pages of the Site read (#144)
This commit is contained in:
@@ -36,28 +36,36 @@ type seriesDetailView struct {
|
|||||||
// adminSeriesDetail renders one Series' page, keyed by the composite
|
// adminSeriesDetail renders one Series' page, keyed by the composite
|
||||||
// "<site>:<series_id>" the list row already shows. The row is read through
|
// "<site>:<series_id>" the list row already shows. The row is read through
|
||||||
// the list's own SeriesPage read narrowed to the key's Site: the admin
|
// the list's own SeriesPage read narrowed to the key's Site: the admin
|
||||||
// projection is the privacy boundary, and a second single-row read would be a
|
// projection is the privacy boundary, and a dedicated single-row read would
|
||||||
// second definition of it.
|
// be a second definition of it.
|
||||||
// ponytail: one 50-row page scan per detail view; a keyed read alongside
|
// ponytail: a page scan per detail view, one query per page of the Site's
|
||||||
// SeriesPage when the library outgrows the page size.
|
// rows up to the window total; a keyed read alongside SeriesPage when the
|
||||||
|
// library outgrows the page size.
|
||||||
func (h *Handler) adminSeriesDetail(w http.ResponseWriter, r *http.Request) {
|
func (h *Handler) adminSeriesDetail(w http.ResponseWriter, r *http.Request) {
|
||||||
site, seriesID, ok := strings.Cut(r.PathValue("key"), ":")
|
site, seriesID, ok := strings.Cut(r.PathValue("key"), ":")
|
||||||
if !ok || site == "" || seriesID == "" {
|
if !ok || site == "" || seriesID == "" {
|
||||||
http.NotFound(w, r)
|
http.NotFound(w, r)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
page, err := h.store.SeriesPage(store.SeriesFilter{Site: site})
|
seen := 0
|
||||||
|
for page := 1; ; page++ {
|
||||||
|
p, err := h.store.SeriesPage(store.SeriesFilter{Site: site, Page: page})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("series detail %s: %v", site+":"+seriesID, err)
|
log.Printf("series detail %s: %v", site+":"+seriesID, err)
|
||||||
http.Error(w, "internal error", http.StatusInternalServerError)
|
http.Error(w, "internal error", http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
for i := range page.Rows {
|
seen += len(p.Rows)
|
||||||
if page.Rows[i].SeriesID == seriesID {
|
for i := range p.Rows {
|
||||||
h.renderAdmin(w, adminView{Page: "series-detail", Detail: h.seriesDetailView(page.Rows[i])})
|
if p.Rows[i].SeriesID == seriesID {
|
||||||
|
h.renderAdmin(w, adminView{Page: "series-detail", Detail: h.seriesDetailView(p.Rows[i])})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if seen >= p.Total {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
http.NotFound(w, r)
|
http.NotFound(w, r)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+33
-1
@@ -1881,7 +1881,7 @@ func TestAdminSeriesDetailRendersFacts(t *testing.T) {
|
|||||||
"SP Baby",
|
"SP Baby",
|
||||||
"kagane:sp-baby · kagane · manga",
|
"kagane:sp-baby · kagane · manga",
|
||||||
"ch 45",
|
"ch 45",
|
||||||
"checked 2h0m0s ago",
|
"checked 2h0m",
|
||||||
"2 readers",
|
"2 readers",
|
||||||
} {
|
} {
|
||||||
if !strings.Contains(body, want) {
|
if !strings.Contains(body, want) {
|
||||||
@@ -2009,6 +2009,38 @@ func TestAdminSeriesDetailUnknownKey404(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// A Series past the first page of its Site's read (50 rows) must still
|
||||||
|
// render: the list shows it, so its detail link may not answer 404. The row
|
||||||
|
// lookup walks the window total, not just page 1.
|
||||||
|
func TestAdminSeriesDetailBeyondFirstPage(t *testing.T) {
|
||||||
|
router, st := newWebTestServer(t, testConfig())
|
||||||
|
for i := 0; i < 55; i++ {
|
||||||
|
seed(t, st, store.Bookmark{
|
||||||
|
Key: fmt.Sprintf("asura:s%03d", i), Site: "asura", SeriesID: fmt.Sprintf("s%03d", i),
|
||||||
|
Title: "Bulk", SeriesURL: "https://asurascans.com/series/bulk",
|
||||||
|
Kind: "manga",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
// Rows order by (latest_checked_at, site, series_id), all zero stamps, so
|
||||||
|
// "zzz" lands on page 2 behind the fifty-five "s*" rows.
|
||||||
|
seed(t, st, store.Bookmark{
|
||||||
|
Key: "asura:zzz", Site: "asura", SeriesID: "zzz",
|
||||||
|
Title: "Late", SeriesURL: "https://asurascans.com/series/zzz",
|
||||||
|
Kind: "manga",
|
||||||
|
})
|
||||||
|
|
||||||
|
req := httptest.NewRequest(http.MethodGet, "/admin/series/asura:zzz", nil)
|
||||||
|
req.AddCookie(sessionCookie(t, st))
|
||||||
|
rr := httptest.NewRecorder()
|
||||||
|
router.ServeHTTP(rr, req)
|
||||||
|
if rr.Code != http.StatusOK {
|
||||||
|
t.Fatalf("GET /admin/series/asura:zzz status = %d, want 200 for a page-2 row", rr.Code)
|
||||||
|
}
|
||||||
|
if !strings.Contains(rr.Body.String(), "Late") {
|
||||||
|
t.Errorf("page-2 row did not render:\n%s", rr.Body.String())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// The title and the key line come from the database, so they must render
|
// The title and the key line come from the database, so they must render
|
||||||
// escaped: a title that is markup stays markup in the response, never HTML.
|
// escaped: a title that is markup stays markup in the response, never HTML.
|
||||||
func TestAdminSeriesDetailEscapesStoredStrings(t *testing.T) {
|
func TestAdminSeriesDetailEscapesStoredStrings(t *testing.T) {
|
||||||
|
|||||||
Reference in New Issue
Block a user