Get forum threads mostly done

Still need to do breadcrumbs, but that applies to forum categories too
actually.
This commit is contained in:
Ben Visness 2021-05-06 00:57:14 -05:00
parent c8231750aa
commit d6481ab421
12 changed files with 190 additions and 56 deletions

View File

@ -8,67 +8,67 @@ import (
"git.handmade.network/hmn/hmn/src/oops" "git.handmade.network/hmn/hmn/src/oops"
) )
var RegexHomepage *regexp.Regexp = regexp.MustCompile("^/$") var RegexHomepage = regexp.MustCompile("^/$")
func BuildHomepage() string { func BuildHomepage() string {
return Url("/", nil) return Url("/", nil)
} }
var RegexLogin *regexp.Regexp = regexp.MustCompile("^/login$") var RegexLogin = regexp.MustCompile("^/login$")
func BuildLogin() string { func BuildLogin() string {
return Url("/login", nil) return Url("/login", nil)
} }
var RegexLogout *regexp.Regexp = regexp.MustCompile("^/logout$") var RegexLogout = regexp.MustCompile("^/logout$")
func BuildLogout() string { func BuildLogout() string {
return Url("/logout", nil) return Url("/logout", nil)
} }
var RegexManifesto *regexp.Regexp = regexp.MustCompile("^/manifesto$") var RegexManifesto = regexp.MustCompile("^/manifesto$")
func BuildManifesto() string { func BuildManifesto() string {
return Url("/manifesto", nil) return Url("/manifesto", nil)
} }
var RegexAbout *regexp.Regexp = regexp.MustCompile("^/about$") var RegexAbout = regexp.MustCompile("^/about$")
func BuildAbout() string { func BuildAbout() string {
return Url("/about", nil) return Url("/about", nil)
} }
var RegexCodeOfConduct *regexp.Regexp = regexp.MustCompile("^/code-of-conduct$") var RegexCodeOfConduct = regexp.MustCompile("^/code-of-conduct$")
func BuildCodeOfConduct() string { func BuildCodeOfConduct() string {
return Url("/code-of-conduct", nil) return Url("/code-of-conduct", nil)
} }
var RegexCommunicationGuidelines *regexp.Regexp = regexp.MustCompile("^/communication-guidelines$") var RegexCommunicationGuidelines = regexp.MustCompile("^/communication-guidelines$")
func BuildCommunicationGuidelines() string { func BuildCommunicationGuidelines() string {
return Url("/communication-guidelines", nil) return Url("/communication-guidelines", nil)
} }
var RegexContactPage *regexp.Regexp = regexp.MustCompile("^/contact$") var RegexContactPage = regexp.MustCompile("^/contact$")
func BuildContactPage() string { func BuildContactPage() string {
return Url("/contact", nil) return Url("/contact", nil)
} }
var RegexMonthlyUpdatePolicy *regexp.Regexp = regexp.MustCompile("^/monthly-update-policy$") var RegexMonthlyUpdatePolicy = regexp.MustCompile("^/monthly-update-policy$")
func BuildMonthlyUpdatePolicy() string { func BuildMonthlyUpdatePolicy() string {
return Url("/monthly-update-policy", nil) return Url("/monthly-update-policy", nil)
} }
var RegexProjectSubmissionGuidelines *regexp.Regexp = regexp.MustCompile("^/project-guidelines$") var RegexProjectSubmissionGuidelines = regexp.MustCompile("^/project-guidelines$")
func BuildProjectSubmissionGuidelines() string { func BuildProjectSubmissionGuidelines() string {
return Url("/project-guidelines", nil) return Url("/project-guidelines", nil)
} }
var RegexFeed *regexp.Regexp = regexp.MustCompile(`^/feed(/(?P<page>.+)?)?$`) var RegexFeed = regexp.MustCompile(`^/feed(/(?P<page>.+)?)?$`)
func BuildFeed() string { func BuildFeed() string {
return Url("/feed", nil) return Url("/feed", nil)
@ -84,7 +84,7 @@ func BuildFeedWithPage(page int) string {
return Url("/feed/"+strconv.Itoa(page), nil) return Url("/feed/"+strconv.Itoa(page), nil)
} }
var RegexForumThread *regexp.Regexp = regexp.MustCompile(`^/(?P<cats>forums(/[^\d]+?)*)/t/(?P<threadid>\d+)(/(?P<page>\d+))?$`) var RegexForumThread = regexp.MustCompile(`^/(?P<cats>forums(/[^\d]+?)*)/t/(?P<threadid>\d+)(/(?P<page>\d+))?$`)
func BuildForumThread(projectSlug string, subforums []string, threadId int, page int) string { func BuildForumThread(projectSlug string, subforums []string, threadId int, page int) string {
if page < 1 { if page < 1 {
@ -114,7 +114,7 @@ func BuildForumThread(projectSlug string, subforums []string, threadId int, page
return ProjectUrl(builder.String(), nil, projectSlug) return ProjectUrl(builder.String(), nil, projectSlug)
} }
var RegexForumCategory *regexp.Regexp = regexp.MustCompile(`^/(?P<cats>forums(/[^\d]+?)*)(/(?P<page>\d+))?$`) var RegexForumCategory = regexp.MustCompile(`^/(?P<cats>forums(/[^\d]+?)*)(/(?P<page>\d+))?$`)
func BuildForumCategory(projectSlug string, subforums []string, page int) string { func BuildForumCategory(projectSlug string, subforums []string, page int) string {
if page < 1 { if page < 1 {
@ -142,7 +142,7 @@ func BuildForumCategory(projectSlug string, subforums []string, page int) string
return ProjectUrl(builder.String(), nil, projectSlug) return ProjectUrl(builder.String(), nil, projectSlug)
} }
var RegexForumPost *regexp.Regexp = regexp.MustCompile(``) // TODO(asaf): Complete this and test it var RegexForumPost = regexp.MustCompile(``) // TODO(asaf): Complete this and test it
func BuildForumPost(projectSlug string, subforums []string, threadId int, postId int) string { func BuildForumPost(projectSlug string, subforums []string, threadId int, postId int) string {
var builder strings.Builder var builder strings.Builder
@ -166,13 +166,38 @@ func BuildForumPost(projectSlug string, subforums []string, threadId int, postId
return ProjectUrl(builder.String(), nil, projectSlug) return ProjectUrl(builder.String(), nil, projectSlug)
} }
var RegexProjectCSS *regexp.Regexp = regexp.MustCompile("^/assets/project.css$") var RegexForumPostDelete = regexp.MustCompile(``) // TODO
func BuildForumPostDelete(projectSlug string, subforums []string, threadId int, postId int) string {
return BuildForumPost(projectSlug, subforums, threadId, postId) + "/delete"
}
var RegexForumPostEdit = regexp.MustCompile(``) // TODO
func BuildForumPostEdit(projectSlug string, subforums []string, threadId int, postId int) string {
return BuildForumPost(projectSlug, subforums, threadId, postId) + "/edit"
}
var RegexForumPostReply = regexp.MustCompile(``) // TODO(asaf): Ha ha! I, Ben, have played a trick on you, and forced you to do this regex as well!
// TODO: It's kinda weird that we have "replies" to a specific post. That's not how the data works. I guess this just affects what you see as the "post you're replying to" on the post composer page?
func BuildForumPostReply(projectSlug string, subforums []string, threadId int, postId int) string {
return BuildForumPost(projectSlug, subforums, threadId, postId) + "/reply"
}
var RegexForumPostQuote = regexp.MustCompile(``) // TODO
func BuildForumPostQuote(projectSlug string, subforums []string, threadId int, postId int) string {
return BuildForumPost(projectSlug, subforums, threadId, postId) + "/quote"
}
var RegexProjectCSS = regexp.MustCompile("^/assets/project.css$")
func BuildProjectCSS(color string) string { func BuildProjectCSS(color string) string {
return Url("/assets/project.css", []Q{{"color", color}}) return Url("/assets/project.css", []Q{{"color", color}})
} }
var RegexPublic *regexp.Regexp = regexp.MustCompile("^/public/.+$") var RegexPublic = regexp.MustCompile("^/public/.+$")
func BuildPublic(filepath string) string { func BuildPublic(filepath string) string {
filepath = strings.Trim(filepath, "/") filepath = strings.Trim(filepath, "/")
@ -193,4 +218,4 @@ func BuildPublic(filepath string) string {
return Url(builder.String(), nil) return Url(builder.String(), nil)
} }
var RegexCatchAll *regexp.Regexp = regexp.MustCompile("") var RegexCatchAll = regexp.MustCompile("")

View File

@ -2,6 +2,7 @@ package templates
import ( import (
"html/template" "html/template"
"net"
"git.handmade.network/hmn/hmn/src/hmnurl" "git.handmade.network/hmn/hmn/src/hmnurl"
"git.handmade.network/hmn/hmn/src/models" "git.handmade.network/hmn/hmn/src/models"
@ -15,25 +16,39 @@ func PostToTemplate(p *models.Post, author *models.User) Post {
} }
return Post{ return Post{
ID: p.ID, ID: p.ID,
Url: "nope", // TODO
// Urls not set here. See AddUrls.
Preview: p.Preview, Preview: p.Preview,
ReadOnly: p.ReadOnly, ReadOnly: p.ReadOnly,
Author: authorUser, Author: authorUser,
// No content. Do it yourself if you care. // No content. A lot of the time we don't have this handy and don't need it. See AddContentVersion.
PostDate: p.PostDate, PostDate: p.PostDate,
IP: p.IP.String(), IP: p.IP.String(),
} }
} }
func PostToTemplateWithContent(p *models.Post, author *models.User, content string) Post { func (p *Post) AddContentVersion(ver models.PostVersion, editor *models.User) {
post := PostToTemplate(p, author) p.Content = template.HTML(ver.TextParsed)
post.Content = template.HTML(content)
return post if editor != nil {
editorTmpl := UserToTemplate(editor)
p.Editor = &editorTmpl
p.EditDate = ver.EditDate
p.EditIP = maybeIp(ver.EditIP)
p.EditReason = ver.EditReason
}
}
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)
p.QuoteUrl = hmnurl.BuildForumPostQuote(projectSlug, subforums, threadId, postId)
} }
func ProjectToTemplate(p *models.Project) Project { func ProjectToTemplate(p *models.Project) Project {
@ -103,3 +118,11 @@ func maybeString(s *string) string {
} }
return *s return *s
} }
func maybeIp(ip *net.IPNet) string {
if ip == nil {
return ""
}
return ip.String()
}

View File

@ -2,6 +2,10 @@
{{ define "content" }} {{ define "content" }}
<div class="content-block"> <div class="content-block">
<div class="optionbar">
<a class="button" href="{{ .CategoryUrl }}">&larr; Back to index</a>
{{ template "pagination.html" .Pagination }}
</div>
{{ range .Posts }} {{ range .Posts }}
<div class="post background-even pa3 bbcode"> {{/* TODO: Dynamically switch between bbcode and markdown */}} <div class="post background-even pa3 bbcode"> {{/* TODO: Dynamically switch between bbcode and markdown */}}
<div class="fl w-100 w-25-l pt3 pa3-l flex tc-l"> <div class="fl w-100 w-25-l pt3 pa3-l flex tc-l">
@ -62,28 +66,28 @@
{{ if $.User }} {{ if $.User }}
<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="{{ .Url }}/delete" title="Delete">&#10006;</a>&nbsp; <a class="delete action button" href="{{ .DeleteUrl }}" title="Delete">&#10006;</a>&nbsp;
<a class="edit action button" href="{{ .Url }}/edit" title="Edit">&#9998;</a>&nbsp; <a class="edit action button" href="{{ .EditUrl }}" title="Edit">&#9998;</a>&nbsp;
{{ end }} {{ end }}
{{ if or (not $.Thread.Locked) $.User.IsStaff }} {{ if or (not $.Thread.Locked) $.User.IsStaff }}
{{ if $.Thread.Locked }} {{ if $.Thread.Locked }}
WARNING: locked thread - use power responsibly! WARNING: locked thread - use power responsibly!
{{ end }} {{ end }}
<a class="reply action button" href="{{ .Url }}/reply" title="Reply">&hookrightarrow;</a>&nbsp; <a class="reply action button" href="{{ .ReplyUrl }}" title="Reply">&hookrightarrow;</a>&nbsp;
<a class="quote action button" href="{{ .Url }}/quote" title="Quote">&#10077;</a> <a class="quote action button" href="{{ .QuoteUrl }}" title="Quote">&#10077;</a>
{{ end }} {{ end }}
</div> </div>
{{ end }} {{ end }}
</div> </div>
<div class="w-100 pb3"> <div class="w-100 pb3">
<div class="b" role="heading" aria-level="2">{{ $.Thread.Title }}</div> <div class="b" role="heading" aria-level="2">{{ $.Thread.Title }}</div>
<span>{{ relativedate .PostDate }} ago</span> {{ timehtml (relativedate .PostDate) .PostDate }}
{{ if .Editor }} {{ if .Editor }}
<span class="pl3"> <span class="pl3">
Edited by Edited by
<a class="name" href="{{ .Editor.ProfileUrl }}" target="_blank">{{ coalesce .Editor.Name .Editor.Username }}</a> <a class="name" href="{{ .Editor.ProfileUrl }}" target="_blank">{{ coalesce .Editor.Name .Editor.Username }}</a>
{{ if and $.User.IsStaff .EditIP }}<span class="ip">[{{ .EditIP }}]</span>{{ end }} {{ if and $.User.IsStaff .EditIP }}<span class="ip">[{{ .EditIP }}]</span>{{ end }}
on <span class="editdate">{{ .EditDate }}</span> on {{ timehtml (absolutedate .EditDate) .EditDate }}
{{ with .EditReason }} {{ with .EditReason }}
Reason: {{ . }} Reason: {{ . }}
{{ end }} {{ end }}
@ -108,5 +112,20 @@
<div class="cb"></div> <div class="cb"></div>
</div> </div>
{{ end }} {{ end }}
<div class="optionbar bottom">
<div class="options order-1">
<a class="button" href="{{ .CategoryUrl }}">&larr; Back to index</a>
{{ if .Thread.Locked }}
<span>Thread is locked.</span>
{{ else if .User }}
<a class="button" href="{{ .ReplyUrl }}">&#10551; Reply to Thread</a>
{{ else }}
<span><a href="{% url 'member_login' subdomain=None %}">Log in</a> to reply</span>
{{ end }}
</div>
<div class="options order-0 order-last-ns">
{{ template "pagination.html" .Pagination }}
</div>
</div>
</div> </div>
{{ end }} {{ end }}

View File

@ -87,5 +87,10 @@
loginPopup.classList.toggle("open"); loginPopup.classList.toggle("open");
} }
} }
for (const time of document.querySelectorAll('time')) {
const d = new Date(Date.parse(time.dateTime));
time.title = d.toLocaleString();
}
}); });
</script> </script>

View File

@ -15,7 +15,7 @@ It should be called with PostListItem.
</div> </div>
<div class="title nowrap truncate"><a href="{{ .Url }}">{{ .Title }}</a></div> <div class="title nowrap truncate"><a href="{{ .Url }}">{{ .Title }}</a></div>
<div class="details"> <div class="details">
<a class="user" href="{{ .User.ProfileUrl }}">{{ .User.Name }}</a> &mdash; <span class="datetime">{{ relativedate .Date }}</span> <a class="user" href="{{ .User.ProfileUrl }}">{{ .User.Name }}</a> &mdash; {{ timehtml (relativedate .Date) .Date }}
</div> </div>
{{ with .Content }} {{ with .Content }}
<div class="mt2"> <div class="mt2">

View File

@ -15,7 +15,7 @@ It should be called with ThreadListItem.
</div> </div>
<div class="title nowrap truncate"><a href="{{ .Url }}">{{ .Title }}</a></div> <div class="title nowrap truncate"><a href="{{ .Url }}">{{ .Title }}</a></div>
<div class="details"> <div class="details">
<a class="user" href="{{ .FirstUser.ProfileUrl }}">{{ .FirstUser.Name }}</a> &mdash; <span class="datetime">{{ relativedate .FirstDate }}</span> <a class="user" href="{{ .FirstUser.ProfileUrl }}">{{ .FirstUser.Name }}</a> &mdash; {{ timehtml (relativedate .FirstDate) .FirstDate }}
</div> </div>
{{ with .Content }} {{ with .Content }}
<div class="mt2"> <div class="mt2">
@ -26,7 +26,7 @@ It should be called with ThreadListItem.
<div class="latestpost dn flex-ns flex-shrink-0 items-center ml2"> <div class="latestpost dn flex-ns flex-shrink-0 items-center ml2">
<img class="avatar-icon mr2" src="{{ .LastUser.AvatarUrl }}"> <img class="avatar-icon mr2" src="{{ .LastUser.AvatarUrl }}">
<div> <div>
<div>Last post <span class="datetime">{{ relativedate .LastDate }}</span></div> <div>Last post {{ timehtml (relativedate .LastDate) .LastDate }}</div>
<a class="user" href="{{ .LastUser.ProfileUrl }}">{{ .LastUser.Name }}</a> <a class="user" href="{{ .LastUser.ProfileUrl }}">{{ .LastUser.Name }}</a>
</div> </div>
</div> </div>

View File

@ -271,7 +271,7 @@
<div class="flex-grow-1"> <div class="flex-grow-1">
<div class="title mb1"><a href="{{ .Url }}">{{ .Title }}</a></div> <div class="title mb1"><a href="{{ .Url }}">{{ .Title }}</a></div>
<div class="details"> <div class="details">
<a class="user" href="{{ .User.ProfileUrl }}">{{ .User.Name }}</a> &mdash; <span class="datetime">{{ relativedate .Date }}</span> <a class="user" href="{{ .User.ProfileUrl }}">{{ .User.Name }}</a> &mdash; {{ timehtml (relativedate .Date) .Date }}
</div> </div>
<div class="overflow-hidden mh-5 mt2 relative"> <div class="overflow-hidden mh-5 mt2 relative">
<div> <div>

View File

@ -12,7 +12,7 @@
{{ end }} {{ end }}
{{ end }} {{ end }}
{{ if .Title }} {{ if .Title }}
<title>{{ .Title }} | Handmade Network</title> <title>{{ .Title }} | Handmade Network</title> {{/* TODO: Some parts of the site replace "Handmade Network" with other things like "4coder Forums". */}}
{{ else }} {{ else }}
<title>Handmade Network</title> <title>Handmade Network</title>
{{ end }} {{ end }}

View File

@ -67,6 +67,9 @@ func names(ts []*template.Template) []string {
} }
var HMNTemplateFuncs = template.FuncMap{ var HMNTemplateFuncs = template.FuncMap{
"absolutedate": func(t time.Time) string {
return t.Format("January 2, 2006, 3:04pm")
},
"alpha": func(alpha float64, color noire.Color) noire.Color { "alpha": func(alpha float64, color noire.Color) noire.Color {
color.Alpha = alpha color.Alpha = alpha
return color return color
@ -157,6 +160,10 @@ var HMNTemplateFuncs = template.FuncMap{
"staticthemenobust": func(theme string, filepath string) string { "staticthemenobust": func(theme string, filepath string) string {
return hmnurl.StaticThemeUrl(filepath, theme, nil) return hmnurl.StaticThemeUrl(filepath, theme, nil)
}, },
"timehtml": func(formatted string, t time.Time) template.HTML {
iso := t.Format(time.RFC3339)
return template.HTML(fmt.Sprintf(`<time datetime="%s">%s</time>`, iso, formatted))
},
"url": func(url string) string { "url": func(url string) string {
return hmnurl.Url(url, nil) return hmnurl.Url(url, nil)
}, },

View File

@ -26,8 +26,13 @@ type Thread struct {
} }
type Post struct { type Post struct {
ID int ID int
Url string
Url string
DeleteUrl string
EditUrl string
ReplyUrl string
QuoteUrl string
Preview string Preview string
ReadOnly bool ReadOnly bool

View File

@ -15,6 +15,7 @@ import (
"git.handmade.network/hmn/hmn/src/models" "git.handmade.network/hmn/hmn/src/models"
"git.handmade.network/hmn/hmn/src/oops" "git.handmade.network/hmn/hmn/src/oops"
"git.handmade.network/hmn/hmn/src/templates" "git.handmade.network/hmn/hmn/src/templates"
"git.handmade.network/hmn/hmn/src/utils"
"github.com/jackc/pgx/v4/pgxpool" "github.com/jackc/pgx/v4/pgxpool"
) )
@ -262,7 +263,7 @@ func ForumCategory(c *RequestContext) ResponseData {
baseData := getBaseData(c) baseData := getBaseData(c)
baseData.Title = c.CurrentProject.Name + " Forums" baseData.Title = c.CurrentProject.Name + " Forums"
baseData.Breadcrumbs = []templates.Breadcrumb{ baseData.Breadcrumbs = []templates.Breadcrumb{ // TODO(ben): This is wrong; it needs to account for subcategories.
{ {
Name: c.CurrentProject.Name, Name: c.CurrentProject.Name,
Url: hmnurl.ProjectUrl("/", nil, c.CurrentProject.Slug), Url: hmnurl.ProjectUrl("/", nil, c.CurrentProject.Slug),
@ -299,13 +300,17 @@ func ForumCategory(c *RequestContext) ResponseData {
type forumThreadData struct { type forumThreadData struct {
templates.BaseData templates.BaseData
Thread templates.Thread Thread templates.Thread
Posts []templates.Post Posts []templates.Post
CategoryUrl string
ReplyUrl string
Pagination templates.Pagination
} }
func ForumThread(c *RequestContext) ResponseData { func ForumThread(c *RequestContext) ResponseData {
const postsPerPage = 15 const postsPerPage = 15
// TODO(asaf): Verify that the requested thread is not deleted, and only fetch non-deleted posts.
threadId, err := strconv.Atoi(c.PathParams["threadid"]) threadId, err := strconv.Atoi(c.PathParams["threadid"])
if err != nil { if err != nil {
@ -319,10 +324,16 @@ func ForumThread(c *RequestContext) ResponseData {
irow, err := db.QueryOne(c.Context(), c.Conn, threadQueryResult{}, irow, err := db.QueryOne(c.Context(), c.Conn, threadQueryResult{},
` `
SELECT $columns SELECT $columns
FROM handmade_thread AS thread FROM
WHERE thread.id = $1 handmade_thread AS thread
JOIN handmade_category AS cat ON cat.id = thread.category_id
WHERE
thread.id = $1
AND NOT thread.deleted
AND cat.project_id = $2
`, `,
threadId, threadId,
c.CurrentProject.ID,
) )
c.Perf.EndBlock() c.Perf.EndBlock()
if err != nil { if err != nil {
@ -336,18 +347,46 @@ func ForumThread(c *RequestContext) ResponseData {
categoryUrls := GetProjectCategoryUrls(c.Context(), c.Conn, c.CurrentProject.ID) categoryUrls := GetProjectCategoryUrls(c.Context(), c.Conn, c.CurrentProject.ID)
page, numPages, ok := getPageInfo(c.PathParams["page"], 100, postsPerPage) // TODO: Not 100 c.Perf.StartBlock("SQL", "Fetch category tree")
categoryTree := models.GetFullCategoryTree(c.Context(), c.Conn)
lineageBuilder := models.MakeCategoryLineageBuilder(categoryTree)
subforums := lineageBuilder.GetLineageSlugs(thread.CategoryID)[1:]
c.Perf.EndBlock()
numPosts, err := db.QueryInt(c.Context(), c.Conn,
`
SELECT COUNT(*)
FROM handmade_post
WHERE
thread_id = $1
AND NOT deleted
`,
thread.ID,
)
if err != nil {
panic(oops.New(err, "failed to get count of posts for thread"))
}
page, numPages, ok := getPageInfo(c.PathParams["page"], numPosts, postsPerPage)
if !ok { if !ok {
urlNoPage := ThreadUrl(thread, models.CatKindForum, categoryUrls[thread.CategoryID]) urlNoPage := ThreadUrl(thread, models.CatKindForum, categoryUrls[thread.CategoryID])
return c.Redirect(urlNoPage, http.StatusSeeOther) return c.Redirect(urlNoPage, http.StatusSeeOther)
} }
_ = numPages // TODO pagination := templates.Pagination{
Current: page,
Total: numPages,
FirstUrl: hmnurl.BuildForumThread(c.CurrentProject.Slug, subforums, thread.ID, 1),
LastUrl: hmnurl.BuildForumThread(c.CurrentProject.Slug, subforums, thread.ID, numPages),
NextUrl: hmnurl.BuildForumThread(c.CurrentProject.Slug, subforums, thread.ID, utils.IntClamp(1, page+1, numPages)),
PreviousUrl: hmnurl.BuildForumThread(c.CurrentProject.Slug, subforums, thread.ID, utils.IntClamp(1, page-1, numPages)),
}
c.Perf.StartBlock("SQL", "Fetch posts") c.Perf.StartBlock("SQL", "Fetch posts")
type postsQueryResult struct { type postsQueryResult struct {
Post models.Post `db:"post"` Post models.Post `db:"post"`
Content string `db:"ver.text_parsed"` Ver models.PostVersion `db:"ver"`
Author *models.User `db:"author"` Author *models.User `db:"author"`
Editor *models.User `db:"editor"`
} }
itPosts, err := db.Query(c.Context(), c.Conn, postsQueryResult{}, itPosts, err := db.Query(c.Context(), c.Conn, postsQueryResult{},
` `
@ -356,8 +395,10 @@ func ForumThread(c *RequestContext) ResponseData {
handmade_post AS post handmade_post AS post
JOIN handmade_postversion AS ver ON post.current_id = ver.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 author ON post.author_id = author.id
LEFT JOIN auth_user AS editor ON ver.editor_id = editor.id
WHERE WHERE
post.thread_id = $1 post.thread_id = $1
AND NOT post.deleted
ORDER BY postdate ORDER BY postdate
LIMIT $2 OFFSET $3 LIMIT $2 OFFSET $3
`, `,
@ -374,18 +415,26 @@ func ForumThread(c *RequestContext) ResponseData {
var posts []templates.Post var posts []templates.Post
for _, irow := range itPosts.ToSlice() { for _, irow := range itPosts.ToSlice() {
row := irow.(*postsQueryResult) row := irow.(*postsQueryResult)
posts = append(posts, templates.PostToTemplateWithContent(&row.Post, row.Author, row.Content))
post := templates.PostToTemplate(&row.Post, row.Author)
post.AddContentVersion(row.Ver, row.Editor)
post.AddUrls(c.CurrentProject.Slug, subforums, thread.ID, post.ID)
posts = append(posts, post)
} }
baseData := getBaseData(c) baseData := getBaseData(c)
// TODO(asaf): Replace page title with thread title baseData.Title = thread.Title
// TODO(asaf): Set breadcrumbs // TODO(asaf): Set breadcrumbs
var res ResponseData var res ResponseData
err = res.WriteTemplate("forum_thread.html", forumThreadData{ err = res.WriteTemplate("forum_thread.html", forumThreadData{
BaseData: baseData, BaseData: baseData,
Thread: templates.ThreadToTemplate(&thread), Thread: templates.ThreadToTemplate(&thread),
Posts: posts, Posts: posts,
CategoryUrl: categoryUrls[thread.CategoryID],
ReplyUrl: hmnurl.BuildForumPostReply(c.CurrentProject.Slug, subforums, thread.ID, *thread.FirstID),
Pagination: pagination,
}, c.Perf) }, c.Perf)
if err != nil { if err != nil {
panic(err) panic(err)

View File

@ -44,9 +44,10 @@ func GetProjectCategoryUrls(ctx context.Context, conn *pgxpool.Pool, projectId .
JOIN handmade_project AS project ON project.id = cat.project_id JOIN handmade_project AS project ON project.id = cat.project_id
WHERE WHERE
project.id = ANY ($1) project.id = ANY ($1)
AND cat.kind != 6 AND cat.kind != $2
`, // TODO(asaf): Clean up the db and remove the cat.kind != 6 check `, // TODO(asaf): Clean up the db and remove the cat.kind != library resource check
projectId, projectId,
models.CatKindLibraryResource,
) )
if err != nil { if err != nil {
panic(err) panic(err)
@ -94,13 +95,13 @@ func makeCategoryUrls(rows []interface{}) map[int]string {
} }
func CategoryUrl(projectSlug string, cats ...*models.Category) string { func CategoryUrl(projectSlug string, cats ...*models.Category) string {
catNames := make([]string, 0, len(cats)) catSlugs := make([]string, 0, len(cats))
for _, cat := range cats { for _, cat := range cats {
catNames = append(catNames, *cat.Name) catSlugs = append(catSlugs, *cat.Slug)
} }
switch cats[0].Kind { switch cats[0].Kind {
case models.CatKindForum: case models.CatKindForum:
return hmnurl.BuildForumCategory(projectSlug, catNames[1:], 1) return hmnurl.BuildForumCategory(projectSlug, catSlugs[1:], 1)
default: default:
return "" return ""
} }