Implement blog posts
This commit is contained in:
parent
958aeb45e4
commit
b0f75675c8
|
@ -0,0 +1,44 @@
|
||||||
|
package migrations
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"git.handmade.network/hmn/hmn/src/migration/types"
|
||||||
|
"git.handmade.network/hmn/hmn/src/oops"
|
||||||
|
"github.com/jackc/pgx/v4"
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
registerMigration(DropPostParentID{})
|
||||||
|
}
|
||||||
|
|
||||||
|
type DropPostParentID struct{}
|
||||||
|
|
||||||
|
func (m DropPostParentID) Version() types.MigrationVersion {
|
||||||
|
return types.MigrationVersion(time.Date(2021, 7, 30, 16, 42, 40, 0, time.UTC))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m DropPostParentID) Name() string {
|
||||||
|
return "DropPostParentID"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m DropPostParentID) Description() string {
|
||||||
|
return "Drop the parent_id field from posts"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m DropPostParentID) Up(ctx context.Context, tx pgx.Tx) error {
|
||||||
|
_, err := tx.Exec(ctx, `
|
||||||
|
ALTER TABLE handmade_post
|
||||||
|
DROP parent_id;
|
||||||
|
`)
|
||||||
|
if err != nil {
|
||||||
|
return oops.New(err, "failed to drop parent_id field")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m DropPostParentID) Down(ctx context.Context, tx pgx.Tx) error {
|
||||||
|
panic("Implement me")
|
||||||
|
}
|
|
@ -10,7 +10,6 @@ type Post struct {
|
||||||
|
|
||||||
// TODO: Document each of these
|
// TODO: Document each of these
|
||||||
AuthorID *int `db:"author_id"`
|
AuthorID *int `db:"author_id"`
|
||||||
ParentID *int `db:"parent_id"`
|
|
||||||
ThreadID int `db:"thread_id"`
|
ThreadID int `db:"thread_id"`
|
||||||
CurrentID int `db:"current_id"`
|
CurrentID int `db:"current_id"`
|
||||||
ProjectID int `db:"project_id"`
|
ProjectID int `db:"project_id"`
|
||||||
|
|
|
@ -15,7 +15,7 @@ func PostToTemplate(p *models.Post, author *models.User, currentTheme string) Po
|
||||||
return Post{
|
return Post{
|
||||||
ID: p.ID,
|
ID: p.ID,
|
||||||
|
|
||||||
// Urls not set here. See AddUrls.
|
// Urls not set here. They vary per thread type. Set 'em yourself!
|
||||||
|
|
||||||
Preview: p.Preview,
|
Preview: p.Preview,
|
||||||
ReadOnly: p.ReadOnly,
|
ReadOnly: p.ReadOnly,
|
||||||
|
@ -38,13 +38,6 @@ func (p *Post) AddContentVersion(ver models.PostVersion, editor *models.User) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *Post) AddUrls(projectSlug string, subforums []string, threadId int, postId int) {
|
|
||||||
p.Url = hmnurl.BuildForumPost(projectSlug, subforums, threadId, postId)
|
|
||||||
p.DeleteUrl = hmnurl.BuildForumPostDelete(projectSlug, subforums, threadId, postId)
|
|
||||||
p.EditUrl = hmnurl.BuildForumPostEdit(projectSlug, subforums, threadId, postId)
|
|
||||||
p.ReplyUrl = hmnurl.BuildForumPostReply(projectSlug, subforums, threadId, postId)
|
|
||||||
}
|
|
||||||
|
|
||||||
var LifecycleBadgeClasses = map[models.ProjectLifecycle]string{
|
var LifecycleBadgeClasses = map[models.ProjectLifecycle]string{
|
||||||
models.ProjectLifecycleUnapproved: "",
|
models.ProjectLifecycleUnapproved: "",
|
||||||
models.ProjectLifecycleApprovalRequired: "",
|
models.ProjectLifecycleApprovalRequired: "",
|
||||||
|
|
|
@ -0,0 +1,119 @@
|
||||||
|
{{ template "base.html" . }}
|
||||||
|
|
||||||
|
{{ define "content" }}
|
||||||
|
<div class="mw7 margin-center tl post ph3 ph0-ns">
|
||||||
|
<h1>{{ .Thread.Title }}</h1>
|
||||||
|
{{ with .MainPost }}
|
||||||
|
<div class="flex justify-between items-center mt2 mb3">
|
||||||
|
<div class="flex items-center">
|
||||||
|
<div class="avatar-icon contain bg-center" style="background-image:url('{{ .Author.AvatarUrl }}');"></div>
|
||||||
|
<div class="flex flex-column ml2">
|
||||||
|
<div>
|
||||||
|
<a class="username" href="{{ .Author.ProfileUrl }}" target="_blank">{{ .Author.Name }}</a>
|
||||||
|
<div class="di ph1">
|
||||||
|
{{ if .Author.IsStaff }}
|
||||||
|
<div class="badge staff"></div>
|
||||||
|
{{ end }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="c--dim f7">{{ timehtml (absoluteshortdate .PostDate) .PostDate }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
{{ if $.User }}
|
||||||
|
<div class="flex">
|
||||||
|
{{ if or (eq .Author.ID $.User.ID) $.User.IsStaff }}
|
||||||
|
<a class="delete action button" href="{{ .DeleteUrl }}" title="Delete">✖</a>
|
||||||
|
<a class="edit action button" href="{{ .EditUrl }}" title="Edit">✎</a>
|
||||||
|
{{ end }}
|
||||||
|
{{ if or (not $.Thread.Locked) $.User.IsStaff }}
|
||||||
|
{{ if $.Thread.Locked }}
|
||||||
|
WARNING: locked thread - use power responsibly!
|
||||||
|
{{ end }}
|
||||||
|
<a class="reply action button" href="{{ .ReplyUrl }}" title="Reply">↪</a>
|
||||||
|
{{ end }}
|
||||||
|
</div>
|
||||||
|
{{ end }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{{ end }}
|
||||||
|
|
||||||
|
<!-- Main post -->
|
||||||
|
<div class="mb3">
|
||||||
|
<div class="contents overflow-x-auto">
|
||||||
|
{{ .MainPost.Content }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="optionbar"></div>
|
||||||
|
|
||||||
|
{{ range .Comments }}
|
||||||
|
<div class="pa3 flex items-start background-even">
|
||||||
|
<div>
|
||||||
|
<div class="avatar-icon contain bg-center" style="background-image:url('{{ .Author.AvatarUrl }}');"></div>
|
||||||
|
</div>
|
||||||
|
<div class="pl3 flex flex-column w-100">
|
||||||
|
<div class="flex justify-between">
|
||||||
|
<div>
|
||||||
|
<div>
|
||||||
|
<a class="username" href="{{ .Author.ProfileUrl }}" target="_blank">{{ .Author.Username }}</a>
|
||||||
|
<div class="di ph1">
|
||||||
|
{{ if .Author.IsStaff }}
|
||||||
|
<div class="badge staff"></div>
|
||||||
|
{{ end }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="c--dim f7">
|
||||||
|
{{ if and .Author.Name (ne .Author.Name .Author.Username) }}
|
||||||
|
{{ .Author.Name }},
|
||||||
|
{{ end }}
|
||||||
|
{{ timehtml (relativedate .PostDate) .PostDate }}
|
||||||
|
{{ if .Editor }}
|
||||||
|
<span class="pl3">
|
||||||
|
Edited by
|
||||||
|
<a class="name" href="{{ .Editor.ProfileUrl }}" target="_blank">{{ coalesce .Editor.Name .Editor.Username }}</a>
|
||||||
|
on {{ timehtml (absolutedate .EditDate) .EditDate }}
|
||||||
|
{{ with .EditReason }}
|
||||||
|
Reason: {{ . }}
|
||||||
|
{{ end }}
|
||||||
|
</span>
|
||||||
|
{{ end }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
{{ if $.User }}
|
||||||
|
<div class="flex">
|
||||||
|
{{ if or (eq .Author.ID $.User.ID) $.User.IsStaff }}
|
||||||
|
<a class="delete action button" href="{{ .DeleteUrl }}" title="Delete">✖</a>
|
||||||
|
<a class="edit action button" href="{{ .EditUrl }}" title="Edit">✎</a>
|
||||||
|
{{ end }}
|
||||||
|
{{ if or (not $.Thread.Locked) $.User.IsStaff }}
|
||||||
|
{{ if $.Thread.Locked }}
|
||||||
|
WARNING: locked thread - use power responsibly!
|
||||||
|
{{ end }}
|
||||||
|
<a class="reply action button" href="{{ .ReplyUrl }}" title="Reply">↪</a>
|
||||||
|
{{ end }}
|
||||||
|
</div>
|
||||||
|
{{ end }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="w-100 pt3">
|
||||||
|
<div class="contents overflow-x-auto">
|
||||||
|
{{ .Content }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{{ end }}
|
||||||
|
|
||||||
|
<div class="optionbar bottom">
|
||||||
|
<div class="options">
|
||||||
|
{{ if $.User }}
|
||||||
|
<a class="button" href="{{ .ReplyLink }}"><span class="big pr1">+</span> Add Comment</a>
|
||||||
|
{{ else }}
|
||||||
|
<a class="button" href="{{ .LoginLink }}">Log in to comment</a>
|
||||||
|
{{ end }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{{ end }}
|
|
@ -19,7 +19,7 @@
|
||||||
</div>
|
</div>
|
||||||
<div class="w-100-l pl3 pl0-l flex flex-column items-center-l">
|
<div class="w-100-l pl3 pl0-l flex flex-column items-center-l">
|
||||||
<div>
|
<div>
|
||||||
<a class="username" href="{{ .Author.ProfileUrl }}" target="_blank">{{ .Author.Username }}</a> {{/* TODO: Text scale stuff? Seems unnecessary. */}}
|
<a class="username" href="{{ .Author.ProfileUrl }}" target="_blank">{{ .Author.Username }}</a>
|
||||||
<!-- Mobile badges -->
|
<!-- Mobile badges -->
|
||||||
<div class="di dn-l ph1">
|
<div class="di dn-l ph1">
|
||||||
{{ if .Author.IsStaff }}
|
{{ if .Author.IsStaff }}
|
||||||
|
|
|
@ -0,0 +1,153 @@
|
||||||
|
package website
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
"git.handmade.network/hmn/hmn/src/db"
|
||||||
|
"git.handmade.network/hmn/hmn/src/hmnurl"
|
||||||
|
"git.handmade.network/hmn/hmn/src/templates"
|
||||||
|
)
|
||||||
|
|
||||||
|
func BlogThread(c *RequestContext) ResponseData {
|
||||||
|
type blogPostData struct {
|
||||||
|
templates.BaseData
|
||||||
|
Thread templates.Thread
|
||||||
|
MainPost templates.Post
|
||||||
|
Comments []templates.Post
|
||||||
|
ReplyLink string
|
||||||
|
LoginLink string
|
||||||
|
}
|
||||||
|
|
||||||
|
cd, ok := getCommonBlogData(c)
|
||||||
|
if !ok {
|
||||||
|
return FourOhFour(c)
|
||||||
|
}
|
||||||
|
|
||||||
|
thread, posts := FetchThreadPostsAndStuff(
|
||||||
|
c.Context(),
|
||||||
|
c.Conn,
|
||||||
|
cd.ThreadID,
|
||||||
|
0, 0,
|
||||||
|
)
|
||||||
|
|
||||||
|
var templatePosts []templates.Post
|
||||||
|
for _, p := range posts {
|
||||||
|
post := templates.PostToTemplate(&p.Post, p.Author, c.Theme)
|
||||||
|
post.AddContentVersion(p.CurrentVersion, p.Editor)
|
||||||
|
addBlogUrlsToPost(&post, c.CurrentProject.Slug, p.Thread.ID, p.Post.ID)
|
||||||
|
|
||||||
|
if p.ReplyPost != nil {
|
||||||
|
reply := templates.PostToTemplate(p.ReplyPost, p.ReplyAuthor, c.Theme)
|
||||||
|
addBlogUrlsToPost(&reply, c.CurrentProject.Slug, p.Thread.ID, p.Post.ID)
|
||||||
|
post.ReplyPost = &reply
|
||||||
|
}
|
||||||
|
|
||||||
|
templatePosts = append(templatePosts, post)
|
||||||
|
}
|
||||||
|
|
||||||
|
baseData := getBaseData(c)
|
||||||
|
baseData.Title = thread.Title
|
||||||
|
|
||||||
|
var res ResponseData
|
||||||
|
res.MustWriteTemplate("blog_post.html", blogPostData{
|
||||||
|
BaseData: baseData,
|
||||||
|
Thread: templates.ThreadToTemplate(&thread),
|
||||||
|
MainPost: templatePosts[0],
|
||||||
|
Comments: templatePosts[1:],
|
||||||
|
}, c.Perf)
|
||||||
|
return res
|
||||||
|
}
|
||||||
|
|
||||||
|
func BlogPostRedirectToThread(c *RequestContext) ResponseData {
|
||||||
|
cd, ok := getCommonBlogData(c)
|
||||||
|
if !ok {
|
||||||
|
return FourOhFour(c)
|
||||||
|
}
|
||||||
|
|
||||||
|
thread := FetchThread(c.Context(), c.Conn, cd.ThreadID)
|
||||||
|
|
||||||
|
threadUrl := hmnurl.BuildBlogThread(c.CurrentProject.Slug, cd.ThreadID, thread.Title, 1)
|
||||||
|
return c.Redirect(threadUrl, http.StatusFound)
|
||||||
|
}
|
||||||
|
|
||||||
|
type commonBlogData struct {
|
||||||
|
c *RequestContext
|
||||||
|
|
||||||
|
ThreadID int
|
||||||
|
PostID int
|
||||||
|
}
|
||||||
|
|
||||||
|
func getCommonBlogData(c *RequestContext) (commonBlogData, bool) {
|
||||||
|
c.Perf.StartBlock("BLOGS", "Fetch common blog data")
|
||||||
|
defer c.Perf.EndBlock()
|
||||||
|
|
||||||
|
res := commonBlogData{
|
||||||
|
c: c,
|
||||||
|
}
|
||||||
|
|
||||||
|
if threadIdStr, hasThreadId := c.PathParams["threadid"]; hasThreadId {
|
||||||
|
threadId, err := strconv.Atoi(threadIdStr)
|
||||||
|
if err != nil {
|
||||||
|
return commonBlogData{}, false
|
||||||
|
}
|
||||||
|
res.ThreadID = threadId
|
||||||
|
|
||||||
|
c.Perf.StartBlock("SQL", "Verify that the thread exists")
|
||||||
|
threadExists, err := db.QueryBool(c.Context(), c.Conn,
|
||||||
|
`
|
||||||
|
SELECT COUNT(*) > 0
|
||||||
|
FROM handmade_thread
|
||||||
|
WHERE
|
||||||
|
id = $1
|
||||||
|
AND project_id = $2
|
||||||
|
`,
|
||||||
|
res.ThreadID,
|
||||||
|
c.CurrentProject.ID,
|
||||||
|
)
|
||||||
|
c.Perf.EndBlock()
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
if !threadExists {
|
||||||
|
return commonBlogData{}, false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if postIdStr, hasPostId := c.PathParams["postid"]; hasPostId {
|
||||||
|
postId, err := strconv.Atoi(postIdStr)
|
||||||
|
if err != nil {
|
||||||
|
return commonBlogData{}, false
|
||||||
|
}
|
||||||
|
res.PostID = postId
|
||||||
|
|
||||||
|
c.Perf.StartBlock("SQL", "Verify that the post exists")
|
||||||
|
postExists, err := db.QueryBool(c.Context(), c.Conn,
|
||||||
|
`
|
||||||
|
SELECT COUNT(*) > 0
|
||||||
|
FROM handmade_post
|
||||||
|
WHERE
|
||||||
|
id = $1
|
||||||
|
AND thread_id = $2
|
||||||
|
`,
|
||||||
|
res.PostID,
|
||||||
|
res.ThreadID,
|
||||||
|
)
|
||||||
|
c.Perf.EndBlock()
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
if !postExists {
|
||||||
|
return commonBlogData{}, false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return res, true
|
||||||
|
}
|
||||||
|
|
||||||
|
func addBlogUrlsToPost(p *templates.Post, projectSlug string, threadId int, postId int) {
|
||||||
|
p.Url = hmnurl.BuildBlogPost(projectSlug, threadId, postId)
|
||||||
|
p.DeleteUrl = hmnurl.BuildBlogPostDelete(projectSlug, threadId, postId)
|
||||||
|
p.EditUrl = hmnurl.BuildBlogPostEdit(projectSlug, threadId, postId)
|
||||||
|
p.ReplyUrl = hmnurl.BuildBlogPostReply(projectSlug, threadId, postId)
|
||||||
|
}
|
|
@ -421,7 +421,7 @@ func ForumThread(c *RequestContext) ResponseData {
|
||||||
|
|
||||||
currentSubforumSlugs := cd.LineageBuilder.GetSubforumLineageSlugs(cd.SubforumID)
|
currentSubforumSlugs := cd.LineageBuilder.GetSubforumLineageSlugs(cd.SubforumID)
|
||||||
|
|
||||||
thread := cd.FetchThread(c.Context(), c.Conn)
|
thread := FetchThread(c.Context(), c.Conn, cd.ThreadID)
|
||||||
|
|
||||||
numPosts, err := db.QueryInt(c.Context(), c.Conn,
|
numPosts, err := db.QueryInt(c.Context(), c.Conn,
|
||||||
`
|
`
|
||||||
|
@ -452,52 +452,23 @@ func ForumThread(c *RequestContext) ResponseData {
|
||||||
}
|
}
|
||||||
|
|
||||||
c.Perf.StartBlock("SQL", "Fetch posts")
|
c.Perf.StartBlock("SQL", "Fetch posts")
|
||||||
type postsQueryResult struct {
|
_, postsAndStuff := FetchThreadPostsAndStuff(
|
||||||
Post models.Post `db:"post"`
|
c.Context(),
|
||||||
Ver models.PostVersion `db:"ver"`
|
c.Conn,
|
||||||
Author *models.User `db:"author"`
|
cd.ThreadID,
|
||||||
Editor *models.User `db:"editor"`
|
page, threadViewPostsPerPage,
|
||||||
|
|
||||||
ReplyPost *models.Post `db:"reply"`
|
|
||||||
ReplyAuthor *models.User `db:"reply_author"`
|
|
||||||
}
|
|
||||||
itPosts, err := db.Query(c.Context(), c.Conn, postsQueryResult{},
|
|
||||||
`
|
|
||||||
SELECT $columns
|
|
||||||
FROM
|
|
||||||
handmade_post AS post
|
|
||||||
JOIN handmade_postversion AS ver ON post.current_id = ver.id
|
|
||||||
LEFT JOIN auth_user AS author ON post.author_id = author.id
|
|
||||||
LEFT JOIN auth_user AS editor ON ver.editor_id = editor.id
|
|
||||||
LEFT JOIN handmade_post AS reply ON post.reply_id = reply.id
|
|
||||||
LEFT JOIN auth_user AS reply_author ON reply.author_id = reply_author.id
|
|
||||||
WHERE
|
|
||||||
post.thread_id = $1
|
|
||||||
AND NOT post.deleted
|
|
||||||
ORDER BY post.postdate
|
|
||||||
LIMIT $2 OFFSET $3
|
|
||||||
`,
|
|
||||||
thread.ID,
|
|
||||||
threadViewPostsPerPage,
|
|
||||||
(page-1)*threadViewPostsPerPage,
|
|
||||||
)
|
)
|
||||||
c.Perf.EndBlock()
|
c.Perf.EndBlock()
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
defer itPosts.Close()
|
|
||||||
|
|
||||||
var posts []templates.Post
|
var posts []templates.Post
|
||||||
for _, irow := range itPosts.ToSlice() {
|
for _, p := range postsAndStuff {
|
||||||
row := irow.(*postsQueryResult)
|
post := templates.PostToTemplate(&p.Post, p.Author, c.Theme)
|
||||||
|
post.AddContentVersion(p.CurrentVersion, p.Editor)
|
||||||
|
addForumUrlsToPost(&post, c.CurrentProject.Slug, currentSubforumSlugs, thread.ID, post.ID)
|
||||||
|
|
||||||
post := templates.PostToTemplate(&row.Post, row.Author, c.Theme)
|
if p.ReplyPost != nil {
|
||||||
post.AddContentVersion(row.Ver, row.Editor)
|
reply := templates.PostToTemplate(p.ReplyPost, p.ReplyAuthor, c.Theme)
|
||||||
post.AddUrls(c.CurrentProject.Slug, currentSubforumSlugs, thread.ID, post.ID)
|
addForumUrlsToPost(&reply, c.CurrentProject.Slug, currentSubforumSlugs, thread.ID, post.ID)
|
||||||
|
|
||||||
if row.ReplyPost != nil {
|
|
||||||
reply := templates.PostToTemplate(row.ReplyPost, row.ReplyAuthor, c.Theme)
|
|
||||||
reply.AddUrls(c.CurrentProject.Slug, currentSubforumSlugs, thread.ID, post.ID)
|
|
||||||
post.ReplyPost = &reply
|
post.ReplyPost = &reply
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -531,7 +502,7 @@ func ForumThread(c *RequestContext) ResponseData {
|
||||||
var res ResponseData
|
var res ResponseData
|
||||||
res.MustWriteTemplate("forum_thread.html", forumThreadData{
|
res.MustWriteTemplate("forum_thread.html", forumThreadData{
|
||||||
BaseData: baseData,
|
BaseData: baseData,
|
||||||
Thread: templates.ThreadToTemplate(thread),
|
Thread: templates.ThreadToTemplate(&thread),
|
||||||
Posts: posts,
|
Posts: posts,
|
||||||
SubforumUrl: hmnurl.BuildForum(c.CurrentProject.Slug, currentSubforumSlugs, 1),
|
SubforumUrl: hmnurl.BuildForum(c.CurrentProject.Slug, currentSubforumSlugs, 1),
|
||||||
ReplyUrl: hmnurl.BuildForumPostReply(c.CurrentProject.Slug, currentSubforumSlugs, thread.ID, *thread.FirstID),
|
ReplyUrl: hmnurl.BuildForumPostReply(c.CurrentProject.Slug, currentSubforumSlugs, thread.ID, *thread.FirstID),
|
||||||
|
@ -704,7 +675,7 @@ func ForumPostReply(c *RequestContext) ResponseData {
|
||||||
return FourOhFour(c)
|
return FourOhFour(c)
|
||||||
}
|
}
|
||||||
|
|
||||||
postData := cd.FetchPostAndStuff(c.Context(), c.Conn)
|
postData := FetchPostAndStuff(c.Context(), c.Conn, cd.ThreadID, cd.PostID)
|
||||||
|
|
||||||
baseData := getBaseData(c)
|
baseData := getBaseData(c)
|
||||||
baseData.Title = fmt.Sprintf("Replying to \"%s\" | %s", postData.Thread.Title, cd.SubforumTree[cd.SubforumID].Name)
|
baseData.Title = fmt.Sprintf("Replying to \"%s\" | %s", postData.Thread.Title, cd.SubforumTree[cd.SubforumID].Name)
|
||||||
|
@ -764,7 +735,7 @@ func ForumPostEdit(c *RequestContext) ResponseData {
|
||||||
return FourOhFour(c)
|
return FourOhFour(c)
|
||||||
}
|
}
|
||||||
|
|
||||||
postData := cd.FetchPostAndStuff(c.Context(), c.Conn)
|
postData := FetchPostAndStuff(c.Context(), c.Conn, cd.ThreadID, cd.PostID)
|
||||||
|
|
||||||
baseData := getBaseData(c)
|
baseData := getBaseData(c)
|
||||||
baseData.Title = fmt.Sprintf("Editing \"%s\" | %s", postData.Thread.Title, cd.SubforumTree[cd.SubforumID].Name)
|
baseData.Title = fmt.Sprintf("Editing \"%s\" | %s", postData.Thread.Title, cd.SubforumTree[cd.SubforumID].Name)
|
||||||
|
@ -829,7 +800,7 @@ func ForumPostDelete(c *RequestContext) ResponseData {
|
||||||
return FourOhFour(c)
|
return FourOhFour(c)
|
||||||
}
|
}
|
||||||
|
|
||||||
postData := cd.FetchPostAndStuff(c.Context(), c.Conn)
|
postData := FetchPostAndStuff(c.Context(), c.Conn, cd.ThreadID, cd.PostID)
|
||||||
|
|
||||||
baseData := getBaseData(c)
|
baseData := getBaseData(c)
|
||||||
baseData.Title = fmt.Sprintf("Deleting post in \"%s\" | %s", postData.Thread.Title, cd.SubforumTree[cd.SubforumID].Name)
|
baseData.Title = fmt.Sprintf("Deleting post in \"%s\" | %s", postData.Thread.Title, cd.SubforumTree[cd.SubforumID].Name)
|
||||||
|
@ -1131,6 +1102,7 @@ func getCommonForumData(c *RequestContext) (commonForumData, bool) {
|
||||||
WHERE
|
WHERE
|
||||||
id = $1
|
id = $1
|
||||||
AND subforum_id = $2
|
AND subforum_id = $2
|
||||||
|
AND NOT deleted
|
||||||
`,
|
`,
|
||||||
res.ThreadID,
|
res.ThreadID,
|
||||||
res.SubforumID,
|
res.SubforumID,
|
||||||
|
@ -1159,6 +1131,7 @@ func getCommonForumData(c *RequestContext) (commonForumData, bool) {
|
||||||
WHERE
|
WHERE
|
||||||
id = $1
|
id = $1
|
||||||
AND thread_id = $2
|
AND thread_id = $2
|
||||||
|
AND NOT deleted
|
||||||
`,
|
`,
|
||||||
res.PostID,
|
res.PostID,
|
||||||
res.ThreadID,
|
res.ThreadID,
|
||||||
|
@ -1175,114 +1148,6 @@ func getCommonForumData(c *RequestContext) (commonForumData, bool) {
|
||||||
return res, true
|
return res, true
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
|
||||||
Fetches the current thread according to the parsed path params. Since the ID was already validated
|
|
||||||
in the constructor, this should always succeed.
|
|
||||||
|
|
||||||
It will not, of course, succeed if there was no thread id in the path params, so don't do that.
|
|
||||||
*/
|
|
||||||
func (cd *commonForumData) FetchThread(ctx context.Context, connOrTx db.ConnOrTx) *models.Thread {
|
|
||||||
cd.c.Perf.StartBlock("SQL", "Fetch current thread")
|
|
||||||
type threadQueryResult struct {
|
|
||||||
Thread models.Thread `db:"thread"`
|
|
||||||
}
|
|
||||||
irow, err := db.QueryOne(ctx, connOrTx, threadQueryResult{},
|
|
||||||
`
|
|
||||||
SELECT $columns
|
|
||||||
FROM
|
|
||||||
handmade_thread AS thread
|
|
||||||
JOIN handmade_subforum AS sf ON sf.id = thread.subforum_id
|
|
||||||
WHERE
|
|
||||||
thread.id = $1
|
|
||||||
AND NOT thread.deleted
|
|
||||||
AND sf.id = $2
|
|
||||||
`,
|
|
||||||
cd.ThreadID,
|
|
||||||
cd.SubforumID, // NOTE(asaf): This verifies that the requested thread is under the requested subforum.
|
|
||||||
)
|
|
||||||
cd.c.Perf.EndBlock()
|
|
||||||
if err != nil {
|
|
||||||
// We shouldn't encounter db.ErrNoMatchingRows, because validation should have verified that everything exists.
|
|
||||||
panic(oops.New(err, "failed to fetch thread"))
|
|
||||||
}
|
|
||||||
|
|
||||||
thread := irow.(*threadQueryResult).Thread
|
|
||||||
return &thread
|
|
||||||
}
|
|
||||||
|
|
||||||
type postAndRelatedModels struct {
|
|
||||||
Thread models.Thread `db:"thread"`
|
|
||||||
Post models.Post `db:"post"`
|
|
||||||
CurrentVersion models.PostVersion `db:"ver"`
|
|
||||||
Author *models.User `db:"author"`
|
|
||||||
Editor *models.User `db:"editor"`
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
Fetches the post, the thread, and author / editor information for the post defined by the path
|
|
||||||
params. Because all the IDs were validated in the constructor, this should always succeed.
|
|
||||||
|
|
||||||
It will not succeed if there were missing path params though.
|
|
||||||
*/
|
|
||||||
func (cd *commonForumData) FetchPostAndStuff(ctx context.Context, connOrTx db.ConnOrTx) *postAndRelatedModels {
|
|
||||||
cd.c.Perf.StartBlock("SQL", "Fetch post to reply to")
|
|
||||||
postQueryResult, err := db.QueryOne(ctx, connOrTx, postAndRelatedModels{},
|
|
||||||
`
|
|
||||||
SELECT $columns
|
|
||||||
FROM
|
|
||||||
handmade_thread AS thread
|
|
||||||
JOIN handmade_post AS post ON post.thread_id = thread.id
|
|
||||||
JOIN handmade_postversion AS ver ON post.current_id = ver.id
|
|
||||||
LEFT JOIN auth_user AS author ON post.author_id = author.id
|
|
||||||
LEFT JOIN auth_user AS editor ON ver.editor_id = editor.id
|
|
||||||
WHERE
|
|
||||||
post.thread_id = $1
|
|
||||||
AND post.id = $2
|
|
||||||
AND NOT post.deleted
|
|
||||||
`,
|
|
||||||
cd.ThreadID,
|
|
||||||
cd.PostID,
|
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
// We shouldn't encounter db.ErrNoMatchingRows, because validation should have verified that everything exists.
|
|
||||||
panic(oops.New(err, "failed to fetch post and related data"))
|
|
||||||
}
|
|
||||||
|
|
||||||
result := postQueryResult.(*postAndRelatedModels)
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
func (cd *commonForumData) UserCanEditPost(ctx context.Context, connOrTx db.ConnOrTx, user models.User) bool {
|
|
||||||
if user.IsStaff {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
type postResult struct {
|
|
||||||
AuthorID *int `db:"post.author_id"`
|
|
||||||
}
|
|
||||||
iresult, err := db.QueryOne(ctx, connOrTx, postResult{},
|
|
||||||
`
|
|
||||||
SELECT $columns
|
|
||||||
FROM
|
|
||||||
handmade_post AS post
|
|
||||||
WHERE
|
|
||||||
post.id = $1
|
|
||||||
AND NOT post.deleted
|
|
||||||
`,
|
|
||||||
cd.PostID,
|
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
if errors.Is(err, db.ErrNoMatchingRows) {
|
|
||||||
return false
|
|
||||||
} else {
|
|
||||||
panic(oops.New(err, "failed to get author of post when checking permissions"))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
result := iresult.(*postResult)
|
|
||||||
|
|
||||||
return result.AuthorID != nil && *result.AuthorID == user.ID
|
|
||||||
}
|
|
||||||
|
|
||||||
func validateSubforums(lineageBuilder *models.SubforumLineageBuilder, project *models.Project, sfPath string) (int, bool) {
|
func validateSubforums(lineageBuilder *models.SubforumLineageBuilder, project *models.Project, sfPath string) (int, bool) {
|
||||||
if project.ForumID == nil {
|
if project.ForumID == nil {
|
||||||
return -1, false
|
return -1, false
|
||||||
|
@ -1316,3 +1181,10 @@ func validateSubforums(lineageBuilder *models.SubforumLineageBuilder, project *m
|
||||||
}
|
}
|
||||||
return subforumId, valid
|
return subforumId, valid
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func addForumUrlsToPost(p *templates.Post, projectSlug string, subforums []string, threadId int, postId int) {
|
||||||
|
p.Url = hmnurl.BuildForumPost(projectSlug, subforums, threadId, postId)
|
||||||
|
p.DeleteUrl = hmnurl.BuildForumPostDelete(projectSlug, subforums, threadId, postId)
|
||||||
|
p.EditUrl = hmnurl.BuildForumPostEdit(projectSlug, subforums, threadId, postId)
|
||||||
|
p.ReplyUrl = hmnurl.BuildForumPostReply(projectSlug, subforums, threadId, postId)
|
||||||
|
}
|
||||||
|
|
|
@ -156,7 +156,7 @@ func Index(c *RequestContext) ResponseData {
|
||||||
|
|
||||||
featurable := (!proj.IsHMN() &&
|
featurable := (!proj.IsHMN() &&
|
||||||
projectPost.Post.ThreadType == models.ThreadTypeProjectArticle &&
|
projectPost.Post.ThreadType == models.ThreadTypeProjectArticle &&
|
||||||
projectPost.Post.ParentID == nil &&
|
*projectPost.Thread.FirstID == projectPost.Post.ID &&
|
||||||
landingPageProject.FeaturedPost == nil)
|
landingPageProject.FeaturedPost == nil)
|
||||||
|
|
||||||
if featurable {
|
if featurable {
|
||||||
|
|
|
@ -19,6 +19,7 @@ func UrlForGenericPost(thread *models.Thread, post *models.Post, lineageBuilder
|
||||||
}
|
}
|
||||||
|
|
||||||
var PostTypeMap = map[models.ThreadType][]templates.PostType{
|
var PostTypeMap = map[models.ThreadType][]templates.PostType{
|
||||||
|
// { First post , Subsequent post }
|
||||||
models.ThreadTypeProjectArticle: {templates.PostTypeBlogPost, templates.PostTypeBlogComment},
|
models.ThreadTypeProjectArticle: {templates.PostTypeBlogPost, templates.PostTypeBlogComment},
|
||||||
models.ThreadTypeForumPost: {templates.PostTypeForumThread, templates.PostTypeForumReply},
|
models.ThreadTypeForumPost: {templates.PostTypeForumThread, templates.PostTypeForumReply},
|
||||||
}
|
}
|
||||||
|
@ -76,11 +77,11 @@ func MakePostListItem(
|
||||||
postType := templates.PostTypeUnknown
|
postType := templates.PostTypeUnknown
|
||||||
postTypeOptions, found := PostTypeMap[post.ThreadType]
|
postTypeOptions, found := PostTypeMap[post.ThreadType]
|
||||||
if found {
|
if found {
|
||||||
var hasParent int
|
isNotFirst := 0
|
||||||
if post.ParentID != nil {
|
if *thread.FirstID != post.ID {
|
||||||
hasParent = 1
|
isNotFirst = 1
|
||||||
}
|
}
|
||||||
postType = postTypeOptions[hasParent]
|
postType = postTypeOptions[isNotFirst]
|
||||||
}
|
}
|
||||||
result.PostType = postType
|
result.PostType = postType
|
||||||
result.PostTypePrefix = PostTypePrefix[result.PostType]
|
result.PostTypePrefix = PostTypePrefix[result.PostType]
|
||||||
|
|
|
@ -166,6 +166,9 @@ func NewWebsiteRoutes(conn *pgxpool.Pool, perfCollector *perf.PerfCollector) htt
|
||||||
mainRoutes.GET(hmnurl.RegexForumPostDelete, authMiddleware(ForumPostDelete))
|
mainRoutes.GET(hmnurl.RegexForumPostDelete, authMiddleware(ForumPostDelete))
|
||||||
mainRoutes.POST(hmnurl.RegexForumPostDelete, authMiddleware(csrfMiddleware(ForumPostDeleteSubmit)))
|
mainRoutes.POST(hmnurl.RegexForumPostDelete, authMiddleware(csrfMiddleware(ForumPostDeleteSubmit)))
|
||||||
|
|
||||||
|
mainRoutes.GET(hmnurl.RegexBlogThread, BlogThread)
|
||||||
|
mainRoutes.GET(hmnurl.RegexBlogPost, BlogPostRedirectToThread)
|
||||||
|
|
||||||
mainRoutes.GET(hmnurl.RegexPodcast, PodcastIndex)
|
mainRoutes.GET(hmnurl.RegexPodcast, PodcastIndex)
|
||||||
mainRoutes.GET(hmnurl.RegexPodcastEdit, PodcastEdit)
|
mainRoutes.GET(hmnurl.RegexPodcastEdit, PodcastEdit)
|
||||||
mainRoutes.POST(hmnurl.RegexPodcastEdit, PodcastEditSubmit)
|
mainRoutes.POST(hmnurl.RegexPodcastEdit, PodcastEditSubmit)
|
||||||
|
|
|
@ -0,0 +1,219 @@
|
||||||
|
package website
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"math"
|
||||||
|
|
||||||
|
"git.handmade.network/hmn/hmn/src/db"
|
||||||
|
"git.handmade.network/hmn/hmn/src/models"
|
||||||
|
"git.handmade.network/hmn/hmn/src/oops"
|
||||||
|
)
|
||||||
|
|
||||||
|
type postAndRelatedModels struct {
|
||||||
|
Thread models.Thread
|
||||||
|
Post models.Post
|
||||||
|
CurrentVersion models.PostVersion
|
||||||
|
|
||||||
|
Author *models.User
|
||||||
|
Editor *models.User
|
||||||
|
|
||||||
|
ReplyPost *models.Post
|
||||||
|
ReplyAuthor *models.User
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
Fetches the thread defined by your (already parsed) path params.
|
||||||
|
|
||||||
|
YOU MUST VERIFY THAT THE THREAD ID IS VALID BEFORE CALLING THIS FUNCTION. It will
|
||||||
|
not check, for example, that the thread belongs to the correct subforum.
|
||||||
|
*/
|
||||||
|
func FetchThread(ctx context.Context, connOrTx db.ConnOrTx, threadId int) models.Thread {
|
||||||
|
type threadQueryResult struct {
|
||||||
|
Thread models.Thread `db:"thread"`
|
||||||
|
}
|
||||||
|
irow, err := db.QueryOne(ctx, connOrTx, threadQueryResult{},
|
||||||
|
`
|
||||||
|
SELECT $columns
|
||||||
|
FROM
|
||||||
|
handmade_thread AS thread
|
||||||
|
WHERE
|
||||||
|
id = $1
|
||||||
|
AND NOT deleted
|
||||||
|
`,
|
||||||
|
threadId,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
// We shouldn't encounter db.ErrNoMatchingRows, because validation should have verified that everything exists.
|
||||||
|
panic(oops.New(err, "failed to fetch thread"))
|
||||||
|
}
|
||||||
|
|
||||||
|
thread := irow.(*threadQueryResult).Thread
|
||||||
|
return thread
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
Fetches the post, the thread, and author / editor information for the post defined in
|
||||||
|
your path params.
|
||||||
|
|
||||||
|
YOU MUST VERIFY THAT THE THREAD ID AND POST ID ARE VALID BEFORE CALLING THIS FUNCTION.
|
||||||
|
It will not check that the post belongs to the correct subforum, for example, or the
|
||||||
|
correct project blog. This logic varies per route and per use of threads, so it doesn't
|
||||||
|
happen here.
|
||||||
|
*/
|
||||||
|
func FetchPostAndStuff(
|
||||||
|
ctx context.Context,
|
||||||
|
connOrTx db.ConnOrTx,
|
||||||
|
threadId, postId int,
|
||||||
|
) postAndRelatedModels {
|
||||||
|
type resultRow struct {
|
||||||
|
Thread models.Thread `db:"thread"`
|
||||||
|
Post models.Post `db:"post"`
|
||||||
|
CurrentVersion models.PostVersion `db:"ver"`
|
||||||
|
Author *models.User `db:"author"`
|
||||||
|
Editor *models.User `db:"editor"`
|
||||||
|
ReplyPost *models.Post `db:"reply"`
|
||||||
|
ReplyAuthor *models.User `db:"reply_author"`
|
||||||
|
}
|
||||||
|
postQueryResult, err := db.QueryOne(ctx, connOrTx, resultRow{},
|
||||||
|
`
|
||||||
|
SELECT $columns
|
||||||
|
FROM
|
||||||
|
handmade_thread AS thread
|
||||||
|
JOIN handmade_post AS post ON post.thread_id = thread.id
|
||||||
|
JOIN handmade_postversion AS ver ON post.current_id = ver.id
|
||||||
|
LEFT JOIN auth_user AS author ON post.author_id = author.id
|
||||||
|
LEFT JOIN auth_user AS editor ON ver.editor_id = editor.id
|
||||||
|
LEFT JOIN handmade_post AS reply ON post.reply_id = reply.id
|
||||||
|
LEFT JOIN auth_user AS reply_author ON reply.author_id = reply_author.id
|
||||||
|
WHERE
|
||||||
|
post.thread_id = $1
|
||||||
|
AND post.id = $2
|
||||||
|
AND NOT post.deleted
|
||||||
|
`,
|
||||||
|
threadId,
|
||||||
|
postId,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
// We shouldn't encounter db.ErrNoMatchingRows, because validation should have verified that everything exists.
|
||||||
|
panic(oops.New(err, "failed to fetch post and related data"))
|
||||||
|
}
|
||||||
|
|
||||||
|
result := postQueryResult.(*resultRow)
|
||||||
|
return postAndRelatedModels{
|
||||||
|
Thread: result.Thread,
|
||||||
|
Post: result.Post,
|
||||||
|
CurrentVersion: result.CurrentVersion,
|
||||||
|
Author: result.Author,
|
||||||
|
Editor: result.Editor,
|
||||||
|
ReplyPost: result.ReplyPost,
|
||||||
|
ReplyAuthor: result.ReplyAuthor,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
Fetches all the posts (and related models) for a given thread.
|
||||||
|
|
||||||
|
YOU MUST VERIFY THAT THE THREAD ID IS VALID BEFORE CALLING THIS FUNCTION. It will
|
||||||
|
not check, for example, that the thread belongs to the correct subforum.
|
||||||
|
*/
|
||||||
|
func FetchThreadPostsAndStuff(
|
||||||
|
ctx context.Context,
|
||||||
|
connOrTx db.ConnOrTx,
|
||||||
|
threadId int,
|
||||||
|
page, postsPerPage int,
|
||||||
|
) (models.Thread, []postAndRelatedModels) {
|
||||||
|
limit := postsPerPage
|
||||||
|
offset := (page - 1) * postsPerPage
|
||||||
|
if postsPerPage == 0 {
|
||||||
|
limit = math.MaxInt32
|
||||||
|
offset = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
thread := FetchThread(ctx, connOrTx, threadId)
|
||||||
|
|
||||||
|
type postResult struct {
|
||||||
|
Post models.Post `db:"post"`
|
||||||
|
CurrentVersion models.PostVersion `db:"ver"`
|
||||||
|
Author *models.User `db:"author"`
|
||||||
|
Editor *models.User `db:"editor"`
|
||||||
|
ReplyPost *models.Post `db:"reply"`
|
||||||
|
ReplyAuthor *models.User `db:"reply_author"`
|
||||||
|
}
|
||||||
|
itPosts, err := db.Query(ctx, connOrTx, postResult{},
|
||||||
|
`
|
||||||
|
SELECT $columns
|
||||||
|
FROM
|
||||||
|
handmade_post AS post
|
||||||
|
JOIN handmade_postversion AS ver ON post.current_id = ver.id
|
||||||
|
LEFT JOIN auth_user AS author ON post.author_id = author.id
|
||||||
|
LEFT JOIN auth_user AS editor ON ver.editor_id = editor.id
|
||||||
|
LEFT JOIN handmade_post AS reply ON post.reply_id = reply.id
|
||||||
|
LEFT JOIN auth_user AS reply_author ON reply.author_id = reply_author.id
|
||||||
|
WHERE
|
||||||
|
post.thread_id = $1
|
||||||
|
AND NOT post.deleted
|
||||||
|
ORDER BY post.postdate
|
||||||
|
LIMIT $2 OFFSET $3
|
||||||
|
`,
|
||||||
|
thread.ID,
|
||||||
|
limit,
|
||||||
|
offset,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
panic(oops.New(err, "failed to fetch posts for thread"))
|
||||||
|
}
|
||||||
|
defer itPosts.Close()
|
||||||
|
|
||||||
|
var posts []postAndRelatedModels
|
||||||
|
for {
|
||||||
|
irow, hasNext := itPosts.Next()
|
||||||
|
if !hasNext {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
row := irow.(*postResult)
|
||||||
|
posts = append(posts, postAndRelatedModels{
|
||||||
|
Thread: thread,
|
||||||
|
Post: row.Post,
|
||||||
|
CurrentVersion: row.CurrentVersion,
|
||||||
|
Author: row.Author,
|
||||||
|
Editor: row.Editor,
|
||||||
|
ReplyPost: row.ReplyPost,
|
||||||
|
ReplyAuthor: row.ReplyAuthor,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return thread, posts
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cd *commonForumData) UserCanEditPost(ctx context.Context, connOrTx db.ConnOrTx, user models.User) bool {
|
||||||
|
if user.IsStaff {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
type postResult struct {
|
||||||
|
AuthorID *int `db:"post.author_id"`
|
||||||
|
}
|
||||||
|
iresult, err := db.QueryOne(ctx, connOrTx, postResult{},
|
||||||
|
`
|
||||||
|
SELECT $columns
|
||||||
|
FROM
|
||||||
|
handmade_post AS post
|
||||||
|
WHERE
|
||||||
|
post.id = $1
|
||||||
|
AND NOT post.deleted
|
||||||
|
`,
|
||||||
|
cd.PostID,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, db.ErrNoMatchingRows) {
|
||||||
|
return false
|
||||||
|
} else {
|
||||||
|
panic(oops.New(err, "failed to get author of post when checking permissions"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
result := iresult.(*postResult)
|
||||||
|
|
||||||
|
return result.AuthorID != nil && *result.AuthorID == user.ID
|
||||||
|
}
|
|
@ -11,7 +11,7 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
var TimelineTypeMap = map[models.ThreadType][]templates.TimelineType{
|
var TimelineTypeMap = map[models.ThreadType][]templates.TimelineType{
|
||||||
// { No parent , Has parent }
|
// { First post , Subsequent post }
|
||||||
models.ThreadTypeProjectArticle: {templates.TimelineTypeBlogPost, templates.TimelineTypeBlogComment},
|
models.ThreadTypeProjectArticle: {templates.TimelineTypeBlogPost, templates.TimelineTypeBlogComment},
|
||||||
models.ThreadTypeForumPost: {templates.TimelineTypeForumThread, templates.TimelineTypeForumReply},
|
models.ThreadTypeForumPost: {templates.TimelineTypeForumThread, templates.TimelineTypeForumReply},
|
||||||
}
|
}
|
||||||
|
@ -50,11 +50,11 @@ func PostToTimelineItem(lineageBuilder *models.SubforumLineageBuilder, post *mod
|
||||||
itemType := templates.TimelineTypeUnknown
|
itemType := templates.TimelineTypeUnknown
|
||||||
typeByCatKind, found := TimelineTypeMap[post.ThreadType]
|
typeByCatKind, found := TimelineTypeMap[post.ThreadType]
|
||||||
if found {
|
if found {
|
||||||
hasParent := 0
|
isNotFirst := 0
|
||||||
if post.ParentID != nil {
|
if *thread.FirstID != post.ID {
|
||||||
hasParent = 1
|
isNotFirst = 1
|
||||||
}
|
}
|
||||||
itemType = typeByCatKind[hasParent]
|
itemType = typeByCatKind[isNotFirst]
|
||||||
}
|
}
|
||||||
|
|
||||||
return templates.TimelineItem{
|
return templates.TimelineItem{
|
||||||
|
|
Loading…
Reference in New Issue