From b8146ae4850a16236ed049582836e91c70ddac4d Mon Sep 17 00:00:00 2001 From: Noah Petherbridge Date: Thu, 22 Aug 2024 21:57:14 -0700 Subject: [PATCH] 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. --- pkg/config/config.go | 18 ++ pkg/controller/forum/add_edit.go | 7 + pkg/controller/forum/browse.go | 7 +- pkg/controller/forum/forums.go | 5 +- pkg/controller/forum/manage.go | 16 +- pkg/models/certification.go | 22 +++ pkg/models/forum.go | 7 +- pkg/models/forum_quota.go | 67 ++++++++ pkg/templates/template_funcs.go | 28 ++-- web/templates/account/profile.html | 12 +- web/templates/faq.html | 257 ++++++++++++++++++++++++++++- web/templates/forum/admin.html | 45 ++++- web/templates/forum/index.html | 16 +- 13 files changed, 473 insertions(+), 34 deletions(-) create mode 100644 pkg/models/forum_quota.go diff --git a/pkg/config/config.go b/pkg/config/config.go index 32322fc..3a93a2f 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -127,6 +127,24 @@ const ( UserForumsEnabled = true ) +// User-Owned Forums: Quota settings for how many forums a user can own. +var ( + // They get one forum after they've been Certified for 45 days. + UserForumQuotaCertLifetimeDays = time.Hour * 24 * 45 + + // Schedule for gaining additional quota for a number of comments written + // on any forum thread. The user must have the sum of all of these post + // counts to gain one forum per level. + UserForumQuotaCommentCountSchedule = []int64{ + 10, // Get a forum after your first 10 posts. + 20, // Get a 2nd forum after 20 additional posts (30 total) + 30, // 30 more posts (60 total) + 60, // 60 more posts (120 total) + 80, // 80 more posts (200 total) + 100, // and then one new forum for every 100 additional posts + } +) + // Poll settings var ( // Max number of responses to accept for a poll (how many form diff --git a/pkg/controller/forum/add_edit.go b/pkg/controller/forum/add_edit.go index ec60300..ba196f2 100644 --- a/pkg/controller/forum/add_edit.go +++ b/pkg/controller/forum/add_edit.go @@ -54,6 +54,13 @@ func AddEdit() http.HandlerFunc { } } + // If we are over our quota for User Forums, do not allow creating a new one. + if forum == nil && !currentUser.HasAdminScope(config.ScopeForumAdmin) && currentUser.ForumQuotaRemaining() <= 0 { + session.FlashError(w, r, "You do not currently have spare quota to create a new forum.") + templates.Redirect(w, "/forum/admin") + return + } + // Saving? if r.Method == http.MethodPost { var ( diff --git a/pkg/controller/forum/browse.go b/pkg/controller/forum/browse.go index fdbde91..cf4ae55 100644 --- a/pkg/controller/forum/browse.go +++ b/pkg/controller/forum/browse.go @@ -16,10 +16,10 @@ func Explore() http.HandlerFunc { // Whitelist for ordering options. var sortWhitelist = []string{ - "title asc", - "title desc", "created_at desc", "created_at asc", + "title asc", + "title desc", } return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { @@ -97,6 +97,9 @@ func Explore() http.HandlerFunc { "SearchTerm": searchTerm, "Show": show, "Sort": sort, + + // Current viewer's forum quota. + "ForumQuota": models.ComputeForumQuota(currentUser), } if err := tmpl.Execute(w, r, vars); err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) diff --git a/pkg/controller/forum/forums.go b/pkg/controller/forum/forums.go index 8972fc7..eb69e08 100644 --- a/pkg/controller/forum/forums.go +++ b/pkg/controller/forum/forums.go @@ -55,7 +55,7 @@ func Landing() http.HandlerFunc { myList, err := models.PaginateForums(currentUser, nil, nil, true, pager) if err != nil { session.FlashError(w, r, "Couldn't get your followed forums: %s", err) - } else { + } else if len(myList) > 0 { forums = append(forums, myList...) categorized = append([]*models.CategorizedForum{ { @@ -75,6 +75,9 @@ func Landing() http.HandlerFunc { "Categories": categorized, "ForumMap": forumMap, "FollowMap": followMap, + + // Current viewer's forum quota. + "ForumQuota": models.ComputeForumQuota(currentUser), } if err := tmpl.Execute(w, r, vars); err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) diff --git a/pkg/controller/forum/manage.go b/pkg/controller/forum/manage.go index 33905ca..9c2e941 100644 --- a/pkg/controller/forum/manage.go +++ b/pkg/controller/forum/manage.go @@ -22,6 +22,8 @@ func Manage() http.HandlerFunc { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { var ( searchTerm = r.FormValue("q") + show = r.FormValue("show") + categories = []string{} sort = r.FormValue("sort") sortOK bool ) @@ -37,6 +39,13 @@ func Manage() http.HandlerFunc { sort = sortWhitelist[0] } + // Show options. + if show == "official" { + categories = config.ForumCategories + } else if show == "community" { + categories = []string{""} + } + // Get the current user. currentUser, err := session.CurrentUser(r) if err != nil { @@ -56,7 +65,7 @@ func Manage() http.HandlerFunc { } pager.ParsePage(r) - forums, err := models.PaginateOwnedForums(currentUser.ID, currentUser.IsAdmin, search, pager) + forums, err := models.PaginateOwnedForums(currentUser.ID, currentUser.IsAdmin, categories, search, pager) if err != nil { session.FlashError(w, r, "Couldn't paginate owned forums: %s", err) templates.Redirect(w, "/") @@ -67,8 +76,13 @@ func Manage() http.HandlerFunc { "Pager": pager, "Forums": forums, + // Quote settings. + "QuotaLimit": models.ComputeForumQuota(currentUser), + "QuotaCount": models.CountOwnedUserForums(currentUser), + // Search filters. "SearchTerm": searchTerm, + "Show": show, "Sort": sort, } if err := tmpl.Execute(w, r, vars); err != nil { diff --git a/pkg/models/certification.go b/pkg/models/certification.go index de44fce..5377c5d 100644 --- a/pkg/models/certification.go +++ b/pkg/models/certification.go @@ -1,6 +1,8 @@ package models import ( + "errors" + "fmt" "time" "gorm.io/gorm" @@ -50,6 +52,26 @@ func GetCertificationPhoto(userID uint64) (*CertificationPhoto, error) { return p, result.Error } +// CertifiedSince retrieve's the last updated date of the user's certification photo, if approved. +// +// This incurs a DB query for their cert photo. +func (u *User) CertifiedSince() (time.Time, error) { + if !u.Certified { + return time.Time{}, errors.New("user is not certified") + } + + cert, err := GetCertificationPhoto(u.ID) + if err != nil { + return time.Time{}, err + } + + if cert.Status != CertificationPhotoApproved { + return time.Time{}, fmt.Errorf("cert photo status is: %s (expected 'approved')", cert.Status) + } + + return cert.UpdatedAt, nil +} + // CertificationPhotosNeedingApproval returns a pager of the pictures that require admin approval. func CertificationPhotosNeedingApproval(status CertificationPhotoStatus, pager *Pagination) ([]*CertificationPhoto, error) { var p = []*CertificationPhoto{} diff --git a/pkg/models/forum.go b/pkg/models/forum.go index 3ccb0d6..a58366d 100644 --- a/pkg/models/forum.go +++ b/pkg/models/forum.go @@ -166,7 +166,7 @@ func PaginateForums(user *User, categories []string, search *Search, subscribed } // PaginateOwnedForums returns forums the user owns (or all forums to admins). -func PaginateOwnedForums(userID uint64, isAdmin bool, search *Search, pager *Pagination) ([]*Forum, error) { +func PaginateOwnedForums(userID uint64, isAdmin bool, categories []string, search *Search, pager *Pagination) ([]*Forum, error) { var ( fs = []*Forum{} query = (&Forum{}).Preload() @@ -180,6 +180,11 @@ func PaginateOwnedForums(userID uint64, isAdmin bool, search *Search, pager *Pag 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 { diff --git a/pkg/models/forum_quota.go b/pkg/models/forum_quota.go new file mode 100644 index 0000000..3695d06 --- /dev/null +++ b/pkg/models/forum_quota.go @@ -0,0 +1,67 @@ +package models + +import ( + "time" + + "code.nonshy.com/nonshy/website/pkg/config" + "code.nonshy.com/nonshy/website/pkg/log" +) + +// Functions dealing with quota allowances for user-created forums. + +// ComputeForumQuota returns a count of how many user-created forums this user is allowed to own. +// +// The forum quota slowly increases over time and quantity of forum posts written by this user. +func ComputeForumQuota(user *User) int64 { + var ( + credits int64 + numPosts = CountCommentsByUser(user, "threads") + ) + + // Get the user's certification date. They get one credit for having a long standing + // certified status on their account. + if certSince, err := user.CertifiedSince(); err != nil { + return 0 + } else if time.Since(certSince) > config.UserForumQuotaCertLifetimeDays { + credits++ + } + + // Take their number of posts and compute their quota. + var schedule int64 + for _, schedule = range config.UserForumQuotaCommentCountSchedule { + if numPosts > schedule { + credits++ + numPosts -= schedule + } + } + + // If they still have posts, repeat the final schedule per credit. + for numPosts > schedule { + credits++ + numPosts -= schedule + } + + return credits +} + +// ForumQuotaRemaining computes the amount of additional forums the current user can create. +func (u *User) ForumQuotaRemaining() int64 { + var ( + quota = ComputeForumQuota(u) + owned = CountOwnedUserForums(u) + ) + return quota - owned +} + +// CountOwnedUserForums returns the total number of user forums owned by the given user. +func CountOwnedUserForums(user *User) int64 { + var count int64 + result := DB.Model(&Forum{}).Where( + "owner_id = ? AND (category='' OR category IS NULL)", + user.ID, + ).Count(&count) + if result.Error != nil { + log.Error("CountOwnedUserForums(%d): %s", user.ID, result.Error) + } + return count +} diff --git a/pkg/templates/template_funcs.go b/pkg/templates/template_funcs.go index af56e89..9a22860 100644 --- a/pkg/templates/template_funcs.go +++ b/pkg/templates/template_funcs.go @@ -52,17 +52,18 @@ func TemplateFuncs(r *http.Request) template.FuncMap { `s`, ) }, - "Pluralize": Pluralize[int], - "Pluralize64": Pluralize[int64], - "PluralizeU64": Pluralize[uint64], - "Substring": Substring, - "TrimEllipses": TrimEllipses, - "IterRange": IterRange, - "SubtractInt": SubtractInt, - "UrlEncode": UrlEncode, - "QueryPlus": QueryPlus(r), - "SimplePager": SimplePager(r), - "HasSuffix": strings.HasSuffix, + "Pluralize": Pluralize[int], + "Pluralize64": Pluralize[int64], + "PluralizeU64": Pluralize[uint64], + "Substring": Substring, + "TrimEllipses": TrimEllipses, + "IterRange": IterRange, + "SubtractInt": SubtractInt, + "SubtractInt64": SubtractInt64, + "UrlEncode": UrlEncode, + "QueryPlus": QueryPlus(r), + "SimplePager": SimplePager(r), + "HasSuffix": strings.HasSuffix, // Test if a photo should be blurred ({{BlurExplicit .Photo}}) "BlurExplicit": BlurExplicit(r), @@ -232,6 +233,11 @@ func SubtractInt(a, b int) int { return a - b } +// SubtractInt64 subtracts two numbers. +func SubtractInt64(a, b int64) int64 { + return a - b +} + // UrlEncode escapes a series of values (joined with no delimiter) func UrlEncode(values ...interface{}) string { var result string diff --git a/web/templates/account/profile.html b/web/templates/account/profile.html index f07bd74..4d51a4d 100644 --- a/web/templates/account/profile.html +++ b/web/templates/account/profile.html @@ -72,17 +72,23 @@ {{end}} {{if .User.Certified}}
-
+
Certified! + {{$CertSince := .User.CertifiedSince}} + + {{SincePrettyCoarse $CertSince}} ago + + {{if .CurrentUser.IsAdmin}} + class="fa fa-image ml-1 is-size-7" + title="Search for certification picture"> + {{end}}
diff --git a/web/templates/faq.html b/web/templates/faq.html index 098e41b..2c95073 100644 --- a/web/templates/faq.html +++ b/web/templates/faq.html @@ -62,7 +62,21 @@
  • @@ -859,28 +873,257 @@

    Can I create my own forums?

    +

    + NEW: August 30, 2024 +

    + + {{if .FeatureUserForumsEnabled}} +

    + Yes! As of August 30, 2024 (the two-year anniversary of {{PrettyTitle}}), + we now have Community Forums where members are allowed to create their own + boards. +

    + +

    + This feature is available to {{PrettyTitle}} members who have been Certified and continued to hang around + on the site for a while, or who have written a post of posts on the forums that we already have. +

    + +

    + To create your own forum, look for the "Create a forum" button in the header of the + Forums landing page. The button should appear on the Categories or the Explore tab. +

    + +

    + If you do not see the button, it may be because your {{PrettyTitle}} account is too new and you have + not earned allowance to create your first forum yet. See the next question + for more information. +

    + {{else}}

    This feature is coming soon! Users will be allowed to create their own forums and act as moderator within their own board. The forum admin pages need a bit more spit & polish before it's ready!

    + {{end}} + + {{if .FeatureUserForumsEnabled}} +

    Why can I only create a couple of forums?

    - Some related features with managing your own forums will include: + Managing your own forum can be a big responsibility, and it is preferable that a forum's owner + should be an active participant on the website and is not likely to delete their account in the + near future (which would leave their forums orphaned). We + also wish to avoid a "power moderator" who might snipe all of the best names for forums before + anybody else could have a chance to create those forums themselves. +

    + +

    + So, the allowance to create your own forums is a privileged and is earned over time. + A couple of the easiest ways you are likely to gain your first forum include: +

    + +
      +
    1. + Simply owning a Certified {{PrettyTitle}} account for at least 45 days. +
    2. +
    3. + Participating with us on the forums that we already have and writing + at least 10 posts or replies. +
    4. +
    + +

    + If you achieve both, you will have allowance to create two forums to call + your own. +

    + +

    + You can gain additional allowance by continuing to participate on the forum. + As you write more posts yourself, on any forum, you will be able to create and manage additional + forums of your own! +

    + +

    What should I make a forum about?

    + +

    + Make it about anything you want! (Within reason -- the global website rules always apply!) +

    + +

    + Here are some examples for inspiration on what your forum could be about:

    • - You'll be able to make your forum "invite-only" if you want, where only approved - members can see and reply to threads. + Regional forums: create one for your city or country, so that {{PrettyTitle}} members who live near + the area can meet up discuss.
    • - You'll be able to choose other users to help you moderate your forum. As the forum - owner, you'll retain admin control of your forum unless you assign ownership away - to another member. + Hobbies or interests: create forums about board games, Star Trek, knitting or sewing, golf or + scrabble -- and then nerd out with like-minded members of the {{PrettyTitle}} community! +
    • +
    • + Nude beaches, resorts or clubs: create a forum for your favorite spot for local fans to follow + and chat about!
    +

    How do I find all these forums that people are creating?

    + +

    + From the Forums page, click on the Explore tab. +

    + +

    + This page will show a view into all of the forums that exist on {{PrettyTitle}}. + You may click into the "Search Filters" box to search and sort the forum list so you may narrow + in on interesting forums to follow. +

    + +

    + To follow a forum, click into it so that you see its posts and look for the + " Follow" button in the page header. This will add the + forum to "My List" and you will be able to easily find it again from there. +

    + +

    What is "My List?"

    + +

    + When you have followed a forum, it will be added to + "My List" and it will now appear on your Forums home page. +

    + +

    + On the Forums home page, a "My List" category will appear up top and + show the latest posts on all of your favorite forums. On the "Newest" + tab, you can toggle to see only "My List" and then you can easily catch up with only + the newest posts on forums you care about and hide all the rest! +

    + +

    How do I keep up with new forum posts only from My List?

    + +

    + On the "Newest" tab of the forums, there are controls near the + top of the page to select Which forums you want to see new posts from. +

    + +

    + By selecting "My List", the "Newest" tab will only show new posts from the forums that you have + specifically followed. This way, you can tune out all the rest of the noise across the forums if + you don't care about them, and keep up with only the topics you want to see. +

    + +

    + Note: the Newest tab will remember your last setting! So if you leave it on "My List," + that will be the new default when you come back later. +

    + +

    How do I follow a forum that I'm interested in?

    + +

    + At the top of the forum's home page (where you can see all its threads), look for the + " Follow" button in the header of the page. The forum will + be added to "My List" which will appear on the Forum home page. +

    + +

    + You can also un-follow a forum by using the same button, which will be updated with the + text " Unfollow" and will confirm that you're sure when clicked. +

    + +

    How do I appoint moderators for my forum?

    + +

    + You may appoint other members from the {{PrettyTitle}} community to help you with moderating + your forum. You can choose any certified member who you know and trust. +

    + +

    + Forum moderators are able to help you with: +

    + +
      +
    • Deleting threads and replies that people have posted on your forum.
    • +
    • Locking threads to any new comments in case a conversation is going off the rails.
    • +
    + +

    + To appoint a moderator, go to your Forum Management page and click + on the "Edit" button for one of your existing forums. At the bottom of its settings is the + Moderators list, with a link below to appoint a new moderator. You may also + remove moderators from this page when you have any. +

    + +

    + Copy and paste their username and confirm that you got the right profile, and they can be added + to your moderator team. They will receive a notification and be subscribed to your forum + automatically. +

    + +

    What can forum owners and moderators do?

    + +

    + Starting from the bottom up: +

    + +

    + + Forum Moderators are appointed by a forum's owner to help them moderate it. + They can: +

      +
    • Delete posts or replies written by anybody on their specific forum.
    • +
    • Lock or unlock threads to prevent new replies in case a discussion is going off the rails.
    • +
    +

    +

    + + Forum Owners have additional management controls over their forum. They can + do everything Moderators can, plus the ability to: +

      +
    • Manage the forum's settings (title, description, options).
    • +
    • Pin threads to the top of the forum.
    • +
    • Add or remove additional moderators.
    • +
    +

    +

    + + Website Admins who manage the forums overall can do all of the above. +

    + +

    What happens if the forum owner deletes their account?

    + +

    + If you create your own Forum, and then decide to fully delete your {{PrettyTitle}} account in the + future, then you will leave your forums without an owner. +

    + +

    + Your forums will still exist and other members' posts and replies in them will remain. Your + posts and replies will have been deleted, along with your account, but your forums remain because + it wouldn't be fair to your forum's members if _all of their_ posts would need to be deleted along + with it. +

    + +

    + Your forums will just be without an Owner and may be put up for adoption by another member. Any + moderators that you had appointed to help you manage your forum will also remain as moderators, + and they would have priority if any of them wanted to step in as the forum's new owner. +

    + +

    + Note: all forums may be moderated by (some) {{PrettyTitle}} admins, even if a + forum is currently without an owner or any appointed moderators. +

    + +

    How can I request to adopt a forum without an owner?

    + +

    + For now, use the red "Report this forum" link at the bottom of the page and write a message + requesting to take ownership of the forum. +

    + {{end}} +

    Chat Room FAQs

    Who can access the chat rooms?

    diff --git a/web/templates/forum/admin.html b/web/templates/forum/admin.html index b0ee37e..e2ee4c8 100644 --- a/web/templates/forum/admin.html +++ b/web/templates/forum/admin.html @@ -6,7 +6,7 @@

    - Forum Administration + My Forums

  • @@ -15,12 +15,40 @@ {{$Root := .}} +
    +

    + On this page, you may create your own Forums around any topic of your choosing. Create + a forum for people in your local area, or create one for your favorite hobby so that others who share + your interests may join your forum. +

    +
    + +
    +

    + You currently own {{.QuotaCount}} of your allowed {{.QuotaLimit}} forum{{Pluralize64 .QuotaLimit}}. + + {{if ge .QuotaCount .QuotaLimit}} + You have reached your maximum amount of owned forums at this time. + {{else}} + You may create {{SubtractInt64 .QuotaLimit .QuotaCount}} more forum{{Pluralize64 (SubtractInt64 .QuotaLimit .QuotaCount)}}. + {{end}} +

    + +

    + As you continue to participate on the Forums, your allowance of forums you may create yourself will increase over time. + Learn more +

    +
    + + +{{if or (.CurrentUser.HasAdminScope "admin.forum.manage") (gt .QuotaLimit .QuotaCount)}}
    Create New Forum
    +{{end}}
    @@ -54,6 +82,21 @@
    + {{if .CurrentUser.IsAdmin}} +
    +
    + +
    + +
    +
    +
    + {{end}} +
    diff --git a/web/templates/forum/index.html b/web/templates/forum/index.html index 2ac741b..0589325 100644 --- a/web/templates/forum/index.html +++ b/web/templates/forum/index.html @@ -12,14 +12,16 @@
    {{if or .FeatureUserForumsEnabled (.CurrentUser.HasAdminScope "admin.forum.manage")}} -
    -
    - - - Create a forum - + {{if .ForumQuota}} + -
    + {{end}} {{end}}