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):
|
||||
raise newForumError("You're posting too fast!")
|
||||
|
||||
if not validateRst(c, content):
|
||||
raise newForumError("Message needs to be valid RST", @["msg"])
|
||||
|
||||
# TODO: Replying to.
|
||||
# Verify that content can be parsed as RST.
|
||||
let retID = insertID(
|
||||
|
|
@ -1039,6 +1042,42 @@ proc executeReply(c: TForumData, threadId: int, content: string,
|
|||
|
||||
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) =
|
||||
const
|
||||
query = sql"""
|
||||
|
|
@ -1472,6 +1511,34 @@ routes:
|
|||
except ForumError as exc:
|
||||
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":
|
||||
createTFD()
|
||||
if not c.loggedIn():
|
||||
|
|
|
|||
|
|
@ -1,21 +1,30 @@
|
|||
when defined(js):
|
||||
import httpcore, options, sugar
|
||||
import httpcore, options, sugar, json
|
||||
|
||||
include karax/prelude
|
||||
import karax/kajax
|
||||
|
||||
import replybox, post, karaxutils, threadlist
|
||||
import replybox, post, karaxutils, threadlist, error
|
||||
|
||||
type
|
||||
OnEditPosted* = proc (id: int, content: string, subject: Option[string])
|
||||
|
||||
EditBox* = ref object
|
||||
box: ReplyBox
|
||||
post: Option[Post]
|
||||
post: Post
|
||||
rawContent: Option[kstring] ## The raw rst for a post (needs to be loaded)
|
||||
loading: bool
|
||||
status: HttpCode
|
||||
error: Option[PostError]
|
||||
onEditPosted: OnEditPosted
|
||||
onEditCancel: proc ()
|
||||
|
||||
proc newEditBox*(): EditBox =
|
||||
proc newEditBox*(onEditPosted: OnEditPosted, onEditCancel: proc ()): EditBox =
|
||||
EditBox(
|
||||
box: newReplyBox(nil)
|
||||
box: newReplyBox(nil),
|
||||
onEditPosted: onEditPosted,
|
||||
onEditCancel: onEditCancel,
|
||||
status: Http200
|
||||
)
|
||||
|
||||
proc onRawContent(httpStatus: int, response: kstring, state: EditBox) =
|
||||
|
|
@ -23,21 +32,60 @@ when defined(js):
|
|||
if state.status != Http200: return
|
||||
|
||||
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 =
|
||||
if state.post.isNone() or state.post.get().id != post.id:
|
||||
state.post = some(post)
|
||||
if state.rawContent.isNone() or state.post.id != post.id:
|
||||
state.post = post
|
||||
state.rawContent = none[kstring]()
|
||||
var params = @[("id", $post.id)]
|
||||
let uri = makeUri("post.rst", params)
|
||||
ajaxGet(uri, @[], (s: int, r: kstring) => onRawContent(s, r, state))
|
||||
|
||||
return buildHtml(tdiv(class="loading"))
|
||||
|
||||
state.box.setText(state.rawContent.get())
|
||||
result = buildHtml():
|
||||
tdiv(class="edit-box"):
|
||||
renderContent(
|
||||
state.box,
|
||||
none[Thread](),
|
||||
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 {
|
||||
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 {
|
||||
margin-bottom: $control-padding-y*2;
|
||||
}
|
||||
}
|
||||
|
||||
.edit-buttons {
|
||||
float: right;
|
||||
|
||||
> div {
|
||||
display: inline-block;
|
||||
}
|
||||
}
|
||||
|
||||
@import "syntax.scss";
|
||||
|
||||
// - Profile view
|
||||
|
|
|
|||
|
|
@ -30,6 +30,8 @@ when defined(js):
|
|||
editBox: EditBox
|
||||
|
||||
proc onReplyPosted(id: int)
|
||||
proc onEditPosted(id: int, content: string, subject: Option[string])
|
||||
proc onEditCancelled()
|
||||
proc newState(): State =
|
||||
State(
|
||||
list: none[PostList](),
|
||||
|
|
@ -37,7 +39,7 @@ when defined(js):
|
|||
status: Http200,
|
||||
replyingTo: none[Post](),
|
||||
replyBox: newReplyBox(onReplyPosted),
|
||||
editBox: newEditBox()
|
||||
editBox: newEditBox(onEditPosted, onEditCancelled)
|
||||
)
|
||||
|
||||
var
|
||||
|
|
@ -109,17 +111,17 @@ when defined(js):
|
|||
## Executed when a reply has been successfully posted.
|
||||
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.
|
||||
state.editing = none[Post]()
|
||||
let list = state.list.get()
|
||||
for i in 0 ..< list.posts.len:
|
||||
if list.posts[i].id == id:
|
||||
list.posts[i].info.content = content
|
||||
break
|
||||
|
||||
proc onEditConfirm(e: Event, n: VNode, p: Post) =
|
||||
discard
|
||||
|
||||
proc onReplyClick(e: Event, n: VNode, p: Option[Post]) =
|
||||
state.replyingTo = p
|
||||
state.replyBox.show()
|
||||
|
|
@ -151,48 +153,37 @@ when defined(js):
|
|||
loggedIn and currentUser.get().name == post.author.name
|
||||
|
||||
if state.editing.isSome() and state.editing.get() == post:
|
||||
result = buildHtml():
|
||||
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")
|
||||
return buildHtml(tdiv())
|
||||
|
||||
if loggedIn:
|
||||
tdiv(class="flag-button"):
|
||||
button(class="btn"):
|
||||
italic(class="far fa-flag")
|
||||
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:
|
||||
tdiv(class="reply-button"):
|
||||
button(class="btn", onClick=(e: Event, n: VNode) =>
|
||||
onReplyClick(e, n, some(post))):
|
||||
italic(class="fas fa-reply")
|
||||
text " Reply"
|
||||
tdiv(class="flag-button"):
|
||||
button(class="btn"):
|
||||
italic(class="far fa-flag")
|
||||
|
||||
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 =
|
||||
let postCopy = post # TODO: Another workaround here, closure capture :(
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ type
|
|||
Moderated ## new member: posts manually reviewed before everybody
|
||||
## can see them
|
||||
User ## Ordinary user
|
||||
Moderator ## Moderator: can ban/moderate users
|
||||
Moderator ## Moderator: can change a user's rank
|
||||
Admin ## Admin: can do everything
|
||||
|
||||
User* = object
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue