diff --git a/Makefile b/Makefile index f5eadc0..94eafeb 100644 --- a/Makefile +++ b/Makefile @@ -15,7 +15,11 @@ test: srv.dev-shell: $$(nix-build --no-out-link -A pkgs.bash)/bin/bash srv-dev-env.sh \ - --command "cd srv/src; return" + --command " \ + cd srv/src; \ + go run cmd/import-posts/main.go ../../static/src/_posts/*; \ + return; \ + " srv.shell: nix-shell -A srv.shellWithBuild --arg baseConfig '(import ${CONFIG})' \ diff --git a/srv/src/api/render.go b/srv/src/api/render.go index b2ca3c8..6359505 100644 --- a/srv/src/api/render.go +++ b/srv/src/api/render.go @@ -59,7 +59,7 @@ func (a *api) renderIndexHandler() http.Handler { return } - posts, _, err := a.params.PostStore.Get(page, pageCount) + posts, hasMore, err := a.params.PostStore.WithOrderDesc().Get(page, pageCount) if err != nil { apiutil.InternalServerError( rw, r, fmt.Errorf("fetching page %d of posts: %w", page, err), @@ -68,9 +68,20 @@ func (a *api) renderIndexHandler() http.Handler { } tplData := struct { - Posts []post.StoredPost + Posts []post.StoredPost + PrevPage, NextPage int }{ - Posts: posts, + Posts: posts, + PrevPage: -1, + NextPage: -1, + } + + if page > 0 { + tplData.PrevPage = page - 1 + } + + if hasMore { + tplData.NextPage = page + 1 } if err := tpl.Execute(rw, tplData); err != nil { diff --git a/srv/src/api/tpl/index.html b/srv/src/api/tpl/index.html index 240df92..b71dc01 100644 --- a/srv/src/api/tpl/index.html +++ b/srv/src/api/tpl/index.html @@ -17,4 +17,12 @@ {{ end }} +{{ if ge .PrevPage 0 }} +Previous +{{ end }} + +{{ if ge .NextPage 0 }} +Next +{{ end }} + {{ template "base.html" . }} diff --git a/srv/src/post/post.go b/srv/src/post/post.go index 30ded15..cadfcfc 100644 --- a/srv/src/post/post.go +++ b/srv/src/post/post.go @@ -74,21 +74,33 @@ type Store interface { // ascending, or empty slice. GetByTag(tag string) ([]StoredPost, error) + // WithOrderDesc will return a Store whose Get operations return Posts in + // time descending order, rather than ascending. + WithOrderDesc() Store + // Delete will delete the StoredPost with the given ID. Delete(id string) error } type store struct { - db *sql.DB + db *sql.DB + order string } // NewStore initializes a new Store using an existing SQLDB. func NewStore(db *SQLDB) Store { return &store{ - db: db.db, + db: db.db, + order: "ASC", } } +func (s *store) WithOrderDesc() Store { + s2 := *s + s2.order = "DESC" + return &s2 +} + // if the callback returns an error then the transaction is aborted. func (s *store) withTx(cb func(*sql.Tx) error) error { @@ -185,15 +197,17 @@ func (s *store) get( []StoredPost, error, ) { - query := ` - SELECT + query := fmt.Sprintf( + `SELECT p.id, p.title, p.description, p.series, pt.tag, p.published_at, p.last_updated_at, p.body FROM posts p LEFT JOIN post_tags pt ON (p.id = pt.post_id) - ` + where + ` - ORDER BY p.published_at ASC, p.title ASC` + `+where+` + ORDER BY p.published_at %s, p.title %s`, + s.order, s.order, + ) if limit > 0 { query += fmt.Sprintf(" LIMIT %d", limit) diff --git a/srv/src/post/post_test.go b/srv/src/post/post_test.go index 55a29ea..c5587c8 100644 --- a/srv/src/post/post_test.go +++ b/srv/src/post/post_test.go @@ -187,6 +187,47 @@ func TestStore(t *testing.T) { assertPostsEqual(t, posts[4:], gotPosts) }) + t.Run("get_desc", func(t *testing.T) { + h := newStoreTestHarness(t) + h.store = h.store.WithOrderDesc() + + now := h.clock.Now().UTC() + + posts := []StoredPost{ + h.testStoredPost(3), + h.testStoredPost(2), + h.testStoredPost(1), + h.testStoredPost(0), + } + + for _, post := range posts { + assert.NoError(t, h.store.Set(post.Post, now)) + } + + gotPosts, hasMore, err := h.store.Get(0, 2) + assert.NoError(t, err) + assert.True(t, hasMore) + assertPostsEqual(t, posts[:2], gotPosts) + + gotPosts, hasMore, err = h.store.Get(1, 2) + assert.NoError(t, err) + assert.False(t, hasMore) + assertPostsEqual(t, posts[2:4], gotPosts) + + posts = append([]StoredPost{h.testStoredPost(4)}, posts...) + assert.NoError(t, h.store.Set(posts[0].Post, now)) + + gotPosts, hasMore, err = h.store.Get(1, 2) + assert.NoError(t, err) + assert.True(t, hasMore) + assertPostsEqual(t, posts[2:4], gotPosts) + + gotPosts, hasMore, err = h.store.Get(2, 2) + assert.NoError(t, err) + assert.False(t, hasMore) + assertPostsEqual(t, posts[4:], gotPosts) + }) + t.Run("get_by_series", func(t *testing.T) { h := newStoreTestHarness(t)