Implements delete button fully in frontend and backend for posts and thread.

This commit is contained in:
Dominik Picheta 2018-05-18 19:54:33 +01:00
commit 41a6790fe8
3 changed files with 224 additions and 15 deletions

View file

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

View file

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