Compare commits
7 commits
master
...
github_act
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e896ddd65a | ||
|
|
11ee8b16e7 |
||
|
|
17e5cbaa29 |
||
|
|
a8e577e2f3 |
||
|
|
91eae347d5 |
||
|
|
7da8f58210 |
||
|
|
5e92ff28ac |
11 changed files with 25 additions and 225 deletions
|
|
@ -276,7 +276,7 @@ template createTFD() =
|
|||
new(c)
|
||||
init(c)
|
||||
c.req = request
|
||||
if cookies(request).len > 0:
|
||||
if request.cookies.len > 0:
|
||||
checkLoggedIn(c)
|
||||
|
||||
#[ DB functions. TODO: Move to another module? ]#
|
||||
|
|
@ -400,10 +400,10 @@ proc selectThread(threadRow: seq[string], author: User): Thread =
|
|||
id: threadRow[0].parseInt,
|
||||
topic: threadRow[1],
|
||||
category: Category(
|
||||
id: threadRow[6].parseInt,
|
||||
name: threadRow[7],
|
||||
description: threadRow[8],
|
||||
color: threadRow[9]
|
||||
id: threadRow[5].parseInt,
|
||||
name: threadRow[6],
|
||||
description: threadRow[7],
|
||||
color: threadRow[8]
|
||||
),
|
||||
users: @[],
|
||||
replies: posts[0].parseInt-1,
|
||||
|
|
@ -412,7 +412,6 @@ proc selectThread(threadRow: seq[string], author: User): Thread =
|
|||
creation: posts[1].parseInt,
|
||||
isLocked: threadRow[4] == "1",
|
||||
isSolved: false, # TODO: Add a field to `post` to identify the solution.
|
||||
isPinned: threadRow[5] == "1"
|
||||
)
|
||||
|
||||
# Gather the users list.
|
||||
|
|
@ -710,13 +709,6 @@ proc executeLockState(c: TForumData, threadId: int, locked: bool) =
|
|||
# Save the like.
|
||||
exec(db, crud(crUpdate, "thread", "isLocked"), locked.int, threadId)
|
||||
|
||||
proc executePinState(c: TForumData, threadId: int, pinned: bool) =
|
||||
if c.rank < Moderator:
|
||||
raise newForumError("You do not have permission to pin this thread.")
|
||||
|
||||
# (Un)pin this thread
|
||||
exec(db, crud(crUpdate, "thread", "isPinned"), pinned.int, threadId)
|
||||
|
||||
proc executeDeletePost(c: TForumData, postId: int) =
|
||||
# Verify that this post belongs to the user.
|
||||
const postQuery = sql"""
|
||||
|
|
@ -841,7 +833,7 @@ routes:
|
|||
categoryArgs.insert($categoryId, 0)
|
||||
|
||||
const threadsQuery =
|
||||
"""select t.id, t.name, views, strftime('%s', modified), isLocked, isPinned,
|
||||
"""select t.id, t.name, views, strftime('%s', modified), isLocked,
|
||||
c.id, c.name, c.description, c.color,
|
||||
u.id, u.name, u.email, strftime('%s', u.lastOnline),
|
||||
strftime('%s', u.previousVisitAt), u.status, u.isDeleted
|
||||
|
|
@ -854,14 +846,14 @@ routes:
|
|||
order by p.author
|
||||
limit 1
|
||||
)
|
||||
order by isPinned desc, modified desc limit ?, ?;"""
|
||||
order by modified desc limit ?, ?;"""
|
||||
|
||||
let thrCount = getValue(db, countQuery, countArgs).parseInt()
|
||||
let moreCount = max(0, thrCount - (start + count))
|
||||
|
||||
var list = ThreadList(threads: @[], moreCount: moreCount)
|
||||
for data in getAllRows(db, sql(threadsQuery % categorySection), categoryArgs):
|
||||
let thread = selectThread(data[0 .. 9], selectUser(data[10 .. ^1]))
|
||||
let thread = selectThread(data[0 .. 8], selectUser(data[9 .. ^1]))
|
||||
list.threads.add(thread)
|
||||
|
||||
resp $(%list), "application/json"
|
||||
|
|
@ -876,17 +868,12 @@ routes:
|
|||
count = 10
|
||||
|
||||
const threadsQuery =
|
||||
sql"""select t.id, t.name, views, strftime('%s', modified), isLocked, isPinned,
|
||||
sql"""select t.id, t.name, views, strftime('%s', modified), isLocked,
|
||||
c.id, c.name, c.description, c.color
|
||||
from thread t, category c
|
||||
where t.id = ? and isDeleted = 0 and category = c.id;"""
|
||||
|
||||
let threadRow = getRow(db, threadsQuery, id)
|
||||
if threadRow[0].len == 0:
|
||||
let err = PostError(
|
||||
message: "Specified thread does not exist"
|
||||
)
|
||||
resp Http404, $(%err), "application/json"
|
||||
let thread = selectThread(threadRow, selectThreadAuthor(id))
|
||||
|
||||
let postsQuery =
|
||||
|
|
@ -932,14 +919,9 @@ routes:
|
|||
|
||||
get "/specific_posts.json":
|
||||
createTFD()
|
||||
var ids: JsonNode
|
||||
try:
|
||||
var
|
||||
ids = parseJson(@"ids")
|
||||
except JsonParsingError:
|
||||
let err = PostError(
|
||||
message: "Invalid JSON in the `ids` parameter"
|
||||
)
|
||||
resp Http400, $(%err), "application/json"
|
||||
|
||||
cond ids.kind == JArray
|
||||
let intIDs = ids.elems.map(x => x.getInt())
|
||||
let postsQuery = sql("""
|
||||
|
|
@ -1357,33 +1339,6 @@ routes:
|
|||
except ForumError as exc:
|
||||
resp Http400, $(%exc.data), "application/json"
|
||||
|
||||
post re"/(pin|unpin)":
|
||||
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 threadId = getInt(formData["id"].body, -1)
|
||||
cond threadId != -1
|
||||
|
||||
try:
|
||||
case request.path
|
||||
of "/pin":
|
||||
executePinState(c, threadId, true)
|
||||
of "/unpin":
|
||||
executePinState(c, threadId, false)
|
||||
else:
|
||||
assert false
|
||||
resp Http200, "{}", "application/json"
|
||||
except ForumError as exc:
|
||||
resp Http400, $(%exc.data), "application/json"
|
||||
|
||||
post re"/delete(Post|Thread)":
|
||||
createTFD()
|
||||
if not c.loggedIn():
|
||||
|
|
|
|||
|
|
@ -96,8 +96,8 @@ when defined(js):
|
|||
section(class="navbar-section"):
|
||||
tdiv(class="input-group input-inline"):
|
||||
input(class="search-input input-sm",
|
||||
`type`="search", placeholder="Search",
|
||||
id="search-box", required="required",
|
||||
`type`="text", placeholder="search",
|
||||
id="search-box",
|
||||
onKeyDown=onKeyDown)
|
||||
if state.loading:
|
||||
tdiv(class="loading")
|
||||
|
|
|
|||
|
|
@ -64,4 +64,4 @@ when defined(js):
|
|||
renderPostUrl(thread.id, post.id)
|
||||
|
||||
proc renderPostUrl*(link: PostLink): string =
|
||||
renderPostUrl(link.threadId, link.postId)
|
||||
renderPostUrl(link.threadId, link.postId)
|
||||
|
|
@ -190,7 +190,7 @@ when defined(js):
|
|||
else: ""
|
||||
|
||||
result = buildHtml():
|
||||
button(class="btn btn-secondary", id="lock-btn",
|
||||
button(class="btn btn-secondary",
|
||||
onClick=(e: Event, n: VNode) =>
|
||||
onLockClick(e, n, state, thread),
|
||||
"data-tooltip"=tooltip,
|
||||
|
|
@ -201,61 +201,4 @@ when defined(js):
|
|||
text " Unlock Thread"
|
||||
else:
|
||||
italic(class="fas fa-lock")
|
||||
text " Lock Thread"
|
||||
|
||||
type
|
||||
PinButton* = ref object
|
||||
error: Option[PostError]
|
||||
loading: bool
|
||||
|
||||
proc newPinButton*(): PinButton =
|
||||
PinButton()
|
||||
|
||||
proc onPost(httpStatus: int, response: kstring, state: PinButton,
|
||||
thread: var Thread) =
|
||||
postFinished:
|
||||
thread.isPinned = not thread.isPinned
|
||||
|
||||
proc onPinClick(ev: Event, n: VNode, state: PinButton, thread: var Thread) =
|
||||
if state.loading: return
|
||||
|
||||
state.loading = true
|
||||
state.error = none[PostError]()
|
||||
|
||||
# Same as LockButton so the following is still a hack and karax should support this.
|
||||
var formData = newFormData()
|
||||
formData.append("id", $thread.id)
|
||||
let uri =
|
||||
if thread.isPinned:
|
||||
makeUri("/unpin")
|
||||
else:
|
||||
makeUri("/pin")
|
||||
ajaxPost(uri, @[], formData.to(cstring),
|
||||
(s: int, r: kstring) => onPost(s, r, state, thread))
|
||||
|
||||
ev.preventDefault()
|
||||
|
||||
proc render*(state: PinButton, thread: var Thread,
|
||||
currentUser: Option[User]): VNode =
|
||||
if currentUser.isNone() or
|
||||
currentUser.get().rank < Moderator:
|
||||
return buildHtml(tdiv())
|
||||
|
||||
let tooltip =
|
||||
if state.error.isSome(): state.error.get().message
|
||||
else: ""
|
||||
|
||||
result = buildHtml():
|
||||
button(class="btn btn-secondary", id="pin-btn",
|
||||
onClick=(e: Event, n: VNode) =>
|
||||
onPinClick(e, n, state, thread),
|
||||
"data-tooltip"=tooltip,
|
||||
onmouseleave=(e: Event, n: VNode) =>
|
||||
(state.error = none[PostError]())):
|
||||
if thread.isPinned:
|
||||
italic(class="fas fa-thumbtack")
|
||||
text " Unpin Thread"
|
||||
else:
|
||||
italic(class="fas fa-thumbtack")
|
||||
text " Pin Thread"
|
||||
|
||||
text " Lock Thread"
|
||||
|
|
@ -36,7 +36,6 @@ when defined(js):
|
|||
likeButton: LikeButton
|
||||
deleteModal: DeleteModal
|
||||
lockButton: LockButton
|
||||
pinButton: PinButton
|
||||
categoryPicker: CategoryPicker
|
||||
|
||||
proc onReplyPosted(id: int)
|
||||
|
|
@ -57,7 +56,6 @@ when defined(js):
|
|||
likeButton: newLikeButton(),
|
||||
deleteModal: newDeleteModal(onDeletePost, onDeleteThread, nil),
|
||||
lockButton: newLockButton(),
|
||||
pinButton: newPinButton(),
|
||||
categoryPicker: newCategoryPicker(onCategoryChanged)
|
||||
)
|
||||
|
||||
|
|
@ -413,7 +411,6 @@ when defined(js):
|
|||
text " Reply"
|
||||
|
||||
render(state.lockButton, list.thread, currentUser)
|
||||
render(state.pinButton, list.thread, currentUser)
|
||||
|
||||
render(state.replyBox, list.thread, state.replyingTo, false)
|
||||
|
||||
|
|
|
|||
|
|
@ -15,7 +15,6 @@ type
|
|||
creation*: int64 ## Unix timestamp
|
||||
isLocked*: bool
|
||||
isSolved*: bool
|
||||
isPinned*: bool
|
||||
|
||||
ThreadList* = ref object
|
||||
threads*: seq[Thread]
|
||||
|
|
@ -97,18 +96,15 @@ when defined(js):
|
|||
else:
|
||||
return $duration.inSeconds & "s"
|
||||
|
||||
proc genThread(pos: int, thread: Thread, isNew: bool, noBorder: bool, displayCategory=true): VNode =
|
||||
proc genThread(thread: Thread, isNew: bool, noBorder: bool, displayCategory=true): VNode =
|
||||
let isOld = (getTime() - thread.creation.fromUnix).inWeeks > 2
|
||||
let isBanned = thread.author.rank.isBanned()
|
||||
result = buildHtml():
|
||||
tr(class=class({"no-border": noBorder, "banned": isBanned, "pinned": thread.isPinned, "thread-" & $pos: true})):
|
||||
tr(class=class({"no-border": noBorder, "banned": isBanned})):
|
||||
td(class="thread-title"):
|
||||
if thread.isLocked:
|
||||
italic(class="fas fa-lock fa-xs",
|
||||
title="Thread cannot be replied to")
|
||||
if thread.isPinned:
|
||||
italic(class="fas fa-thumbtack fa-xs",
|
||||
title="Pinned post")
|
||||
if isBanned:
|
||||
italic(class="fas fa-ban fa-xs",
|
||||
title="Thread author is banned")
|
||||
|
|
@ -227,7 +223,7 @@ when defined(js):
|
|||
|
||||
let isLastThread = i+1 == list.threads.len
|
||||
let (isLastUnseen, isNew) = getInfo(list.threads, i, currentUser)
|
||||
genThread(i+1, thread, isNew,
|
||||
genThread(thread, isNew,
|
||||
noBorder=isLastUnseen or isLastThread,
|
||||
displayCategory=displayCategory)
|
||||
if isLastUnseen and (not isLastThread):
|
||||
|
|
|
|||
|
|
@ -7,7 +7,6 @@ SELECT
|
|||
post_id,
|
||||
post_content,
|
||||
cdate,
|
||||
person.id,
|
||||
person.name AS author,
|
||||
person.email AS email,
|
||||
strftime('%s', person.lastOnline) AS lastOnline,
|
||||
|
|
|
|||
|
|
@ -81,7 +81,6 @@ proc initialiseDb(admin: tuple[username, password, email: string],
|
|||
isLocked boolean not null default 0,
|
||||
solution integer,
|
||||
isDeleted boolean not null default 0,
|
||||
isPinned boolean not null default 0,
|
||||
|
||||
foreign key (category) references category(id),
|
||||
foreign key (solution) references post(id)
|
||||
|
|
|
|||
|
|
@ -30,8 +30,7 @@ proc elementIsSome(element: Option[Element]): bool =
|
|||
proc elementIsNone(element: Option[Element]): bool =
|
||||
return element.isNone
|
||||
|
||||
proc waitForElement*(session: Session, selector: string, strategy=CssSelector, timeout=20000, pollTime=50,
|
||||
waitCondition: proc(element: Option[Element]): bool = elementIsSome): Option[Element]
|
||||
proc waitForElement*(session: Session, selector: string, strategy=CssSelector, timeout=20000, pollTime=50, waitCondition=elementIsSome): Option[Element]
|
||||
|
||||
proc click*(session: Session, element: string, strategy=CssSelector) =
|
||||
let el = session.waitForElement(element, strategy)
|
||||
|
|
@ -72,14 +71,14 @@ proc setColor*(session: Session, element, color: string, strategy=CssSelector) =
|
|||
proc checkIsNone*(session: Session, element: string, strategy=CssSelector) =
|
||||
discard session.waitForElement(element, strategy, waitCondition=elementIsNone)
|
||||
|
||||
template checkText*(session: Session, element, expectedValue: string) =
|
||||
proc checkText*(session: Session, element, expectedValue: string) =
|
||||
let el = session.waitForElement(element)
|
||||
check el.get().getText() == expectedValue
|
||||
|
||||
proc waitForElement*(
|
||||
session: Session, selector: string, strategy=CssSelector,
|
||||
timeout=20000, pollTime=50,
|
||||
waitCondition: proc(element: Option[Element]): bool = elementIsSome
|
||||
waitCondition=elementIsSome
|
||||
): Option[Element] =
|
||||
var waitTime = 0
|
||||
|
||||
|
|
|
|||
|
|
@ -40,4 +40,4 @@ proc test*(session: Session, baseUrl: string) =
|
|||
register "TEst1", "test1", verify = false
|
||||
|
||||
ensureExists "#signup-form .has-error"
|
||||
navigate baseUrl
|
||||
navigate baseUrl
|
||||
|
|
@ -1,4 +1,5 @@
|
|||
import unittest, common
|
||||
|
||||
import webdriver
|
||||
|
||||
let
|
||||
|
|
@ -57,35 +58,10 @@ proc userTests(session: Session, baseUrl: string) =
|
|||
# Make sure the forum post is gone
|
||||
checkIsNone "To be deleted", LinkTextSelector
|
||||
|
||||
test "cannot (un)pin thread":
|
||||
with session:
|
||||
navigate(baseUrl)
|
||||
|
||||
click "#new-thread-btn"
|
||||
|
||||
sendKeys "#thread-title", "Unpinnable"
|
||||
sendKeys "#reply-textarea", "Cannot (un)pin as an user"
|
||||
|
||||
click "#create-thread-btn"
|
||||
|
||||
checkIsNone "#pin-btn"
|
||||
|
||||
test "cannot lock threads":
|
||||
with session:
|
||||
navigate(baseUrl)
|
||||
|
||||
click "#new-thread-btn"
|
||||
|
||||
sendKeys "#thread-title", "Locking"
|
||||
sendkeys "#reply-textarea", "Cannot lock as an user"
|
||||
|
||||
click "#create-thread-btn"
|
||||
|
||||
checkIsNone "#lock-btn"
|
||||
|
||||
session.logout()
|
||||
|
||||
proc anonymousTests(session: Session, baseUrl: string) =
|
||||
|
||||
suite "anonymous user tests":
|
||||
with session:
|
||||
navigate baseUrl
|
||||
|
|
@ -185,70 +161,6 @@ proc adminTests(session: Session, baseUrl: string) =
|
|||
# Make sure the forum post is gone
|
||||
checkIsNone adminTitleStr, LinkTextSelector
|
||||
|
||||
test "can pin a thread":
|
||||
with session:
|
||||
click "#new-thread-btn"
|
||||
sendKeys "#thread-title", "Pinned post"
|
||||
sendKeys "#reply-textarea", "A pinned post"
|
||||
click "#create-thread-btn"
|
||||
|
||||
navigate(baseUrl)
|
||||
click "#new-thread-btn"
|
||||
sendKeys "#thread-title", "Normal post"
|
||||
sendKeys "#reply-textarea", "A normal post"
|
||||
click "#create-thread-btn"
|
||||
|
||||
navigate(baseUrl)
|
||||
click "Pinned post", LinkTextSelector
|
||||
click "#pin-btn"
|
||||
checkText "#pin-btn", "Unpin Thread"
|
||||
|
||||
navigate(baseUrl)
|
||||
|
||||
# Make sure pin exists
|
||||
ensureExists "#threads-list .thread-1 .thread-title i"
|
||||
|
||||
checkText "#threads-list .thread-1 .thread-title a", "Pinned post"
|
||||
checkText "#threads-list .thread-2 .thread-title a", "Normal post"
|
||||
|
||||
test "can unpin a thread":
|
||||
with session:
|
||||
click "Pinned post", LinkTextSelector
|
||||
click "#pin-btn"
|
||||
checkText "#pin-btn", "Pin Thread"
|
||||
|
||||
navigate(baseUrl)
|
||||
|
||||
checkIsNone "#threads-list .thread-2 .thread-title i"
|
||||
|
||||
checkText "#threads-list .thread-1 .thread-title a", "Normal post"
|
||||
checkText "#threads-list .thread-2 .thread-title a", "Pinned post"
|
||||
|
||||
test "can lock a thread":
|
||||
with session:
|
||||
click "Locking", LinkTextSelector
|
||||
click "#lock-btn"
|
||||
|
||||
ensureExists "#thread-title i.fas.fa-lock.fa-xs"
|
||||
|
||||
test "locked thread appears on frontpage":
|
||||
with session:
|
||||
click "#new-thread-btn"
|
||||
sendKeys "#thread-title", "A new locked thread"
|
||||
sendKeys "#reply-textarea", "This thread should appear locked on the frontpage"
|
||||
click "#create-thread-btn"
|
||||
click "#lock-btn"
|
||||
|
||||
navigate(baseUrl)
|
||||
ensureExists "#threads-list .thread-1 .thread-title i.fas.fa-lock.fa-xs"
|
||||
|
||||
test "can unlock a thread":
|
||||
with session:
|
||||
click "Locking", LinkTextSelector
|
||||
click "#lock-btn"
|
||||
|
||||
checkIsNone "#thread-title i.fas.fa-lock.fa-xs"
|
||||
|
||||
session.logout()
|
||||
|
||||
proc test*(session: Session, baseUrl: string) =
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue