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{}) }