Forum Admin Page for Regular Users
Allow regular (non-admin) users access to the Manage Forums page so they can create and manage their own forums. Things that were already working: * The admin forum page was already anticipating regular LoginRequired credential * Users only see their owned forums, while admins can see and manage ALL forums Improvements made to the Forum Admin page: * Change the title color from admin-red to user-blue. * Add ability to search (filter) and sort the forums. Other changes: * Turn the Forum tab bar into a reusable component.
This commit is contained in:
parent
ef95d05453
commit
1bf846e78b
|
@ -122,6 +122,9 @@ const (
|
|||
// rapidly it does not increment the view counter more.
|
||||
ThreadViewDebounceRedisKey = "debounce-view/user=%d/thr=%d"
|
||||
ThreadViewDebounceCooldown = 1 * time.Hour
|
||||
|
||||
// Enable user-owned forums (feature flag)
|
||||
UserForumsEnabled = true
|
||||
)
|
||||
|
||||
// Poll settings
|
||||
|
|
|
@ -12,7 +12,31 @@ import (
|
|||
// Manage page for forums -- admin only for now but may open up later.
|
||||
func Manage() http.HandlerFunc {
|
||||
tmpl := templates.Must("forum/admin.html")
|
||||
|
||||
// Whitelist for ordering options.
|
||||
var sortWhitelist = []string{
|
||||
"updated_at desc",
|
||||
"created_at desc",
|
||||
}
|
||||
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
var (
|
||||
searchTerm = r.FormValue("q")
|
||||
sort = r.FormValue("sort")
|
||||
sortOK bool
|
||||
)
|
||||
|
||||
// Sort options.
|
||||
for _, v := range sortWhitelist {
|
||||
if sort == v {
|
||||
sortOK = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !sortOK {
|
||||
sort = sortWhitelist[0]
|
||||
}
|
||||
|
||||
// Get the current user.
|
||||
currentUser, err := session.CurrentUser(r)
|
||||
if err != nil {
|
||||
|
@ -21,15 +45,18 @@ func Manage() http.HandlerFunc {
|
|||
return
|
||||
}
|
||||
|
||||
// Parse their search term.
|
||||
var search = models.ParseSearchString(searchTerm)
|
||||
|
||||
// Get forums the user owns or can manage.
|
||||
var pager = &models.Pagination{
|
||||
Page: 1,
|
||||
PerPage: config.PageSizeForumAdmin,
|
||||
Sort: "updated_at desc",
|
||||
Sort: sort,
|
||||
}
|
||||
pager.ParsePage(r)
|
||||
|
||||
forums, err := models.PaginateOwnedForums(currentUser.ID, currentUser.IsAdmin, pager)
|
||||
forums, err := models.PaginateOwnedForums(currentUser.ID, currentUser.IsAdmin, search, pager)
|
||||
if err != nil {
|
||||
session.FlashError(w, r, "Couldn't paginate owned forums: %s", err)
|
||||
templates.Redirect(w, "/")
|
||||
|
@ -39,6 +66,10 @@ func Manage() http.HandlerFunc {
|
|||
var vars = map[string]interface{}{
|
||||
"Pager": pager,
|
||||
"Forums": forums,
|
||||
|
||||
// Search filters.
|
||||
"SearchTerm": searchTerm,
|
||||
"Sort": sort,
|
||||
}
|
||||
if err := tmpl.Execute(w, r, vars); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
|
|
|
@ -52,10 +52,11 @@ func Newest() http.HandlerFunc {
|
|||
}
|
||||
|
||||
var vars = map[string]interface{}{
|
||||
"Pager": pager,
|
||||
"RecentPosts": posts,
|
||||
"PhotoMap": photos,
|
||||
"AllComments": allComments,
|
||||
"CurrentForumTab": "newest",
|
||||
"Pager": pager,
|
||||
"RecentPosts": posts,
|
||||
"PhotoMap": photos,
|
||||
"AllComments": allComments,
|
||||
}
|
||||
if err := tmpl.Execute(w, r, vars); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
|
|
|
@ -100,10 +100,11 @@ func Search() http.HandlerFunc {
|
|||
}
|
||||
|
||||
var vars = map[string]interface{}{
|
||||
"Pager": pager,
|
||||
"Comments": posts,
|
||||
"ThreadMap": threadMap,
|
||||
"PhotoMap": photos,
|
||||
"CurrentForumTab": "search",
|
||||
"Pager": pager,
|
||||
"Comments": posts,
|
||||
"ThreadMap": threadMap,
|
||||
"PhotoMap": photos,
|
||||
|
||||
"SearchTerm": searchTerm,
|
||||
"ByUsername": byUsername,
|
||||
|
|
|
@ -116,20 +116,39 @@ func PaginateForums(user *User, categories []string, pager *Pagination) ([]*Foru
|
|||
}
|
||||
|
||||
// PaginateOwnedForums returns forums the user owns (or all forums to admins).
|
||||
func PaginateOwnedForums(userID uint64, isAdmin bool, pager *Pagination) ([]*Forum, error) {
|
||||
func PaginateOwnedForums(userID uint64, isAdmin bool, search *Search, pager *Pagination) ([]*Forum, error) {
|
||||
var (
|
||||
fs = []*Forum{}
|
||||
query = (&Forum{}).Preload()
|
||||
fs = []*Forum{}
|
||||
query = (&Forum{}).Preload()
|
||||
wheres = []string{}
|
||||
placeholders = []interface{}{}
|
||||
)
|
||||
|
||||
// Users see only their owned forums.
|
||||
if !isAdmin {
|
||||
query = query.Where(
|
||||
"owner_id = ?",
|
||||
userID,
|
||||
)
|
||||
wheres = append(wheres, "owner_id = ?")
|
||||
placeholders = append(placeholders, userID)
|
||||
}
|
||||
|
||||
query = query.Order(pager.Sort)
|
||||
// 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
|
||||
|
|
|
@ -100,8 +100,8 @@ func New() http.Handler {
|
|||
mux.Handle("/admin/user-action", middleware.AdminRequired("", admin.UserActions()))
|
||||
mux.Handle("/admin/maintenance", middleware.AdminRequired(config.ScopeMaintenance, admin.Maintenance()))
|
||||
mux.Handle("/admin/add-user", middleware.AdminRequired(config.ScopeUserCreate, admin.AddUser()))
|
||||
mux.Handle("/forum/admin", middleware.AdminRequired(config.ScopeForumAdmin, forum.Manage()))
|
||||
mux.Handle("/forum/admin/edit", middleware.AdminRequired(config.ScopeForumAdmin, forum.AddEdit()))
|
||||
mux.Handle("/forum/admin", middleware.CertRequired(forum.Manage()))
|
||||
mux.Handle("/forum/admin/edit", middleware.CertRequired(forum.AddEdit()))
|
||||
mux.Handle("/admin/photo/mark-explicit", middleware.AdminRequired("", admin.MarkPhotoExplicit()))
|
||||
mux.Handle("GET /admin/changelog", middleware.AdminRequired(config.ScopeChangeLog, admin.ChangeLog()))
|
||||
|
||||
|
|
|
@ -23,6 +23,9 @@ func MergeVars(r *http.Request, m map[string]interface{}) {
|
|||
// Integrations
|
||||
m["TurnstileCAPTCHA"] = config.Current.Turnstile
|
||||
|
||||
// Feature flags
|
||||
m["FeatureUserForumsEnabled"] = config.UserForumsEnabled
|
||||
|
||||
if r == nil {
|
||||
return
|
||||
}
|
||||
|
|
|
@ -167,6 +167,7 @@ var baseTemplates = []string{
|
|||
config.TemplatePath + "/partials/right_click.html",
|
||||
config.TemplatePath + "/partials/mark_explicit.html",
|
||||
config.TemplatePath + "/partials/themes.html",
|
||||
config.TemplatePath + "/partials/forum_tabs.html",
|
||||
}
|
||||
|
||||
// templates returns a template chain with the base templates preceding yours.
|
||||
|
|
|
@ -1,11 +1,11 @@
|
|||
{{define "title"}}Forums{{end}}
|
||||
{{define "content"}}
|
||||
<div class="block">
|
||||
<section class="hero is-light is-danger">
|
||||
<section class="hero is-info is-bold">
|
||||
<div class="hero-body">
|
||||
<div class="container">
|
||||
<h1 class="title">
|
||||
<span class="icon mr-4"><i class="fa fa-peace"></i></span>
|
||||
<span class="icon mr-4"><i class="fa fa-book"></i></span>
|
||||
<span>Forum Administration</span>
|
||||
</h1>
|
||||
</div>
|
||||
|
@ -22,6 +22,65 @@
|
|||
</a>
|
||||
</div>
|
||||
|
||||
<div class="block p-2">
|
||||
<form action="{{.Request.URL.Path}}" method="GET">
|
||||
|
||||
<div class="card nonshy-collapsible-mobile">
|
||||
<header class="card-header has-background-link-light">
|
||||
<p class="card-header-title has-text-dark">
|
||||
Search Filters
|
||||
</p>
|
||||
<button class="card-header-icon" type="button">
|
||||
<span class="icon">
|
||||
<i class="fa fa-angle-up"></i>
|
||||
</span>
|
||||
</button>
|
||||
</header>
|
||||
<div class="card-content">
|
||||
<div class="columns">
|
||||
|
||||
<div class="column pr-1">
|
||||
<div class="field">
|
||||
<label class="label" for="q">Search terms:</label>
|
||||
<input type="text" class="input"
|
||||
name="q" id="q"
|
||||
autocomplete="off"
|
||||
value="{{.SearchTerm}}">
|
||||
<p class="help">
|
||||
Tip: you can <span class="has-text-success">"quote exact phrases"</span> and
|
||||
<span class="has-text-success">-exclude</span> words (or
|
||||
<span class="has-text-success">-"exclude phrases"</span>) from your search.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="column px-1">
|
||||
<div class="field">
|
||||
<label class="label" for="sort">Sort by:</label>
|
||||
<div class="select is-fullwidth">
|
||||
<select id="sort" name="sort">
|
||||
<option value="updated_at desc"{{if eq .Sort "updated_at desc"}} selected{{end}}>Last updated</option>
|
||||
<option value="created_at desc"{{if eq .Sort "created_at desc"}} selected{{end}}>Newest created</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="column is-narrow pl-1 has-text-right">
|
||||
<label class="label"> </label>
|
||||
<a href="{{.Request.URL.Path}}" class="button">Reset</a>
|
||||
<button type="submit" class="button is-success">
|
||||
<span>Search</span>
|
||||
<span class="icon"><i class="fa fa-search"></i></span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<p class="block p-2">
|
||||
Found <strong>{{.Pager.Total}}</strong> forum{{Pluralize64 .Pager.Total}} you can manage
|
||||
(page {{.Pager.Page}} of {{.Pager.Pages}}).
|
||||
|
|
|
@ -5,18 +5,18 @@
|
|||
<div class="hero-body">
|
||||
<div class="container">
|
||||
<div class="level">
|
||||
<div class="level-left">
|
||||
<div class="level-left mb-4">
|
||||
<h1 class="title">
|
||||
<span class="icon mr-4"><i class="fa fa-comments"></i></span>
|
||||
<span>Forums</span>
|
||||
</h1>
|
||||
</div>
|
||||
{{if .CurrentUser.HasAdminScope "admin.forum.manage"}}
|
||||
{{if or .FeatureUserForumsEnabled (.CurrentUser.HasAdminScope "admin.forum.manage")}}
|
||||
<div class="level-right">
|
||||
<div>
|
||||
<a href="/forum/admin" class="button is-small has-text-danger">
|
||||
<span class="icon"><i class="fa fa-peace"></i></span>
|
||||
<span>Manage Forums</span>
|
||||
<a href="/forum/admin" class="button is-small">
|
||||
<span class="icon"><i class="fa fa-circle-plus"></i></span>
|
||||
<span>Create a forum</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -29,27 +29,8 @@
|
|||
|
||||
{{$Root := .}}
|
||||
|
||||
<div class="block p-4 mb-0">
|
||||
<div class="tabs is-boxed">
|
||||
<ul>
|
||||
<li class="is-active">
|
||||
<a href="/forum">
|
||||
Categories
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="/forum/newest">
|
||||
Newest
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="/forum/search">
|
||||
<i class="fa fa-search mr-2"></i> Search
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Tab bar -->
|
||||
{{template "ForumTabs" .}}
|
||||
|
||||
{{range .Categories}}
|
||||
<div class="block p-4">
|
||||
|
|
|
@ -16,27 +16,8 @@
|
|||
|
||||
{{$Root := .}}
|
||||
|
||||
<div class="block p-4 mb-0">
|
||||
<div class="tabs is-boxed">
|
||||
<ul>
|
||||
<li>
|
||||
<a href="/forum">
|
||||
Categories
|
||||
</a>
|
||||
</li>
|
||||
<li class="is-active">
|
||||
<a href="/forum/newest">
|
||||
Newest
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="/forum/search">
|
||||
<i class="fa fa-search mr-2"></i> Search
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Tab bar -->
|
||||
{{template "ForumTabs" .}}
|
||||
|
||||
<div class="p-4">
|
||||
Found {{FormatNumberCommas .Pager.Total}} {{if .AllComments}}posts{{else}}threads{{end}} (page {{.Pager.Page}} of {{.Pager.Pages}})
|
||||
|
|
|
@ -16,27 +16,8 @@
|
|||
|
||||
{{$Root := .}}
|
||||
|
||||
<div class="block p-4 mb-0">
|
||||
<div class="tabs is-boxed">
|
||||
<ul>
|
||||
<li>
|
||||
<a href="/forum">
|
||||
Categories
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="/forum/newest">
|
||||
Newest
|
||||
</a>
|
||||
</li>
|
||||
<li class="is-active">
|
||||
<a href="/forum/search">
|
||||
<i class="fa fa-search mr-2"></i> Search
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Tab bar -->
|
||||
{{template "ForumTabs" .}}
|
||||
|
||||
<!-- Search fields -->
|
||||
<div class="p-4">
|
||||
|
|
32
web/templates/partials/forum_tabs.html
Normal file
32
web/templates/partials/forum_tabs.html
Normal file
|
@ -0,0 +1,32 @@
|
|||
<!--
|
||||
Reusable Forums tab bar.
|
||||
|
||||
Usage: [[ForumTabs .]]
|
||||
|
||||
Variables that your template should set:
|
||||
|
||||
- CurrentForumTab (string): one of categories, newest, search.
|
||||
-->
|
||||
{{define "ForumTabs"}}
|
||||
<div class="block p-4 mb-0">
|
||||
<div class="tabs is-boxed">
|
||||
<ul>
|
||||
<li {{if or (eq .CurrentForumTab "categories") (not .CurrentForumTab) }}class="is-active"{{end}}>
|
||||
<a href="/forum">
|
||||
Categories
|
||||
</a>
|
||||
</li>
|
||||
<li {{if eq .CurrentForumTab "newest" }}class="is-active"{{end}}>
|
||||
<a href="/forum/newest">
|
||||
Newest
|
||||
</a>
|
||||
</li>
|
||||
<li {{if eq .CurrentForumTab "search" }}class="is-active"{{end}}>
|
||||
<a href="/forum/search">
|
||||
<i class="fa fa-search mr-2"></i> Search
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
Loading…
Reference in New Issue
Block a user