Finalises implementation of post editing.
This commit is contained in:
parent
1be362259c
commit
fc7dabddda
5 changed files with 174 additions and 63 deletions
67
forum.nim
67
forum.nim
|
|
@ -1021,6 +1021,9 @@ proc executeReply(c: TForumData, threadId: int, content: string,
|
||||||
if rateLimitCheck(c):
|
if rateLimitCheck(c):
|
||||||
raise newForumError("You're posting too fast!")
|
raise newForumError("You're posting too fast!")
|
||||||
|
|
||||||
|
if not validateRst(c, content):
|
||||||
|
raise newForumError("Message needs to be valid RST", @["msg"])
|
||||||
|
|
||||||
# TODO: Replying to.
|
# TODO: Replying to.
|
||||||
# Verify that content can be parsed as RST.
|
# Verify that content can be parsed as RST.
|
||||||
let retID = insertID(
|
let retID = insertID(
|
||||||
|
|
@ -1039,6 +1042,42 @@ proc executeReply(c: TForumData, threadId: int, content: string,
|
||||||
|
|
||||||
return retID
|
return retID
|
||||||
|
|
||||||
|
proc updatePost(c: TForumData, postId: int, content: string,
|
||||||
|
subject: Option[string]) =
|
||||||
|
## Updates an existing post.
|
||||||
|
assert c.loggedIn()
|
||||||
|
|
||||||
|
let postQuery = sql"""
|
||||||
|
select author, strftime('%s', creation), thread
|
||||||
|
from post where id = ?
|
||||||
|
"""
|
||||||
|
|
||||||
|
let postRow = getRow(db, postQuery, postId)
|
||||||
|
|
||||||
|
# Verify that the current user has permissions to edit the specified post.
|
||||||
|
let creation = fromUnix(postRow[1].parseInt)
|
||||||
|
let isArchived = (getTime() - creation).weeks > 8
|
||||||
|
let canEdit = c.rank == Admin or c.username == postRow[0]
|
||||||
|
if isArchived:
|
||||||
|
raise newForumError("This post is archived and can no longer be edited")
|
||||||
|
if not canEdit:
|
||||||
|
raise newForumError("You cannot edit this post")
|
||||||
|
|
||||||
|
if not validateRst(c, content):
|
||||||
|
raise newForumError("Message needs to be valid RST", @["msg"])
|
||||||
|
|
||||||
|
# Update post.
|
||||||
|
exec(db, crud(crUpdate, "post", "content"), content, $postId)
|
||||||
|
exec(db, crud(crUpdate, "post_fts", "content"), content, $postId)
|
||||||
|
# Check if post is the first post of the thread.
|
||||||
|
if subject.isSome():
|
||||||
|
let threadId = postRow[2]
|
||||||
|
let row = db.getRow(sql("""
|
||||||
|
select id from post where thread = ? order by id asc
|
||||||
|
"""), threadId)
|
||||||
|
if row[0] == $postId:
|
||||||
|
exec(db, crud(crUpdate, "thread", "name"), subject.get(), threadId)
|
||||||
|
|
||||||
proc executeNewThread(c: TForumData, subject, msg: string): (int64, int64) =
|
proc executeNewThread(c: TForumData, subject, msg: string): (int64, int64) =
|
||||||
const
|
const
|
||||||
query = sql"""
|
query = sql"""
|
||||||
|
|
@ -1472,6 +1511,34 @@ routes:
|
||||||
except ForumError as exc:
|
except ForumError as exc:
|
||||||
resp Http400, $(%exc.data), "application/json"
|
resp Http400, $(%exc.data), "application/json"
|
||||||
|
|
||||||
|
post "/karax/updatePost":
|
||||||
|
createTFD()
|
||||||
|
if not c.loggedIn():
|
||||||
|
let err = PostError(
|
||||||
|
errorFields: @[],
|
||||||
|
message: "Not logged in."
|
||||||
|
)
|
||||||
|
resp Http401, $(%err), "application/json"
|
||||||
|
|
||||||
|
let formData = request.formData
|
||||||
|
cond "msg" in formData
|
||||||
|
cond "postId" in formData
|
||||||
|
|
||||||
|
let msg = formData["msg"].body
|
||||||
|
let postId = getInt(formData["postId"].body, -1)
|
||||||
|
cond postId != -1
|
||||||
|
let subject =
|
||||||
|
if "subject" in formData:
|
||||||
|
some(formData["subject"].body)
|
||||||
|
else:
|
||||||
|
none[string]()
|
||||||
|
|
||||||
|
try:
|
||||||
|
updatePost(c, postId, msg, subject)
|
||||||
|
resp Http200, msg.rstToHtml(), "text/html"
|
||||||
|
except ForumError as exc:
|
||||||
|
resp Http400, $(%exc.data), "application/json"
|
||||||
|
|
||||||
post "/karax/newthread":
|
post "/karax/newthread":
|
||||||
createTFD()
|
createTFD()
|
||||||
if not c.loggedIn():
|
if not c.loggedIn():
|
||||||
|
|
|
||||||
|
|
@ -1,21 +1,30 @@
|
||||||
when defined(js):
|
when defined(js):
|
||||||
import httpcore, options, sugar
|
import httpcore, options, sugar, json
|
||||||
|
|
||||||
include karax/prelude
|
include karax/prelude
|
||||||
import karax/kajax
|
import karax/kajax
|
||||||
|
|
||||||
import replybox, post, karaxutils, threadlist
|
import replybox, post, karaxutils, threadlist, error
|
||||||
|
|
||||||
type
|
type
|
||||||
|
OnEditPosted* = proc (id: int, content: string, subject: Option[string])
|
||||||
|
|
||||||
EditBox* = ref object
|
EditBox* = ref object
|
||||||
box: ReplyBox
|
box: ReplyBox
|
||||||
post: Option[Post]
|
post: Post
|
||||||
rawContent: Option[kstring] ## The raw rst for a post (needs to be loaded)
|
rawContent: Option[kstring] ## The raw rst for a post (needs to be loaded)
|
||||||
|
loading: bool
|
||||||
status: HttpCode
|
status: HttpCode
|
||||||
|
error: Option[PostError]
|
||||||
|
onEditPosted: OnEditPosted
|
||||||
|
onEditCancel: proc ()
|
||||||
|
|
||||||
proc newEditBox*(): EditBox =
|
proc newEditBox*(onEditPosted: OnEditPosted, onEditCancel: proc ()): EditBox =
|
||||||
EditBox(
|
EditBox(
|
||||||
box: newReplyBox(nil)
|
box: newReplyBox(nil),
|
||||||
|
onEditPosted: onEditPosted,
|
||||||
|
onEditCancel: onEditCancel,
|
||||||
|
status: Http200
|
||||||
)
|
)
|
||||||
|
|
||||||
proc onRawContent(httpStatus: int, response: kstring, state: EditBox) =
|
proc onRawContent(httpStatus: int, response: kstring, state: EditBox) =
|
||||||
|
|
@ -23,21 +32,60 @@ when defined(js):
|
||||||
if state.status != Http200: return
|
if state.status != Http200: return
|
||||||
|
|
||||||
state.rawContent = some(response)
|
state.rawContent = some(response)
|
||||||
|
state.box.setText(state.rawContent.get())
|
||||||
|
|
||||||
|
proc onEditPost(httpStatus: int, response: kstring, state: EditBox) =
|
||||||
|
postFinished:
|
||||||
|
state.onEditPosted(
|
||||||
|
state.post.id,
|
||||||
|
$response,
|
||||||
|
none[string]()
|
||||||
|
)
|
||||||
|
|
||||||
|
proc save(state: EditBox) =
|
||||||
|
if state.loading:
|
||||||
|
# TODO: Weird behaviour: onClick handler gets called 80+ times.
|
||||||
|
return
|
||||||
|
state.loading = true
|
||||||
|
state.error = none[PostError]()
|
||||||
|
|
||||||
|
let formData = newFormData()
|
||||||
|
formData.append("msg", state.box.getText())
|
||||||
|
formData.append("postId", $state.post.id)
|
||||||
|
# TODO: Subject
|
||||||
|
let uri = makeUri("/updatePost")
|
||||||
|
ajaxPost(uri, @[], cast[cstring](formData),
|
||||||
|
(s: int, r: kstring) => onEditPost(s, r, state))
|
||||||
|
|
||||||
proc render*(state: EditBox, post: Post): VNode =
|
proc render*(state: EditBox, post: Post): VNode =
|
||||||
if state.post.isNone() or state.post.get().id != post.id:
|
if state.rawContent.isNone() or state.post.id != post.id:
|
||||||
state.post = some(post)
|
state.post = post
|
||||||
|
state.rawContent = none[kstring]()
|
||||||
var params = @[("id", $post.id)]
|
var params = @[("id", $post.id)]
|
||||||
let uri = makeUri("post.rst", params)
|
let uri = makeUri("post.rst", params)
|
||||||
ajaxGet(uri, @[], (s: int, r: kstring) => onRawContent(s, r, state))
|
ajaxGet(uri, @[], (s: int, r: kstring) => onRawContent(s, r, state))
|
||||||
|
|
||||||
return buildHtml(tdiv(class="loading"))
|
return buildHtml(tdiv(class="loading"))
|
||||||
|
|
||||||
state.box.setText(state.rawContent.get())
|
|
||||||
result = buildHtml():
|
result = buildHtml():
|
||||||
tdiv(class="edit-box"):
|
tdiv(class="edit-box"):
|
||||||
renderContent(
|
renderContent(
|
||||||
state.box,
|
state.box,
|
||||||
none[Thread](),
|
none[Thread](),
|
||||||
none[Post]()
|
none[Post]()
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if state.error.isSome():
|
||||||
|
span(class="text-error"):
|
||||||
|
text state.error.get().message
|
||||||
|
|
||||||
|
tdiv(class="edit-buttons"):
|
||||||
|
tdiv(class="reply-button"):
|
||||||
|
button(class="btn btn-link",
|
||||||
|
onClick=(e: Event, n: VNode) => (state.onEditCancel())):
|
||||||
|
text " Cancel"
|
||||||
|
tdiv(class="save-button"):
|
||||||
|
button(class=class({"loading": state.loading}, "btn btn-primary"),
|
||||||
|
onClick=(e: Event, n: VNode) => state.save()):
|
||||||
|
italic(class="fas fa-check")
|
||||||
|
text " Save"
|
||||||
|
|
@ -502,21 +502,26 @@ hr {
|
||||||
}
|
}
|
||||||
|
|
||||||
.edit-box {
|
.edit-box {
|
||||||
margin-bottom: $control-padding-y;
|
.edit-buttons {
|
||||||
|
margin-top: $control-padding-y*2;
|
||||||
|
|
||||||
|
float: right;
|
||||||
|
|
||||||
|
> div {
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-error {
|
||||||
|
margin-top: $control-padding-y*3;
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
|
||||||
.form-input.post-text-area {
|
.form-input.post-text-area {
|
||||||
margin-bottom: $control-padding-y*2;
|
margin-bottom: $control-padding-y*2;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.edit-buttons {
|
|
||||||
float: right;
|
|
||||||
|
|
||||||
> div {
|
|
||||||
display: inline-block;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@import "syntax.scss";
|
@import "syntax.scss";
|
||||||
|
|
||||||
// - Profile view
|
// - Profile view
|
||||||
|
|
|
||||||
|
|
@ -30,6 +30,8 @@ when defined(js):
|
||||||
editBox: EditBox
|
editBox: EditBox
|
||||||
|
|
||||||
proc onReplyPosted(id: int)
|
proc onReplyPosted(id: int)
|
||||||
|
proc onEditPosted(id: int, content: string, subject: Option[string])
|
||||||
|
proc onEditCancelled()
|
||||||
proc newState(): State =
|
proc newState(): State =
|
||||||
State(
|
State(
|
||||||
list: none[PostList](),
|
list: none[PostList](),
|
||||||
|
|
@ -37,7 +39,7 @@ when defined(js):
|
||||||
status: Http200,
|
status: Http200,
|
||||||
replyingTo: none[Post](),
|
replyingTo: none[Post](),
|
||||||
replyBox: newReplyBox(onReplyPosted),
|
replyBox: newReplyBox(onReplyPosted),
|
||||||
editBox: newEditBox()
|
editBox: newEditBox(onEditPosted, onEditCancelled)
|
||||||
)
|
)
|
||||||
|
|
||||||
var
|
var
|
||||||
|
|
@ -109,17 +111,17 @@ when defined(js):
|
||||||
## Executed when a reply has been successfully posted.
|
## Executed when a reply has been successfully posted.
|
||||||
loadMore(state.list.get().posts.len, @[id])
|
loadMore(state.list.get().posts.len, @[id])
|
||||||
|
|
||||||
proc onEditPosted(id: int, content: string) =
|
proc onEditCancelled() = state.editing = none[Post]()
|
||||||
|
|
||||||
|
proc onEditPosted(id: int, content: string, subject: Option[string]) =
|
||||||
## Executed when an edit has been successfully posted.
|
## Executed when an edit has been successfully posted.
|
||||||
|
state.editing = none[Post]()
|
||||||
let list = state.list.get()
|
let list = state.list.get()
|
||||||
for i in 0 ..< list.posts.len:
|
for i in 0 ..< list.posts.len:
|
||||||
if list.posts[i].id == id:
|
if list.posts[i].id == id:
|
||||||
list.posts[i].info.content = content
|
list.posts[i].info.content = content
|
||||||
break
|
break
|
||||||
|
|
||||||
proc onEditConfirm(e: Event, n: VNode, p: Post) =
|
|
||||||
discard
|
|
||||||
|
|
||||||
proc onReplyClick(e: Event, n: VNode, p: Option[Post]) =
|
proc onReplyClick(e: Event, n: VNode, p: Option[Post]) =
|
||||||
state.replyingTo = p
|
state.replyingTo = p
|
||||||
state.replyBox.show()
|
state.replyBox.show()
|
||||||
|
|
@ -151,48 +153,37 @@ when defined(js):
|
||||||
loggedIn and currentUser.get().name == post.author.name
|
loggedIn and currentUser.get().name == post.author.name
|
||||||
|
|
||||||
if state.editing.isSome() and state.editing.get() == post:
|
if state.editing.isSome() and state.editing.get() == post:
|
||||||
result = buildHtml():
|
return buildHtml(tdiv())
|
||||||
tdiv(class="edit-buttons"):
|
|
||||||
tdiv(class="reply-button"):
|
|
||||||
button(class="btn btn-link",
|
|
||||||
onClick=(e: Event, n: VNode) =>
|
|
||||||
(state.editing = none[Post]())):
|
|
||||||
text " Cancel"
|
|
||||||
tdiv(class="save-button"):
|
|
||||||
button(class="btn btn-primary", onClick=(e: Event, n: VNode) =>
|
|
||||||
onEditConfirm(e, n, post)):
|
|
||||||
italic(class="fas fa-check")
|
|
||||||
text " Save"
|
|
||||||
else:
|
|
||||||
result = buildHtml():
|
|
||||||
tdiv(class="post-buttons"):
|
|
||||||
if authoredByUser:
|
|
||||||
tdiv(class="edit-button", onClick=(e: Event, n: VNode) =>
|
|
||||||
onEditClick(e, n, post)):
|
|
||||||
button(class="btn"):
|
|
||||||
italic(class="far fa-edit")
|
|
||||||
tdiv(class="delete-button"):
|
|
||||||
button(class="btn"):
|
|
||||||
italic(class="far fa-trash-alt")
|
|
||||||
else:
|
|
||||||
tdiv(class="like-button"):
|
|
||||||
button(class="btn"):
|
|
||||||
span(class="like-count"):
|
|
||||||
if post.likes.len > 0:
|
|
||||||
text $post.likes.len
|
|
||||||
italic(class="far fa-heart")
|
|
||||||
|
|
||||||
if loggedIn:
|
result = buildHtml():
|
||||||
tdiv(class="flag-button"):
|
tdiv(class="post-buttons"):
|
||||||
button(class="btn"):
|
if authoredByUser:
|
||||||
italic(class="far fa-flag")
|
tdiv(class="edit-button", onClick=(e: Event, n: VNode) =>
|
||||||
|
onEditClick(e, n, post)):
|
||||||
|
button(class="btn"):
|
||||||
|
italic(class="far fa-edit")
|
||||||
|
tdiv(class="delete-button"):
|
||||||
|
button(class="btn"):
|
||||||
|
italic(class="far fa-trash-alt")
|
||||||
|
else:
|
||||||
|
tdiv(class="like-button"):
|
||||||
|
button(class="btn"):
|
||||||
|
span(class="like-count"):
|
||||||
|
if post.likes.len > 0:
|
||||||
|
text $post.likes.len
|
||||||
|
italic(class="far fa-heart")
|
||||||
|
|
||||||
if loggedIn:
|
if loggedIn:
|
||||||
tdiv(class="reply-button"):
|
tdiv(class="flag-button"):
|
||||||
button(class="btn", onClick=(e: Event, n: VNode) =>
|
button(class="btn"):
|
||||||
onReplyClick(e, n, some(post))):
|
italic(class="far fa-flag")
|
||||||
italic(class="fas fa-reply")
|
|
||||||
text " Reply"
|
if loggedIn:
|
||||||
|
tdiv(class="reply-button"):
|
||||||
|
button(class="btn", onClick=(e: Event, n: VNode) =>
|
||||||
|
onReplyClick(e, n, some(post))):
|
||||||
|
italic(class="fas fa-reply")
|
||||||
|
text " Reply"
|
||||||
|
|
||||||
proc genPost(post: Post, thread: Thread, currentUser: Option[User]): VNode =
|
proc genPost(post: Post, thread: Thread, currentUser: Option[User]): VNode =
|
||||||
let postCopy = post # TODO: Another workaround here, closure capture :(
|
let postCopy = post # TODO: Another workaround here, closure capture :(
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@ type
|
||||||
Moderated ## new member: posts manually reviewed before everybody
|
Moderated ## new member: posts manually reviewed before everybody
|
||||||
## can see them
|
## can see them
|
||||||
User ## Ordinary user
|
User ## Ordinary user
|
||||||
Moderator ## Moderator: can ban/moderate users
|
Moderator ## Moderator: can change a user's rank
|
||||||
Admin ## Admin: can do everything
|
Admin ## Admin: can do everything
|
||||||
|
|
||||||
User* = object
|
User* = object
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue