Implements delete button fully in frontend and backend for posts and thread.
This commit is contained in:
parent
dd9be8f639
commit
41a6790fe8
3 changed files with 224 additions and 15 deletions
76
forum.nim
76
forum.nim
|
|
@ -953,6 +953,20 @@ proc selectLikes(postId: int): seq[User] =
|
||||||
for row in getAllRows(db, likeQuery, $postId):
|
for row in getAllRows(db, likeQuery, $postId):
|
||||||
result.add(selectUser(row))
|
result.add(selectUser(row))
|
||||||
|
|
||||||
|
proc selectThreadAuthor(threadId: int): User =
|
||||||
|
const authorQuery =
|
||||||
|
sql"""
|
||||||
|
select name, email, strftime('%s', lastOnline), status
|
||||||
|
from person where id in (
|
||||||
|
select author from post
|
||||||
|
where thread = ?
|
||||||
|
order by id
|
||||||
|
limit 1
|
||||||
|
)
|
||||||
|
"""
|
||||||
|
|
||||||
|
return selectUser(getRow(db, authorQuery, threadId))
|
||||||
|
|
||||||
proc selectThread(threadRow: seq[string]): Thread =
|
proc selectThread(threadRow: seq[string]): Thread =
|
||||||
const postsQuery =
|
const postsQuery =
|
||||||
sql"""select count(*), strftime('%s', creation) from post
|
sql"""select count(*), strftime('%s', creation) from post
|
||||||
|
|
@ -964,16 +978,6 @@ proc selectThread(threadRow: seq[string]): Thread =
|
||||||
from person u, post p where p.author = u.id and p.thread = ?
|
from person u, post p where p.author = u.id and p.thread = ?
|
||||||
group by name order by count(*) desc limit 5;
|
group by name order by count(*) desc limit 5;
|
||||||
"""
|
"""
|
||||||
const authorQuery =
|
|
||||||
sql"""
|
|
||||||
select name, email, strftime('%s', lastOnline), status
|
|
||||||
from person where id in (
|
|
||||||
select author from post
|
|
||||||
where thread = ?
|
|
||||||
order by id
|
|
||||||
limit 1
|
|
||||||
)
|
|
||||||
"""
|
|
||||||
|
|
||||||
let posts = getRow(db, postsQuery, threadRow[0])
|
let posts = getRow(db, postsQuery, threadRow[0])
|
||||||
|
|
||||||
|
|
@ -1000,7 +1004,7 @@ proc selectThread(threadRow: seq[string]): Thread =
|
||||||
thread.users.add(selectUser(user))
|
thread.users.add(selectUser(user))
|
||||||
|
|
||||||
# Grab the author.
|
# Grab the author.
|
||||||
thread.author = selectUser(getRow(db, authorQuery, thread.id))
|
thread.author = selectThreadAuthor(thread.id)
|
||||||
|
|
||||||
return thread
|
return thread
|
||||||
|
|
||||||
|
|
@ -1220,6 +1224,29 @@ proc executeUnlike(c: TForumData, postId: int) =
|
||||||
# Delete the like.
|
# Delete the like.
|
||||||
exec(db, crud(crDelete, "like"), likeId)
|
exec(db, crud(crDelete, "like"), likeId)
|
||||||
|
|
||||||
|
proc executeDeletePost(c: TForumData, postId: int) =
|
||||||
|
# Verify that this post belongs to the user.
|
||||||
|
const postQuery = sql"""
|
||||||
|
select p.id from post p
|
||||||
|
where p.author = ? and p.id = ?
|
||||||
|
"""
|
||||||
|
let id = getValue(db, postQuery, postId, c.username)
|
||||||
|
|
||||||
|
if id.len == 0 and c.rank < Admin:
|
||||||
|
raise newForumError("You cannot delete this post")
|
||||||
|
|
||||||
|
# Set the `isDeleted` flag.
|
||||||
|
exec(db, crud(crUpdate, "post", "isDeleted"), "1", postId)
|
||||||
|
|
||||||
|
proc executeDeleteThread(c: TForumData, threadId: int) =
|
||||||
|
# Verify that this thread belongs to the user.
|
||||||
|
let author = selectThreadAuthor(threadId)
|
||||||
|
if author.name != c.username and c.rank < Admin:
|
||||||
|
raise newForumError("You cannot delete this thread")
|
||||||
|
|
||||||
|
# Set the `isDeleted` flag.
|
||||||
|
exec(db, crud(crUpdate, "thread", "isDeleted"), "1", threadId)
|
||||||
|
|
||||||
initialise()
|
initialise()
|
||||||
|
|
||||||
routes:
|
routes:
|
||||||
|
|
@ -1645,6 +1672,33 @@ routes:
|
||||||
except ForumError as exc:
|
except ForumError as exc:
|
||||||
resp Http400, $(%exc.data), "application/json"
|
resp Http400, $(%exc.data), "application/json"
|
||||||
|
|
||||||
|
post re"/delete(Post|Thread)":
|
||||||
|
createTFD()
|
||||||
|
if not c.loggedIn():
|
||||||
|
let err = PostError(
|
||||||
|
errorFields: @[],
|
||||||
|
message: "Not logged in."
|
||||||
|
)
|
||||||
|
resp Http401, $(%err), "application/json"
|
||||||
|
|
||||||
|
let formData = request.formData
|
||||||
|
cond "id" in formData
|
||||||
|
|
||||||
|
let id = getInt(formData["id"].body, -1)
|
||||||
|
cond id != -1
|
||||||
|
|
||||||
|
try:
|
||||||
|
case request.path
|
||||||
|
of "/deletePost":
|
||||||
|
executeDeletePost(c, id)
|
||||||
|
of "/deleteThread":
|
||||||
|
executeDeleteThread(c, id)
|
||||||
|
else:
|
||||||
|
assert false
|
||||||
|
resp Http200, "{}", "application/json"
|
||||||
|
except ForumError as exc:
|
||||||
|
resp Http400, $(%exc.data), "application/json"
|
||||||
|
|
||||||
get "/t/@id":
|
get "/t/@id":
|
||||||
cond "id" in request.params
|
cond "id" in request.params
|
||||||
|
|
||||||
|
|
|
||||||
132
frontend/delete.nim
Normal file
132
frontend/delete.nim
Normal file
|
|
@ -0,0 +1,132 @@
|
||||||
|
when defined(js):
|
||||||
|
import sugar, httpcore, options, json
|
||||||
|
import dom except Event
|
||||||
|
|
||||||
|
include karax/prelude
|
||||||
|
import karax / [kajax, kdom]
|
||||||
|
|
||||||
|
import error, post, threadlist, user
|
||||||
|
import karaxutils
|
||||||
|
|
||||||
|
type
|
||||||
|
DeleteKind* = enum
|
||||||
|
DeleteUser, DeletePost, DeleteThread
|
||||||
|
|
||||||
|
DeleteModal* = ref object
|
||||||
|
shown: bool
|
||||||
|
loading: bool
|
||||||
|
onDeletePost: proc (post: Post)
|
||||||
|
onDeleteThread: proc (thread: Thread)
|
||||||
|
onDeleteUser: proc (user: User)
|
||||||
|
error: Option[PostError]
|
||||||
|
case kind: DeleteKind
|
||||||
|
of DeleteUser:
|
||||||
|
user: User
|
||||||
|
of DeletePost:
|
||||||
|
post: Post
|
||||||
|
of DeleteThread:
|
||||||
|
thread: Thread
|
||||||
|
|
||||||
|
proc onDeletePost(httpStatus: int, response: kstring, state: DeleteModal) =
|
||||||
|
postFinished:
|
||||||
|
state.shown = false
|
||||||
|
case state.kind
|
||||||
|
of DeleteUser:
|
||||||
|
state.onDeleteUser(state.user)
|
||||||
|
of DeletePost:
|
||||||
|
state.onDeletePost(state.post)
|
||||||
|
of DeleteThread:
|
||||||
|
state.onDeleteThread(state.thread)
|
||||||
|
|
||||||
|
proc onDelete(ev: Event, n: VNode, state: DeleteModal) =
|
||||||
|
state.loading = true
|
||||||
|
state.error = none[PostError]()
|
||||||
|
|
||||||
|
let uri =
|
||||||
|
case state.kind
|
||||||
|
of DeleteUser:
|
||||||
|
makeUri("/deleteUser")
|
||||||
|
of DeleteThread:
|
||||||
|
makeUri("/deleteThread")
|
||||||
|
of DeletePost:
|
||||||
|
makeUri("/deletePost")
|
||||||
|
# TODO: This is a hack, karax should support this.
|
||||||
|
let formData = newFormData()
|
||||||
|
case state.kind
|
||||||
|
of DeleteUser:
|
||||||
|
formData.append("username", state.user.name)
|
||||||
|
of DeletePost:
|
||||||
|
formData.append("id", $state.post.id)
|
||||||
|
of DeleteThread:
|
||||||
|
formData.append("id", $state.thread.id)
|
||||||
|
ajaxPost(uri, @[], cast[cstring](formData),
|
||||||
|
(s: int, r: kstring) => onDeletePost(s, r, state))
|
||||||
|
|
||||||
|
proc onClose(ev: Event, n: VNode, state: DeleteModal) =
|
||||||
|
state.shown = false
|
||||||
|
ev.preventDefault()
|
||||||
|
|
||||||
|
proc newDeleteModal*(
|
||||||
|
onDeletePost: proc (post: Post),
|
||||||
|
onDeleteThread: proc (thread: Thread),
|
||||||
|
onDeleteUser: proc (user: User),
|
||||||
|
): DeleteModal =
|
||||||
|
DeleteModal(
|
||||||
|
shown: false,
|
||||||
|
onDeletePost: onDeletePost,
|
||||||
|
onDeleteThread: onDeleteThread,
|
||||||
|
onDeleteUser: onDeleteUser,
|
||||||
|
)
|
||||||
|
|
||||||
|
proc show*(state: DeleteModal, thing: User | Post | Thread) =
|
||||||
|
state.shown = true
|
||||||
|
state.error = none[PostError]()
|
||||||
|
when thing is User:
|
||||||
|
state.kind = DeleteUser
|
||||||
|
state.user = thing
|
||||||
|
when thing is Post:
|
||||||
|
state.kind = DeletePost
|
||||||
|
state.post = thing
|
||||||
|
when thing is Thread:
|
||||||
|
state.kind = DeleteThread
|
||||||
|
state.thread = thing
|
||||||
|
|
||||||
|
proc render*(state: DeleteModal): VNode =
|
||||||
|
result = buildHtml():
|
||||||
|
tdiv(class=class({"active": state.shown}, "modal modal-sm"),
|
||||||
|
id="login-modal"):
|
||||||
|
a(href="", class="modal-overlay", "aria-label"="close",
|
||||||
|
onClick=(ev: Event, n: VNode) => onClose(ev, n, state))
|
||||||
|
tdiv(class="modal-container"):
|
||||||
|
tdiv(class="modal-header"):
|
||||||
|
a(href="", class="btn btn-clear float-right",
|
||||||
|
"aria-label"="close",
|
||||||
|
onClick=(ev: Event, n: VNode) => onClose(ev, n, state))
|
||||||
|
tdiv(class="modal-title h5"):
|
||||||
|
text "Delete"
|
||||||
|
tdiv(class="modal-body"):
|
||||||
|
tdiv(class="content"):
|
||||||
|
p():
|
||||||
|
text "Are you sure you want to delete this "
|
||||||
|
case state.kind
|
||||||
|
of DeleteUser:
|
||||||
|
text "user account?"
|
||||||
|
of DeleteThread:
|
||||||
|
text "thread?"
|
||||||
|
of DeletePost:
|
||||||
|
text "post?"
|
||||||
|
tdiv(class="modal-footer"):
|
||||||
|
if state.error.isSome():
|
||||||
|
p(class="text-error"):
|
||||||
|
text state.error.get().message
|
||||||
|
|
||||||
|
button(class=class(
|
||||||
|
{"loading": state.loading},
|
||||||
|
"btn btn-primary"
|
||||||
|
),
|
||||||
|
onClick=(ev: Event, n: VNode) => onDelete(ev, n, state)):
|
||||||
|
italic(class="fas fa-trash-alt")
|
||||||
|
text " Delete"
|
||||||
|
button(class="btn",
|
||||||
|
onClick=(ev: Event, n: VNode) => (state.shown = false)):
|
||||||
|
text "Cancel"
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
|
|
||||||
import options, json, times, httpcore, strformat, sugar, math, strutils
|
import options, json, times, httpcore, strformat, sugar, math, strutils
|
||||||
|
import sequtils
|
||||||
|
|
||||||
import threadlist, category, post, user
|
import threadlist, category, post, user
|
||||||
type
|
type
|
||||||
|
|
@ -17,7 +18,7 @@ when defined(js):
|
||||||
include karax/prelude
|
include karax/prelude
|
||||||
import karax / [vstyles, kajax, kdom]
|
import karax / [vstyles, kajax, kdom]
|
||||||
|
|
||||||
import karaxutils, error, replybox, editbox, postbutton
|
import karaxutils, error, replybox, editbox, postbutton, delete
|
||||||
|
|
||||||
type
|
type
|
||||||
State = ref object
|
State = ref object
|
||||||
|
|
@ -29,10 +30,13 @@ when defined(js):
|
||||||
editing: Option[Post] ## If in edit mode, this contains the post.
|
editing: Option[Post] ## If in edit mode, this contains the post.
|
||||||
editBox: EditBox
|
editBox: EditBox
|
||||||
likeButton: LikeButton
|
likeButton: LikeButton
|
||||||
|
deleteModal: DeleteModal
|
||||||
|
|
||||||
proc onReplyPosted(id: int)
|
proc onReplyPosted(id: int)
|
||||||
proc onEditPosted(id: int, content: string, subject: Option[string])
|
proc onEditPosted(id: int, content: string, subject: Option[string])
|
||||||
proc onEditCancelled()
|
proc onEditCancelled()
|
||||||
|
proc onDeletePost(post: Post)
|
||||||
|
proc onDeleteThread(thread: Thread)
|
||||||
proc newState(): State =
|
proc newState(): State =
|
||||||
State(
|
State(
|
||||||
list: none[PostList](),
|
list: none[PostList](),
|
||||||
|
|
@ -41,7 +45,8 @@ when defined(js):
|
||||||
replyingTo: none[Post](),
|
replyingTo: none[Post](),
|
||||||
replyBox: newReplyBox(onReplyPosted),
|
replyBox: newReplyBox(onReplyPosted),
|
||||||
editBox: newEditBox(onEditPosted, onEditCancelled),
|
editBox: newEditBox(onEditPosted, onEditCancelled),
|
||||||
likeButton: newLikeButton()
|
likeButton: newLikeButton(),
|
||||||
|
deleteModal: newDeleteModal(onDeletePost, onDeleteThread, nil)
|
||||||
)
|
)
|
||||||
|
|
||||||
var
|
var
|
||||||
|
|
@ -137,6 +142,21 @@ when defined(js):
|
||||||
# TODO: Ensure the edit box is as big as its content. Auto resize the
|
# TODO: Ensure the edit box is as big as its content. Auto resize the
|
||||||
# text area.
|
# text area.
|
||||||
|
|
||||||
|
proc onDeletePost(post: Post) =
|
||||||
|
state.list.get().posts.keepIf(
|
||||||
|
x => x.id != post.id
|
||||||
|
)
|
||||||
|
|
||||||
|
proc onDeleteThread(thread: Thread) =
|
||||||
|
window.location.href = makeUri("/")
|
||||||
|
|
||||||
|
proc onDeleteClick(e: Event, n: VNode, p: Post) =
|
||||||
|
let list = state.list.get()
|
||||||
|
if list.posts[0].id == p.id:
|
||||||
|
state.deleteModal.show(list.thread)
|
||||||
|
else:
|
||||||
|
state.deleteModal.show(p)
|
||||||
|
|
||||||
proc onLoadMore(ev: Event, n: VNode, start: int, post: Post) =
|
proc onLoadMore(ev: Event, n: VNode, start: int, post: Post) =
|
||||||
loadMore(start, post.moreBefore) # TODO: Don't load all!
|
loadMore(start, post.moreBefore) # TODO: Don't load all!
|
||||||
|
|
||||||
|
|
@ -173,7 +193,8 @@ when defined(js):
|
||||||
onEditClick(e, n, post)):
|
onEditClick(e, n, post)):
|
||||||
button(class="btn"):
|
button(class="btn"):
|
||||||
italic(class="far fa-edit")
|
italic(class="far fa-edit")
|
||||||
tdiv(class="delete-button"):
|
tdiv(class="delete-button",
|
||||||
|
onClick=(e: Event, n: VNode) => onDeleteClick(e, n, post)):
|
||||||
button(class="btn"):
|
button(class="btn"):
|
||||||
italic(class="far fa-trash-alt")
|
italic(class="far fa-trash-alt")
|
||||||
|
|
||||||
|
|
@ -338,4 +359,6 @@ when defined(js):
|
||||||
italic(class="fas fa-reply")
|
italic(class="fas fa-reply")
|
||||||
text " Reply"
|
text " Reply"
|
||||||
|
|
||||||
render(state.replyBox, list.thread, state.replyingTo, false)
|
render(state.replyBox, list.thread, state.replyingTo, false)
|
||||||
|
|
||||||
|
render(state.deleteModal)
|
||||||
Loading…
Add table
Add a link
Reference in a new issue