Disable forum/blog actions for projects that don't have them enabled

I implemented this for personal projects, but I think it was actually
affecting official projects too that didn't have forums or blogs
enabled.
This commit is contained in:
Ben Visness 2021-11-10 09:13:56 -08:00
parent 702036eac3
commit c6387e2885
8 changed files with 82 additions and 40 deletions

View File

@ -67,7 +67,7 @@ type Project struct {
ForumEnabled bool `db:"forum_enabled"` ForumEnabled bool `db:"forum_enabled"`
BlogEnabled bool `db:"blog_enabled"` BlogEnabled bool `db:"blog_enabled"`
LibraryEnabled bool `db:"library_enabled"` LibraryEnabled bool `db:"library_enabled"` // TODO: Delete this field from the db
} }
func (p *Project) IsHMN() bool { func (p *Project) IsHMN() bool {
@ -82,6 +82,19 @@ func (p *Project) Subdomain() string {
return p.Slug return p.Slug
} }
// Checks whether the project has forums enabled. This should restrict the creation of new forum
// content, but it should NOT prevent the viewing of existing forum content. (Projects may at one
// point have forums enabled, write some stuff, and then later disable forums, and we want that
// content to stay accessible.) Hiding the navigation is ok.
func (p *Project) HasForums() bool {
return !p.Personal && p.ForumEnabled
}
// Same as HasForums, but for blogs.
func (p *Project) HasBlog() bool {
return !p.Personal && p.BlogEnabled
}
var slugUnsafeChars = regexp.MustCompile(`[^a-zA-Z0-9-]`) var slugUnsafeChars = regexp.MustCompile(`[^a-zA-Z0-9-]`)
var slugHyphenRun = regexp.MustCompile(`-+`) var slugHyphenRun = regexp.MustCompile(`-+`)

View File

@ -80,8 +80,8 @@ func ProjectToTemplate(p *models.Project, url string, theme string) Project {
IsHMN: p.IsHMN(), IsHMN: p.IsHMN(),
HasBlog: p.BlogEnabled, HasBlog: p.HasBlog(),
HasForum: p.ForumEnabled, HasForum: p.HasForums(),
DateApproved: p.DateApproved, DateApproved: p.DateApproved,
} }

View File

@ -20,7 +20,7 @@
</div> </div>
</div> </div>
<div> <div>
{{ if $.User }} {{ if and $.User $.Project.HasBlog }}
<div class="flex"> <div class="flex">
{{ if or (eq .Author.ID $.User.ID) $.User.IsStaff }} {{ if or (eq .Author.ID $.User.ID) $.User.IsStaff }}
<a class="delete action button" href="{{ .DeleteUrl }}" title="Delete">&#10006;</a>&nbsp; <a class="delete action button" href="{{ .DeleteUrl }}" title="Delete">&#10006;</a>&nbsp;
@ -81,7 +81,7 @@
</div> </div>
</div> </div>
<div> <div>
{{ if $.User }} {{ if and $.User $.Project.HasBlog }}
<div class="flex"> <div class="flex">
{{ if or (eq .Author.ID $.User.ID) $.User.IsStaff }} {{ if or (eq .Author.ID $.User.ID) $.User.IsStaff }}
<a class="delete action button" href="{{ .DeleteUrl }}" title="Delete">&#10006;</a>&nbsp; <a class="delete action button" href="{{ .DeleteUrl }}" title="Delete">&#10006;</a>&nbsp;
@ -111,11 +111,13 @@
<div class="optionbar bottom"> <div class="optionbar bottom">
<div class="options"> <div class="options">
{{ if .Project.HasBlog }}
{{ if $.User }} {{ if $.User }}
<a class="button" href="{{ .ReplyLink }}"><span class="big pr1">+</span> Add Comment</a> <a class="button" href="{{ .ReplyLink }}"><span class="big pr1">+</span> Add Comment</a>
{{ else }} {{ else }}
<a class="button" href="{{ .LoginLink }}">Log in to comment</a> <a class="button" href="{{ .LoginLink }}">Log in to comment</a>
{{ end }} {{ end }}
{{ end }}
</div> </div>
</div> </div>
</div> </div>

View File

@ -34,14 +34,18 @@
{{ define "subforum_options" }} {{ define "subforum_options" }}
<div class="options"> <div class="options">
{{ if .Project.HasForum }}
{{ if .User }} {{ if .User }}
<a class="button new-thread" href="{{ .NewThreadUrl }}"><span class="big pr1">+</span> New Thread</a> <a class="button new-thread" href="{{ .NewThreadUrl }}"><span class="big pr1">+</span> New Thread</a>
{{ else }}
<a class="button" href="{{ .LoginPageUrl }}">Log in to post a new thread</a>
{{ end }}
{{ end }}
{{ if .User }}
<form method="POST" action="{{ .MarkReadUrl }}"> <form method="POST" action="{{ .MarkReadUrl }}">
{{ csrftoken .Session }} {{ csrftoken .Session }}
<button type="submit"><span class="big pr1">&#x2713;</span> Mark threads here as read</button> <button type="submit"><span class="big pr1">&#x2713;</span> Mark threads here as read</button>
</form> </form>
{{ else }}
<a class="button" href="{{ .LoginPageUrl }}">Log in to post a new thread</a>
{{ end }} {{ end }}
</div> </div>
<div class="options"> <div class="options">

View File

@ -61,7 +61,7 @@
<div class="postid"> <div class="postid">
<a name="{{ .ID }}" href="{{ .Url }}">#{{ .ID }}</a> <a name="{{ .ID }}" href="{{ .Url }}">#{{ .ID }}</a>
</div> </div>
{{ if $.User }} {{ if and $.User $.Project.HasForum }}
<div class="flex pr3"> <div class="flex pr3">
{{ if or (eq .Author.ID $.User.ID) $.User.IsStaff }} {{ if or (eq .Author.ID $.User.ID) $.User.IsStaff }}
<a class="delete action button" href="{{ .DeleteUrl }}" title="Delete">&#10006;</a>&nbsp; <a class="delete action button" href="{{ .DeleteUrl }}" title="Delete">&#10006;</a>&nbsp;
@ -120,11 +120,13 @@
<a class="button" href="{{ .SubforumUrl }}">&larr; Back to index</a> <a class="button" href="{{ .SubforumUrl }}">&larr; Back to index</a>
{{ if .Thread.Locked }} {{ if .Thread.Locked }}
<span>Thread is locked.</span> <span>Thread is locked.</span>
{{ else if .User }} {{ else if .Project.HasForum }}
{{ if .User }}
<a class="button" href="{{ .ReplyUrl }}">&#10551; Reply to Thread</a> <a class="button" href="{{ .ReplyUrl }}">&#10551; Reply to Thread</a>
{{ else }} {{ else }}
<span class="pa2"><a href="{{ .LoginPageUrl }}">Log in</a> to reply</span> <span class="pa2"><a href="{{ .LoginPageUrl }}">Log in</a> to reply</span>
{{ end }} {{ end }}
{{ end }}
</div> </div>
<div class="options order-0 order-last-ns"> <div class="options order-0 order-last-ns">
{{ template "pagination.html" .Pagination }} {{ template "pagination.html" .Pagination }}

View File

@ -94,8 +94,8 @@ func getBaseData(c *RequestContext, title string, breadcrumbs []templates.Breadc
} }
baseData.Header.Project = &templates.ProjectHeader{ baseData.Header.Project = &templates.ProjectHeader{
HasForums: c.CurrentProject.ForumEnabled, HasForums: c.CurrentProject.HasForums(),
HasBlog: c.CurrentProject.BlogEnabled, HasBlog: c.CurrentProject.HasBlog(),
HasEpisodeGuide: hasAnnotations, HasEpisodeGuide: hasAnnotations,
ForumsUrl: c.UrlContext.BuildForum(nil, 1), ForumsUrl: c.UrlContext.BuildForum(nil, 1),
BlogUrl: c.UrlContext.BuildBlog(1), BlogUrl: c.UrlContext.BuildBlog(1),

View File

@ -35,7 +35,7 @@ func BlogIndex(c *RequestContext) ResponseData {
const postsPerPage = 5 const postsPerPage = 5
numPosts, err := CountPosts(c.Context(), c.Conn, c.CurrentUser, PostsQuery{ numThreads, err := CountThreads(c.Context(), c.Conn, c.CurrentUser, ThreadsQuery{
ProjectIDs: []int{c.CurrentProject.ID}, ProjectIDs: []int{c.CurrentProject.ID},
ThreadTypes: []models.ThreadType{models.ThreadTypeProjectBlogPost}, ThreadTypes: []models.ThreadType{models.ThreadTypeProjectBlogPost},
}) })
@ -43,7 +43,7 @@ func BlogIndex(c *RequestContext) ResponseData {
return c.ErrorResponse(http.StatusInternalServerError, oops.New(err, "failed to fetch total number of blog posts")) return c.ErrorResponse(http.StatusInternalServerError, oops.New(err, "failed to fetch total number of blog posts"))
} }
numPages := utils.NumPages(numPosts, postsPerPage) numPages := utils.NumPages(numThreads, postsPerPage)
page, ok := ParsePageNumber(c, "page", numPages) page, ok := ParsePageNumber(c, "page", numPages)
if !ok { if !ok {
c.Redirect(c.UrlContext.BuildBlog(page), http.StatusSeeOther) c.Redirect(c.UrlContext.BuildBlog(page), http.StatusSeeOther)
@ -73,7 +73,7 @@ func BlogIndex(c *RequestContext) ResponseData {
baseData := getBaseData(c, fmt.Sprintf("%s Blog", c.CurrentProject.Name), []templates.Breadcrumb{BlogBreadcrumb(c.UrlContext)}) baseData := getBaseData(c, fmt.Sprintf("%s Blog", c.CurrentProject.Name), []templates.Breadcrumb{BlogBreadcrumb(c.UrlContext)})
canCreate := false canCreate := false
if c.CurrentUser != nil { if c.CurrentProject.HasBlog() && c.CurrentUser != nil {
isProjectOwner := false isProjectOwner := false
owners, err := FetchProjectOwners(c.Context(), c.Conn, c.CurrentProject.ID) owners, err := FetchProjectOwners(c.Context(), c.Conn, c.CurrentProject.ID)
if err != nil { if err != nil {

View File

@ -216,7 +216,6 @@ func NewWebsiteRoutes(longRequestContext context.Context, conn *pgxpool.Pool, pe
hmnOnly.GET(hmnurl.RegexLibraryAny, LibraryNotPortedYet) hmnOnly.GET(hmnurl.RegexLibraryAny, LibraryNotPortedYet)
// NOTE(asaf): Any-project routes:
attachProjectRoutes := func(rb *RouteBuilder) { attachProjectRoutes := func(rb *RouteBuilder) {
rb.GET(hmnurl.RegexHomepage, func(c *RequestContext) ResponseData { rb.GET(hmnurl.RegexHomepage, func(c *RequestContext) ResponseData {
if c.CurrentProject.IsHMN() { if c.CurrentProject.IsHMN() {
@ -226,31 +225,53 @@ func NewWebsiteRoutes(longRequestContext context.Context, conn *pgxpool.Pool, pe
} }
}) })
rb.POST(hmnurl.RegexForumNewThreadSubmit, authMiddleware(csrfMiddleware(ForumNewThreadSubmit))) // Middleware used for forum action routes - anything related to actually creating or editing forum content
rb.GET(hmnurl.RegexForumNewThread, authMiddleware(ForumNewThread)) needsForums := func(h Handler) Handler {
return func(c *RequestContext) ResponseData {
// 404 if the project has forums disabled
if !c.CurrentProject.HasForums() {
return FourOhFour(c)
}
// Require auth if forums are enabled
return authMiddleware(h)(c)
}
}
rb.POST(hmnurl.RegexForumNewThreadSubmit, needsForums(csrfMiddleware(ForumNewThreadSubmit)))
rb.GET(hmnurl.RegexForumNewThread, needsForums(ForumNewThread))
rb.GET(hmnurl.RegexForumThread, ForumThread) rb.GET(hmnurl.RegexForumThread, ForumThread)
rb.GET(hmnurl.RegexForum, Forum) rb.GET(hmnurl.RegexForum, Forum)
rb.POST(hmnurl.RegexForumMarkRead, authMiddleware(csrfMiddleware(ForumMarkRead))) rb.POST(hmnurl.RegexForumMarkRead, authMiddleware(csrfMiddleware(ForumMarkRead))) // needs auth but doesn't need forums enabled
rb.GET(hmnurl.RegexForumPost, ForumPostRedirect) rb.GET(hmnurl.RegexForumPost, ForumPostRedirect)
rb.GET(hmnurl.RegexForumPostReply, authMiddleware(ForumPostReply)) rb.GET(hmnurl.RegexForumPostReply, needsForums(ForumPostReply))
rb.POST(hmnurl.RegexForumPostReply, authMiddleware(csrfMiddleware(ForumPostReplySubmit))) rb.POST(hmnurl.RegexForumPostReply, needsForums(csrfMiddleware(ForumPostReplySubmit)))
rb.GET(hmnurl.RegexForumPostEdit, authMiddleware(ForumPostEdit)) rb.GET(hmnurl.RegexForumPostEdit, needsForums(ForumPostEdit))
rb.POST(hmnurl.RegexForumPostEdit, authMiddleware(csrfMiddleware(ForumPostEditSubmit))) rb.POST(hmnurl.RegexForumPostEdit, needsForums(csrfMiddleware(ForumPostEditSubmit)))
rb.GET(hmnurl.RegexForumPostDelete, authMiddleware(ForumPostDelete)) rb.GET(hmnurl.RegexForumPostDelete, needsForums(ForumPostDelete))
rb.POST(hmnurl.RegexForumPostDelete, authMiddleware(csrfMiddleware(ForumPostDeleteSubmit))) rb.POST(hmnurl.RegexForumPostDelete, needsForums(csrfMiddleware(ForumPostDeleteSubmit)))
rb.GET(hmnurl.RegexWikiArticle, WikiArticleRedirect) rb.GET(hmnurl.RegexWikiArticle, WikiArticleRedirect)
// Middleware used for blog action routes - anything related to actually creating or editing blog content
needsBlogs := func(h Handler) Handler {
return func(c *RequestContext) ResponseData {
// 404 if the project has blogs disabled
if !c.CurrentProject.HasBlog() {
return FourOhFour(c)
}
// Require auth if blogs are enabled
return authMiddleware(h)(c)
}
}
rb.GET(hmnurl.RegexBlog, BlogIndex) rb.GET(hmnurl.RegexBlog, BlogIndex)
rb.GET(hmnurl.RegexBlogNewThread, authMiddleware(BlogNewThread)) rb.GET(hmnurl.RegexBlogNewThread, needsBlogs(BlogNewThread))
rb.POST(hmnurl.RegexBlogNewThread, authMiddleware(csrfMiddleware(BlogNewThreadSubmit))) rb.POST(hmnurl.RegexBlogNewThread, needsBlogs(csrfMiddleware(BlogNewThreadSubmit)))
rb.GET(hmnurl.RegexBlogThread, BlogThread) rb.GET(hmnurl.RegexBlogThread, BlogThread)
rb.GET(hmnurl.RegexBlogPost, BlogPostRedirectToThread) rb.GET(hmnurl.RegexBlogPost, BlogPostRedirectToThread)
rb.GET(hmnurl.RegexBlogPostReply, authMiddleware(BlogPostReply)) rb.GET(hmnurl.RegexBlogPostReply, needsBlogs(BlogPostReply))
rb.POST(hmnurl.RegexBlogPostReply, authMiddleware(csrfMiddleware(BlogPostReplySubmit))) rb.POST(hmnurl.RegexBlogPostReply, needsBlogs(csrfMiddleware(BlogPostReplySubmit)))
rb.GET(hmnurl.RegexBlogPostEdit, authMiddleware(BlogPostEdit)) rb.GET(hmnurl.RegexBlogPostEdit, needsBlogs(BlogPostEdit))
rb.POST(hmnurl.RegexBlogPostEdit, authMiddleware(csrfMiddleware(BlogPostEditSubmit))) rb.POST(hmnurl.RegexBlogPostEdit, needsBlogs(csrfMiddleware(BlogPostEditSubmit)))
rb.GET(hmnurl.RegexBlogPostDelete, authMiddleware(BlogPostDelete)) rb.GET(hmnurl.RegexBlogPostDelete, needsBlogs(BlogPostDelete))
rb.POST(hmnurl.RegexBlogPostDelete, authMiddleware(csrfMiddleware(BlogPostDeleteSubmit))) rb.POST(hmnurl.RegexBlogPostDelete, needsBlogs(csrfMiddleware(BlogPostDeleteSubmit)))
rb.GET(hmnurl.RegexBlogsRedirect, func(c *RequestContext) ResponseData { rb.GET(hmnurl.RegexBlogsRedirect, func(c *RequestContext) ResponseData {
return c.Redirect(c.UrlContext.Url( return c.Redirect(c.UrlContext.Url(
fmt.Sprintf("blog%s", c.PathParams["remainder"]), nil, fmt.Sprintf("blog%s", c.PathParams["remainder"]), nil,