diff --git a/src/templates/src/editor.html b/src/templates/src/editor.html index d8bf9d3f..c37072d3 100644 --- a/src/templates/src/editor.html +++ b/src/templates/src/editor.html @@ -26,8 +26,8 @@
{{ csrftoken .Session }} - {{ if not .PostReplyingTo }} - + {{ if not (or .PostReplyingTo .IsEditing) }} + {{ end }} {{/* TODO: Reintroduce the toolbar in a way that makes sense for Markdown */}} {{/* @@ -51,7 +51,7 @@ */}} - +
@@ -124,15 +124,6 @@ {{/* - {% if context_reply_to %} -

The post you're replying to:

-
- {% with post=post_reply_to %} - {% include "forum_thread_single_post.html" %} - {% endwith %} -
- {% endif %} - {% if context_newer %}

Replies since then:

@@ -187,7 +178,7 @@ // Load any stored content from localStorage const storageKey = `${storagePrefix}/${window.location.host}${window.location.pathname}`; const storedContents = window.localStorage.getItem(storageKey); - if (storedContents) { + if (storedContents && !tf.value) { try { const { contents } = JSON.parse(storedContents); tf.value = contents; diff --git a/src/website/forums.go b/src/website/forums.go index 1f7b381a..81a609b6 100644 --- a/src/website/forums.go +++ b/src/website/forums.go @@ -41,12 +41,12 @@ type forumSubcategoryData struct { type editorData struct { templates.BaseData SubmitUrl string - PostTitle string - PostBody string + ThreadTitle string SubmitLabel string - IsEditing bool // false if new post, true if updating existing one - ThreadTitle string + IsEditing bool // false if new post, true if updating existing one + EditInitialContents string + PostReplyingTo *templates.Post } @@ -607,7 +607,7 @@ func ForumNewThreadSubmit(c *RequestContext) ResponseData { 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 _, err = tx.Exec(c.Context(), @@ -658,7 +658,7 @@ func ForumPostReply(c *RequestContext) ResponseData { 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 type postQuery struct { Thread models.Thread `db:"thread"` @@ -747,40 +747,154 @@ func ForumPostReplySubmit(c *RequestContext) ResponseData { 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()) 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) 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) { - parsed := parsing.ParsePostInput(unparsedContent, parsing.RealMarkdown) - now := time.Now() - ip := net.ParseIP(ipString) +func ForumPostEdit(c *RequestContext) ResponseData { + // TODO(compression): This logic for fetching posts by thread id / post id is gonna + // show up in a lot of places. It's used multiple times for forums, and also for blogs. + // 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 - parsedPlaintext := parsing.ParsePostInput(unparsedContent, parsing.PlaintextMarkdown) - preview := parsedPlaintext - if len(preview) > previewMaxLength-1 { - preview = preview[:previewMaxLength-1] + "…" + currentCatId, valid := validateSubforums(lineageBuilder, c.CurrentProject, c.PathParams["cats"]) + if !valid { + return FourOhFour(c) } + 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 err := tx.QueryRow(ctx, ` - INSERT INTO handmade_post (postdate, category_id, thread_id, preview, current_id, author_id, category_kind, project_id, reply_id) - VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9) + 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) RETURNING id `, - now, + time.Now(), catId, threadId, - preview, -1, userId, models.CatKindForum, @@ -791,35 +905,54 @@ func createForumPostAndVersion(ctx context.Context, tx pgx.Tx, catId, threadId, 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 - err = tx.QueryRow(ctx, + err := tx.QueryRow(ctx, ` - INSERT INTO handmade_postversion (post_id, text_raw, text_parsed, ip, date) - VALUES ($1, $2, $3, $4, $5) + INSERT INTO handmade_postversion (post_id, text_raw, text_parsed, ip, date, edit_reason, editor_id) + VALUES ($1, $2, $3, $4, $5, $6, $7) RETURNING id `, postId, unparsedContent, parsed, ip, - now, + time.Now(), + editReason, + editorId, ).Scan(&versionId) if err != nil { 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, ` UPDATE handmade_post - SET current_id = $1 - WHERE id = $2 + SET current_id = $1, preview = $2 + WHERE id = $3 `, versionId, + preview, postId, ) 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 diff --git a/src/website/routes.go b/src/website/routes.go index 3fb581d2..96dac27c 100644 --- a/src/website/routes.go +++ b/src/website/routes.go @@ -160,6 +160,8 @@ func NewWebsiteRoutes(conn *pgxpool.Pool, perfCollector *perf.PerfCollector) htt mainRoutes.GET(hmnurl.RegexForumPost, ForumPostRedirect) mainRoutes.GET(hmnurl.RegexForumPostReply, authMiddleware(ForumPostReply)) mainRoutes.POST(hmnurl.RegexForumPostReply, authMiddleware(ForumPostReplySubmit)) + mainRoutes.GET(hmnurl.RegexForumPostEdit, authMiddleware(ForumPostEdit)) + mainRoutes.POST(hmnurl.RegexForumPostEdit, authMiddleware(ForumPostEditSubmit)) mainRoutes.GET(hmnurl.RegexProjectCSS, ProjectCSS)