b8146ae485
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.
275 lines
6.6 KiB
Go
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
|
|
}
|