feat(11.1-05): add the walkthrough's admin controller, publish command and published_at migration

- make:admin-controller, make:command and make:migration output, finished:
  the Posts controller serves models.Post behind acme.blog.access_posts,
  WinterCMS-style form and list YAML embedded through pact.AdminAssets,
  blog:publish sets published_at by slug with a bound parameter
- the posts route lists published posts only, newest first
- Docker tests migrate an ICU pl-PL database, roll back published_at, serve
  the route and run blog:publish with a published and an opened database
- the page gains the admin controller, console command and added-column
  sections
This commit is contained in:
Jakub Zych
2026-09-30 23:35:44 +02:00
parent 41a3190956
commit dd82b8a2ad
17 changed files with 1223 additions and 59 deletions

View File

@@ -23,15 +23,16 @@ func (p *Plugin) Routes(r pact.Router) error {
// postJSON is the response shape of one post. It is built field by field,
// so a column added to the model never leaks into the API.
type postJSON struct {
ID uint `json:"id"`
Title string `json:"title"`
Slug string `json:"slug"`
Body string `json:"body"`
CreatedAt wire.Time `json:"created_at"`
ID uint `json:"id"`
Title string `json:"title"`
Slug string `json:"slug"`
Body string `json:"body"`
PublishedAt wire.Time `json:"published_at"`
}
// listPosts answers GET /api/blog/posts?page=N&per_page=M with one page of
// posts, newest first, in the {data, meta} shape of Laravel's paginator.
// published posts, newest first, in the {data, meta} shape of Laravel's
// paginator. Drafts, whose published_at is NULL, are never listed.
func (p *Plugin) listPosts(w http.ResponseWriter, r *http.Request) {
db, ok := p.app.Lookup[*gorm.DB]()
if !ok {
@@ -41,25 +42,25 @@ func (p *Plugin) listPosts(w http.ResponseWriter, r *http.Request) {
page := queryInt(r, "page", 1, 1, 10000)
perPage := queryInt(r, "per_page", p.app.Config.Int("acme.blog.per_page"), 1, 100)
q := db.WithContext(r.Context()).Model(&models.Post{})
q := db.WithContext(r.Context()).Model(&models.Post{}).Where("published_at IS NOT NULL")
var total int64
if err := q.Count(&total).Error; err != nil {
wire.WriteOpaque500(w)
return
}
var posts []models.Post
if err := q.Order("created_at DESC, id DESC").Offset((page - 1) * perPage).Limit(perPage).Find(&posts).Error; err != nil {
if err := q.Order("published_at DESC, id DESC").Offset((page - 1) * perPage).Limit(perPage).Find(&posts).Error; err != nil {
wire.WriteOpaque500(w)
return
}
rows := make([]postJSON, 0, len(posts))
for _, post := range posts {
rows = append(rows, postJSON{
ID: post.ID,
Title: post.Title,
Slug: post.Slug,
Body: post.Body,
CreatedAt: wire.Time{Time: post.CreatedAt.UTC().Truncate(time.Second)},
ID: post.ID,
Title: post.Title,
Slug: post.Slug,
Body: post.Body,
PublishedAt: wire.Time{Time: post.PublishedAt.UTC().Truncate(time.Second)},
})
}
wire.WriteJSON(w, http.StatusOK, lagoon.Paginate(rows, page, perPage, total))