website/pkg/models/forum.go
Noah Petherbridge b8146ae485 Forum Creation Quotas
Add minimum quotas for users to earn the ability to create custom forums.

The entry requirements that could earn the first forum include:
1. Having a Certified account status for at least 45 days.
2. Having written 10 posts or replies in the forums.

Additional quota is granted in increasing difficulty based on the count of
forum posts created.

Other changes:

* Admin view of Manage Forums can filter for official/community.
* "Certified Since" now shown on profile pages.
* Update FAQ page for Forums feature.
2024-08-22 21:57:14 -07:00

275 lines
6.6 KiB
Go

package models
import (
"errors"
"strings"
"time"
"code.nonshy.com/nonshy/website/pkg/config"
"gorm.io/gorm"
)
// Forum table.
type Forum struct {
ID uint64 `gorm:"primaryKey"`
OwnerID uint64 `gorm:"index"`
Owner User `gorm:"foreignKey:owner_id"`
Category string `gorm:"index"`
Fragment string `gorm:"uniqueIndex"`
Title string
Description string
Explicit bool `gorm:"index"`
Privileged bool
PermitPhotos bool
Private bool `gorm:"index"`
CreatedAt time.Time
UpdatedAt time.Time
}
// Preload related tables for the forum (classmethod).
func (f *Forum) Preload() *gorm.DB {
return DB.Preload("Owner").Preload("Owner.ProfilePhoto")
}
// GetForum by ID.
func GetForum(id uint64) (*Forum, error) {
forum := &Forum{}
result := forum.Preload().First(&forum, id)
return forum, result.Error
}
// GetForums queries a set of thread IDs and returns them mapped.
func GetForums(IDs []uint64) (map[uint64]*Forum, error) {
var (
mt = map[uint64]*Forum{}
fs = []*Forum{}
)
result := (&Forum{}).Preload().Where("id IN ?", IDs).Find(&fs)
for _, row := range fs {
mt[row.ID] = row
}
return mt, result.Error
}
// ForumByFragment looks up a forum by its URL fragment.
func ForumByFragment(fragment string) (*Forum, error) {
if fragment == "" {
return nil, errors.New("the URL fragment is required")
}
var (
f = &Forum{}
result = f.Preload().Where(
"fragment = ?",
fragment,
).First(&f)
)
return f, result.Error
}
// CanEdit checks if the user has edit rights over this forum.
//
// That is, they are its Owner or they are an admin with Manage Forums permission.
func (f *Forum) CanEdit(user *User) bool {
return user.HasAdminScope(config.ScopeForumAdmin) || f.OwnerID == user.ID
}
/*
PaginateForums scans over the available forums for a user.
Parameters:
- userID: of who is looking
- categories: optional, filter within categories
- pager
*/
func PaginateForums(user *User, categories []string, search *Search, subscribed bool, pager *Pagination) ([]*Forum, error) {
var (
fs = []*Forum{}
query = (&Forum{}).Preload()
wheres = []string{}
placeholders = []interface{}{}
)
if len(categories) > 0 {
wheres = append(wheres, "category IN ?")
placeholders = append(placeholders, categories)
}
// Hide explicit forum if user hasn't opted into it.
if !user.Explicit && !user.IsAdmin {
wheres = append(wheres, "explicit = false")
}
// Hide private forums except for admins and approved users.
if !user.IsAdmin {
wheres = append(wheres, `
(
private IS NOT TRUE
OR EXISTS (
SELECT 1
FROM forum_memberships
WHERE forum_id = forums.id
AND user_id = ?
AND (
is_moderator IS TRUE
OR approved IS TRUE
)
)
)`,
)
placeholders = append(placeholders, user.ID)
}
// Followed forums only? (for the My List category on home page)
if subscribed {
wheres = append(wheres, `
EXISTS (
SELECT 1
FROM forum_memberships
WHERE user_id = ?
AND forum_id = forums.id
)
`)
placeholders = append(placeholders, user.ID)
}
// Apply their search terms.
if search != nil {
for _, term := range search.Includes {
var ilike = "%" + strings.ToLower(term) + "%"
wheres = append(wheres, "(fragment ILIKE ? OR title ILIKE ? OR description ILIKE ?)")
placeholders = append(placeholders, ilike, ilike, ilike)
}
for _, term := range search.Excludes {
var ilike = "%" + strings.ToLower(term) + "%"
wheres = append(wheres, "(fragment NOT ILIKE ? AND title NOT ILIKE ? AND description NOT ILIKE ?)")
placeholders = append(placeholders, ilike, ilike, ilike)
}
}
// Filters?
if len(wheres) > 0 {
query = query.Where(
strings.Join(wheres, " AND "),
placeholders...,
)
}
query = query.Order(pager.Sort)
query.Model(&Forum{}).Count(&pager.Total)
result := query.Offset(pager.GetOffset()).Limit(pager.PerPage).Find(&fs)
return fs, result.Error
}
// PaginateOwnedForums returns forums the user owns (or all forums to admins).
func PaginateOwnedForums(userID uint64, isAdmin bool, categories []string, search *Search, pager *Pagination) ([]*Forum, error) {
var (
fs = []*Forum{}
query = (&Forum{}).Preload()
wheres = []string{}
placeholders = []interface{}{}
)
// Users see only their owned forums.
if !isAdmin {
wheres = append(wheres, "owner_id = ?")
placeholders = append(placeholders, userID)
}
if len(categories) > 0 {
wheres = append(wheres, "category IN ?")
placeholders = append(placeholders, categories)
}
// Apply their search terms.
if search != nil {
for _, term := range search.Includes {
var ilike = "%" + strings.ToLower(term) + "%"
wheres = append(wheres, "(fragment ILIKE ? OR title ILIKE ? OR description ILIKE ?)")
placeholders = append(placeholders, ilike, ilike, ilike)
}
for _, term := range search.Excludes {
var ilike = "%" + strings.ToLower(term) + "%"
wheres = append(wheres, "(fragment NOT ILIKE ? AND title NOT ILIKE ? AND description NOT ILIKE ?)")
placeholders = append(placeholders, ilike, ilike, ilike)
}
}
query = query.Where(
strings.Join(wheres, " AND "),
placeholders...,
).Order(pager.Sort)
query.Model(&Forum{}).Count(&pager.Total)
result := query.Offset(pager.GetOffset()).Limit(pager.PerPage).Find(&fs)
return fs, result.Error
}
// CreateForum.
func CreateForum(f *Forum) error {
result := DB.Create(f)
return result.Error
}
// Save a forum.
func (f *Forum) Save() error {
return DB.Save(f).Error
}
// CategorizedForum supports the main index page with custom categories.
type CategorizedForum struct {
Category string
Forums []*Forum
}
// CategorizeForums buckets forums into categories for front-end.
func CategorizeForums(fs []*Forum, categories []string) []*CategorizedForum {
var (
result = []*CategorizedForum{}
idxMap = map[string]int{}
)
// Forum Browse page: we are not grouping by categories but still need at least one.
if len(categories) == 0 {
return []*CategorizedForum{
{
Forums: fs,
},
}
}
// Initialize the result set.
for i, category := range categories {
result = append(result, &CategorizedForum{
Category: category,
Forums: []*Forum{},
})
idxMap[category] = i
}
// Bucket the forums into their categories.
for _, forum := range fs {
category := forum.Category
if category == "" {
continue
}
idx := idxMap[category]
result[idx].Forums = append(result[idx].Forums, forum)
}
// Remove any blank categories with no boards.
var filtered = []*CategorizedForum{}
for _, forum := range result {
if len(forum.Forums) == 0 {
continue
}
filtered = append(filtered, forum)
}
return filtered
}