YAML-driven Posts screen stores translatable markdown and regenerates content_html in the plugin. Publish writes without access_publish are refused; list and form queries stay on the principal without access_other_posts. Co-authored-by: Cursor <cursoragent@cursor.com>
190 lines
5.5 KiB
Go
190 lines
5.5 KiB
Go
package models
|
|
|
|
import (
|
|
"fmt"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
"git.golem15.com/golem15/summercms/modules/bouncer"
|
|
"git.golem15.com/golem15/summercms/modules/cabana"
|
|
"git.golem15.com/golem15/summercms/modules/lagoon/attach"
|
|
"gorm.io/gorm"
|
|
)
|
|
|
|
// Post is a journal entry stored on golem15_journal_posts.
|
|
type Post struct {
|
|
ID uint `gorm:"column:id;primaryKey"`
|
|
UserID *uint `gorm:"column:user_id"`
|
|
RedactorID *uint `gorm:"column:redactor_id"`
|
|
Title string `gorm:"column:title"`
|
|
Slug string `gorm:"column:slug"`
|
|
Excerpt string `gorm:"column:excerpt"`
|
|
Content string `gorm:"column:content"`
|
|
ContentHTML string `gorm:"column:content_html"`
|
|
PublishedAt *time.Time `gorm:"column:published_at"`
|
|
Published bool `gorm:"column:published"`
|
|
IsPinned bool `gorm:"column:is_pinned"`
|
|
Metadata *string `gorm:"column:metadata;type:jsonb"`
|
|
Sources *string `gorm:"column:sources;type:jsonb"`
|
|
CreatedAt *time.Time `gorm:"column:created_at"`
|
|
UpdatedAt *time.Time `gorm:"column:updated_at"`
|
|
|
|
User *cabana.BackendUser `gorm:"foreignKey:UserID" json:"-"`
|
|
Categories []Category `gorm:"many2many:golem15_journal_posts_categories;" json:"-"`
|
|
Tags []Tag `gorm:"many2many:golem15_journal_posts_tags;" json:"-"`
|
|
}
|
|
|
|
func (Post) TableName() string { return "golem15_journal_posts" }
|
|
|
|
// MorphName is the PHP class string stored on translate attributes and system_files.
|
|
func (Post) MorphName() string { return `Golem15\Journal\Models\Post` }
|
|
|
|
func (Post) Translatable() []string {
|
|
return []string{"title", "content", "content_html", "excerpt", "metadata", "slug"}
|
|
}
|
|
|
|
func (Post) TranslatableIndexes() []string { return []string{"slug"} }
|
|
|
|
// Fillable is the tracer allow-list. redactor_id, content_html, and user_id
|
|
// are never mass-assigned from a map.
|
|
func (Post) Fillable() []string {
|
|
return []string{
|
|
"title", "slug", "excerpt", "content",
|
|
"published", "published_at", "is_pinned", "metadata", "sources",
|
|
}
|
|
}
|
|
|
|
func (Post) AttachRelations() []attach.Relation {
|
|
return []attach.Relation{
|
|
{Name: "featured_images", Many: true, Public: true},
|
|
{Name: "content_images", Many: true, Public: true},
|
|
}
|
|
}
|
|
|
|
func (Post) Rules() map[string]string {
|
|
return map[string]string{
|
|
"title": "required",
|
|
"slug": "required",
|
|
"content": "required",
|
|
}
|
|
}
|
|
|
|
const (
|
|
// FilterPublished is the model scope PHP config_filter.yaml expressed as
|
|
// conditions. Cabana switch filters use column: published instead; the
|
|
// scope remains registered for FilterScopes completeness.
|
|
FilterPublished = "FilterPublished"
|
|
// FilterCategories is the config_filter.yaml category scope, including
|
|
// NestedTree children via nest_left/nest_right.
|
|
FilterCategories = "FilterCategories"
|
|
)
|
|
|
|
func (Post) FilterScopes() []string {
|
|
return []string{FilterPublished, FilterCategories}
|
|
}
|
|
|
|
func (Post) FilterScope(name string, db *gorm.DB, value any) *gorm.DB {
|
|
if db == nil {
|
|
return db
|
|
}
|
|
switch name {
|
|
case FilterPublished:
|
|
on := parseBoolFilter(value)
|
|
if on {
|
|
return db.Where("published = ?", true)
|
|
}
|
|
return db.Where("published <> ?", true)
|
|
case FilterCategories:
|
|
id, ok := parseFilterID(value)
|
|
if !ok {
|
|
return db.Where("1 = 0")
|
|
}
|
|
ids := categoryIDsWithChildren(db, uint(id))
|
|
if len(ids) == 0 {
|
|
return db.Where("1 = 0")
|
|
}
|
|
return db.Where("id IN (SELECT post_id FROM golem15_journal_posts_categories WHERE category_id IN (?))", ids)
|
|
default:
|
|
return db.Where("1 = 0")
|
|
}
|
|
}
|
|
|
|
func parseBoolFilter(value any) bool {
|
|
switch v := value.(type) {
|
|
case bool:
|
|
return v
|
|
case string:
|
|
s := strings.ToLower(strings.TrimSpace(v))
|
|
return s == "1" || s == "true" || s == "on"
|
|
default:
|
|
s := strings.ToLower(strings.TrimSpace(fmt.Sprint(value)))
|
|
return s == "1" || s == "true" || s == "on"
|
|
}
|
|
}
|
|
|
|
func parseFilterID(value any) (uint64, bool) {
|
|
text := strings.TrimSpace(fmt.Sprint(value))
|
|
if value == nil || text == "" {
|
|
return 0, false
|
|
}
|
|
id, err := strconv.ParseUint(text, 10, 32)
|
|
if err != nil || id == 0 {
|
|
return 0, false
|
|
}
|
|
return id, true
|
|
}
|
|
|
|
func categoryIDsWithChildren(db *gorm.DB, id uint) []uint {
|
|
var cat Category
|
|
if err := db.Session(&gorm.Session{NewDB: true}).Where("id = ?", id).Take(&cat).Error; err != nil {
|
|
return nil
|
|
}
|
|
ids := []uint{cat.ID}
|
|
if cat.NestLeft == nil || cat.NestRight == nil {
|
|
return ids
|
|
}
|
|
var kids []Category
|
|
if err := db.Session(&gorm.Session{NewDB: true}).
|
|
Where("nest_left > ? AND nest_right < ?", *cat.NestLeft, *cat.NestRight).
|
|
Find(&kids).Error; err != nil {
|
|
return ids
|
|
}
|
|
for _, kid := range kids {
|
|
ids = append(ids, kid.ID)
|
|
}
|
|
return ids
|
|
}
|
|
|
|
// CanEdit reports whether principal may edit this post: the owner, or an
|
|
// administrator with golem15.journal.access_other_posts.
|
|
func (p *Post) CanEdit(principal *bouncer.Principal) bool {
|
|
if p == nil || principal == nil {
|
|
return false
|
|
}
|
|
if cabana.Allows(principal, []string{"golem15.journal.access_other_posts"}) {
|
|
return true
|
|
}
|
|
return p.UserID != nil && *p.UserID == principal.ID
|
|
}
|
|
|
|
// PostCategory is the golem15_journal_posts_categories pivot.
|
|
type PostCategory struct {
|
|
PostID uint `gorm:"column:post_id;primaryKey"`
|
|
CategoryID uint `gorm:"column:category_id;primaryKey"`
|
|
}
|
|
|
|
func (PostCategory) TableName() string { return "golem15_journal_posts_categories" }
|
|
|
|
// PostTag is the golem15_journal_posts_tags pivot.
|
|
type PostTag struct {
|
|
PostID uint `gorm:"column:post_id;primaryKey"`
|
|
TagID uint `gorm:"column:tag_id;primaryKey"`
|
|
}
|
|
|
|
func (PostTag) TableName() string { return "golem15_journal_posts_tags" }
|
|
|
|
func init() {
|
|
Register(Post{})
|
|
}
|