Add forum post editing
This commit is contained in:
parent
1ccf715c2d
commit
b27c673c15
|
@ -26,8 +26,8 @@
|
||||||
<form id="form" action="{{ .SubmitUrl }}" method="post" class="flex-fair-ns">
|
<form id="form" action="{{ .SubmitUrl }}" method="post" class="flex-fair-ns">
|
||||||
{{ csrftoken .Session }}
|
{{ csrftoken .Session }}
|
||||||
|
|
||||||
{{ if not .PostReplyingTo }}
|
{{ if not (or .PostReplyingTo .IsEditing) }}
|
||||||
<input class="b w-100 mb1" name="title" type="text" placeholder="Post title..." value="{{ .PostTitle }}"/>
|
<input class="b w-100 mb1" name="title" type="text" placeholder="Post title..."/>
|
||||||
{{ end }}
|
{{ end }}
|
||||||
{{/* TODO: Reintroduce the toolbar in a way that makes sense for Markdown */}}
|
{{/* TODO: Reintroduce the toolbar in a way that makes sense for Markdown */}}
|
||||||
{{/*
|
{{/*
|
||||||
|
@ -51,7 +51,7 @@
|
||||||
<input type="button" id="youtube" value="youtube" />
|
<input type="button" id="youtube" value="youtube" />
|
||||||
</div>
|
</div>
|
||||||
*/}}
|
*/}}
|
||||||
<textarea id="editor" class="w-100 h5 minh-5" name="body">{{ .PostBody }}</textarea>
|
<textarea id="editor" class="w-100 h5 minh-5" name="body">{{ if .IsEditing }}{{ .EditInitialContents }}{{ end }}</textarea>
|
||||||
|
|
||||||
<div class="flex flex-row-reverse justify-start mt2">
|
<div class="flex flex-row-reverse justify-start mt2">
|
||||||
<input type="submit" class="button ml2" name="submit" value="{{ .SubmitLabel }}" />
|
<input type="submit" class="button ml2" name="submit" value="{{ .SubmitLabel }}" />
|
||||||
|
@ -124,15 +124,6 @@
|
||||||
|
|
||||||
{{/*
|
{{/*
|
||||||
|
|
||||||
{% if context_reply_to %}
|
|
||||||
<h4>The post you're replying to:</h4>
|
|
||||||
<div class="recent-posts">
|
|
||||||
{% with post=post_reply_to %}
|
|
||||||
{% include "forum_thread_single_post.html" %}
|
|
||||||
{% endwith %}
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
{% if context_newer %}
|
{% if context_newer %}
|
||||||
<h4>Replies since then:</h4>
|
<h4>Replies since then:</h4>
|
||||||
<div class="recent-posts">
|
<div class="recent-posts">
|
||||||
|
@ -187,7 +178,7 @@
|
||||||
// Load any stored content from localStorage
|
// Load any stored content from localStorage
|
||||||
const storageKey = `${storagePrefix}/${window.location.host}${window.location.pathname}`;
|
const storageKey = `${storagePrefix}/${window.location.host}${window.location.pathname}`;
|
||||||
const storedContents = window.localStorage.getItem(storageKey);
|
const storedContents = window.localStorage.getItem(storageKey);
|
||||||
if (storedContents) {
|
if (storedContents && !tf.value) {
|
||||||
try {
|
try {
|
||||||
const { contents } = JSON.parse(storedContents);
|
const { contents } = JSON.parse(storedContents);
|
||||||
tf.value = contents;
|
tf.value = contents;
|
||||||
|
|
|
@ -41,12 +41,12 @@ type forumSubcategoryData struct {
|
||||||
type editorData struct {
|
type editorData struct {
|
||||||
templates.BaseData
|
templates.BaseData
|
||||||
SubmitUrl string
|
SubmitUrl string
|
||||||
PostTitle string
|
|
||||||
PostBody string
|
|
||||||
SubmitLabel string
|
|
||||||
IsEditing bool // false if new post, true if updating existing one
|
|
||||||
|
|
||||||
ThreadTitle string
|
ThreadTitle string
|
||||||
|
SubmitLabel string
|
||||||
|
|
||||||
|
IsEditing bool // false if new post, true if updating existing one
|
||||||
|
EditInitialContents string
|
||||||
|
|
||||||
PostReplyingTo *templates.Post
|
PostReplyingTo *templates.Post
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -607,7 +607,7 @@ func ForumNewThreadSubmit(c *RequestContext) ResponseData {
|
||||||
panic(oops.New(err, "failed to create thread"))
|
panic(oops.New(err, "failed to create thread"))
|
||||||
}
|
}
|
||||||
|
|
||||||
postId, _ := createForumPostAndVersion(c.Context(), tx, currentCatId, threadId, c.CurrentUser.ID, c.CurrentProject.ID, unparsed, c.Req.Host, nil)
|
postId, _ := createNewForumPostAndVersion(c.Context(), tx, currentCatId, threadId, c.CurrentUser.ID, c.CurrentProject.ID, unparsed, c.Req.Host, nil)
|
||||||
|
|
||||||
// Update thread with post id
|
// Update thread with post id
|
||||||
_, err = tx.Exec(c.Context(),
|
_, err = tx.Exec(c.Context(),
|
||||||
|
@ -658,7 +658,7 @@ func ForumPostReply(c *RequestContext) ResponseData {
|
||||||
return FourOhFour(c)
|
return FourOhFour(c)
|
||||||
}
|
}
|
||||||
|
|
||||||
c.Perf.StartBlock("SQL", "Fetch post ids for thread")
|
c.Perf.StartBlock("SQL", "Fetch post to reply to")
|
||||||
// TODO: Scope this down to just what you need
|
// TODO: Scope this down to just what you need
|
||||||
type postQuery struct {
|
type postQuery struct {
|
||||||
Thread models.Thread `db:"thread"`
|
Thread models.Thread `db:"thread"`
|
||||||
|
@ -747,40 +747,154 @@ func ForumPostReplySubmit(c *RequestContext) ResponseData {
|
||||||
|
|
||||||
unparsed := c.Req.Form.Get("body")
|
unparsed := c.Req.Form.Get("body")
|
||||||
|
|
||||||
newPostId, _ := createForumPostAndVersion(c.Context(), tx, currentCatId, threadId, c.CurrentUser.ID, c.CurrentProject.ID, unparsed, c.Req.Host, &postId)
|
newPostId, _ := createNewForumPostAndVersion(c.Context(), tx, currentCatId, threadId, c.CurrentUser.ID, c.CurrentProject.ID, unparsed, c.Req.Host, &postId)
|
||||||
|
|
||||||
err = tx.Commit(c.Context())
|
err = tx.Commit(c.Context())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return ErrorResponse(http.StatusInternalServerError, oops.New(err, "failed to create new forum thread"))
|
return ErrorResponse(http.StatusInternalServerError, oops.New(err, "failed to reply to forum post"))
|
||||||
}
|
}
|
||||||
|
|
||||||
newPostUrl := hmnurl.BuildForumPost(c.CurrentProject.Slug, lineageBuilder.GetSubforumLineageSlugs(currentCatId), threadId, newPostId)
|
newPostUrl := hmnurl.BuildForumPost(c.CurrentProject.Slug, lineageBuilder.GetSubforumLineageSlugs(currentCatId), threadId, newPostId)
|
||||||
return c.Redirect(newPostUrl, http.StatusSeeOther)
|
return c.Redirect(newPostUrl, http.StatusSeeOther)
|
||||||
}
|
}
|
||||||
|
|
||||||
func createForumPostAndVersion(ctx context.Context, tx pgx.Tx, catId, threadId, userId, projectId int, unparsedContent string, ipString string, replyId *int) (postId, versionId int) {
|
func ForumPostEdit(c *RequestContext) ResponseData {
|
||||||
parsed := parsing.ParsePostInput(unparsedContent, parsing.RealMarkdown)
|
// TODO(compression): This logic for fetching posts by thread id / post id is gonna
|
||||||
now := time.Now()
|
// show up in a lot of places. It's used multiple times for forums, and also for blogs.
|
||||||
ip := net.ParseIP(ipString)
|
// Consider compressing this later.
|
||||||
|
c.Perf.StartBlock("SQL", "Fetch category tree")
|
||||||
|
categoryTree := models.GetFullCategoryTree(c.Context(), c.Conn)
|
||||||
|
lineageBuilder := models.MakeCategoryLineageBuilder(categoryTree)
|
||||||
|
c.Perf.EndBlock()
|
||||||
|
|
||||||
const previewMaxLength = 100
|
currentCatId, valid := validateSubforums(lineageBuilder, c.CurrentProject, c.PathParams["cats"])
|
||||||
parsedPlaintext := parsing.ParsePostInput(unparsedContent, parsing.PlaintextMarkdown)
|
if !valid {
|
||||||
preview := parsedPlaintext
|
return FourOhFour(c)
|
||||||
if len(preview) > previewMaxLength-1 {
|
|
||||||
preview = preview[:previewMaxLength-1] + "…"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
requestedThreadId, err := strconv.Atoi(c.PathParams["threadid"])
|
||||||
|
if err != nil {
|
||||||
|
return FourOhFour(c)
|
||||||
|
}
|
||||||
|
|
||||||
|
requestedPostId, err := strconv.Atoi(c.PathParams["postid"])
|
||||||
|
if err != nil {
|
||||||
|
return FourOhFour(c)
|
||||||
|
}
|
||||||
|
|
||||||
|
c.Perf.StartBlock("SQL", "Fetch post to edit")
|
||||||
|
// TODO: Scope this down to just what you need
|
||||||
|
type postQuery 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"`
|
||||||
|
}
|
||||||
|
postQueryResult, err := db.QueryOne(c.Context(), c.Conn, postQuery{},
|
||||||
|
`
|
||||||
|
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.category_id = $1
|
||||||
|
AND post.thread_id = $2
|
||||||
|
AND post.id = $3
|
||||||
|
AND NOT post.deleted
|
||||||
|
ORDER BY postdate
|
||||||
|
`,
|
||||||
|
currentCatId,
|
||||||
|
requestedThreadId,
|
||||||
|
requestedPostId,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, db.ErrNoMatchingRows) {
|
||||||
|
return FourOhFour(c)
|
||||||
|
} else {
|
||||||
|
return ErrorResponse(http.StatusInternalServerError, oops.New(err, "failed to fetch reply post"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
result := postQueryResult.(*postQuery)
|
||||||
|
|
||||||
|
baseData := getBaseData(c)
|
||||||
|
baseData.Title = fmt.Sprintf("Editing \"%s\" | %s", result.Thread.Title, *categoryTree[currentCatId].Name)
|
||||||
|
baseData.MathjaxEnabled = true
|
||||||
|
// TODO(ben): Set breadcrumbs
|
||||||
|
|
||||||
|
templatePost := templates.PostToTemplate(&result.Post, result.Author, c.Theme)
|
||||||
|
templatePost.AddContentVersion(result.CurrentVersion, result.Editor)
|
||||||
|
|
||||||
|
var res ResponseData
|
||||||
|
res.MustWriteTemplate("editor.html", editorData{
|
||||||
|
BaseData: baseData,
|
||||||
|
SubmitUrl: hmnurl.BuildForumPostEdit(c.CurrentProject.Slug, lineageBuilder.GetSubforumLineageSlugs(currentCatId), requestedThreadId, requestedPostId),
|
||||||
|
ThreadTitle: result.Thread.Title,
|
||||||
|
SubmitLabel: "Submit Edited Post",
|
||||||
|
|
||||||
|
IsEditing: true,
|
||||||
|
EditInitialContents: result.CurrentVersion.TextRaw,
|
||||||
|
}, c.Perf)
|
||||||
|
return res
|
||||||
|
}
|
||||||
|
|
||||||
|
func ForumPostEditSubmit(c *RequestContext) ResponseData {
|
||||||
|
tx, err := c.Conn.Begin(c.Context())
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
defer tx.Rollback(c.Context())
|
||||||
|
|
||||||
|
c.Perf.StartBlock("SQL", "Fetch category tree")
|
||||||
|
categoryTree := models.GetFullCategoryTree(c.Context(), c.Conn)
|
||||||
|
lineageBuilder := models.MakeCategoryLineageBuilder(categoryTree)
|
||||||
|
c.Perf.EndBlock()
|
||||||
|
|
||||||
|
currentCatId, valid := validateSubforums(lineageBuilder, c.CurrentProject, c.PathParams["cats"])
|
||||||
|
if !valid {
|
||||||
|
return FourOhFour(c)
|
||||||
|
}
|
||||||
|
|
||||||
|
threadId, err := strconv.Atoi(c.PathParams["threadid"])
|
||||||
|
if err != nil {
|
||||||
|
return FourOhFour(c)
|
||||||
|
}
|
||||||
|
|
||||||
|
postId, err := strconv.Atoi(c.PathParams["postid"])
|
||||||
|
if err != nil {
|
||||||
|
return FourOhFour(c)
|
||||||
|
}
|
||||||
|
|
||||||
|
c.Req.ParseForm()
|
||||||
|
|
||||||
|
unparsed := c.Req.Form.Get("body")
|
||||||
|
editReason := c.Req.Form.Get("editreason")
|
||||||
|
|
||||||
|
createForumPostVersion(c.Context(), tx, postId, unparsed, c.Req.Host, editReason, &c.CurrentUser.ID)
|
||||||
|
|
||||||
|
err = tx.Commit(c.Context())
|
||||||
|
if err != nil {
|
||||||
|
return ErrorResponse(http.StatusInternalServerError, oops.New(err, "failed to edit forum post"))
|
||||||
|
}
|
||||||
|
|
||||||
|
postUrl := hmnurl.BuildForumPost(c.CurrentProject.Slug, lineageBuilder.GetSubforumLineageSlugs(currentCatId), threadId, postId)
|
||||||
|
return c.Redirect(postUrl, http.StatusSeeOther)
|
||||||
|
}
|
||||||
|
|
||||||
|
func createNewForumPostAndVersion(ctx context.Context, tx pgx.Tx, catId, threadId, userId, projectId int, unparsedContent string, ipString string, replyId *int) (postId, versionId int) {
|
||||||
// Create post
|
// Create post
|
||||||
err := tx.QueryRow(ctx,
|
err := tx.QueryRow(ctx,
|
||||||
`
|
`
|
||||||
INSERT INTO handmade_post (postdate, category_id, thread_id, preview, current_id, author_id, category_kind, project_id, reply_id)
|
INSERT INTO handmade_post (postdate, category_id, thread_id, current_id, author_id, category_kind, project_id, reply_id)
|
||||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)
|
VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
|
||||||
RETURNING id
|
RETURNING id
|
||||||
`,
|
`,
|
||||||
now,
|
time.Now(),
|
||||||
catId,
|
catId,
|
||||||
threadId,
|
threadId,
|
||||||
preview,
|
|
||||||
-1,
|
-1,
|
||||||
userId,
|
userId,
|
||||||
models.CatKindForum,
|
models.CatKindForum,
|
||||||
|
@ -791,35 +905,54 @@ func createForumPostAndVersion(ctx context.Context, tx pgx.Tx, catId, threadId,
|
||||||
panic(oops.New(err, "failed to create post"))
|
panic(oops.New(err, "failed to create post"))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
versionId = createForumPostVersion(ctx, tx, postId, unparsedContent, ipString, "", nil)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func createForumPostVersion(ctx context.Context, tx pgx.Tx, postId int, unparsedContent string, ipString string, editReason string, editorId *int) (versionId int) {
|
||||||
|
parsed := parsing.ParsePostInput(unparsedContent, parsing.RealMarkdown)
|
||||||
|
ip := net.ParseIP(ipString)
|
||||||
|
|
||||||
|
const previewMaxLength = 100
|
||||||
|
parsedPlaintext := parsing.ParsePostInput(unparsedContent, parsing.PlaintextMarkdown)
|
||||||
|
preview := parsedPlaintext
|
||||||
|
if len(preview) > previewMaxLength-1 {
|
||||||
|
preview = preview[:previewMaxLength-1] + "…"
|
||||||
|
}
|
||||||
|
|
||||||
// Create post version
|
// Create post version
|
||||||
err = tx.QueryRow(ctx,
|
err := tx.QueryRow(ctx,
|
||||||
`
|
`
|
||||||
INSERT INTO handmade_postversion (post_id, text_raw, text_parsed, ip, date)
|
INSERT INTO handmade_postversion (post_id, text_raw, text_parsed, ip, date, edit_reason, editor_id)
|
||||||
VALUES ($1, $2, $3, $4, $5)
|
VALUES ($1, $2, $3, $4, $5, $6, $7)
|
||||||
RETURNING id
|
RETURNING id
|
||||||
`,
|
`,
|
||||||
postId,
|
postId,
|
||||||
unparsedContent,
|
unparsedContent,
|
||||||
parsed,
|
parsed,
|
||||||
ip,
|
ip,
|
||||||
now,
|
time.Now(),
|
||||||
|
editReason,
|
||||||
|
editorId,
|
||||||
).Scan(&versionId)
|
).Scan(&versionId)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(oops.New(err, "failed to create post version"))
|
panic(oops.New(err, "failed to create post version"))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update post with version id
|
// Update post with version id and preview
|
||||||
_, err = tx.Exec(ctx,
|
_, err = tx.Exec(ctx,
|
||||||
`
|
`
|
||||||
UPDATE handmade_post
|
UPDATE handmade_post
|
||||||
SET current_id = $1
|
SET current_id = $1, preview = $2
|
||||||
WHERE id = $2
|
WHERE id = $3
|
||||||
`,
|
`,
|
||||||
versionId,
|
versionId,
|
||||||
|
preview,
|
||||||
postId,
|
postId,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(oops.New(err, "failed to set current post version"))
|
panic(oops.New(err, "failed to set current post version and preview"))
|
||||||
}
|
}
|
||||||
|
|
||||||
return
|
return
|
||||||
|
|
|
@ -160,6 +160,8 @@ func NewWebsiteRoutes(conn *pgxpool.Pool, perfCollector *perf.PerfCollector) htt
|
||||||
mainRoutes.GET(hmnurl.RegexForumPost, ForumPostRedirect)
|
mainRoutes.GET(hmnurl.RegexForumPost, ForumPostRedirect)
|
||||||
mainRoutes.GET(hmnurl.RegexForumPostReply, authMiddleware(ForumPostReply))
|
mainRoutes.GET(hmnurl.RegexForumPostReply, authMiddleware(ForumPostReply))
|
||||||
mainRoutes.POST(hmnurl.RegexForumPostReply, authMiddleware(ForumPostReplySubmit))
|
mainRoutes.POST(hmnurl.RegexForumPostReply, authMiddleware(ForumPostReplySubmit))
|
||||||
|
mainRoutes.GET(hmnurl.RegexForumPostEdit, authMiddleware(ForumPostEdit))
|
||||||
|
mainRoutes.POST(hmnurl.RegexForumPostEdit, authMiddleware(ForumPostEditSubmit))
|
||||||
|
|
||||||
mainRoutes.GET(hmnurl.RegexProjectCSS, ProjectCSS)
|
mainRoutes.GET(hmnurl.RegexProjectCSS, ProjectCSS)
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue