Finalises implementation of post editing.

This commit is contained in:
Dominik Picheta 2018-05-16 22:41:36 +01:00
commit fc7dabddda
5 changed files with 174 additions and 63 deletions

View file

@ -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():

View file

@ -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"

View file

@ -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

View file

@ -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 :(

View file

@ -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