Compare commits
No commits in common. "master" and "fix-cannot-edit-category" have entirely different histories.
master
...
fix-cannot
15 changed files with 75 additions and 310 deletions
80
.github/workflows/main.yml
vendored
80
.github/workflows/main.yml
vendored
|
|
@ -1,80 +0,0 @@
|
||||||
# This is a basic workflow to help you get started with Actions
|
|
||||||
|
|
||||||
name: CI
|
|
||||||
|
|
||||||
# Controls when the action will run.
|
|
||||||
on:
|
|
||||||
# Triggers the workflow on push or pull request events but only for the master branch
|
|
||||||
push:
|
|
||||||
branches: [ master ]
|
|
||||||
pull_request:
|
|
||||||
branches: [ master ]
|
|
||||||
|
|
||||||
# Allows you to run this workflow manually from the Actions tab
|
|
||||||
workflow_dispatch:
|
|
||||||
|
|
||||||
# A workflow run is made up of one or more jobs that can run sequentially or in parallel
|
|
||||||
jobs:
|
|
||||||
test_stable:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
strategy:
|
|
||||||
matrix:
|
|
||||||
firefox: [ '73.0' ]
|
|
||||||
include:
|
|
||||||
- nim-version: 'stable'
|
|
||||||
cache-key: 'stable'
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v2
|
|
||||||
- name: Checkout submodules
|
|
||||||
run: git submodule update --init --recursive
|
|
||||||
|
|
||||||
- name: Setup firefox
|
|
||||||
uses: browser-actions/setup-firefox@latest
|
|
||||||
with:
|
|
||||||
firefox-version: ${{ matrix.firefox }}
|
|
||||||
|
|
||||||
- name: Get Date
|
|
||||||
id: get-date
|
|
||||||
run: echo "::set-output name=date::$(date "+%Y-%m-%d")"
|
|
||||||
shell: bash
|
|
||||||
|
|
||||||
- name: Cache choosenim
|
|
||||||
uses: actions/cache@v2
|
|
||||||
with:
|
|
||||||
path: ~/.choosenim
|
|
||||||
key: ${{ runner.os }}-choosenim-${{ matrix.cache-key }}
|
|
||||||
|
|
||||||
- name: Cache nimble
|
|
||||||
uses: actions/cache@v2
|
|
||||||
with:
|
|
||||||
path: ~/.nimble
|
|
||||||
key: ${{ runner.os }}-nimble-${{ hashFiles('*.nimble') }}
|
|
||||||
|
|
||||||
- uses: jiro4989/setup-nim-action@v1
|
|
||||||
with:
|
|
||||||
nim-version: "${{ matrix.nim-version }}"
|
|
||||||
|
|
||||||
- name: Install geckodriver
|
|
||||||
run: |
|
|
||||||
sudo apt-get -qq update
|
|
||||||
sudo apt-get install autoconf libtool libsass-dev
|
|
||||||
|
|
||||||
wget https://github.com/mozilla/geckodriver/releases/download/v0.29.1/geckodriver-v0.29.1-linux64.tar.gz
|
|
||||||
mkdir geckodriver
|
|
||||||
tar -xzf geckodriver-v0.29.1-linux64.tar.gz -C geckodriver
|
|
||||||
export PATH=$PATH:$PWD/geckodriver
|
|
||||||
|
|
||||||
- name: Install choosenim
|
|
||||||
run: |
|
|
||||||
export CHOOSENIM_CHOOSE_VERSION="stable"
|
|
||||||
curl https://nim-lang.org/choosenim/init.sh -sSf > init.sh
|
|
||||||
sh init.sh -y
|
|
||||||
export PATH=$HOME/.nimble/bin:$PATH
|
|
||||||
nimble refresh -y
|
|
||||||
|
|
||||||
- name: Run tests
|
|
||||||
run: |
|
|
||||||
export MOZ_HEADLESS=1
|
|
||||||
nimble -y install
|
|
||||||
nimble -y test
|
|
||||||
|
|
||||||
45
.travis.yml
Normal file
45
.travis.yml
Normal file
|
|
@ -0,0 +1,45 @@
|
||||||
|
os:
|
||||||
|
- linux
|
||||||
|
|
||||||
|
language: c
|
||||||
|
|
||||||
|
cache:
|
||||||
|
directories:
|
||||||
|
- "$HOME/.nimble"
|
||||||
|
- "$HOME/.choosenim"
|
||||||
|
|
||||||
|
addons:
|
||||||
|
firefox: "73.0"
|
||||||
|
|
||||||
|
before_install:
|
||||||
|
- sudo apt-get -qq update
|
||||||
|
- sudo apt-get install autoconf libtool
|
||||||
|
- git clone -b 3.5.4 https://github.com/sass/libsass.git
|
||||||
|
- cd libsass
|
||||||
|
- autoreconf --force --install
|
||||||
|
- |
|
||||||
|
./configure \
|
||||||
|
--disable-tests \
|
||||||
|
--disable-static \
|
||||||
|
--enable-shared \
|
||||||
|
--prefix=/usr
|
||||||
|
- sudo make -j5 install
|
||||||
|
- cd ..
|
||||||
|
|
||||||
|
- wget https://github.com/mozilla/geckodriver/releases/download/v0.26.0/geckodriver-v0.26.0-linux64.tar.gz
|
||||||
|
- mkdir geckodriver
|
||||||
|
- tar -xzf geckodriver-v0.26.0-linux64.tar.gz -C geckodriver
|
||||||
|
- export PATH=$PATH:$PWD/geckodriver
|
||||||
|
|
||||||
|
install:
|
||||||
|
- export CHOOSENIM_CHOOSE_VERSION="stable"
|
||||||
|
- |
|
||||||
|
curl https://nim-lang.org/choosenim/init.sh -sSf > init.sh
|
||||||
|
sh init.sh -y
|
||||||
|
- export PATH=$HOME/.nimble/bin:$PATH
|
||||||
|
- nimble refresh -y
|
||||||
|
|
||||||
|
script:
|
||||||
|
- export MOZ_HEADLESS=1
|
||||||
|
- nimble -y install
|
||||||
|
- nimble -y test
|
||||||
|
|
@ -301,7 +301,7 @@ $threads-meta-color: #545d70;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.thread-replies, .thread-time, .views-text, .popular-text, .centered-header {
|
.thread-replies, .thread-time, .views-text, .centered-header {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -276,7 +276,7 @@ template createTFD() =
|
||||||
new(c)
|
new(c)
|
||||||
init(c)
|
init(c)
|
||||||
c.req = request
|
c.req = request
|
||||||
if cookies(request).len > 0:
|
if request.cookies.len > 0:
|
||||||
checkLoggedIn(c)
|
checkLoggedIn(c)
|
||||||
|
|
||||||
#[ DB functions. TODO: Move to another module? ]#
|
#[ DB functions. TODO: Move to another module? ]#
|
||||||
|
|
@ -400,10 +400,10 @@ proc selectThread(threadRow: seq[string], author: User): Thread =
|
||||||
id: threadRow[0].parseInt,
|
id: threadRow[0].parseInt,
|
||||||
topic: threadRow[1],
|
topic: threadRow[1],
|
||||||
category: Category(
|
category: Category(
|
||||||
id: threadRow[6].parseInt,
|
id: threadRow[5].parseInt,
|
||||||
name: threadRow[7],
|
name: threadRow[6],
|
||||||
description: threadRow[8],
|
description: threadRow[7],
|
||||||
color: threadRow[9]
|
color: threadRow[8]
|
||||||
),
|
),
|
||||||
users: @[],
|
users: @[],
|
||||||
replies: posts[0].parseInt-1,
|
replies: posts[0].parseInt-1,
|
||||||
|
|
@ -412,7 +412,6 @@ proc selectThread(threadRow: seq[string], author: User): Thread =
|
||||||
creation: posts[1].parseInt,
|
creation: posts[1].parseInt,
|
||||||
isLocked: threadRow[4] == "1",
|
isLocked: threadRow[4] == "1",
|
||||||
isSolved: false, # TODO: Add a field to `post` to identify the solution.
|
isSolved: false, # TODO: Add a field to `post` to identify the solution.
|
||||||
isPinned: threadRow[5] == "1"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
# Gather the users list.
|
# Gather the users list.
|
||||||
|
|
@ -500,10 +499,10 @@ proc updatePost(c: TForumData, postId: int, content: string,
|
||||||
|
|
||||||
# Verify that the current user has permissions to edit the specified post.
|
# Verify that the current user has permissions to edit the specified post.
|
||||||
let creation = fromUnix(postRow[1].parseInt)
|
let creation = fromUnix(postRow[1].parseInt)
|
||||||
let isArchived = (getTime() - creation).inHours >= 2
|
let isArchived = (getTime() - creation).inWeeks > 8
|
||||||
let canEdit = c.rank == Admin or c.userid == postRow[0]
|
let canEdit = c.rank == Admin or c.userid == postRow[0]
|
||||||
if isArchived and c.rank < Admin:
|
if isArchived:
|
||||||
raise newForumError("This post is too old and can no longer be edited")
|
raise newForumError("This post is archived and can no longer be edited")
|
||||||
if not canEdit:
|
if not canEdit:
|
||||||
raise newForumError("You cannot edit this post")
|
raise newForumError("You cannot edit this post")
|
||||||
|
|
||||||
|
|
@ -710,13 +709,6 @@ proc executeLockState(c: TForumData, threadId: int, locked: bool) =
|
||||||
# Save the like.
|
# Save the like.
|
||||||
exec(db, crud(crUpdate, "thread", "isLocked"), locked.int, threadId)
|
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) =
|
proc executeDeletePost(c: TForumData, postId: int) =
|
||||||
# Verify that this post belongs to the user.
|
# Verify that this post belongs to the user.
|
||||||
const postQuery = sql"""
|
const postQuery = sql"""
|
||||||
|
|
@ -841,7 +833,7 @@ routes:
|
||||||
categoryArgs.insert($categoryId, 0)
|
categoryArgs.insert($categoryId, 0)
|
||||||
|
|
||||||
const threadsQuery =
|
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,
|
c.id, c.name, c.description, c.color,
|
||||||
u.id, u.name, u.email, strftime('%s', u.lastOnline),
|
u.id, u.name, u.email, strftime('%s', u.lastOnline),
|
||||||
strftime('%s', u.previousVisitAt), u.status, u.isDeleted
|
strftime('%s', u.previousVisitAt), u.status, u.isDeleted
|
||||||
|
|
@ -854,14 +846,14 @@ routes:
|
||||||
order by p.author
|
order by p.author
|
||||||
limit 1
|
limit 1
|
||||||
)
|
)
|
||||||
order by isPinned desc, modified desc limit ?, ?;"""
|
order by modified desc limit ?, ?;"""
|
||||||
|
|
||||||
let thrCount = getValue(db, countQuery, countArgs).parseInt()
|
let thrCount = getValue(db, countQuery, countArgs).parseInt()
|
||||||
let moreCount = max(0, thrCount - (start + count))
|
let moreCount = max(0, thrCount - (start + count))
|
||||||
|
|
||||||
var list = ThreadList(threads: @[], moreCount: moreCount)
|
var list = ThreadList(threads: @[], moreCount: moreCount)
|
||||||
for data in getAllRows(db, sql(threadsQuery % categorySection), categoryArgs):
|
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)
|
list.threads.add(thread)
|
||||||
|
|
||||||
resp $(%list), "application/json"
|
resp $(%list), "application/json"
|
||||||
|
|
@ -876,17 +868,12 @@ routes:
|
||||||
count = 10
|
count = 10
|
||||||
|
|
||||||
const threadsQuery =
|
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
|
c.id, c.name, c.description, c.color
|
||||||
from thread t, category c
|
from thread t, category c
|
||||||
where t.id = ? and isDeleted = 0 and category = c.id;"""
|
where t.id = ? and isDeleted = 0 and category = c.id;"""
|
||||||
|
|
||||||
let threadRow = getRow(db, threadsQuery, 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 thread = selectThread(threadRow, selectThreadAuthor(id))
|
||||||
|
|
||||||
let postsQuery =
|
let postsQuery =
|
||||||
|
|
@ -932,14 +919,9 @@ routes:
|
||||||
|
|
||||||
get "/specific_posts.json":
|
get "/specific_posts.json":
|
||||||
createTFD()
|
createTFD()
|
||||||
var ids: JsonNode
|
var
|
||||||
try:
|
|
||||||
ids = parseJson(@"ids")
|
ids = parseJson(@"ids")
|
||||||
except JsonParsingError:
|
|
||||||
let err = PostError(
|
|
||||||
message: "Invalid JSON in the `ids` parameter"
|
|
||||||
)
|
|
||||||
resp Http400, $(%err), "application/json"
|
|
||||||
cond ids.kind == JArray
|
cond ids.kind == JArray
|
||||||
let intIDs = ids.elems.map(x => x.getInt())
|
let intIDs = ids.elems.map(x => x.getInt())
|
||||||
let postsQuery = sql("""
|
let postsQuery = sql("""
|
||||||
|
|
@ -1357,33 +1339,6 @@ routes:
|
||||||
except ForumError as exc:
|
except ForumError as exc:
|
||||||
resp Http400, $(%exc.data), "application/json"
|
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)":
|
post re"/delete(Post|Thread)":
|
||||||
createTFD()
|
createTFD()
|
||||||
if not c.loggedIn():
|
if not c.loggedIn():
|
||||||
|
|
|
||||||
|
|
@ -96,8 +96,8 @@ when defined(js):
|
||||||
section(class="navbar-section"):
|
section(class="navbar-section"):
|
||||||
tdiv(class="input-group input-inline"):
|
tdiv(class="input-group input-inline"):
|
||||||
input(class="search-input input-sm",
|
input(class="search-input input-sm",
|
||||||
`type`="search", placeholder="Search",
|
`type`="text", placeholder="search",
|
||||||
id="search-box", required="required",
|
id="search-box",
|
||||||
onKeyDown=onKeyDown)
|
onKeyDown=onKeyDown)
|
||||||
if state.loading:
|
if state.loading:
|
||||||
tdiv(class="loading")
|
tdiv(class="loading")
|
||||||
|
|
|
||||||
|
|
@ -190,7 +190,7 @@ when defined(js):
|
||||||
else: ""
|
else: ""
|
||||||
|
|
||||||
result = buildHtml():
|
result = buildHtml():
|
||||||
button(class="btn btn-secondary", id="lock-btn",
|
button(class="btn btn-secondary",
|
||||||
onClick=(e: Event, n: VNode) =>
|
onClick=(e: Event, n: VNode) =>
|
||||||
onLockClick(e, n, state, thread),
|
onLockClick(e, n, state, thread),
|
||||||
"data-tooltip"=tooltip,
|
"data-tooltip"=tooltip,
|
||||||
|
|
@ -202,60 +202,3 @@ when defined(js):
|
||||||
else:
|
else:
|
||||||
italic(class="fas fa-lock")
|
italic(class="fas fa-lock")
|
||||||
text " Lock Thread"
|
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"
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -36,7 +36,6 @@ when defined(js):
|
||||||
likeButton: LikeButton
|
likeButton: LikeButton
|
||||||
deleteModal: DeleteModal
|
deleteModal: DeleteModal
|
||||||
lockButton: LockButton
|
lockButton: LockButton
|
||||||
pinButton: PinButton
|
|
||||||
categoryPicker: CategoryPicker
|
categoryPicker: CategoryPicker
|
||||||
|
|
||||||
proc onReplyPosted(id: int)
|
proc onReplyPosted(id: int)
|
||||||
|
|
@ -57,7 +56,6 @@ when defined(js):
|
||||||
likeButton: newLikeButton(),
|
likeButton: newLikeButton(),
|
||||||
deleteModal: newDeleteModal(onDeletePost, onDeleteThread, nil),
|
deleteModal: newDeleteModal(onDeletePost, onDeleteThread, nil),
|
||||||
lockButton: newLockButton(),
|
lockButton: newLockButton(),
|
||||||
pinButton: newPinButton(),
|
|
||||||
categoryPicker: newCategoryPicker(onCategoryChanged)
|
categoryPicker: newCategoryPicker(onCategoryChanged)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -413,7 +411,6 @@ when defined(js):
|
||||||
text " Reply"
|
text " Reply"
|
||||||
|
|
||||||
render(state.lockButton, list.thread, currentUser)
|
render(state.lockButton, list.thread, currentUser)
|
||||||
render(state.pinButton, list.thread, currentUser)
|
|
||||||
|
|
||||||
render(state.replyBox, list.thread, state.replyingTo, false)
|
render(state.replyBox, list.thread, state.replyingTo, false)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,6 @@ type
|
||||||
creation*: int64 ## Unix timestamp
|
creation*: int64 ## Unix timestamp
|
||||||
isLocked*: bool
|
isLocked*: bool
|
||||||
isSolved*: bool
|
isSolved*: bool
|
||||||
isPinned*: bool
|
|
||||||
|
|
||||||
ThreadList* = ref object
|
ThreadList* = ref object
|
||||||
threads*: seq[Thread]
|
threads*: seq[Thread]
|
||||||
|
|
@ -97,18 +96,15 @@ when defined(js):
|
||||||
else:
|
else:
|
||||||
return $duration.inSeconds & "s"
|
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 isOld = (getTime() - thread.creation.fromUnix).inWeeks > 2
|
||||||
let isBanned = thread.author.rank.isBanned()
|
let isBanned = thread.author.rank.isBanned()
|
||||||
result = buildHtml():
|
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"):
|
td(class="thread-title"):
|
||||||
if thread.isLocked:
|
if thread.isLocked:
|
||||||
italic(class="fas fa-lock fa-xs",
|
italic(class="fas fa-lock fa-xs",
|
||||||
title="Thread cannot be replied to")
|
title="Thread cannot be replied to")
|
||||||
if thread.isPinned:
|
|
||||||
italic(class="fas fa-thumbtack fa-xs",
|
|
||||||
title="Pinned post")
|
|
||||||
if isBanned:
|
if isBanned:
|
||||||
italic(class="fas fa-ban fa-xs",
|
italic(class="fas fa-ban fa-xs",
|
||||||
title="Thread author is banned")
|
title="Thread author is banned")
|
||||||
|
|
@ -227,7 +223,7 @@ when defined(js):
|
||||||
|
|
||||||
let isLastThread = i+1 == list.threads.len
|
let isLastThread = i+1 == list.threads.len
|
||||||
let (isLastUnseen, isNew) = getInfo(list.threads, i, currentUser)
|
let (isLastUnseen, isNew) = getInfo(list.threads, i, currentUser)
|
||||||
genThread(i+1, thread, isNew,
|
genThread(thread, isNew,
|
||||||
noBorder=isLastUnseen or isLastThread,
|
noBorder=isLastUnseen or isLastThread,
|
||||||
displayCategory=displayCategory)
|
displayCategory=displayCategory)
|
||||||
if isLastUnseen and (not isLastThread):
|
if isLastUnseen and (not isLastThread):
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,6 @@ SELECT
|
||||||
post_id,
|
post_id,
|
||||||
post_content,
|
post_content,
|
||||||
cdate,
|
cdate,
|
||||||
person.id,
|
|
||||||
person.name AS author,
|
person.name AS author,
|
||||||
person.email AS email,
|
person.email AS email,
|
||||||
strftime('%s', person.lastOnline) AS lastOnline,
|
strftime('%s', person.lastOnline) AS lastOnline,
|
||||||
|
|
|
||||||
|
|
@ -81,7 +81,6 @@ proc initialiseDb(admin: tuple[username, password, email: string],
|
||||||
isLocked boolean not null default 0,
|
isLocked boolean not null default 0,
|
||||||
solution integer,
|
solution integer,
|
||||||
isDeleted boolean not null default 0,
|
isDeleted boolean not null default 0,
|
||||||
isPinned boolean not null default 0,
|
|
||||||
|
|
||||||
foreign key (category) references category(id),
|
foreign key (category) references category(id),
|
||||||
foreign key (solution) references post(id)
|
foreign key (solution) references post(id)
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@ from times import getTime, utc, format
|
||||||
# Used to be:
|
# Used to be:
|
||||||
# {'A'..'Z', 'a'..'z', '0'..'9', '_', '\128'..'\255'}
|
# {'A'..'Z', 'a'..'z', '0'..'9', '_', '\128'..'\255'}
|
||||||
let
|
let
|
||||||
UsernameIdent* = IdentChars + {'-'} # TODO: Double check that everyone follows this.
|
UsernameIdent* = IdentChars # TODO: Double check that everyone follows this.
|
||||||
|
|
||||||
import frontend/[karaxutils, error]
|
import frontend/[karaxutils, error]
|
||||||
export parseInt
|
export parseInt
|
||||||
|
|
|
||||||
|
|
@ -30,8 +30,7 @@ proc elementIsSome(element: Option[Element]): bool =
|
||||||
proc elementIsNone(element: Option[Element]): bool =
|
proc elementIsNone(element: Option[Element]): bool =
|
||||||
return element.isNone
|
return element.isNone
|
||||||
|
|
||||||
proc waitForElement*(session: Session, selector: string, strategy=CssSelector, timeout=20000, pollTime=50,
|
proc waitForElement*(session: Session, selector: string, strategy=CssSelector, timeout=20000, pollTime=50, waitCondition=elementIsSome): Option[Element]
|
||||||
waitCondition: proc(element: Option[Element]): bool = elementIsSome): Option[Element]
|
|
||||||
|
|
||||||
proc click*(session: Session, element: string, strategy=CssSelector) =
|
proc click*(session: Session, element: string, strategy=CssSelector) =
|
||||||
let el = session.waitForElement(element, strategy)
|
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) =
|
proc checkIsNone*(session: Session, element: string, strategy=CssSelector) =
|
||||||
discard session.waitForElement(element, strategy, waitCondition=elementIsNone)
|
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)
|
let el = session.waitForElement(element)
|
||||||
check el.get().getText() == expectedValue
|
check el.get().getText() == expectedValue
|
||||||
|
|
||||||
proc waitForElement*(
|
proc waitForElement*(
|
||||||
session: Session, selector: string, strategy=CssSelector,
|
session: Session, selector: string, strategy=CssSelector,
|
||||||
timeout=20000, pollTime=50,
|
timeout=20000, pollTime=50,
|
||||||
waitCondition: proc(element: Option[Element]): bool = elementIsSome
|
waitCondition=elementIsSome
|
||||||
): Option[Element] =
|
): Option[Element] =
|
||||||
var waitTime = 0
|
var waitTime = 0
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
import unittest, common
|
import unittest, common
|
||||||
|
|
||||||
import webdriver
|
import webdriver
|
||||||
|
|
||||||
let
|
let
|
||||||
|
|
@ -57,35 +58,10 @@ proc userTests(session: Session, baseUrl: string) =
|
||||||
# Make sure the forum post is gone
|
# Make sure the forum post is gone
|
||||||
checkIsNone "To be deleted", LinkTextSelector
|
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()
|
session.logout()
|
||||||
|
|
||||||
proc anonymousTests(session: Session, baseUrl: string) =
|
proc anonymousTests(session: Session, baseUrl: string) =
|
||||||
|
|
||||||
suite "anonymous user tests":
|
suite "anonymous user tests":
|
||||||
with session:
|
with session:
|
||||||
navigate baseUrl
|
navigate baseUrl
|
||||||
|
|
@ -185,70 +161,6 @@ proc adminTests(session: Session, baseUrl: string) =
|
||||||
# Make sure the forum post is gone
|
# Make sure the forum post is gone
|
||||||
checkIsNone adminTitleStr, LinkTextSelector
|
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()
|
session.logout()
|
||||||
|
|
||||||
proc test*(session: Session, baseUrl: string) =
|
proc test*(session: Session, baseUrl: string) =
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue