From 8d317ae0e3fc773609b85c35dbce15145947d0fd Mon Sep 17 00:00:00 2001 From: Joey Yakimowich-Payne Date: Sat, 15 Feb 2020 18:41:59 -0700 Subject: [PATCH 01/80] Get rid of casts --- src/frontend/categorypicker.nim | 3 ++- src/frontend/delete.nim | 3 ++- src/frontend/editbox.nim | 3 ++- src/frontend/login.nim | 3 ++- src/frontend/postbutton.nim | 5 +++-- src/frontend/profilesettings.nim | 3 ++- src/frontend/replybox.nim | 5 +++-- src/frontend/resetpassword.nim | 3 ++- src/frontend/signup.nim | 3 ++- 9 files changed, 20 insertions(+), 11 deletions(-) diff --git a/src/frontend/categorypicker.nim b/src/frontend/categorypicker.nim index 0ea771f..98adde1 100644 --- a/src/frontend/categorypicker.nim +++ b/src/frontend/categorypicker.nim @@ -1,6 +1,7 @@ when defined(js): import sugar, httpcore, options, json, strutils, algorithm import dom except Event + import jsffi except `&` include karax/prelude import karax / [kajax, kdom, vstyles, vdom] @@ -108,7 +109,7 @@ when defined(js): let form = dom.document.getElementById("add-category-form") let formData = newFormData(form) - ajaxPost(uri, @[], cast[cstring](formData), + ajaxPost(uri, @[], formData.to(cstring), (s: int, r: kstring) => onAddCategoryPost(s, r, state)) proc onClose(ev: Event, n: VNode, state: CategoryPicker) = diff --git a/src/frontend/delete.nim b/src/frontend/delete.nim index 37446e5..d5415cf 100644 --- a/src/frontend/delete.nim +++ b/src/frontend/delete.nim @@ -1,6 +1,7 @@ when defined(js): import sugar, httpcore, options, json import dom except Event + import jsffi except `&` include karax/prelude import karax / [kajax, kdom] @@ -59,7 +60,7 @@ when defined(js): formData.append("id", $state.post.id) of DeleteThread: formData.append("id", $state.thread.id) - ajaxPost(uri, @[], cast[cstring](formData), + ajaxPost(uri, @[], formData.to(cstring), (s: int, r: kstring) => onDeletePost(s, r, state)) proc onClose(ev: Event, n: VNode, state: DeleteModal) = diff --git a/src/frontend/editbox.nim b/src/frontend/editbox.nim index d461f90..61d2a1c 100644 --- a/src/frontend/editbox.nim +++ b/src/frontend/editbox.nim @@ -1,5 +1,6 @@ when defined(js): import httpcore, options, sugar, json + import jsffi except `&` include karax/prelude import karax/kajax @@ -54,7 +55,7 @@ when defined(js): formData.append("postId", $state.post.id) # TODO: Subject let uri = makeUri("/updatePost") - ajaxPost(uri, @[], cast[cstring](formData), + ajaxPost(uri, @[], formData.to(cstring), (s: int, r: kstring) => onEditPost(s, r, state)) proc render*(state: EditBox, post: Post): VNode = diff --git a/src/frontend/login.nim b/src/frontend/login.nim index c19088e..1bc9e30 100644 --- a/src/frontend/login.nim +++ b/src/frontend/login.nim @@ -1,6 +1,7 @@ when defined(js): import sugar, httpcore, options, json import dom except Event, KeyboardEvent + import jsffi except `&` include karax/prelude import karax / [kajax, kdom] @@ -30,7 +31,7 @@ when defined(js): let form = dom.document.getElementById("login-form") # TODO: This is a hack, karax should support this. let formData = newFormData(form) - ajaxPost(uri, @[], cast[cstring](formData), + ajaxPost(uri, @[], formData.to(cstring), (s: int, r: kstring) => onLogInPost(s, r, state)) proc onClose(ev: Event, n: VNode, state: LoginModal) = diff --git a/src/frontend/postbutton.nim b/src/frontend/postbutton.nim index b76f6a0..8bd9c34 100644 --- a/src/frontend/postbutton.nim +++ b/src/frontend/postbutton.nim @@ -7,6 +7,7 @@ import options, httpcore, json, sugar, sequtils, strutils when defined(js): include karax/prelude import karax/[kajax, kdom] + import jsffi except `&` import error, karaxutils, post, user, threadlist @@ -116,7 +117,7 @@ when defined(js): makeUri("/unlike") else: makeUri("/like") - ajaxPost(uri, @[], cast[cstring](formData), + ajaxPost(uri, @[], formData.to(cstring), (s: int, r: kstring) => onPost(s, r, state, post, currentUser.get())) @@ -172,7 +173,7 @@ when defined(js): makeUri("/unlock") else: makeUri("/lock") - ajaxPost(uri, @[], cast[cstring](formData), + ajaxPost(uri, @[], formData.to(cstring), (s: int, r: kstring) => onPost(s, r, state, thread)) diff --git a/src/frontend/profilesettings.nim b/src/frontend/profilesettings.nim index 56ea7b2..5c2e1eb 100644 --- a/src/frontend/profilesettings.nim +++ b/src/frontend/profilesettings.nim @@ -1,5 +1,6 @@ when defined(js): import httpcore, options, sugar, json, strutils, strformat + import jsffi except `&` include karax/prelude import karax/[kajax, kdom] @@ -68,7 +69,7 @@ when defined(js): formData.append("rank", $state.rank) formData.append("username", $state.profile.user.name) let uri = makeUri("/saveProfile") - ajaxPost(uri, @[], cast[cstring](formData), + ajaxPost(uri, @[], formData.to(cstring), (s: int, r: kstring) => onProfilePost(s, r, state)) proc needsSave(state: ProfileSettings): bool = diff --git a/src/frontend/replybox.nim b/src/frontend/replybox.nim index e386dcb..64e5864 100644 --- a/src/frontend/replybox.nim +++ b/src/frontend/replybox.nim @@ -1,5 +1,6 @@ when defined(js): import strformat, options, httpcore, json, sugar + import jsffi except `&` from dom import getElementById, scrollIntoView, setTimeout @@ -56,7 +57,7 @@ when defined(js): let formData = newFormData() formData.append("msg", state.text) let uri = makeUri("/preview") - ajaxPost(uri, @[], cast[cstring](formData), + ajaxPost(uri, @[], formData.to(cstring), (s: int, r: kstring) => onPreviewPost(s, r, state)) proc onMessageClick(e: Event, n: VNode, state: ReplyBox) = @@ -80,7 +81,7 @@ when defined(js): if replyingTo.isSome: formData.append("replyingTo", $replyingTo.get().id) let uri = makeUri("/createPost") - ajaxPost(uri, @[], cast[cstring](formData), + ajaxPost(uri, @[], formData.to(cstring), (s: int, r: kstring) => onReplyPost(s, r, state)) proc onCancelClick(e: Event, n: VNode, state: ReplyBox) = diff --git a/src/frontend/resetpassword.nim b/src/frontend/resetpassword.nim index 51b467b..d8f2446 100644 --- a/src/frontend/resetpassword.nim +++ b/src/frontend/resetpassword.nim @@ -1,6 +1,7 @@ when defined(js): import sugar, httpcore, options, json import dom except Event, KeyboardEvent + import jsffi except `&` include karax/prelude import karax / [kajax, kdom] @@ -86,7 +87,7 @@ when defined(js): let form = dom.document.getElementById("resetpassword-form") # TODO: This is a hack, karax should support this. let formData = newFormData(form) - ajaxPost(uri, @[], cast[cstring](formData), + ajaxPost(uri, @[], formData.to(cstring), (s: int, r: kstring) => onPost(s, r, state)) ev.preventDefault() diff --git a/src/frontend/signup.nim b/src/frontend/signup.nim index 6a422d6..98e330d 100644 --- a/src/frontend/signup.nim +++ b/src/frontend/signup.nim @@ -1,6 +1,7 @@ when defined(js): import sugar, httpcore, options, json import dom except Event + import jsffi except `&` include karax/prelude import karax / [kajax, kdom] @@ -28,7 +29,7 @@ when defined(js): let form = dom.document.getElementById("signup-form") # TODO: This is a hack, karax should support this. let formData = newFormData(form) - ajaxPost(uri, @[], cast[cstring](formData), + ajaxPost(uri, @[], formData.to(cstring), (s: int, r: kstring) => onSignUpPost(s, r, state)) proc onClose(ev: Event, n: VNode, state: SignupModal) = From 616c6eb1007cacbdbb254ac6eccb07da42613578 Mon Sep 17 00:00:00 2001 From: Joey Yakimowich-Payne Date: Sat, 15 Feb 2020 18:53:09 -0700 Subject: [PATCH 02/80] Cleanup unused imports and compiler warnings --- .gitignore | 2 ++ src/frontend/about.nim | 6 +++--- src/frontend/activateemail.nim | 9 ++------- src/frontend/category.nim | 4 +--- src/frontend/categorypicker.nim | 4 ++-- src/frontend/error.nim | 1 - src/frontend/forum.nim | 2 +- src/frontend/header.nim | 4 ++-- src/frontend/karaxutils.nim | 2 +- src/frontend/newthread.nim | 2 +- src/frontend/post.nim | 2 +- src/frontend/postlist.nim | 12 ++++++------ src/frontend/profile.nim | 6 +++--- src/frontend/profilesettings.nim | 2 +- src/frontend/resetpassword.nim | 2 +- src/frontend/search.nim | 2 +- src/frontend/threadlist.nim | 20 ++++++++++---------- 17 files changed, 38 insertions(+), 44 deletions(-) diff --git a/.gitignore b/.gitignore index b209f11..55c0cb9 100644 --- a/.gitignore +++ b/.gitignore @@ -12,3 +12,5 @@ nimcache/ forum createdb editdb + +.vscode diff --git a/src/frontend/about.nim b/src/frontend/about.nim index a805a12..2ceb1d0 100644 --- a/src/frontend/about.nim +++ b/src/frontend/about.nim @@ -1,11 +1,11 @@ when defined(js): - import sugar, httpcore, options, json + import sugar, httpcore import dom except Event include karax/prelude - import karax / [kajax, kdom] + import karax / [kajax] - import error, replybox, threadlist, post + import error import karaxutils type diff --git a/src/frontend/activateemail.nim b/src/frontend/activateemail.nim index 4b049da..607bd4f 100644 --- a/src/frontend/activateemail.nim +++ b/src/frontend/activateemail.nim @@ -5,7 +5,7 @@ when defined(js): include karax/prelude import karax / [kajax, kdom] - import error, replybox, threadlist, post + import error import karaxutils type @@ -13,17 +13,12 @@ when defined(js): loading: bool status: HttpCode error: Option[PostError] - newPassword: kstring proc newActivateEmail*(): ActivateEmail = ActivateEmail( - status: Http200, - newPassword: "" + status: Http200 ) - proc onPassChange(e: Event, n: VNode, state: ActivateEmail) = - state.newPassword = n.value - proc onPost(httpStatus: int, response: kstring, state: ActivateEmail) = postFinished: navigateTo(makeUri("/activateEmail/success")) diff --git a/src/frontend/category.nim b/src/frontend/category.nim index de2d248..b1a68d2 100644 --- a/src/frontend/category.nim +++ b/src/frontend/category.nim @@ -14,9 +14,7 @@ proc cmpNames*(cat1: Category, cat2: Category): int = when defined(js): include karax/prelude - import karax / [vstyles, kajax, kdom] - - import karaxutils + import karax / [vstyles] proc render*(category: Category): VNode = result = buildHtml(): diff --git a/src/frontend/categorypicker.nim b/src/frontend/categorypicker.nim index 98adde1..32a107b 100644 --- a/src/frontend/categorypicker.nim +++ b/src/frontend/categorypicker.nim @@ -4,9 +4,9 @@ when defined(js): import jsffi except `&` include karax/prelude - import karax / [kajax, kdom, vstyles, vdom] + import karax / [kajax, kdom, vdom] - import error, replybox, threadlist, post, category, user + import error, category, user import category, karaxutils type diff --git a/src/frontend/error.nim b/src/frontend/error.nim index 06a8d07..27f1e7c 100644 --- a/src/frontend/error.nim +++ b/src/frontend/error.nim @@ -7,7 +7,6 @@ type when defined(js): import json include karax/prelude - import karax / [vstyles, kajax, kdom] import karaxutils diff --git a/src/frontend/forum.nim b/src/frontend/forum.nim index efb072e..ed62870 100644 --- a/src/frontend/forum.nim +++ b/src/frontend/forum.nim @@ -1,4 +1,4 @@ -import strformat, times, options, json, tables, sugar, httpcore, uri +import options, tables, sugar, httpcore from dom import window, Location, document, decodeURI include karax/prelude diff --git a/src/frontend/header.nim b/src/frontend/header.nim index fc16941..edd1ade 100644 --- a/src/frontend/header.nim +++ b/src/frontend/header.nim @@ -1,6 +1,6 @@ import options, times, httpcore, json, sugar -import threadlist, user +import user type UserStatus* = object user*: Option[User] @@ -63,7 +63,7 @@ when defined(js): proc getStatus(logout: bool=false) = if state.loading: return let diff = getTime() - state.lastUpdate - if diff.minutes < 5: + if diff.inMinutes < 5: return state.loading = true diff --git a/src/frontend/karaxutils.nim b/src/frontend/karaxutils.nim index 2aa9d39..8b2fe8f 100644 --- a/src/frontend/karaxutils.nim +++ b/src/frontend/karaxutils.nim @@ -1,4 +1,4 @@ -import strutils, options, strformat, parseutils, tables +import strutils, strformat, parseutils, tables proc parseIntSafe*(s: string, value: var int) {.noSideEffect.} = ## parses `s` into an integer in the range `validRange`. If successful, diff --git a/src/frontend/newthread.nim b/src/frontend/newthread.nim index fe4e619..d314985 100644 --- a/src/frontend/newthread.nim +++ b/src/frontend/newthread.nim @@ -6,7 +6,7 @@ when defined(js): include karax/prelude import karax / [kajax, kdom] - import error, replybox, threadlist, post, category, user + import error, replybox, threadlist, post, user import karaxutils, categorypicker type diff --git a/src/frontend/post.nim b/src/frontend/post.nim index 5ca0d5f..7c27dec 100644 --- a/src/frontend/post.nim +++ b/src/frontend/post.nim @@ -1,4 +1,4 @@ -import strformat, options +import options import user, threadlist diff --git a/src/frontend/postlist.nim b/src/frontend/postlist.nim index 46dee9a..a516a9f 100644 --- a/src/frontend/postlist.nim +++ b/src/frontend/postlist.nim @@ -1,6 +1,6 @@ import system except Thread -import options, json, times, httpcore, strformat, sugar, math, strutils +import options, json, times, httpcore, sugar, strutils import sequtils import threadlist, category, post, user @@ -18,7 +18,7 @@ when defined(js): import jsffi except `&` include karax/prelude - import karax / [vstyles, kajax, kdom] + import karax / [kajax, kdom] import karaxutils, error, replybox, editbox, postbutton, delete import categorypicker @@ -326,12 +326,12 @@ when defined(js): ] var diffStr = tmpl[0] let diff = latestTime - prevPost.info.creation.fromUnix() - if diff.weeks > 48: - let years = diff.weeks div 48 + if diff.inWeeks > 48: + let years = diff.inWeeks div 48 diffStr = (if years == 1: tmpl[1] else: tmpl[2]) % $years - elif diff.weeks > 4: - let months = diff.weeks div 4 + elif diff.inWeeks > 4: + let months = diff.inWeeks div 4 diffStr = (if months == 1: tmpl[3] else: tmpl[4]) % $months else: diff --git a/src/frontend/profile.nim b/src/frontend/profile.nim index 60f9daf..fbfea68 100644 --- a/src/frontend/profile.nim +++ b/src/frontend/profile.nim @@ -1,12 +1,12 @@ -import options, httpcore, json, sugar, times, strformat, strutils +import options, httpcore, json, sugar, times, strutils -import threadlist, post, category, error, user +import threadlist, post, error, user when defined(js): from dom import document include karax/prelude import karax/[kajax, kdom] - import karaxutils, postbutton, delete, profilesettings + import karaxutils, profilesettings type ProfileTab* = enum diff --git a/src/frontend/profilesettings.nim b/src/frontend/profilesettings.nim index 5c2e1eb..8d7fda3 100644 --- a/src/frontend/profilesettings.nim +++ b/src/frontend/profilesettings.nim @@ -5,7 +5,7 @@ when defined(js): include karax/prelude import karax/[kajax, kdom] - import replybox, post, karaxutils, postbutton, error, delete, user + import post, karaxutils, postbutton, error, delete, user type ProfileSettings* = ref object diff --git a/src/frontend/resetpassword.nim b/src/frontend/resetpassword.nim index d8f2446..497af98 100644 --- a/src/frontend/resetpassword.nim +++ b/src/frontend/resetpassword.nim @@ -6,7 +6,7 @@ when defined(js): include karax/prelude import karax / [kajax, kdom] - import error, replybox, threadlist, post + import error import karaxutils type diff --git a/src/frontend/search.nim b/src/frontend/search.nim index 2edb90e..a1ef3fa 100644 --- a/src/frontend/search.nim +++ b/src/frontend/search.nim @@ -20,7 +20,7 @@ when defined(js): from dom import nil include karax/prelude - import karax / [vstyles, kajax, kdom] + import karax / [kajax] import karaxutils, error, threadlist, sugar diff --git a/src/frontend/threadlist.nim b/src/frontend/threadlist.nim index b3b3d81..776c5ca 100644 --- a/src/frontend/threadlist.nim +++ b/src/frontend/threadlist.nim @@ -1,4 +1,4 @@ -import strformat, times, options, json, httpcore, sugar +import strformat, times, options, json, httpcore import category, user @@ -98,19 +98,19 @@ when defined(js): let duration = currentTime - activityTime if currentTime.local().year != activityTime.local().year: return activityTime.local().format("MMM yyyy") - elif duration.days > 30 and duration.days < 300: + elif duration.inDays > 30 and duration.inDays < 300: return activityTime.local().format("MMM dd") - elif duration.days != 0: - return $duration.days & "d" - elif duration.hours != 0: - return $duration.hours & "h" - elif duration.minutes != 0: - return $duration.minutes & "m" + elif duration.inDays != 0: + return $duration.inDays & "d" + elif duration.inHours != 0: + return $duration.inHours & "h" + elif duration.inMinutes != 0: + return $duration.inMinutes & "m" else: - return $duration.seconds & "s" + return $duration.inSeconds & "s" proc genThread(thread: Thread, isNew: bool, noBorder: bool): VNode = - let isOld = (getTime() - thread.creation.fromUnix).weeks > 2 + let isOld = (getTime() - thread.creation.fromUnix).inWeeks > 2 let isBanned = thread.author.rank.isBanned() result = buildHtml(): tr(class=class({"no-border": noBorder, "banned": isBanned})): From 5a4f44b4ee425d8e5164769253369c76987986fb Mon Sep 17 00:00:00 2001 From: Joey Yakimowich-Payne Date: Sun, 16 Feb 2020 19:19:21 -0700 Subject: [PATCH 03/80] Speedup and simplify tests This drastically speeds up tests and simplifies test writing by making it so that no explicit calls for waiting are needed. Elements that are queried for now implicitly waits for them to be available. On my machine, tests used to take 3-4 minutes to complete. Now they take ~1 minute to complete. --- nimforum.nimble | 4 +- src/frontend/forum.nim | 2 +- tests/browsertests/categories.nim | 14 +------ tests/browsertests/common.nim | 66 +++++++++++++++---------------- tests/browsertests/issue181.nim | 4 -- tests/browsertests/scenario1.nim | 6 +-- tests/browsertests/threads.nim | 27 +------------ 7 files changed, 40 insertions(+), 83 deletions(-) diff --git a/nimforum.nimble b/nimforum.nimble index 97581b0..1b32ba2 100644 --- a/nimforum.nimble +++ b/nimforum.nimble @@ -55,7 +55,7 @@ task blankdb, "Creates a blank DB": task test, "Runs tester": exec "nimble c -y src/forum.nim" - exec "nimble c -y -r tests/browsertester" + exec "nimble c -y -r -d:actionDelayMs=0 tests/browsertester" task fasttest, "Runs tester without recompiling backend": - exec "nimble c -r tests/browsertester" + exec "nimble c -r -d:actionDelayMs=0 tests/browsertester" diff --git a/src/frontend/forum.nim b/src/frontend/forum.nim index ed62870..b6a9d10 100644 --- a/src/frontend/forum.nim +++ b/src/frontend/forum.nim @@ -149,4 +149,4 @@ proc render(): VNode = ]) window.onPopState = onPopState -setRenderer render +setRenderer render \ No newline at end of file diff --git a/tests/browsertests/categories.nim b/tests/browsertests/categories.nim index 7924ac4..9c6cbf2 100644 --- a/tests/browsertests/categories.nim +++ b/tests/browsertests/categories.nim @@ -18,25 +18,21 @@ proc categoriesUserTests(session: Session, baseUrl: string) = with session: navigate baseUrl - wait() login "user", "user" setup: with session: navigate baseUrl - wait() test "no category add available": with session: click "#new-thread-btn" - wait() checkIsNone "#add-category" test "can create category thread": with session: click "#new-thread-btn" - wait() sendKeys "#thread-title", title @@ -45,12 +41,10 @@ proc categoriesUserTests(session: Session, baseUrl: string) = sendKeys "#reply-textarea", content click "#create-thread-btn" - wait() checkText "#thread-title .category", "Fun" navigate baseUrl - wait() ensureExists title, LinkTextSelector @@ -65,18 +59,15 @@ proc categoriesAdminTests(session: Session, baseUrl: string) = suite "admin tests": with session: navigate baseUrl - wait() login "admin", "admin" test "can create category": with session: click "#new-thread-btn" - wait() ensureExists "#add-category" click "#add-category .plus-btn" - wait() clear "#add-category input[name='name']" clear "#add-category input[name='color']" @@ -88,7 +79,6 @@ proc categoriesAdminTests(session: Session, baseUrl: string) = sendKeys "#add-category input[name='description']", description click "#add-category #add-category-btn" - wait() checkText "#category-selection .selected-category", name @@ -96,10 +86,8 @@ proc categoriesAdminTests(session: Session, baseUrl: string) = proc test*(session: Session, baseUrl: string) = session.navigate(baseUrl) - session.wait() categoriesUserTests(session, baseUrl) categoriesAdminTests(session, baseUrl) - session.navigate(baseUrl) - session.wait() \ No newline at end of file + session.navigate(baseUrl) \ No newline at end of file diff --git a/tests/browsertests/common.nim b/tests/browsertests/common.nim index 02236ca..e967abd 100644 --- a/tests/browsertests/common.nim +++ b/tests/browsertests/common.nim @@ -2,6 +2,9 @@ import os, options, unittest, strutils import webdriver import macros +const actionDelayMs {.intdefine.} = 0 +## Inserts a delay in milliseconds between automated actions. Useful for debugging tests + macro with*(obj: typed, code: untyped): untyped = ## Execute a set of statements with an object expectKind code, nnkStmtList @@ -12,24 +15,28 @@ macro with*(obj: typed, code: untyped): untyped = if result[i].kind in {nnkCommand, nnkCall}: result[i].insert(1, obj) +proc elementIsSome(element: Option[Element]): bool = + return element.isSome + +proc elementIsNone(element: Option[Element]): bool = + return element.isNone + +proc waitForElement*(session: Session, selector: string, strategy=CssSelector, timeout=20000, pollTime=50, waitCondition=elementIsSome): Option[Element] + template click*(session: Session, element: string, strategy=CssSelector) = - let el = session.findElement(element, strategy) - check el.isSome() + let el = session.waitForElement(element, strategy) el.get().click() template sendKeys*(session: Session, element, keys: string) = - let el = session.findElement(element) - check el.isSome() + let el = session.waitForElement(element) el.get().sendKeys(keys) template clear*(session: Session, element: string) = - let el = session.findElement(element) - check el.isSome() + let el = session.waitForElement(element) el.get().clear() template sendKeys*(session: Session, element: string, keys: varargs[Key]) = - let el = session.findElement(element) - check el.isSome() + let el = session.waitForElement(element) # focus el.get().click() @@ -37,47 +44,47 @@ template sendKeys*(session: Session, element: string, keys: varargs[Key]) = session.press(key) template ensureExists*(session: Session, element: string, strategy=CssSelector) = - let el = session.findElement(element, strategy) - check el.isSome() + discard session.waitForElement(element, strategy) template check*(session: Session, element: string, function: untyped) = - let el = session.findElement(element) + let el = session.waitForElement(element) check function(el) template check*(session: Session, element: string, strategy: LocationStrategy, function: untyped) = - let el = session.findElement(element, strategy) + let el = session.waitForElement(element, strategy) check function(el) template checkIsNone*(session: Session, element: string, strategy=CssSelector) = - let el = session.findElement(element, strategy) - check el.isNone() + discard session.waitForElement(element, strategy, waitCondition=elementIsNone) template checkText*(session: Session, element, expectedValue: string) = - let el = session.findElement(element) - check el.isSome() + let el = session.waitForElement(element) check el.get().getText() == expectedValue -proc waitForLoad*(session: Session, timeout=20000) = +proc waitForElement*( + session: Session, selector: string, strategy=CssSelector, + timeout=20000, pollTime=50, + waitCondition=elementIsSome +): Option[Element] = var waitTime = 0 - sleep(2000) + + when actionDelayMs > 0: + sleep(actionDelayMs) while true: - let loading = session.findElement(".loading") - if loading.isNone: return - sleep(1000) - waitTime += 1000 + let loading = session.findElement(selector, strategy) + if waitCondition(loading): + return loading + sleep(pollTime) + waitTime += pollTime if waitTime > timeout: doAssert false, "Wait for load time exceeded" -proc wait*(session: Session, msTimeout: int = 5000) = - session.waitForLoad(msTimeout) - proc setUserRank*(session: Session, baseUrl, user, rank: string) = with session: navigate(baseUrl & "profile/" & user) - wait() click "#settings-tab" @@ -85,13 +92,11 @@ proc setUserRank*(session: Session, baseUrl, user, rank: string) = click("#rank-field option#rank-" & rank.toLowerAscii) click "#save-btn" - wait() proc logout*(session: Session) = with session: click "#profile-btn" click "#profile-btn #logout-btn" - wait() # Verify we have logged out by looking for the log in button. ensureExists "#login-btn" @@ -108,8 +113,6 @@ proc login*(session: Session, user, password: string) = sendKeys "#login-form input[name='password']", Key.Enter - wait() - # Verify that the user menu has been initialised properly. click "#profile-btn" checkText "#profile-btn #profile-name", user @@ -128,7 +131,6 @@ proc register*(session: Session, user, password: string, verify = true) = sendKeys "#signup-form input[name='password']", password click "#signup-modal .create-account-btn" - wait() if verify: with session: @@ -141,13 +143,11 @@ proc register*(session: Session, user, password: string, verify = true) = proc createThread*(session: Session, title, content: string) = with session: click "#new-thread-btn" - wait() sendKeys "#thread-title", title sendKeys "#reply-textarea", content click "#create-thread-btn" - wait() checkText "#thread-title .title-text", title checkText ".original-post div.post-content", content diff --git a/tests/browsertests/issue181.nim b/tests/browsertests/issue181.nim index 7a646c2..504677c 100644 --- a/tests/browsertests/issue181.nim +++ b/tests/browsertests/issue181.nim @@ -5,8 +5,6 @@ import webdriver proc test*(session: Session, baseUrl: string) = session.navigate(baseUrl) - waitForLoad(session) - test "can see banned posts": with session: register("issue181", "issue181") @@ -19,7 +17,6 @@ proc test*(session: Session, baseUrl: string) = login("issue181", "issue181") navigate(baseUrl) - wait() const title = "Testing issue 181." createThread(title, "Test for issue #181") @@ -33,7 +30,6 @@ proc test*(session: Session, baseUrl: string) = # Make sure the banned user's thread is still visible. navigate(baseUrl) - wait() ensureExists("tr.banned") checkText("tr.banned .thread-title > a", title) logout() diff --git a/tests/browsertests/scenario1.nim b/tests/browsertests/scenario1.nim index b5912ec..054f7b9 100644 --- a/tests/browsertests/scenario1.nim +++ b/tests/browsertests/scenario1.nim @@ -5,8 +5,6 @@ import webdriver proc test*(session: Session, baseUrl: string) = session.navigate(baseUrl) - waitForLoad(session) - # Sanity checks test "shows sign up": session.checkText("#signup-btn", "Sign up") @@ -38,10 +36,8 @@ proc test*(session: Session, baseUrl: string) = logout() navigate baseUrl - wait() register "TEst1", "test1", verify = false ensureExists "#signup-form .has-error" - navigate baseUrl - wait() \ No newline at end of file + navigate baseUrl \ No newline at end of file diff --git a/tests/browsertests/threads.nim b/tests/browsertests/threads.nim index 45b971b..53fbb23 100644 --- a/tests/browsertests/threads.nim +++ b/tests/browsertests/threads.nim @@ -1,4 +1,4 @@ -import unittest, options, os, common +import unittest, options, common import webdriver @@ -27,18 +27,15 @@ proc userTests(session: Session, baseUrl: string) = setup: session.navigate(baseUrl) - session.wait() test "can create thread": with session: click "#new-thread-btn" - wait() sendKeys "#thread-title", userTitleStr sendKeys "#reply-textarea", userContentStr click "#create-thread-btn" - wait() checkText "#thread-title .title-text", userTitleStr checkText ".original-post div.post-content", userContentStr @@ -50,7 +47,6 @@ proc anonymousTests(session: Session, baseUrl: string) = suite "anonymous user tests": with session: navigate baseUrl - wait() test "can view banned thread": with session: @@ -58,25 +54,21 @@ proc anonymousTests(session: Session, baseUrl: string) = with session: navigate baseUrl - wait() proc bannedTests(session: Session, baseUrl: string) = suite "banned user thread tests": with session: navigate baseUrl - wait() login "banned", "banned" test "can't start thread": with session: click "#new-thread-btn" - wait() sendKeys "#thread-title", "test" sendKeys "#reply-textarea", "test" click "#create-thread-btn" - wait() ensureExists "#new-thread p.text-error" @@ -88,7 +80,6 @@ proc adminTests(session: Session, baseUrl: string) = setup: session.navigate(baseUrl) - session.wait() test "can view banned thread": with session: @@ -97,13 +88,11 @@ proc adminTests(session: Session, baseUrl: string) = test "can create thread": with session: click "#new-thread-btn" - wait() sendKeys "#thread-title", adminTitleStr sendKeys "#reply-textarea", adminContentStr click "#create-thread-btn" - wait() checkText "#thread-title .title-text", adminTitleStr checkText ".original-post div.post-content", adminContentStr @@ -111,7 +100,6 @@ proc adminTests(session: Session, baseUrl: string) = test "try create duplicate thread": with session: click "#new-thread-btn" - wait() ensureExists "#new-thread" sendKeys "#thread-title", adminTitleStr @@ -119,22 +107,17 @@ proc adminTests(session: Session, baseUrl: string) = click "#create-thread-btn" - wait() - ensureExists "#new-thread p.text-error" test "can edit post": let modificationText = " and I edited it!" with session: click adminTitleStr, LinkTextSelector - wait() click ".post-buttons .edit-button" - wait() sendKeys ".original-post #reply-textarea", modificationText click ".edit-buttons .save-button" - wait() checkText ".original-post div.post-content", adminContentStr & modificationText @@ -143,7 +126,6 @@ proc adminTests(session: Session, baseUrl: string) = with session: click userTitleStr, LinkTextSelector - wait() click ".post-buttons .like-button" @@ -152,14 +134,11 @@ proc adminTests(session: Session, baseUrl: string) = test "can delete thread": with session: click adminTitleStr, LinkTextSelector - wait() click ".post-buttons .delete-button" - wait() # click delete confirmation click "#delete-modal .delete-btn" - wait() # Make sure the forum post is gone checkIsNone adminTitleStr, LinkTextSelector @@ -168,7 +147,6 @@ proc adminTests(session: Session, baseUrl: string) = proc test*(session: Session, baseUrl: string) = session.navigate(baseUrl) - session.wait() userTests(session, baseUrl) @@ -180,5 +158,4 @@ proc test*(session: Session, baseUrl: string) = unBanUser(session, baseUrl) - session.navigate(baseUrl) - session.wait() + session.navigate(baseUrl) \ No newline at end of file From 9d19d70558743c4304fdc8ec84be3eabe82d2879 Mon Sep 17 00:00:00 2001 From: Joey Payne Date: Fri, 25 Jan 2019 16:40:04 -0700 Subject: [PATCH 04/80] Add categories page --- public/css/nimforum.scss | 35 ++++++++++++--- src/forum.nim | 22 ++++++++-- src/frontend/categorylist.nim | 83 +++++++++++++++++++++++++++++++++++ src/frontend/forum.nim | 9 ++++ src/frontend/mainbuttons.nim | 37 ++++++++++++++++ src/frontend/threadlist.nim | 42 +++++------------- 6 files changed, 187 insertions(+), 41 deletions(-) create mode 100644 src/frontend/categorylist.nim create mode 100644 src/frontend/mainbuttons.nim diff --git a/public/css/nimforum.scss b/public/css/nimforum.scss index 9842d8d..7f3b257 100644 --- a/public/css/nimforum.scss +++ b/public/css/nimforum.scss @@ -22,6 +22,7 @@ table th { // Custom styles. // - Navigation bar. $navbar-height: 60px; +$default-category-color: #98c766; $logo-height: $navbar-height - 20px; .navbar-button { @@ -166,6 +167,33 @@ $logo-height: $navbar-height - 20px; } } +.thread-list { + @extend .container; + @extend .grid-xl; +} + +.category-list { + @extend .thread-list; + + + .category-title { + @extend .thread-title; + a, a:hover { + color: lighten($body-font-color, 10%); + text-decoration: none; + } + } + + .category-description { + opacity: 0.6; + } +} + +#categories-list .category { + border-left: 6px solid; + border-left-color: $default-category-color; +} + $super-popular-color: #f86713; $popular-color: darken($super-popular-color, 25%); $threads-meta-color: #545d70; @@ -217,7 +245,7 @@ $threads-meta-color: #545d70; .category-color { width: 0; height: 0; - border: 0.3rem solid #98c766; + border: 0.3rem solid $default-category-color; display: inline-block; margin-right: 5px; } @@ -726,8 +754,3 @@ hr { margin-top: $control-padding-y*2; } } - -// - Hide features that have not been implemented yet. -#main-buttons > section.navbar-section:nth-child(1) { - display: none; -} \ No newline at end of file diff --git a/src/forum.nim b/src/forum.nim index c635f1a..fc9a17b 100644 --- a/src/forum.nim +++ b/src/forum.nim @@ -806,14 +806,28 @@ routes: var start = getInt(@"start", 0) count = getInt(@"count", 30) + categoryId = getInt(@"categoryId", -1) + + var + categorySection = "" + categoryArgs: seq[string] = @[$start, $count] + countQuery = sql"select count(*) from thread;" + countArgs: seq[string] = @[] + + + if categoryId != -1: + categorySection = "c.id == ? and " + countQuery = sql"select count(*) from thread t, category c where category == c.id and c.id == ?;" + countArgs.add($categoryId) + categoryArgs.insert($categoryId, 0) const threadsQuery = - sql"""select t.id, t.name, views, strftime('%s', modified), isLocked, + """select t.id, t.name, views, strftime('%s', modified), isLocked, c.id, c.name, c.description, c.color, u.name, u.email, strftime('%s', u.lastOnline), strftime('%s', u.previousVisitAt), u.status, u.isDeleted from thread t, category c, person u - where t.isDeleted = 0 and category = c.id and + where t.isDeleted = 0 and category = c.id and $# u.status <> 'Spammer' and u.status <> 'Troll' and u.id in ( select u.id from post p, person u @@ -823,11 +837,11 @@ routes: ) order by modified desc limit ?, ?;""" - let thrCount = getValue(db, sql"select count(*) from thread;").parseInt() + 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, threadsQuery, start, count): + for data in getAllRows(db, sql(threadsQuery % categorySection), categoryArgs): let thread = selectThread(data[0 .. 8], selectUser(data[9 .. ^1])) list.threads.add(thread) diff --git a/src/frontend/categorylist.nim b/src/frontend/categorylist.nim new file mode 100644 index 0000000..231d697 --- /dev/null +++ b/src/frontend/categorylist.nim @@ -0,0 +1,83 @@ +import strformat, times, options, json, httpcore, sugar, strutils, uri + +import category + +when defined(js): + include karax/prelude + import karax / [vstyles, kajax, kdom] + + import karaxutils, error, user, mainbuttons + + type + State = ref object + list: Option[CategoryList] + loading: bool + status: HttpCode + + proc newState(): State = + State( + list: none[CategoryList](), + loading: false, + status: Http200 + ) + var + state = newState() + + proc genCategory(category: Category, noBorder = false): VNode = + result = buildHtml(): + tr(class=class({"no-border": noBorder})): + td(style=style((StyleAttr.borderLeftColor, kstring("#" & category.color))), class="category"): + h4(class="category-title"): + a(href=makeUri("/c/" & $category.id)): + tdiv(): + tdiv(class="category-name"): + text category.name + tdiv(class="category-description"): + text category.description + td(class="topics"): + text "Topics" + + proc onCategoriesRetrieved(httpStatus: int, response: kstring) = + state.loading = false + state.status = httpStatus.HttpCode + if state.status != Http200: return + + let parsed = parseJson($response) + let list = to(parsed, CategoryList) + + if state.list.isSome: + state.list.get().categories.add(list.categories) + else: + state.list = some(list) + + proc renderCategories(): VNode = + if state.status != Http200: + return renderError("Couldn't retrieve threads.", state.status) + + if state.list.isNone: + if not state.loading: + state.loading = true + ajaxGet(makeUri("categories.json"), @[], onCategoriesRetrieved) + + return buildHtml(tdiv(class="loading loading-lg")) + + let list = state.list.get() + + return buildHtml(): + section(class="category-list"): + table(id="categories-list", class="table"): + thead(): + tr: + th(text "Category") + th(text "Topics") + tbody(): + for i in 0 ..< list.categories.len: + let category = list.categories[i] + + let isLastCategory = i+1 == list.categories.len + genCategory(category, noBorder=isLastCategory) + + proc renderCategoryList*(currentUser: Option[User]): VNode = + result = buildHtml(tdiv): + renderMainButtons(currentUser) + renderCategories() diff --git a/src/frontend/forum.nim b/src/frontend/forum.nim index b6a9d10..9285e72 100644 --- a/src/frontend/forum.nim +++ b/src/frontend/forum.nim @@ -5,6 +5,7 @@ include karax/prelude import jester/[patterns] import threadlist, postlist, header, profile, newthread, error, about +import categorylist import resetpassword, activateemail, search import karaxutils @@ -81,6 +82,14 @@ proc render(): VNode = result = buildHtml(tdiv()): renderHeader() route([ + r("/categories", + (params: Params) => + (renderCategoryList(getLoggedInUser())) + ), + r("/c/@id", + (params: Params) => + (renderThreadList(getLoggedInUser(), params["id"].parseInt)) + ), r("/newthread", (params: Params) => (render(state.newThread, getLoggedInUser())) diff --git a/src/frontend/mainbuttons.nim b/src/frontend/mainbuttons.nim new file mode 100644 index 0000000..4c46478 --- /dev/null +++ b/src/frontend/mainbuttons.nim @@ -0,0 +1,37 @@ +import options +import user + +when defined(js): + include karax/prelude + import karax / [vstyles, kajax, kdom] + + import karaxutils, error, user + + let buttons = [ + (name: "Latest", url: makeUri("/"), id: "latest-btn"), + (name: "Categories", url: makeUri("/categories"), id: "categories-btn"), + ] + + proc renderMainButtons*(currentUser: Option[User]): VNode = + result = buildHtml(): + section(class="navbar container grid-xl", id="main-buttons"): + section(class="navbar-section"): + #[tdiv(class="dropdown"): + a(href="#", class="btn dropdown-toggle"): + text "Filter " + italic(class="fas fa-caret-down") + ul(class="menu"): + li: text "community" + li: text "dev" ]# + + for btn in buttons: + let active = btn.url == window.location.href + a(id=btn.id, href=btn.url): + button(class=class({"btn-primary": active, "btn-link": not active}, "btn")): + text btn.name + section(class="navbar-section"): + if currentUser.isSome(): + a(id="new-thread-btn", href=makeUri("/newthread"), onClick=anchorCB): + button(class="btn btn-secondary"): + italic(class="fas fa-plus") + text " New Thread" diff --git a/src/frontend/threadlist.nim b/src/frontend/threadlist.nim index 776c5ca..ba9e244 100644 --- a/src/frontend/threadlist.nim +++ b/src/frontend/threadlist.nim @@ -29,7 +29,7 @@ when defined(js): include karax/prelude import karax / [vstyles, kajax, kdom] - import karaxutils, error, user + import karaxutils, error, user, mainbuttons type State = ref object @@ -65,27 +65,6 @@ when defined(js): return true - proc genTopButtons(currentUser: Option[User]): VNode = - result = buildHtml(): - section(class="navbar container grid-xl", id="main-buttons"): - section(class="navbar-section"): - tdiv(class="dropdown"): - a(href="#", class="btn dropdown-toggle"): - text "Filter " - italic(class="fas fa-caret-down") - ul(class="menu"): - li: text "community" - li: text "dev" - button(class="btn btn-primary"): text "Latest" - button(class="btn btn-link"): text "Most Active" - button(class="btn btn-link"): text "Categories" - section(class="navbar-section"): - if currentUser.isSome(): - a(id="new-thread-btn", href=makeUri("/newthread"), onClick=anchorCB): - button(class="btn btn-secondary"): - italic(class="fas fa-plus") - text " New Thread" - proc genUserAvatars(users: seq[User]): VNode = result = buildHtml(td): for user in users: @@ -168,10 +147,10 @@ when defined(js): else: state.list = some(list) - proc onLoadMore(ev: Event, n: VNode) = + proc onLoadMore(ev: Event, n: VNode, categoryId: int) = state.loading = true let start = state.list.get().threads.len - ajaxGet(makeUri("threads.json?start=" & $start), @[], onThreadList) + ajaxGet(makeUri("threads.json?start=" & $start & "&categoryId=" & $categoryId), @[], onThreadList) proc getInfo( list: seq[Thread], i: int, currentUser: Option[User] @@ -196,20 +175,20 @@ when defined(js): isNew: thread.creation > previousVisitAt ) - proc genThreadList(currentUser: Option[User]): VNode = + proc genThreadList(currentUser: Option[User], categoryId: int): VNode = if state.status != Http200: return renderError("Couldn't retrieve threads.", state.status) if state.list.isNone: if not state.loading: state.loading = true - ajaxGet(makeUri("threads.json"), @[], onThreadList) + ajaxGet(makeUri("threads.json?categoryId=" & $categoryId), @[], onThreadList) return buildHtml(tdiv(class="loading loading-lg")) let list = state.list.get() result = buildHtml(): - section(class="container grid-xl"): # TODO: Rename to `.thread-list`. + section(class="thread-list"): table(class="table", id="threads-list"): thead(): tr: @@ -239,10 +218,11 @@ when defined(js): td(colspan="6"): tdiv(class="loading loading-lg") else: - td(colspan="6", onClick=onLoadMore): + td(colspan="6", + onClick = (ev: Event, n: VNode) => (onLoadMore(ev, n, categoryId))): span(text "load more threads") - proc renderThreadList*(currentUser: Option[User]): VNode = + proc renderThreadList*(currentUser: Option[User], categoryId = -1): VNode = result = buildHtml(tdiv): - genTopButtons(currentUser) - genThreadList(currentUser) + renderMainButtons(currentUser) + genThreadList(currentUser, categoryId) From da7045ecca6bc9121aac0c97422d46ba2618d2f3 Mon Sep 17 00:00:00 2001 From: Joey Yakimowich-Payne Date: Mon, 17 Feb 2020 19:43:13 -0700 Subject: [PATCH 05/80] Cleanup --- src/buildcss.nim | 2 +- src/email.nim | 2 +- src/forum.nim | 1 - src/frontend/categorylist.nim | 4 ++-- src/frontend/categorypicker.nim | 3 --- src/frontend/error.nim | 4 ++-- src/frontend/header.nim | 3 ++- src/frontend/karaxutils.nim | 11 +++++++++++ src/frontend/post.nim | 4 ++-- src/frontend/threadlist.nim | 2 +- 10 files changed, 22 insertions(+), 14 deletions(-) diff --git a/src/buildcss.nim b/src/buildcss.nim index 551956a..129bb64 100644 --- a/src/buildcss.nim +++ b/src/buildcss.nim @@ -1,4 +1,4 @@ -import os, strutils +import os import sass diff --git a/src/email.nim b/src/email.nim index 8cf7b36..d26f4ad 100644 --- a/src/email.nim +++ b/src/email.nim @@ -20,7 +20,7 @@ proc newMailer*(config: Config): Mailer = proc rateCheck(mailer: Mailer, address: string): bool = ## Returns true if we've emailed the address too much. let diff = getTime() - mailer.lastReset - if diff.hours >= 1: + if diff.inHours >= 1: mailer.lastReset = getTime() mailer.emailsSent.clear() diff --git a/src/forum.nim b/src/forum.nim index fc9a17b..13c095e 100644 --- a/src/forum.nim +++ b/src/forum.nim @@ -814,7 +814,6 @@ routes: countQuery = sql"select count(*) from thread;" countArgs: seq[string] = @[] - if categoryId != -1: categorySection = "c.id == ? and " countQuery = sql"select count(*) from thread t, category c where category == c.id and c.id == ?;" diff --git a/src/frontend/categorylist.nim b/src/frontend/categorylist.nim index 231d697..37659c4 100644 --- a/src/frontend/categorylist.nim +++ b/src/frontend/categorylist.nim @@ -1,10 +1,10 @@ -import strformat, times, options, json, httpcore, sugar, strutils, uri +import options, json, httpcore import category when defined(js): include karax/prelude - import karax / [vstyles, kajax, kdom] + import karax / [vstyles, kajax] import karaxutils, error, user, mainbuttons diff --git a/src/frontend/categorypicker.nim b/src/frontend/categorypicker.nim index 32a107b..1224282 100644 --- a/src/frontend/categorypicker.nim +++ b/src/frontend/categorypicker.nim @@ -21,9 +21,6 @@ when defined(js): onCategoryChange: proc (oldCategory: Category, newCategory: Category) onAddCategory: proc (category: Category) - proc slug(name: string): string = - name.strip().replace(" ", "-").toLowerAscii - proc onCategoryLoad(state: CategoryPicker): proc (httpStatus: int, response: kstring) = return proc (httpStatus: int, response: kstring) = diff --git a/src/frontend/error.nim b/src/frontend/error.nim index 27f1e7c..0b67f5d 100644 --- a/src/frontend/error.nim +++ b/src/frontend/error.nim @@ -1,11 +1,11 @@ -import options, httpcore +import httpcore type PostError* = object errorFields*: seq[string] ## IDs of the fields with an error. message*: string when defined(js): - import json + import json, options include karax/prelude import karaxutils diff --git a/src/frontend/header.nim b/src/frontend/header.nim index edd1ade..aa6e786 100644 --- a/src/frontend/header.nim +++ b/src/frontend/header.nim @@ -1,4 +1,4 @@ -import options, times, httpcore, json, sugar +import options, httpcore import user type @@ -7,6 +7,7 @@ type recaptchaSiteKey*: Option[string] when defined(js): + import times, json, sugar include karax/prelude import karax / [kajax, kdom] diff --git a/src/frontend/karaxutils.nim b/src/frontend/karaxutils.nim index 8b2fe8f..f70ec5d 100644 --- a/src/frontend/karaxutils.nim +++ b/src/frontend/karaxutils.nim @@ -1,5 +1,16 @@ import strutils, strformat, parseutils, tables +proc limit*(str: string, n: int): string = + ## Limit the number of characters in a string. Ends with a elipsis + if str.len > n: + return str[0.. Date: Mon, 17 Feb 2020 19:44:47 -0700 Subject: [PATCH 06/80] Add number of topics to category list --- src/forum.nim | 10 ++++++++-- src/frontend/category.nim | 1 + src/frontend/categorylist.nim | 4 ++-- 3 files changed, 11 insertions(+), 4 deletions(-) diff --git a/src/forum.nim b/src/forum.nim index 13c095e..bc25b7b 100644 --- a/src/forum.nim +++ b/src/forum.nim @@ -791,12 +791,18 @@ routes: get "/categories.json": # TODO: Limit this query in the case of many many categories - const categoriesQuery = sql"""select * from category;""" + const categoriesQuery = + sql""" + select c.*, count(thread.category) + from category c + left join thread on c.id == thread.category + group by c.id; + """ var list = CategoryList(categories: @[]) for data in getAllRows(db, categoriesQuery): let category = Category( - id: data[0].getInt, name: data[1], description: data[2], color: data[3] + id: data[0].getInt, name: data[1], description: data[2], color: data[3], numTopics: data[4].parseInt ) list.categories.add(category) diff --git a/src/frontend/category.nim b/src/frontend/category.nim index b1a68d2..6894127 100644 --- a/src/frontend/category.nim +++ b/src/frontend/category.nim @@ -5,6 +5,7 @@ type name*: string description*: string color*: string + numTopics*: int CategoryList* = ref object categories*: seq[Category] diff --git a/src/frontend/categorylist.nim b/src/frontend/categorylist.nim index 37659c4..d6460a6 100644 --- a/src/frontend/categorylist.nim +++ b/src/frontend/categorylist.nim @@ -28,14 +28,14 @@ when defined(js): tr(class=class({"no-border": noBorder})): td(style=style((StyleAttr.borderLeftColor, kstring("#" & category.color))), class="category"): h4(class="category-title"): - a(href=makeUri("/c/" & $category.id)): + a(href=makeUri("/c/" & $category.id), id="category-" & category.name.slug): tdiv(): tdiv(class="category-name"): text category.name tdiv(class="category-description"): text category.description td(class="topics"): - text "Topics" + text $category.numTopics proc onCategoriesRetrieved(httpStatus: int, response: kstring) = state.loading = false From 38a21f34e6dfcaa3271240b1c885aa207cec84a8 Mon Sep 17 00:00:00 2001 From: Joey Yakimowich-Payne Date: Mon, 17 Feb 2020 19:46:56 -0700 Subject: [PATCH 07/80] Work around elements not refreshing on url operations --- src/frontend/forum.nim | 5 +++++ src/frontend/header.nim | 6 +++--- src/frontend/karaxutils.nim | 1 + 3 files changed, 9 insertions(+), 3 deletions(-) diff --git a/src/frontend/forum.nim b/src/frontend/forum.nim index 9285e72..d8fc003 100644 --- a/src/frontend/forum.nim +++ b/src/frontend/forum.nim @@ -2,6 +2,7 @@ import options, tables, sugar, httpcore from dom import window, Location, document, decodeURI include karax/prelude +import karax/[kdom] import jester/[patterns] import threadlist, postlist, header, profile, newthread, error, about @@ -56,6 +57,10 @@ proc onPopState(event: dom.Event) = state = newState() # Reload the state to remove stale data. state.url = copyLocation(window.location) + # For some reason this is needed so that the back/forward buttons reload + # the post list when different categories are selected + window.location.reload() + redraw() type Params = Table[string, string] diff --git a/src/frontend/header.nim b/src/frontend/header.nim index aa6e786..820de7c 100644 --- a/src/frontend/header.nim +++ b/src/frontend/header.nim @@ -39,15 +39,15 @@ when defined(js): loading: false, status: Http200, loginModal: newLoginModal( - () => (state.lastUpdate = fromUnix(0); getStatus()), + () => (state.lastUpdate = fromUnix(0); getStatus(); window.location.reload()), () => state.signupModal.show() ), signupModal: newSignupModal( - () => (state.lastUpdate = fromUnix(0); getStatus()), + () => (state.lastUpdate = fromUnix(0); getStatus(); window.location.reload()), () => state.loginModal.show() ), userMenu: newUserMenu( - () => (state.lastUpdate = fromUnix(0); getStatus(logout=true)) + () => (state.lastUpdate = fromUnix(0); getStatus(logout=true); window.location.reload()) ) ) diff --git a/src/frontend/karaxutils.nim b/src/frontend/karaxutils.nim index f70ec5d..cc26cd0 100644 --- a/src/frontend/karaxutils.nim +++ b/src/frontend/karaxutils.nim @@ -97,6 +97,7 @@ when defined(js): let url = n.getAttr("href") navigateTo(url) + window.location.href = url proc newFormData*(form: dom.Element): FormData {.importcpp: "new FormData(@)", constructor.} From f3396777fb195395e2b76953ccae193290c98a9f Mon Sep 17 00:00:00 2001 From: Joey Yakimowich-Payne Date: Mon, 17 Feb 2020 19:48:10 -0700 Subject: [PATCH 08/80] Add expanded version of category drop down and add it to categories page --- public/css/nimforum.scss | 13 +++++++++++++ src/frontend/category.nim | 24 ++++++++++++++++++------ src/frontend/categorypicker.nim | 6 +++--- src/frontend/mainbuttons.nim | 15 ++++++++++++--- src/frontend/threadlist.nim | 15 +++++++++------ 5 files changed, 55 insertions(+), 18 deletions(-) diff --git a/public/css/nimforum.scss b/public/css/nimforum.scss index 7f3b257..098ad6e 100644 --- a/public/css/nimforum.scss +++ b/public/css/nimforum.scss @@ -122,6 +122,19 @@ $logo-height: $navbar-height - 20px; } } +.category-description { + opacity: 0.6; + font-size: small; +} + +.category-status { + .topic-count { + margin-left: 5px; + opacity: 0.7; + font-size: small; + } +} + .category { white-space: nowrap; } diff --git a/src/frontend/category.nim b/src/frontend/category.nim index 6894127..0e70bc0 100644 --- a/src/frontend/category.nim +++ b/src/frontend/category.nim @@ -10,16 +10,23 @@ type CategoryList* = ref object categories*: seq[Category] +const categoryDescriptionCharLimit = 250 + proc cmpNames*(cat1: Category, cat2: Category): int = cat1.name.cmp(cat2.name) when defined(js): include karax/prelude import karax / [vstyles] + import karaxutils - proc render*(category: Category): VNode = - result = buildHtml(): - if category.name.len >= 0: + proc render*(category: Category, compact=false): VNode = + if category.name.len == 0: + return buildHtml(): + span() + + result = buildhtml(tdiv): + tdiv(class="category-status"): tdiv(class="category", title=category.description, "data-color"="#" & category.color): @@ -28,6 +35,11 @@ when defined(js): (StyleAttr.border, kstring"0.3rem solid #" & category.color) )) - text category.name - else: - span() + span(class="category-name"): + text category.name + if not compact: + span(class="topic-count"): + text "× " & $category.numTopics + if not compact: + tdiv(class="category-description"): + text category.description.limit(categoryDescriptionCharLimit) \ No newline at end of file diff --git a/src/frontend/categorypicker.nim b/src/frontend/categorypicker.nim index 1224282..6c89846 100644 --- a/src/frontend/categorypicker.nim +++ b/src/frontend/categorypicker.nim @@ -155,7 +155,7 @@ when defined(js): state.onAddCategoryClick()): text "Add" - proc render*(state: CategoryPicker, currentUser: Option[User]): VNode = + proc render*(state: CategoryPicker, currentUser: Option[User], compact=false): VNode = let loggedIn = currentUser.isSome() let currentAdmin = loggedIn and currentUser.get().rank == Admin @@ -178,7 +178,7 @@ when defined(js): tdiv(class="dropdown"): a(class="btn btn-link dropdown-toggle", tabindex="0"): tdiv(class="selected-category d-inline-block"): - render(selectedCategory) + render(selectedCategory, compact=true) text " " italic(class="fas fa-caret-down") ul(class="menu"): @@ -186,6 +186,6 @@ when defined(js): li(class="menu-item"): a(class="category-" & $category.id & " " & category.name.slug, onClick=onCategoryClick(state, category)): - render(category) + render(category, compact) if state.addEnabled: genAddCategory(state) diff --git a/src/frontend/mainbuttons.nim b/src/frontend/mainbuttons.nim index 4c46478..15536d8 100644 --- a/src/frontend/mainbuttons.nim +++ b/src/frontend/mainbuttons.nim @@ -3,16 +3,22 @@ import user when defined(js): include karax/prelude - import karax / [vstyles, kajax, kdom] + import karax / [kdom] - import karaxutils, error, user + import karaxutils, user, categorypicker, category let buttons = [ (name: "Latest", url: makeUri("/"), id: "latest-btn"), (name: "Categories", url: makeUri("/categories"), id: "categories-btn"), ] - proc renderMainButtons*(currentUser: Option[User]): VNode = + proc onSelectedCategoryChanged(oldCategory: Category, newCategory: Category) = + let uri = makeUri("/c/" & $newCategory.id) + navigateTo(uri) + + let catPicker = newCategoryPicker(onCategoryChange=onSelectedCategoryChanged) + + proc renderMainButtons*(currentUser: Option[User], categoryId = -1): VNode = result = buildHtml(): section(class="navbar container grid-xl", id="main-buttons"): section(class="navbar-section"): @@ -23,6 +29,9 @@ when defined(js): ul(class="menu"): li: text "community" li: text "dev" ]# + if categoryId != -1: + catPicker.selectedCategoryID = categoryId + render(catPicker, currentUser) for btn in buttons: let active = btn.url == window.location.href diff --git a/src/frontend/threadlist.nim b/src/frontend/threadlist.nim index 2724f3c..e5f05b1 100644 --- a/src/frontend/threadlist.nim +++ b/src/frontend/threadlist.nim @@ -88,7 +88,7 @@ when defined(js): else: return $duration.inSeconds & "s" - proc genThread(thread: Thread, isNew: bool, noBorder: bool): 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(): @@ -109,8 +109,9 @@ when defined(js): a(href=makeUri("/t/" & $thread.id), onClick=anchorCB): text thread.topic - td(): - render(thread.category) + if displayCategory: + td(): + render(thread.category, compact=true) genUserAvatars(thread.users) td(): text $thread.replies td(class=class({ @@ -193,7 +194,8 @@ when defined(js): thead(): tr: th(text "Topic") - th(text "Category") + if categoryId == -1: + th(text "Category") th(style=style((StyleAttr.width, kstring"8rem"))): text "Users" th(text "Replies") th(text "Views") @@ -206,7 +208,8 @@ when defined(js): let isLastThread = i+1 == list.threads.len let (isLastUnseen, isNew) = getInfo(list.threads, i, currentUser) genThread(thread, isNew, - noBorder=isLastUnseen or isLastThread) + noBorder=isLastUnseen or isLastThread, + displayCategory=categoryId == -1) if isLastUnseen and (not isLastThread): tr(class="last-visit-separator"): td(colspan="6"): @@ -224,5 +227,5 @@ when defined(js): proc renderThreadList*(currentUser: Option[User], categoryId = -1): VNode = result = buildHtml(tdiv): - renderMainButtons(currentUser) + renderMainButtons(currentUser, categoryId=categoryId) genThreadList(currentUser, categoryId) From 6c70713afbc6f07d25899398a5f825558ef88967 Mon Sep 17 00:00:00 2001 From: Joey Yakimowich-Payne Date: Mon, 17 Feb 2020 19:49:22 -0700 Subject: [PATCH 09/80] Cleanup test imports --- tests/browsertests/categories.nim | 8 +------- tests/browsertests/issue181.nim | 2 +- tests/browsertests/scenario1.nim | 2 +- 3 files changed, 3 insertions(+), 9 deletions(-) diff --git a/tests/browsertests/categories.nim b/tests/browsertests/categories.nim index 9c6cbf2..70be70b 100644 --- a/tests/browsertests/categories.nim +++ b/tests/browsertests/categories.nim @@ -1,14 +1,12 @@ -import unittest, options, os, common +import unittest, options, common import webdriver proc selectCategory(session: Session, name: string) = with session: click "#category-selection .dropdown-toggle" - click "#category-selection ." & name - proc categoriesUserTests(session: Session, baseUrl: string) = let title = "Category Test" @@ -33,15 +31,12 @@ proc categoriesUserTests(session: Session, baseUrl: string) = test "can create category thread": with session: click "#new-thread-btn" - sendKeys "#thread-title", title selectCategory "fun" - sendKeys "#reply-textarea", content click "#create-thread-btn" - checkText "#thread-title .category", "Fun" navigate baseUrl @@ -73,7 +68,6 @@ proc categoriesAdminTests(session: Session, baseUrl: string) = clear "#add-category input[name='color']" clear "#add-category input[name='description']" - sendKeys "#add-category input[name='name']", name sendKeys "#add-category input[name='color']", color sendKeys "#add-category input[name='description']", description diff --git a/tests/browsertests/issue181.nim b/tests/browsertests/issue181.nim index 504677c..f48929f 100644 --- a/tests/browsertests/issue181.nim +++ b/tests/browsertests/issue181.nim @@ -1,4 +1,4 @@ -import unittest, options, os, common +import unittest, options, common import webdriver diff --git a/tests/browsertests/scenario1.nim b/tests/browsertests/scenario1.nim index 054f7b9..9237c2e 100644 --- a/tests/browsertests/scenario1.nim +++ b/tests/browsertests/scenario1.nim @@ -1,4 +1,4 @@ -import unittest, options, os, common +import unittest, options, common import webdriver From fd80f754d21ee63fb2d6171fa66e5fc94a540393 Mon Sep 17 00:00:00 2001 From: Joey Yakimowich-Payne Date: Mon, 17 Feb 2020 20:03:18 -0700 Subject: [PATCH 10/80] Make categories compact by default --- src/frontend/category.nim | 2 +- src/frontend/categorypicker.nim | 4 ++-- src/frontend/mainbuttons.nim | 2 +- src/frontend/postlist.nim | 2 +- src/frontend/threadlist.nim | 2 +- 5 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/frontend/category.nim b/src/frontend/category.nim index 0e70bc0..75ee947 100644 --- a/src/frontend/category.nim +++ b/src/frontend/category.nim @@ -20,7 +20,7 @@ when defined(js): import karax / [vstyles] import karaxutils - proc render*(category: Category, compact=false): VNode = + proc render*(category: Category, compact=true): VNode = if category.name.len == 0: return buildHtml(): span() diff --git a/src/frontend/categorypicker.nim b/src/frontend/categorypicker.nim index 6c89846..cc14d62 100644 --- a/src/frontend/categorypicker.nim +++ b/src/frontend/categorypicker.nim @@ -155,7 +155,7 @@ when defined(js): state.onAddCategoryClick()): text "Add" - proc render*(state: CategoryPicker, currentUser: Option[User], compact=false): VNode = + proc render*(state: CategoryPicker, currentUser: Option[User], compact=true): VNode = let loggedIn = currentUser.isSome() let currentAdmin = loggedIn and currentUser.get().rank == Admin @@ -178,7 +178,7 @@ when defined(js): tdiv(class="dropdown"): a(class="btn btn-link dropdown-toggle", tabindex="0"): tdiv(class="selected-category d-inline-block"): - render(selectedCategory, compact=true) + render(selectedCategory) text " " italic(class="fas fa-caret-down") ul(class="menu"): diff --git a/src/frontend/mainbuttons.nim b/src/frontend/mainbuttons.nim index 15536d8..9dc1fe2 100644 --- a/src/frontend/mainbuttons.nim +++ b/src/frontend/mainbuttons.nim @@ -31,7 +31,7 @@ when defined(js): li: text "dev" ]# if categoryId != -1: catPicker.selectedCategoryID = categoryId - render(catPicker, currentUser) + render(catPicker, currentUser, compact=false) for btn in buttons: let active = btn.url == window.location.href diff --git a/src/frontend/postlist.nim b/src/frontend/postlist.nim index a516a9f..305e059 100644 --- a/src/frontend/postlist.nim +++ b/src/frontend/postlist.nim @@ -215,7 +215,7 @@ when defined(js): result = buildHtml(): tdiv(): if authoredByUser or currentAdmin: - render(state.categoryPicker, currentUser) + render(state.categoryPicker, currentUser, compact=false) else: render(thread.category) diff --git a/src/frontend/threadlist.nim b/src/frontend/threadlist.nim index e5f05b1..98a1f8d 100644 --- a/src/frontend/threadlist.nim +++ b/src/frontend/threadlist.nim @@ -111,7 +111,7 @@ when defined(js): text thread.topic if displayCategory: td(): - render(thread.category, compact=true) + render(thread.category) genUserAvatars(thread.users) td(): text $thread.replies td(class=class({ From 433a21aa87cf64002e22828839aa616660cdef18 Mon Sep 17 00:00:00 2001 From: Joey Yakimowich-Payne Date: Mon, 17 Feb 2020 21:01:47 -0700 Subject: [PATCH 11/80] Fix compact and reloading --- src/frontend/header.nim | 31 ++++++++++++++++++------------- src/frontend/newthread.nim | 2 +- 2 files changed, 19 insertions(+), 14 deletions(-) diff --git a/src/frontend/header.nim b/src/frontend/header.nim index 820de7c..679c8fa 100644 --- a/src/frontend/header.nim +++ b/src/frontend/header.nim @@ -32,36 +32,41 @@ when defined(js): var state = newState() - proc getStatus(logout: bool=false) + proc getStatus(logout=false, reload=false) proc newState(): State = State( data: none[UserStatus](), loading: false, status: Http200, loginModal: newLoginModal( - () => (state.lastUpdate = fromUnix(0); getStatus(); window.location.reload()), + () => (state.lastUpdate = fromUnix(0); getStatus(reload=true)), () => state.signupModal.show() ), signupModal: newSignupModal( - () => (state.lastUpdate = fromUnix(0); getStatus(); window.location.reload()), + () => (state.lastUpdate = fromUnix(0); getStatus(reload=true)), () => state.loginModal.show() ), userMenu: newUserMenu( - () => (state.lastUpdate = fromUnix(0); getStatus(logout=true); window.location.reload()) + () => (state.lastUpdate = fromUnix(0); getStatus(logout=true, reload=true)) ) ) - proc onStatus(httpStatus: int, response: kstring) = - state.loading = false - state.status = httpStatus.HttpCode - if state.status != Http200: return + proc onStatus(reload: bool=false): proc (httpStatus: int, response: kstring) = + result = + proc (httpStatus: int, response: kstring) = + state.loading = false + state.status = httpStatus.HttpCode + if state.status != Http200: return - let parsed = parseJson($response) - state.data = some(to(parsed, UserStatus)) + let parsed = parseJson($response) + state.data = some(to(parsed, UserStatus)) - state.lastUpdate = getTime() + state.lastUpdate = getTime() - proc getStatus(logout: bool=false) = + if reload: + window.location.reload() + + proc getStatus(logout=false, reload=false) = if state.loading: return let diff = getTime() - state.lastUpdate if diff.inMinutes < 5: @@ -69,7 +74,7 @@ when defined(js): state.loading = true let uri = makeUri("status.json", [("logout", $logout)]) - ajaxGet(uri, @[], onStatus) + ajaxGet(uri, @[], onStatus(reload)) proc getLoggedInUser*(): Option[User] = state.data.map(x => x.user).flatten diff --git a/src/frontend/newthread.nim b/src/frontend/newthread.nim index d314985..9189bc0 100644 --- a/src/frontend/newthread.nim +++ b/src/frontend/newthread.nim @@ -65,7 +65,7 @@ when defined(js): tdiv(): label(class="d-inline-block form-label"): text "Category" - render(state.categoryPicker, currentUser) + render(state.categoryPicker, currentUser, compact=false) renderContent(state.replyBox, none[Thread](), none[Post]()) tdiv(class="footer"): From 1f0f736915e1ec891a720ebdf021876a45446951 Mon Sep 17 00:00:00 2001 From: Joey Yakimowich-Payne Date: Mon, 17 Feb 2020 21:05:07 -0700 Subject: [PATCH 12/80] Fix category clicking --- src/frontend/categorylist.nim | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/frontend/categorylist.nim b/src/frontend/categorylist.nim index d6460a6..5ad4c7f 100644 --- a/src/frontend/categorylist.nim +++ b/src/frontend/categorylist.nim @@ -27,8 +27,8 @@ when defined(js): result = buildHtml(): tr(class=class({"no-border": noBorder})): td(style=style((StyleAttr.borderLeftColor, kstring("#" & category.color))), class="category"): - h4(class="category-title"): - a(href=makeUri("/c/" & $category.id), id="category-" & category.name.slug): + h4(class="category-title", id="category-" & category.name.slug): + a(href=makeUri("/c/" & $category.id)): tdiv(): tdiv(class="category-name"): text category.name From 5e033d035610f8a535d22aac5fcd3afdcd6da769 Mon Sep 17 00:00:00 2001 From: Joey Yakimowich-Payne Date: Tue, 18 Feb 2020 13:53:30 -0700 Subject: [PATCH 13/80] Upgrade webdriver and run test backend separately --- nimforum.nimble | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/nimforum.nimble b/nimforum.nimble index 1b32ba2..09873ca 100644 --- a/nimforum.nimble +++ b/nimforum.nimble @@ -21,7 +21,7 @@ requires "sass#649e0701fa5c" requires "karax#f6bda9a" -requires "webdriver#c2fee57" +requires "webdriver#7091895" # Tasks @@ -32,6 +32,9 @@ task backend, "Compiles and runs the forum backend": task runbackend, "Runs the forum backend": exec "./src/forum" +task testbackend, "Runs the forum backend in test mode": + exec "nimble c -r -d:skipRateLimitCheck src/forum.nim" + task frontend, "Builds the necessary JS frontend (with CSS)": exec "nimble c -r src/buildcss" exec "nimble js -d:release src/frontend/forum.nim" From 91746924dc8db2f05dee4bf9c107d35d4fbaaf40 Mon Sep 17 00:00:00 2001 From: Joey Yakimowich-Payne Date: Tue, 18 Feb 2020 13:54:04 -0700 Subject: [PATCH 14/80] Remove rate limit check on compiler flag --- src/forum.nim | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/forum.nim b/src/forum.nim index bc25b7b..aee5ad4 100644 --- a/src/forum.nim +++ b/src/forum.nim @@ -434,8 +434,9 @@ proc executeReply(c: TForumData, threadId: int, content: string, else: raise newForumError("You are not allowed to post") - if rateLimitCheck(c): - raise newForumError("You're posting too fast!") + when not defined(skipRateLimitCheck): + if rateLimitCheck(c): + raise newForumError("You're posting too fast!") if content.strip().len == 0: raise newForumError("Message cannot be empty") @@ -567,8 +568,9 @@ proc executeNewThread(c: TForumData, subject, msg, categoryID: string): (int64, if not validateRst(c, msg): raise newForumError("Message needs to be valid RST", @["msg"]) - if rateLimitCheck(c): - raise newForumError("You're posting too fast!") + when not defined(skipRateLimitCheck): + if rateLimitCheck(c): + raise newForumError("You're posting too fast!") result[0] = tryInsertID(db, query, subject, categoryID).int if result[0] < 0: From 5fc811e797e0a49590a6dea99723d78868b0c879 Mon Sep 17 00:00:00 2001 From: Joey Yakimowich-Payne Date: Tue, 18 Feb 2020 13:54:49 -0700 Subject: [PATCH 15/80] Only hide category on category page for testing purposes --- src/frontend/threadlist.nim | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/src/frontend/threadlist.nim b/src/frontend/threadlist.nim index 98a1f8d..c93def1 100644 --- a/src/frontend/threadlist.nim +++ b/src/frontend/threadlist.nim @@ -109,9 +109,8 @@ when defined(js): a(href=makeUri("/t/" & $thread.id), onClick=anchorCB): text thread.topic - if displayCategory: - td(): - render(thread.category) + td(class=class({"d-none": not displayCategory})): + render(thread.category) genUserAvatars(thread.users) td(): text $thread.replies td(class=class({ @@ -187,6 +186,8 @@ when defined(js): return buildHtml(tdiv(class="loading loading-lg")) + let displayCategory = true + let list = state.list.get() result = buildHtml(): section(class="thread-list"): @@ -194,8 +195,8 @@ when defined(js): thead(): tr: th(text "Topic") - if categoryId == -1: - th(text "Category") + th(class=class({"d-none": not displayCategory})): + text "Category" th(style=style((StyleAttr.width, kstring"8rem"))): text "Users" th(text "Replies") th(text "Views") @@ -209,7 +210,7 @@ when defined(js): let (isLastUnseen, isNew) = getInfo(list.threads, i, currentUser) genThread(thread, isNew, noBorder=isLastUnseen or isLastThread, - displayCategory=categoryId == -1) + displayCategory=displayCategory) if isLastUnseen and (not isLastThread): tr(class="last-visit-separator"): td(colspan="6"): From 5ce99a5a3d4fa77a4f05d9a62c18b9526376f353 Mon Sep 17 00:00:00 2001 From: Joey Yakimowich-Payne Date: Tue, 18 Feb 2020 13:56:38 -0700 Subject: [PATCH 16/80] Add tests and improve testing experience --- tests/browsertester.nim | 4 +- tests/browsertests/categories.nim | 74 ++++++++++++++++++++++++++++++- tests/browsertests/common.nim | 40 +++++++++++++++-- 3 files changed, 111 insertions(+), 7 deletions(-) diff --git a/tests/browsertester.nim b/tests/browsertester.nim index 6a5c374..8b6c046 100644 --- a/tests/browsertester.nim +++ b/tests/browsertester.nim @@ -23,7 +23,7 @@ const baseUrl = "http://localhost:" & $port & "/" template withBackend(body: untyped): untyped = ## Starts a new backend instance. - spawn runProcess("nimble -y runbackend") + spawn runProcess("nimble -y testbackend") defer: discard execCmd("killall " & backend) @@ -46,6 +46,8 @@ template withBackend(body: untyped): untyped = import browsertests/[scenario1, threads, issue181, categories] proc main() = + # Kill any already running instances + discard execCmd("killall geckodriver") spawn runProcess("geckodriver -p 4444 --log config") defer: discard execCmd("killall geckodriver") diff --git a/tests/browsertests/categories.nim b/tests/browsertests/categories.nim index 70be70b..d56b47c 100644 --- a/tests/browsertests/categories.nim +++ b/tests/browsertests/categories.nim @@ -1,4 +1,4 @@ -import unittest, options, common +import unittest, options, common, os import webdriver @@ -43,6 +43,65 @@ proc categoriesUserTests(session: Session, baseUrl: string) = ensureExists title, LinkTextSelector + test "can navigate to categories page": + with session: + click "#categories-btn" + + ensureExists "#categories-list" + + test "can view post under category": + with session: + + # create a few threads + click "#new-thread-btn" + sendKeys "#thread-title", "Post 1" + + selectCategory "fun" + sendKeys "#reply-textarea", "Post 1" + + click "#create-thread-btn" + navigate baseUrl + + + click "#new-thread-btn" + sendKeys "#thread-title", "Post 2" + + selectCategory "announcements" + sendKeys "#reply-textarea", "Post 2" + + click "#create-thread-btn" + navigate baseUrl + + + click "#new-thread-btn" + sendKeys "#thread-title", "Post 3" + + selectCategory "default" + sendKeys "#reply-textarea", "Post 3" + + click "#create-thread-btn" + navigate baseUrl + + + click "#categories-btn" + ensureExists "#categories-list" + + click "#category-default" + checkText "#threads-list .thread-title", "Post 3" + for element in session.waitForElements("#threads-list .category-name"): + # Have to user "innerText" because elements are hidden on this page + assert element.getProperty("innerText") == "Default" + + selectCategory "announcements" + checkText "#threads-list .thread-title", "Post 2" + for element in session.waitForElements("#threads-list .category-name"): + assert element.getProperty("innerText") == "Announcements" + + selectCategory "fun" + checkText "#threads-list .thread-title", "Post 1" + for element in session.waitForElements("#threads-list .category-name"): + assert element.getProperty("innerText") == "Fun" + session.logout() proc categoriesAdminTests(session: Session, baseUrl: string) = @@ -76,6 +135,17 @@ proc categoriesAdminTests(session: Session, baseUrl: string) = checkText "#category-selection .selected-category", name + test "category adding disabled on admin logout": + with session: + navigate(baseUrl & "c/0") + ensureExists "#add-category" + logout() + + checkIsNone "#add-category" + navigate baseUrl + + login "admin", "admin" + session.logout() proc test*(session: Session, baseUrl: string) = @@ -84,4 +154,4 @@ proc test*(session: Session, baseUrl: string) = categoriesUserTests(session, baseUrl) categoriesAdminTests(session, baseUrl) - session.navigate(baseUrl) \ No newline at end of file + session.navigate(baseUrl) diff --git a/tests/browsertests/common.nim b/tests/browsertests/common.nim index e967abd..9d8b61a 100644 --- a/tests/browsertests/common.nim +++ b/tests/browsertests/common.nim @@ -8,13 +8,22 @@ const actionDelayMs {.intdefine.} = 0 macro with*(obj: typed, code: untyped): untyped = ## Execute a set of statements with an object expectKind code, nnkStmtList - result = code + + template checkCompiles(res, default) = + when compiles(res): + res + else: + default + + result = code.copy # Simply inject obj into call for i in 0 ..< result.len: - if result[i].kind in {nnkCommand, nnkCall}: + if result[i].kind in {nnkCommand, nnkCall} and $result[i][0].toStrLit != "assert": result[i].insert(1, obj) + result = getAst(checkCompiles(result, code)) + proc elementIsSome(element: Option[Element]): bool = return element.isSome @@ -73,8 +82,31 @@ proc waitForElement*( sleep(actionDelayMs) while true: - let loading = session.findElement(selector, strategy) - if waitCondition(loading): + try: + let loading = session.findElement(selector, strategy) + if waitCondition(loading): + return loading + finally: + discard + sleep(pollTime) + waitTime += pollTime + + if waitTime > timeout: + doAssert false, "Wait for load time exceeded" + +proc waitForElements*( + session: Session, selector: string, strategy=CssSelector, + timeout=20000, pollTime=50 +): seq[Element] = + var waitTime = 0 + + when actionDelayMs > 0: + sleep(actionDelayMs) + + while true: + let loading = session.findElements(selector, strategy) + echo loading + if loading.len > 0: return loading sleep(pollTime) waitTime += pollTime From 7ec3ff9cacc2a3fde35fcafe8f0fc004e95fdebe Mon Sep 17 00:00:00 2001 From: Joey Yakimowich-Payne Date: Wed, 19 Feb 2020 16:57:50 -0700 Subject: [PATCH 17/80] Minor cleanup --- src/forum.nim | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/forum.nim b/src/forum.nim index aee5ad4..70baf54 100644 --- a/src/forum.nim +++ b/src/forum.nim @@ -151,7 +151,7 @@ proc checkLoggedIn(c: TForumData) = ) c.previousVisitAt = personRow[1].parseInt let diff = getTime() - fromUnix(personRow[0].parseInt) - if diff.minutes > 30: + if diff.inMinutes > 30: c.previousVisitAt = personRow[0].parseInt db.exec( sql""" @@ -238,7 +238,7 @@ proc verifyIdentHash( let newIdent = makeIdentHash(name, row[0], epoch, row[1]) # Check that it hasn't expired. let diff = getTime() - epoch.fromUnix() - if diff.hours > 2: + if diff.inHours > 2: raise newForumError("Link expired") if newIdent != ident: raise newForumError("Invalid ident hash") @@ -498,7 +498,7 @@ proc updatePost(c: TForumData, postId: int, content: string, # 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 isArchived = (getTime() - creation).inWeeks > 8 let canEdit = c.rank == Admin or c.userid == postRow[0] if isArchived: raise newForumError("This post is archived and can no longer be edited") From 95b21874dba6840f14b0061b0ffa324f02c0d140 Mon Sep 17 00:00:00 2001 From: Joey Yakimowich-Payne Date: Thu, 20 Feb 2020 17:54:19 -0700 Subject: [PATCH 18/80] Separate out add category modal --- src/frontend/addcategorymodal.nim | 87 ++++++++++++++++++++++++ src/frontend/category.nim | 3 + src/frontend/categorypicker.nim | 109 ++++++++---------------------- src/frontend/user.nim | 5 +- 4 files changed, 121 insertions(+), 83 deletions(-) create mode 100644 src/frontend/addcategorymodal.nim diff --git a/src/frontend/addcategorymodal.nim b/src/frontend/addcategorymodal.nim new file mode 100644 index 0000000..1232afb --- /dev/null +++ b/src/frontend/addcategorymodal.nim @@ -0,0 +1,87 @@ +when defined(js): + import sugar, httpcore, options, json, strutils + import dom except Event + import jsffi except `&` + + include karax/prelude + import karax / [kajax, kdom, vdom] + + import error, category + import category, karaxutils + + type + AddCategoryModal* = ref object of VComponent + modalShown: bool + loading: bool + error: Option[PostError] + onAddCategory: CategoryEvent + + let nullCategory: CategoryEvent = proc (category: Category) = discard + + proc newAddCategoryModal*(onAddCategory=nullCategory): AddCategoryModal = + result = AddCategoryModal( + modalShown: false, + loading: false, + onAddCategory: onAddCategory + ) + + proc onAddCategoryPost(httpStatus: int, response: kstring, state: AddCategoryModal) = + postFinished: + state.modalShown = false + let j = parseJson($response) + let category = j.to(Category) + + state.onAddCategory(category) + + proc onAddCategoryClick(state: AddCategoryModal) = + state.loading = true + state.error = none[PostError]() + + let uri = makeUri("createCategory") + let form = dom.document.getElementById("add-category-form") + let formData = newFormData(form) + + ajaxPost(uri, @[], formData.to(cstring), + (s: int, r: kstring) => onAddCategoryPost(s, r, state)) + + proc setModalShown*(state: AddCategoryModal, visible: bool) = + state.modalShown = visible + state.markDirty() + + proc onModalClose(state: AddCategoryModal, ev: Event, n: VNode) = + state.setModalShown(false) + ev.preventDefault() + + proc render*(state: AddCategoryModal): VNode = + result = buildHtml(): + tdiv(class=class({"active": state.modalShown}, "modal modal-sm")): + a(href="", class="modal-overlay", "aria-label"="close", + onClick=(ev: Event, n: VNode) => onModalClose(state, ev, n)) + tdiv(class="modal-container"): + tdiv(class="modal-header"): + tdiv(class="card-title h5"): + text "Add New Category" + tdiv(class="modal-body"): + form(id="add-category-form"): + genFormField( + state.error, "name", "Name", "text", false, + placeholder="Category Name") + genFormField( + state.error, "color", "Color", "color", false, + placeholder="#XXYYZZ" + ) + genFormField( + state.error, + "description", + "Description", + "text", + true, + placeholder="Description" + ) + tdiv(class="modal-footer"): + button( + id="add-category-btn", + class="btn btn-primary", + onClick=(ev: Event, n: VNode) => + state.onAddCategoryClick()): + text "Add" diff --git a/src/frontend/category.nim b/src/frontend/category.nim index 75ee947..7b46194 100644 --- a/src/frontend/category.nim +++ b/src/frontend/category.nim @@ -10,6 +10,9 @@ type CategoryList* = ref object categories*: seq[Category] + CategoryEvent* = proc (category: Category) {.closure.} + CategoryChangeEvent* = proc (oldCategory: Category, newCategory: Category) {.closure.} + const categoryDescriptionCharLimit = 250 proc cmpNames*(cat1: Category, cat2: Category): int = diff --git a/src/frontend/categorypicker.nim b/src/frontend/categorypicker.nim index cc14d62..38f9300 100644 --- a/src/frontend/categorypicker.nim +++ b/src/frontend/categorypicker.nim @@ -1,25 +1,24 @@ when defined(js): import sugar, httpcore, options, json, strutils, algorithm import dom except Event - import jsffi except `&` include karax/prelude import karax / [kajax, kdom, vdom] import error, category, user - import category, karaxutils + import category, karaxutils, addcategorymodal type CategoryPicker* = ref object of VComponent list: Option[CategoryList] selectedCategoryID*: int loading: bool - modalShown: bool addEnabled: bool status: HttpCode error: Option[PostError] - onCategoryChange: proc (oldCategory: Category, newCategory: Category) - onAddCategory: proc (category: Category) + addCategoryModal: AddCategoryModal + onCategoryChange: CategoryChangeEvent + onAddCategory: CategoryEvent proc onCategoryLoad(state: CategoryPicker): proc (httpStatus: int, response: kstring) = return @@ -51,18 +50,26 @@ when defined(js): return cat raise newException(IndexError, "Category at " & $id & " not found!") - proc nullAddCategory(category: Category) = discard - proc nullCategoryChange(oldCategory: Category, newCategory: Category) = discard + let nullAddCategory: CategoryEvent = proc (category: Category) = discard + let nullCategoryChange: CategoryChangeEvent = proc (oldCategory: Category, newCategory: Category) = discard - proc newCategoryPicker*( - onCategoryChange: proc(oldCategory: Category, newCategory: Category) = nullCategoryChange, - onAddCategory: proc(category: Category) = nullAddCategory - ): CategoryPicker = + proc select*(state: CategoryPicker, id: int) = + state.selectedCategoryID = id + state.markDirty() + + proc onCategory(state: CategoryPicker): CategoryEvent = + result = + proc (category: Category) = + state.list.get().categories.add(category) + state.list.get().categories.sort(cmpNames) + state.select(category.id) + state.onAddCategory(category) + + proc newCategoryPicker*(onCategoryChange=nullCategoryChange, onAddCategory=nullAddCategory): CategoryPicker = result = CategoryPicker( list: none[CategoryList](), selectedCategoryID: 0, loading: false, - modalShown: false, addEnabled: false, status: Http200, error: none[PostError](), @@ -70,13 +77,14 @@ when defined(js): onAddCategory: onAddCategory ) + let state = result + result.addCategoryModal = newAddCategoryModal( + onAddCategory=onCategory(state) + ) + proc setAddEnabled*(state: CategoryPicker, enabled: bool) = state.addEnabled = enabled - proc select*(state: CategoryPicker, id: int) = - state.selectedCategoryID = id - state.markDirty() - proc onCategoryClick(state: CategoryPicker, category: Category): proc (ev: Event, n: VNode) = # this is necessary to capture the right value let cat = category @@ -86,81 +94,18 @@ when defined(js): state.select(cat.id) state.onCategoryChange(oldCategory, cat) - proc onAddCategoryPost(httpStatus: int, response: kstring, state: CategoryPicker) = - postFinished: - state.modalShown = false - let j = parseJson($response) - let category = j.to(Category) - - state.list.get().categories.add(category) - state.list.get().categories.sort(cmpNames) - state.select(category.id) - - state.onAddCategory(category) - - proc onAddCategoryClick(state: CategoryPicker) = - state.loading = true - state.error = none[PostError]() - - let uri = makeUri("createCategory") - let form = dom.document.getElementById("add-category-form") - let formData = newFormData(form) - - ajaxPost(uri, @[], formData.to(cstring), - (s: int, r: kstring) => onAddCategoryPost(s, r, state)) - - proc onClose(ev: Event, n: VNode, state: CategoryPicker) = - state.modalShown = false - state.markDirty() - ev.preventDefault() - proc genAddCategory(state: CategoryPicker): VNode = result = buildHtml(): tdiv(id="add-category"): button(class="plus-btn btn btn-link", onClick=(ev: Event, n: VNode) => ( - state.modalShown = true; - state.markDirty() + state.addCategoryModal.setModalShown(true) )): italic(class="fas fa-plus") - tdiv(class=class({"active": state.modalShown}, "modal modal-sm")): - 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"): - tdiv(class="card-title h5"): - text "Add New Category" - tdiv(class="modal-body"): - form(id="add-category-form"): - genFormField( - state.error, "name", "Name", "text", false, - placeholder="Category Name") - genFormField( - state.error, "color", "Color", "color", false, - placeholder="#XXYYZZ" - ) - genFormField( - state.error, - "description", - "Description", - "text", - true, - placeholder="Description" - ) - tdiv(class="modal-footer"): - button( - id="add-category-btn", - class="btn btn-primary", - onClick=(ev: Event, n: VNode) => - state.onAddCategoryClick()): - text "Add" + render(state.addCategoryModal) proc render*(state: CategoryPicker, currentUser: Option[User], compact=true): VNode = - let loggedIn = currentUser.isSome() - let currentAdmin = - loggedIn and currentUser.get().rank == Admin - - if currentAdmin: + if currentUser.isAdmin(): state.setAddEnabled(true) if state.status != Http200: diff --git a/src/frontend/user.nim b/src/frontend/user.nim index ea0624b..fe4efa7 100644 --- a/src/frontend/user.nim +++ b/src/frontend/user.nim @@ -1,4 +1,4 @@ -import times +import times, options type # If you add more "Banned" states, be sure to modify forum's threadsQuery too. @@ -27,6 +27,9 @@ type proc isOnline*(user: User): bool = return getTime().toUnix() - user.lastOnline < (60*5) +proc isAdmin*(user: Option[User]): bool = + return user.isSome and user.get().rank == Admin + proc `==`*(u1, u2: User): bool = u1.name == u2.name From 9127bc4c884390fdbfd51d3b347fbdc016e1e204 Mon Sep 17 00:00:00 2001 From: Joey Yakimowich-Payne Date: Thu, 20 Feb 2020 18:48:19 -0700 Subject: [PATCH 19/80] Add 'add category button' to categories page --- src/frontend/categorylist.nim | 34 +++++++++++++++++++++++++++------- src/frontend/mainbuttons.nim | 15 ++++++++++++--- 2 files changed, 39 insertions(+), 10 deletions(-) diff --git a/src/frontend/categorylist.nim b/src/frontend/categorylist.nim index 5ad4c7f..b61694a 100644 --- a/src/frontend/categorylist.nim +++ b/src/frontend/categorylist.nim @@ -3,25 +3,33 @@ import options, json, httpcore import category when defined(js): + import sugar include karax/prelude import karax / [vstyles, kajax] - import karaxutils, error, user, mainbuttons + import karaxutils, error, user, mainbuttons, addcategorymodal type State = ref object list: Option[CategoryList] loading: bool status: HttpCode + addCategoryModal: AddCategoryModal + + var state: State proc newState(): State = State( list: none[CategoryList](), loading: false, - status: Http200 + status: Http200, + addCategoryModal: newAddCategoryModal( + onAddCategory= + (category: Category) => state.list.get().categories.add(category) + ) ) - var - state = newState() + + state = newState() proc genCategory(category: Category, noBorder = false): VNode = result = buildHtml(): @@ -50,7 +58,18 @@ when defined(js): else: state.list = some(list) - proc renderCategories(): VNode = + proc renderCategoryHeader*(currentUser: Option[User]): VNode = + result = buildHtml(tdiv(id="add-category")): + text "Category" + if currentUser.isAdmin(): + button(class="plus-btn btn btn-link", + onClick=(ev: Event, n: VNode) => ( + state.addCategoryModal.setModalShown(true) + )): + italic(class="fas fa-plus") + render(state.addCategoryModal) + + proc renderCategories(currentUser: Option[User]): VNode = if state.status != Http200: return renderError("Couldn't retrieve threads.", state.status) @@ -68,7 +87,8 @@ when defined(js): table(id="categories-list", class="table"): thead(): tr: - th(text "Category") + th: + renderCategoryHeader(currentUser) th(text "Topics") tbody(): for i in 0 ..< list.categories.len: @@ -80,4 +100,4 @@ when defined(js): proc renderCategoryList*(currentUser: Option[User]): VNode = result = buildHtml(tdiv): renderMainButtons(currentUser) - renderCategories() + renderCategories(currentUser) diff --git a/src/frontend/mainbuttons.nim b/src/frontend/mainbuttons.nim index 9dc1fe2..a47bb09 100644 --- a/src/frontend/mainbuttons.nim +++ b/src/frontend/mainbuttons.nim @@ -16,7 +16,16 @@ when defined(js): let uri = makeUri("/c/" & $newCategory.id) navigateTo(uri) - let catPicker = newCategoryPicker(onCategoryChange=onSelectedCategoryChanged) + type + State = ref object + categoryPicker: CategoryPicker + + proc newState(): State = + State( + categoryPicker: newCategoryPicker(onCategoryChange=onSelectedCategoryChanged), + ) + + let state = newState() proc renderMainButtons*(currentUser: Option[User], categoryId = -1): VNode = result = buildHtml(): @@ -30,8 +39,8 @@ when defined(js): li: text "community" li: text "dev" ]# if categoryId != -1: - catPicker.selectedCategoryID = categoryId - render(catPicker, currentUser, compact=false) + state.categoryPicker.selectedCategoryID = categoryId + render(state.categoryPicker, currentUser, compact=false) for btn in buttons: let active = btn.url == window.location.href From 31d3b2701dc1fd3b503f19d321bc1413f4894827 Mon Sep 17 00:00:00 2001 From: Joey Yakimowich-Payne Date: Thu, 20 Feb 2020 22:15:36 -0700 Subject: [PATCH 20/80] Add create category test --- tests/browsertester.nims | 3 +- tests/browsertests/categories.nim | 63 ++++++++++++++++++++++--------- tests/browsertests/common.nim | 5 ++- 3 files changed, 52 insertions(+), 19 deletions(-) diff --git a/tests/browsertester.nims b/tests/browsertester.nims index 9d57ecf..3cd49f0 100644 --- a/tests/browsertester.nims +++ b/tests/browsertester.nims @@ -1 +1,2 @@ ---threads:on \ No newline at end of file +--threads:on +--path:"../src/frontend" \ No newline at end of file diff --git a/tests/browsertests/categories.nim b/tests/browsertests/categories.nim index d56b47c..9954877 100644 --- a/tests/browsertests/categories.nim +++ b/tests/browsertests/categories.nim @@ -1,12 +1,33 @@ import unittest, options, common, os - import webdriver +import karaxutils + proc selectCategory(session: Session, name: string) = with session: click "#category-selection .dropdown-toggle" click "#category-selection ." & name +proc createCategory(session: Session, baseUrl, name, color, description: string) = + with session: + navigate baseUrl + click "#categories-btn" + + ensureExists "#add-category" + + click "#add-category .plus-btn" + + clear "#add-category input[name='name']" + clear "#add-category input[name='description']" + + sendKeys "#add-category input[name='name']", name + setColor "#add-category input[name='color']", color + sendKeys "#add-category input[name='description']", description + + click "#add-category #add-category-btn" + + checkText "#category-" & name.slug(), name + proc categoriesUserTests(session: Session, baseUrl: string) = let title = "Category Test" @@ -105,17 +126,17 @@ proc categoriesUserTests(session: Session, baseUrl: string) = session.logout() proc categoriesAdminTests(session: Session, baseUrl: string) = - let - name = "Category Test" - color = "Creating category test" - description = "This is a description" - suite "admin tests": with session: navigate baseUrl login "admin", "admin" - test "can create category": + test "can create category via dropdown": + let + name = "Category Test" + color = "#720904" + description = "This is a description" + with session: click "#new-thread-btn" @@ -124,27 +145,35 @@ proc categoriesAdminTests(session: Session, baseUrl: string) = click "#add-category .plus-btn" clear "#add-category input[name='name']" - clear "#add-category input[name='color']" clear "#add-category input[name='description']" sendKeys "#add-category input[name='name']", name - sendKeys "#add-category input[name='color']", color + setColor "#add-category input[name='color']", color sendKeys "#add-category input[name='description']", description click "#add-category #add-category-btn" checkText "#category-selection .selected-category", name - test "category adding disabled on admin logout": - with session: - navigate(baseUrl & "c/0") - ensureExists "#add-category" - logout() + test "can create category on category page": + let + name = "Category Test Page" + color = "#70B4D4" + description = "This is a description on category page" - checkIsNone "#add-category" - navigate baseUrl + with session: + createCategory baseUrl, name, color, description - login "admin", "admin" + test "category adding disabled on admin logout": + with session: + navigate(baseUrl & "c/0") + ensureExists "#add-category" + logout() + + checkIsNone "#add-category" + navigate baseUrl + + login "admin", "admin" session.logout() diff --git a/tests/browsertests/common.nim b/tests/browsertests/common.nim index 9d8b61a..09e0999 100644 --- a/tests/browsertests/common.nim +++ b/tests/browsertests/common.nim @@ -64,6 +64,10 @@ template check*(session: Session, element: string, let el = session.waitForElement(element, strategy) check function(el) +proc setColor*(session: Session, element, color: string, strategy=CssSelector) = + let el = session.waitForElement(element, strategy) + discard session.execute("arguments[0].setAttribute('value', '" & color & "')", el.get()) + template checkIsNone*(session: Session, element: string, strategy=CssSelector) = discard session.waitForElement(element, strategy, waitCondition=elementIsNone) @@ -105,7 +109,6 @@ proc waitForElements*( while true: let loading = session.findElements(selector, strategy) - echo loading if loading.len > 0: return loading sleep(pollTime) From 3b092ae2d1b138fbee454cefdf781709ac292911 Mon Sep 17 00:00:00 2001 From: Joey Yakimowich-Payne Date: Thu, 20 Feb 2020 22:18:36 -0700 Subject: [PATCH 21/80] Change unnecessary templates to procs --- tests/browsertests/categories.nim | 2 +- tests/browsertests/common.nim | 14 +++++++------- tests/browsertests/issue181.nim | 2 +- tests/browsertests/scenario1.nim | 2 +- tests/browsertests/threads.nim | 2 +- 5 files changed, 11 insertions(+), 11 deletions(-) diff --git a/tests/browsertests/categories.nim b/tests/browsertests/categories.nim index 9954877..3980146 100644 --- a/tests/browsertests/categories.nim +++ b/tests/browsertests/categories.nim @@ -1,4 +1,4 @@ -import unittest, options, common, os +import unittest, common import webdriver import karaxutils diff --git a/tests/browsertests/common.nim b/tests/browsertests/common.nim index 09e0999..e266ed6 100644 --- a/tests/browsertests/common.nim +++ b/tests/browsertests/common.nim @@ -32,19 +32,19 @@ proc elementIsNone(element: Option[Element]): bool = proc waitForElement*(session: Session, selector: string, strategy=CssSelector, timeout=20000, pollTime=50, waitCondition=elementIsSome): Option[Element] -template click*(session: Session, element: string, strategy=CssSelector) = +proc click*(session: Session, element: string, strategy=CssSelector) = let el = session.waitForElement(element, strategy) el.get().click() -template sendKeys*(session: Session, element, keys: string) = +proc sendKeys*(session: Session, element, keys: string) = let el = session.waitForElement(element) el.get().sendKeys(keys) -template clear*(session: Session, element: string) = +proc clear*(session: Session, element: string) = let el = session.waitForElement(element) el.get().clear() -template sendKeys*(session: Session, element: string, keys: varargs[Key]) = +proc sendKeys*(session: Session, element: string, keys: varargs[Key]) = let el = session.waitForElement(element) # focus @@ -52,7 +52,7 @@ template sendKeys*(session: Session, element: string, keys: varargs[Key]) = for key in keys: session.press(key) -template ensureExists*(session: Session, element: string, strategy=CssSelector) = +proc ensureExists*(session: Session, element: string, strategy=CssSelector) = discard session.waitForElement(element, strategy) template check*(session: Session, element: string, function: untyped) = @@ -68,10 +68,10 @@ proc setColor*(session: Session, element, color: string, strategy=CssSelector) = let el = session.waitForElement(element, strategy) discard session.execute("arguments[0].setAttribute('value', '" & color & "')", el.get()) -template checkIsNone*(session: Session, element: 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 diff --git a/tests/browsertests/issue181.nim b/tests/browsertests/issue181.nim index f48929f..031cd92 100644 --- a/tests/browsertests/issue181.nim +++ b/tests/browsertests/issue181.nim @@ -1,4 +1,4 @@ -import unittest, options, common +import unittest, common import webdriver diff --git a/tests/browsertests/scenario1.nim b/tests/browsertests/scenario1.nim index 9237c2e..6e10e4c 100644 --- a/tests/browsertests/scenario1.nim +++ b/tests/browsertests/scenario1.nim @@ -1,4 +1,4 @@ -import unittest, options, common +import unittest, common import webdriver diff --git a/tests/browsertests/threads.nim b/tests/browsertests/threads.nim index 53fbb23..b79d93a 100644 --- a/tests/browsertests/threads.nim +++ b/tests/browsertests/threads.nim @@ -1,4 +1,4 @@ -import unittest, options, common +import unittest, common import webdriver From 5f930c7f5a85cafad04b32cbd9c0da2a2abe9cc8 Mon Sep 17 00:00:00 2001 From: Joey Yakimowich-Payne Date: Thu, 20 Feb 2020 22:21:56 -0700 Subject: [PATCH 22/80] Ignore more generated files --- .gitignore | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/.gitignore b/.gitignore index 55c0cb9..fe26a8a 100644 --- a/.gitignore +++ b/.gitignore @@ -14,3 +14,10 @@ createdb editdb .vscode +forum.json* +browsertester +setup_nimforum +buildcss +nimforum.css + +/src/frontend/forum.js From 60ace9c65a5e5d268cabca984d369a3009ff641e Mon Sep 17 00:00:00 2001 From: Joey Yakimowich-Payne Date: Fri, 21 Feb 2020 13:37:44 -0700 Subject: [PATCH 23/80] Remove unnecessary check --- tests/browsertests/common.nim | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/browsertests/common.nim b/tests/browsertests/common.nim index e266ed6..d906675 100644 --- a/tests/browsertests/common.nim +++ b/tests/browsertests/common.nim @@ -19,7 +19,7 @@ macro with*(obj: typed, code: untyped): untyped = # Simply inject obj into call for i in 0 ..< result.len: - if result[i].kind in {nnkCommand, nnkCall} and $result[i][0].toStrLit != "assert": + if result[i].kind in {nnkCommand, nnkCall}: result[i].insert(1, obj) result = getAst(checkCompiles(result, code)) From b26085cbd02d858dc5097b8e5586fe3fbc0077d3 Mon Sep 17 00:00:00 2001 From: Joey Yakimowich-Payne Date: Mon, 24 Feb 2020 14:49:53 -0700 Subject: [PATCH 24/80] Update webdriver version --- nimforum.nimble | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nimforum.nimble b/nimforum.nimble index 09873ca..3d21367 100644 --- a/nimforum.nimble +++ b/nimforum.nimble @@ -21,7 +21,7 @@ requires "sass#649e0701fa5c" requires "karax#f6bda9a" -requires "webdriver#7091895" +requires "webdriver#429933a" # Tasks From 0d63fef0f7aae306527295dfaf2f0fdf2412608a Mon Sep 17 00:00:00 2001 From: Joey Yakimowich-Payne Date: Mon, 24 Feb 2020 18:57:33 -0700 Subject: [PATCH 25/80] Add one more test for user in categories page --- tests/browsertests/categories.nim | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/tests/browsertests/categories.nim b/tests/browsertests/categories.nim index 3980146..c4ff41c 100644 --- a/tests/browsertests/categories.nim +++ b/tests/browsertests/categories.nim @@ -49,6 +49,11 @@ proc categoriesUserTests(session: Session, baseUrl: string) = checkIsNone "#add-category" + test "no category add available category page": + with session: + click "#categories-btn" + checkIsNone "#add-category" + test "can create category thread": with session: click "#new-thread-btn" From 7a7a7145eec1786e664005faaf887c0311c898e8 Mon Sep 17 00:00:00 2001 From: Joey Yakimowich-Payne Date: Wed, 4 Mar 2020 07:37:11 -0700 Subject: [PATCH 26/80] Fix using magic number as default category id --- src/frontend/forum.nim | 2 +- src/frontend/mainbuttons.nim | 6 +++--- src/frontend/threadlist.nim | 22 ++++++++++++++-------- 3 files changed, 18 insertions(+), 12 deletions(-) diff --git a/src/frontend/forum.nim b/src/frontend/forum.nim index d8fc003..bccbcf6 100644 --- a/src/frontend/forum.nim +++ b/src/frontend/forum.nim @@ -93,7 +93,7 @@ proc render(): VNode = ), r("/c/@id", (params: Params) => - (renderThreadList(getLoggedInUser(), params["id"].parseInt)) + (renderThreadList(getLoggedInUser(), some(params["id"].parseInt))) ), r("/newthread", (params: Params) => diff --git a/src/frontend/mainbuttons.nim b/src/frontend/mainbuttons.nim index a47bb09..9f4d4f8 100644 --- a/src/frontend/mainbuttons.nim +++ b/src/frontend/mainbuttons.nim @@ -27,7 +27,7 @@ when defined(js): let state = newState() - proc renderMainButtons*(currentUser: Option[User], categoryId = -1): VNode = + proc renderMainButtons*(currentUser: Option[User], categoryIdOption = none(int)): VNode = result = buildHtml(): section(class="navbar container grid-xl", id="main-buttons"): section(class="navbar-section"): @@ -38,8 +38,8 @@ when defined(js): ul(class="menu"): li: text "community" li: text "dev" ]# - if categoryId != -1: - state.categoryPicker.selectedCategoryID = categoryId + if categoryIdOption.isSome: + state.categoryPicker.selectedCategoryID = categoryIdOption.get() render(state.categoryPicker, currentUser, compact=false) for btn in buttons: diff --git a/src/frontend/threadlist.nim b/src/frontend/threadlist.nim index c93def1..f300cf9 100644 --- a/src/frontend/threadlist.nim +++ b/src/frontend/threadlist.nim @@ -147,10 +147,13 @@ when defined(js): else: state.list = some(list) - proc onLoadMore(ev: Event, n: VNode, categoryId: int) = + proc onLoadMore(ev: Event, n: VNode, categoryIdOption: Option[int]) = state.loading = true let start = state.list.get().threads.len - ajaxGet(makeUri("threads.json?start=" & $start & "&categoryId=" & $categoryId), @[], onThreadList) + if categoryIdOption.isSome: + ajaxGet(makeUri("threads.json?start=" & $start & "&categoryId=" & $categoryIdOption.get()), @[], onThreadList) + else: + ajaxGet(makeUri("threads.json?start=" & $start), @[], onThreadList) proc getInfo( list: seq[Thread], i: int, currentUser: Option[User] @@ -175,14 +178,17 @@ when defined(js): isNew: thread.creation > previousVisitAt ) - proc genThreadList(currentUser: Option[User], categoryId: int): VNode = + proc genThreadList(currentUser: Option[User], categoryIdOption: Option[int]): VNode = if state.status != Http200: return renderError("Couldn't retrieve threads.", state.status) if state.list.isNone: if not state.loading: state.loading = true - ajaxGet(makeUri("threads.json?categoryId=" & $categoryId), @[], onThreadList) + if categoryIdOption.isSome: + ajaxGet(makeUri("threads.json?categoryId=" & $categoryIdOption.get()), @[], onThreadList) + else: + ajaxGet(makeUri("threads.json"), @[], onThreadList) return buildHtml(tdiv(class="loading loading-lg")) @@ -223,10 +229,10 @@ when defined(js): tdiv(class="loading loading-lg") else: td(colspan="6", - onClick = (ev: Event, n: VNode) => (onLoadMore(ev, n, categoryId))): + onClick = (ev: Event, n: VNode) => (onLoadMore(ev, n, categoryIdOption))): span(text "load more threads") - proc renderThreadList*(currentUser: Option[User], categoryId = -1): VNode = + proc renderThreadList*(currentUser: Option[User], categoryIdOption = none(int)): VNode = result = buildHtml(tdiv): - renderMainButtons(currentUser, categoryId=categoryId) - genThreadList(currentUser, categoryId) + renderMainButtons(currentUser, categoryIdOption=categoryIdOption) + genThreadList(currentUser, categoryIdOption) From b91bdeb450b0afe9fcb24aba4f383f5d22143b79 Mon Sep 17 00:00:00 2001 From: Dominik Picheta Date: Wed, 4 Mar 2020 23:57:27 +0100 Subject: [PATCH 27/80] Remove reloading and fix issues with state not being refreshed (#232) * Remove reloading and fix issues with state not being refreshed * Fix PR comments * Remove unnecessary closure * Change proc to anonymous proc * categoryIdOption -> categoryId --- src/frontend/categorylist.nim | 4 +++- src/frontend/categorypicker.nim | 3 +-- src/frontend/forum.nim | 4 ---- src/frontend/header.nim | 31 +++++++++++++----------------- src/frontend/karaxutils.nim | 1 - src/frontend/mainbuttons.nim | 21 +++++++++++--------- src/frontend/threadlist.nim | 34 ++++++++++++++++++++------------- 7 files changed, 50 insertions(+), 48 deletions(-) diff --git a/src/frontend/categorylist.nim b/src/frontend/categorylist.nim index b61694a..3c0e533 100644 --- a/src/frontend/categorylist.nim +++ b/src/frontend/categorylist.nim @@ -13,6 +13,7 @@ when defined(js): State = ref object list: Option[CategoryList] loading: bool + mainButtons: MainButtons status: HttpCode addCategoryModal: AddCategoryModal @@ -22,6 +23,7 @@ when defined(js): State( list: none[CategoryList](), loading: false, + mainButtons: newMainButtons(), status: Http200, addCategoryModal: newAddCategoryModal( onAddCategory= @@ -99,5 +101,5 @@ when defined(js): proc renderCategoryList*(currentUser: Option[User]): VNode = result = buildHtml(tdiv): - renderMainButtons(currentUser) + state.mainButtons.render(currentUser) renderCategories(currentUser) diff --git a/src/frontend/categorypicker.nim b/src/frontend/categorypicker.nim index 38f9300..f26f6c1 100644 --- a/src/frontend/categorypicker.nim +++ b/src/frontend/categorypicker.nim @@ -105,8 +105,7 @@ when defined(js): render(state.addCategoryModal) proc render*(state: CategoryPicker, currentUser: Option[User], compact=true): VNode = - if currentUser.isAdmin(): - state.setAddEnabled(true) + state.setAddEnabled(currentUser.isAdmin()) if state.status != Http200: return renderError("Couldn't retrieve categories.", state.status) diff --git a/src/frontend/forum.nim b/src/frontend/forum.nim index bccbcf6..da74eab 100644 --- a/src/frontend/forum.nim +++ b/src/frontend/forum.nim @@ -57,10 +57,6 @@ proc onPopState(event: dom.Event) = state = newState() # Reload the state to remove stale data. state.url = copyLocation(window.location) - # For some reason this is needed so that the back/forward buttons reload - # the post list when different categories are selected - window.location.reload() - redraw() type Params = Table[string, string] diff --git a/src/frontend/header.nim b/src/frontend/header.nim index 679c8fa..cde48de 100644 --- a/src/frontend/header.nim +++ b/src/frontend/header.nim @@ -32,41 +32,36 @@ when defined(js): var state = newState() - proc getStatus(logout=false, reload=false) + proc getStatus(logout=false) proc newState(): State = State( data: none[UserStatus](), loading: false, status: Http200, loginModal: newLoginModal( - () => (state.lastUpdate = fromUnix(0); getStatus(reload=true)), + () => (state.lastUpdate = fromUnix(0); getStatus()), () => state.signupModal.show() ), signupModal: newSignupModal( - () => (state.lastUpdate = fromUnix(0); getStatus(reload=true)), + () => (state.lastUpdate = fromUnix(0); getStatus()), () => state.loginModal.show() ), userMenu: newUserMenu( - () => (state.lastUpdate = fromUnix(0); getStatus(logout=true, reload=true)) + () => (state.lastUpdate = fromUnix(0); getStatus(logout=true)) ) ) - proc onStatus(reload: bool=false): proc (httpStatus: int, response: kstring) = - result = - proc (httpStatus: int, response: kstring) = - state.loading = false - state.status = httpStatus.HttpCode - if state.status != Http200: return + proc onStatus(httpStatus: int, response: kstring) = + state.loading = false + state.status = httpStatus.HttpCode + if state.status != Http200: return - let parsed = parseJson($response) - state.data = some(to(parsed, UserStatus)) + let parsed = parseJson($response) + state.data = some(to(parsed, UserStatus)) - state.lastUpdate = getTime() + state.lastUpdate = getTime() - if reload: - window.location.reload() - - proc getStatus(logout=false, reload=false) = + proc getStatus(logout=false) = if state.loading: return let diff = getTime() - state.lastUpdate if diff.inMinutes < 5: @@ -74,7 +69,7 @@ when defined(js): state.loading = true let uri = makeUri("status.json", [("logout", $logout)]) - ajaxGet(uri, @[], onStatus(reload)) + ajaxGet(uri, @[], onStatus) proc getLoggedInUser*(): Option[User] = state.data.map(x => x.user).flatten diff --git a/src/frontend/karaxutils.nim b/src/frontend/karaxutils.nim index cc26cd0..f70ec5d 100644 --- a/src/frontend/karaxutils.nim +++ b/src/frontend/karaxutils.nim @@ -97,7 +97,6 @@ when defined(js): let url = n.getAttr("href") navigateTo(url) - window.location.href = url proc newFormData*(form: dom.Element): FormData {.importcpp: "new FormData(@)", constructor.} diff --git a/src/frontend/mainbuttons.nim b/src/frontend/mainbuttons.nim index 9f4d4f8..c91d354 100644 --- a/src/frontend/mainbuttons.nim +++ b/src/frontend/mainbuttons.nim @@ -17,17 +17,20 @@ when defined(js): navigateTo(uri) type - State = ref object + MainButtons* = ref object categoryPicker: CategoryPicker + onCategoryChange*: CategoryChangeEvent - proc newState(): State = - State( - categoryPicker: newCategoryPicker(onCategoryChange=onSelectedCategoryChanged), + proc newMainButtons*(onCategoryChange: CategoryChangeEvent = onSelectedCategoryChanged): MainButtons = + new result + result.onCategoryChange = onCategoryChange + result.categoryPicker = newCategoryPicker( + onCategoryChange = proc (oldCategory, newCategory: Category) = + onSelectedCategoryChanged(oldCategory, newCategory) + result.onCategoryChange(oldCategory, newCategory) ) - let state = newState() - - proc renderMainButtons*(currentUser: Option[User], categoryIdOption = none(int)): VNode = + proc render*(state: MainButtons, currentUser: Option[User], categoryId = none(int)): VNode = result = buildHtml(): section(class="navbar container grid-xl", id="main-buttons"): section(class="navbar-section"): @@ -38,8 +41,8 @@ when defined(js): ul(class="menu"): li: text "community" li: text "dev" ]# - if categoryIdOption.isSome: - state.categoryPicker.selectedCategoryID = categoryIdOption.get() + if categoryId.isSome: + state.categoryPicker.selectedCategoryID = categoryId.get() render(state.categoryPicker, currentUser, compact=false) for btn in buttons: diff --git a/src/frontend/threadlist.nim b/src/frontend/threadlist.nim index f300cf9..8a2a5ed 100644 --- a/src/frontend/threadlist.nim +++ b/src/frontend/threadlist.nim @@ -26,6 +26,7 @@ proc isModerated*(thread: Thread): bool = thread.author.rank <= Moderated when defined(js): + import sugar include karax/prelude import karax / [vstyles, kajax, kdom] @@ -34,18 +35,25 @@ when defined(js): type State = ref object list: Option[ThreadList] + refreshList: bool loading: bool status: HttpCode + mainButtons: MainButtons + + var state: State proc newState(): State = State( list: none[ThreadList](), loading: false, - status: Http200 + status: Http200, + mainButtons: newMainButtons( + onCategoryChange = + (oldCategory: Category, newCategory: Category) => (state.list = none[ThreadList]()) + ) ) - var - state = newState() + state = newState() proc visibleTo*[T](thread: T, user: Option[User]): bool = ## Determines whether the specified thread (or post) should be @@ -147,11 +155,11 @@ when defined(js): else: state.list = some(list) - proc onLoadMore(ev: Event, n: VNode, categoryIdOption: Option[int]) = + proc onLoadMore(ev: Event, n: VNode, categoryId: Option[int]) = state.loading = true let start = state.list.get().threads.len - if categoryIdOption.isSome: - ajaxGet(makeUri("threads.json?start=" & $start & "&categoryId=" & $categoryIdOption.get()), @[], onThreadList) + if categoryId.isSome: + ajaxGet(makeUri("threads.json?start=" & $start & "&categoryId=" & $categoryId.get()), @[], onThreadList) else: ajaxGet(makeUri("threads.json?start=" & $start), @[], onThreadList) @@ -178,15 +186,15 @@ when defined(js): isNew: thread.creation > previousVisitAt ) - proc genThreadList(currentUser: Option[User], categoryIdOption: Option[int]): VNode = + proc genThreadList(currentUser: Option[User], categoryId: Option[int]): VNode = if state.status != Http200: return renderError("Couldn't retrieve threads.", state.status) if state.list.isNone: if not state.loading: state.loading = true - if categoryIdOption.isSome: - ajaxGet(makeUri("threads.json?categoryId=" & $categoryIdOption.get()), @[], onThreadList) + if categoryId.isSome: + ajaxGet(makeUri("threads.json?categoryId=" & $categoryId.get()), @[], onThreadList) else: ajaxGet(makeUri("threads.json"), @[], onThreadList) @@ -229,10 +237,10 @@ when defined(js): tdiv(class="loading loading-lg") else: td(colspan="6", - onClick = (ev: Event, n: VNode) => (onLoadMore(ev, n, categoryIdOption))): + onClick = (ev: Event, n: VNode) => (onLoadMore(ev, n, categoryId))): span(text "load more threads") - proc renderThreadList*(currentUser: Option[User], categoryIdOption = none(int)): VNode = + proc renderThreadList*(currentUser: Option[User], categoryId = none(int)): VNode = result = buildHtml(tdiv): - renderMainButtons(currentUser, categoryIdOption=categoryIdOption) - genThreadList(currentUser, categoryIdOption) + state.mainButtons.render(currentUser, categoryId=categoryId) + genThreadList(currentUser, categoryId) From c2cc26ea774f0279507ed0b1cd49640f87976d6d Mon Sep 17 00:00:00 2001 From: Joey Yakimowich-Payne Date: Thu, 5 Mar 2020 17:14:02 -0700 Subject: [PATCH 28/80] Make mobile viewing more friendly --- public/css/nimforum.scss | 10 +++++++++- src/frontend/category.nim | 2 +- src/frontend/threadlist.nim | 23 ++++++++++------------- 3 files changed, 20 insertions(+), 15 deletions(-) diff --git a/public/css/nimforum.scss b/public/css/nimforum.scss index 098ad6e..5630b06 100644 --- a/public/css/nimforum.scss +++ b/public/css/nimforum.scss @@ -51,6 +51,7 @@ $logo-height: $navbar-height - 20px; // Unfortunately we must colour the controls in the navbar manually. .search-input { @extend .form-input; + min-width: 120px; border-color: $navbar-border-color-dark; } @@ -128,6 +129,9 @@ $logo-height: $navbar-height - 20px; } .category-status { + font-size: small; + font-weight: bold; + .topic-count { margin-left: 5px; opacity: 0.7; @@ -258,7 +262,7 @@ $threads-meta-color: #545d70; .category-color { width: 0; height: 0; - border: 0.3rem solid $default-category-color; + border: 0.25rem solid $default-category-color; display: inline-block; margin-right: 5px; } @@ -297,6 +301,10 @@ $threads-meta-color: #545d70; } } +.thread-replies, .thread-time, .thread-users, .views-text, .centered-header { + text-align: center; +} + .thread-time { color: $threads-meta-color; diff --git a/src/frontend/category.nim b/src/frontend/category.nim index 7b46194..314720d 100644 --- a/src/frontend/category.nim +++ b/src/frontend/category.nim @@ -36,7 +36,7 @@ when defined(js): tdiv(class="category-color", style=style( (StyleAttr.border, - kstring"0.3rem solid #" & category.color) + kstring"0.25rem solid #" & category.color) )) span(class="category-name"): text category.name diff --git a/src/frontend/threadlist.nim b/src/frontend/threadlist.nim index 8a2a5ed..d2dc104 100644 --- a/src/frontend/threadlist.nim +++ b/src/frontend/threadlist.nim @@ -74,7 +74,7 @@ when defined(js): return true proc genUserAvatars(users: seq[User]): VNode = - result = buildHtml(td): + result = buildHtml(td(class="thread-users")): for user in users: render(user, "avatar avatar-sm", showStatus=true) text " " @@ -114,14 +114,13 @@ when defined(js): if thread.isSolved: italic(class="fas fa-check-square fa-xs", title="Thread has a solution") - a(href=makeUri("/t/" & $thread.id), - onClick=anchorCB): + a(href=makeUri("/t/" & $thread.id), onClick=anchorCB): text thread.topic - td(class=class({"d-none": not displayCategory})): - render(thread.category) + tdiv(class=class({"d-none": not displayCategory})): + render(thread.category) genUserAvatars(thread.users) - td(): text $thread.replies - td(class=class({ + td(class="thread-replies"): text $thread.replies + td(class="hide-sm" & class({ "views-text": thread.views < 999, "popular-text": thread.views > 999 and thread.views < 5000, "super-popular-text": thread.views > 5000 @@ -209,12 +208,10 @@ when defined(js): thead(): tr: th(text "Topic") - th(class=class({"d-none": not displayCategory})): - text "Category" - th(style=style((StyleAttr.width, kstring"8rem"))): text "Users" - th(text "Replies") - th(text "Views") - th(text "Activity") + th(class="centered-header"): text "Users" + th(class="centered-header"): text "Replies" + th(class="hide-sm centered-header"): text "Views" + th(class="centered-header"): text "Activity" tbody(): for i in 0 ..< list.threads.len: let thread = list.threads[i] From dcea4091f49c3136d918b04b64c765ade57e95dd Mon Sep 17 00:00:00 2001 From: Joey Yakimowich-Payne Date: Thu, 5 Mar 2020 17:24:23 -0700 Subject: [PATCH 29/80] Fix tests --- tests/browsertests/categories.nim | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/browsertests/categories.nim b/tests/browsertests/categories.nim index c4ff41c..b1a2edb 100644 --- a/tests/browsertests/categories.nim +++ b/tests/browsertests/categories.nim @@ -113,18 +113,18 @@ proc categoriesUserTests(session: Session, baseUrl: string) = ensureExists "#categories-list" click "#category-default" - checkText "#threads-list .thread-title", "Post 3" + checkText "#threads-list .thread-title a", "Post 3" for element in session.waitForElements("#threads-list .category-name"): # Have to user "innerText" because elements are hidden on this page assert element.getProperty("innerText") == "Default" selectCategory "announcements" - checkText "#threads-list .thread-title", "Post 2" + checkText "#threads-list .thread-title a", "Post 2" for element in session.waitForElements("#threads-list .category-name"): assert element.getProperty("innerText") == "Announcements" selectCategory "fun" - checkText "#threads-list .thread-title", "Post 1" + checkText "#threads-list .thread-title a", "Post 1" for element in session.waitForElements("#threads-list .category-name"): assert element.getProperty("innerText") == "Fun" From 3db01a1d44bee4e838ebce043bc6e33af1d8fcb6 Mon Sep 17 00:00:00 2001 From: Joey Yakimowich-Payne Date: Fri, 6 Mar 2020 13:15:17 -0700 Subject: [PATCH 30/80] Show category column if not on mobile --- src/frontend/threadlist.nim | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/frontend/threadlist.nim b/src/frontend/threadlist.nim index d2dc104..bc5130e 100644 --- a/src/frontend/threadlist.nim +++ b/src/frontend/threadlist.nim @@ -116,8 +116,11 @@ when defined(js): title="Thread has a solution") a(href=makeUri("/t/" & $thread.id), onClick=anchorCB): text thread.topic - tdiv(class=class({"d-none": not displayCategory})): + tdiv(class="show-sm" & class({"d-none": not displayCategory})): render(thread.category) + + td(class="hide-sm" & class({"d-none": not displayCategory})): + render(thread.category) genUserAvatars(thread.users) td(class="thread-replies"): text $thread.replies td(class="hide-sm" & class({ @@ -199,7 +202,7 @@ when defined(js): return buildHtml(tdiv(class="loading loading-lg")) - let displayCategory = true + let displayCategory = categoryId.isNone let list = state.list.get() result = buildHtml(): @@ -208,6 +211,7 @@ when defined(js): thead(): tr: th(text "Topic") + th(class="hide-sm" & class({"d-none": not displayCategory})): text "Category" th(class="centered-header"): text "Users" th(class="centered-header"): text "Replies" th(class="hide-sm centered-header"): text "Views" From f35d6c4a32cf01ba4610b6531ec4005074204b79 Mon Sep 17 00:00:00 2001 From: Joey Yakimowich-Payne Date: Tue, 17 Mar 2020 19:57:29 -0600 Subject: [PATCH 31/80] Rename default category and change color to grey --- public/css/nimforum.scss | 2 +- src/setup_nimforum.nim | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/public/css/nimforum.scss b/public/css/nimforum.scss index 5630b06..d161c19 100644 --- a/public/css/nimforum.scss +++ b/public/css/nimforum.scss @@ -22,7 +22,7 @@ table th { // Custom styles. // - Navigation bar. $navbar-height: 60px; -$default-category-color: #98c766; +$default-category-color: #a3a3a3; $logo-height: $navbar-height - 20px; .navbar-button { diff --git a/src/setup_nimforum.nim b/src/setup_nimforum.nim index b235415..e50b96d 100644 --- a/src/setup_nimforum.nim +++ b/src/setup_nimforum.nim @@ -66,7 +66,7 @@ proc initialiseDb(admin: tuple[username, password, email: string], db.exec(sql""" insert into category (id, name, description, color) - values (0, 'Default', 'The default category', ''); + values (0, 'Unsorted', 'No category has been chosen yet.', ''); """) # -- Thread From 3adba32f1f4fd82e15ea2a8a14fbb15ed7c4a053 Mon Sep 17 00:00:00 2001 From: Joey Yakimowich-Payne Date: Tue, 17 Mar 2020 20:05:22 -0600 Subject: [PATCH 32/80] Left align users --- public/css/nimforum.scss | 6 +++++- src/frontend/threadlist.nim | 2 +- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/public/css/nimforum.scss b/public/css/nimforum.scss index d161c19..313e844 100644 --- a/public/css/nimforum.scss +++ b/public/css/nimforum.scss @@ -301,10 +301,14 @@ $threads-meta-color: #545d70; } } -.thread-replies, .thread-time, .thread-users, .views-text, .centered-header { +.thread-replies, .thread-time, .views-text, .centered-header { text-align: center; } +.thread-users { + text-align: left; +} + .thread-time { color: $threads-meta-color; diff --git a/src/frontend/threadlist.nim b/src/frontend/threadlist.nim index bc5130e..e0b2d36 100644 --- a/src/frontend/threadlist.nim +++ b/src/frontend/threadlist.nim @@ -212,7 +212,7 @@ when defined(js): tr: th(text "Topic") th(class="hide-sm" & class({"d-none": not displayCategory})): text "Category" - th(class="centered-header"): text "Users" + th(class="thread-users"): text "Users" th(class="centered-header"): text "Replies" th(class="hide-sm centered-header"): text "Views" th(class="centered-header"): text "Activity" From 89840748097a44006ee3bd603313c57d3d77e4a6 Mon Sep 17 00:00:00 2001 From: Joey Yakimowich-Payne Date: Thu, 19 Mar 2020 19:43:18 -0600 Subject: [PATCH 33/80] Fix tests --- tests/browsertests/categories.nim | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/browsertests/categories.nim b/tests/browsertests/categories.nim index b1a2edb..c299218 100644 --- a/tests/browsertests/categories.nim +++ b/tests/browsertests/categories.nim @@ -102,7 +102,7 @@ proc categoriesUserTests(session: Session, baseUrl: string) = click "#new-thread-btn" sendKeys "#thread-title", "Post 3" - selectCategory "default" + selectCategory "unsorted" sendKeys "#reply-textarea", "Post 3" click "#create-thread-btn" @@ -112,11 +112,11 @@ proc categoriesUserTests(session: Session, baseUrl: string) = click "#categories-btn" ensureExists "#categories-list" - click "#category-default" + click "#category-unsorted" checkText "#threads-list .thread-title a", "Post 3" for element in session.waitForElements("#threads-list .category-name"): # Have to user "innerText" because elements are hidden on this page - assert element.getProperty("innerText") == "Default" + assert element.getProperty("innerText") == "Unsorted" selectCategory "announcements" checkText "#threads-list .thread-title a", "Post 2" From d8661f62c79bdc0750bf94ac15918e75cec98cb1 Mon Sep 17 00:00:00 2001 From: hlaaftana <10591326+hlaaftana@users.noreply.github.com> Date: Wed, 15 Apr 2020 10:29:42 +0300 Subject: [PATCH 34/80] Let users delete their own posts, fixes #208 --- src/forum.nim | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/forum.nim b/src/forum.nim index 70baf54..21f9f02 100644 --- a/src/forum.nim +++ b/src/forum.nim @@ -716,7 +716,7 @@ proc executeDeletePost(c: TForumData, postId: int) = """ let id = getValue(db, postQuery, c.username, postId) - if id.len == 0 and c.rank < Admin: + if id.len == 0 and (c.rank == Admin or c.userid == postRow[0]): raise newForumError("You cannot delete this post") # Set the `isDeleted` flag. From f0d9a89167b83415edfe77f30a38bd4013d2767d Mon Sep 17 00:00:00 2001 From: hlaaftana <10591326+hlaaftana@users.noreply.github.com> Date: Fri, 17 Apr 2020 14:30:58 +0300 Subject: [PATCH 35/80] Fix query for executeDeletePost --- src/forum.nim | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/forum.nim b/src/forum.nim index 21f9f02..e55ef11 100644 --- a/src/forum.nim +++ b/src/forum.nim @@ -711,12 +711,12 @@ proc executeLockState(c: TForumData, threadId: int, locked: bool) = proc executeDeletePost(c: TForumData, postId: int) = # Verify that this post belongs to the user. const postQuery = sql""" - select p.id from post p + select p.author, p.id from post p where p.author = ? and p.id = ? """ - let id = getValue(db, postQuery, c.username, postId) + let postRow = getValue(db, postQuery, c.username, postId) - if id.len == 0 and (c.rank == Admin or c.userid == postRow[0]): + if postRow[1].len == 0 and not (c.rank == Admin or c.userid == postRow[0]): raise newForumError("You cannot delete this post") # Set the `isDeleted` flag. From cd565eabe085cfd6d8c1231209e08b92c306f8c2 Mon Sep 17 00:00:00 2001 From: hlaaftana <10591326+hlaaftana@users.noreply.github.com> Date: Fri, 17 Apr 2020 14:40:18 +0300 Subject: [PATCH 36/80] Change to row --- src/forum.nim | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/forum.nim b/src/forum.nim index e55ef11..88a6d5e 100644 --- a/src/forum.nim +++ b/src/forum.nim @@ -714,7 +714,7 @@ proc executeDeletePost(c: TForumData, postId: int) = select p.author, p.id from post p where p.author = ? and p.id = ? """ - let postRow = getValue(db, postQuery, c.username, postId) + let postRow = getRow(db, postQuery, c.username, postId) if postRow[1].len == 0 and not (c.rank == Admin or c.userid == postRow[0]): raise newForumError("You cannot delete this post") From 3b6e7363a96817d945de1f27a15a8ff02b270be4 Mon Sep 17 00:00:00 2001 From: Danil Yarantsev <21169548+Yardanico@users.noreply.github.com> Date: Sun, 10 May 2020 12:52:14 +0300 Subject: [PATCH 37/80] Add ability to enable TLS for SMTP --- src/email.nim | 2 ++ src/setup_nimforum.nim | 10 ++++++---- src/utils.nim | 1 + 3 files changed, 9 insertions(+), 4 deletions(-) diff --git a/src/email.nim b/src/email.nim index d26f4ad..fb9d5a3 100644 --- a/src/email.nim +++ b/src/email.nim @@ -45,6 +45,8 @@ proc sendMail( return var client = newAsyncSmtp() + if mailer.config.smtpTls: + await client.startTls() await client.connect(mailer.config.smtpAddress, Port(mailer.config.smtpPort)) if mailer.config.smtpUser.len > 0: await client.auth(mailer.config.smtpUser, mailer.config.smtpPassword) diff --git a/src/setup_nimforum.nim b/src/setup_nimforum.nim index e50b96d..c1f8473 100644 --- a/src/setup_nimforum.nim +++ b/src/setup_nimforum.nim @@ -234,7 +234,7 @@ proc initialiseDb(admin: tuple[username, password, email: string], proc initialiseConfig( name, title, hostname: string, recaptcha: tuple[siteKey, secretKey: string], - smtp: tuple[address, user, password, fromAddr: string], + smtp: tuple[address, user, password, fromAddr: string, tls: bool], isDev: bool, dbPath: string, ga: string="" @@ -251,6 +251,7 @@ proc initialiseConfig( "smtpUser": %smtp.user, "smtpPassword": %smtp.password, "smtpFromAddr": %smtp.fromAddr, + "smtpTls": %smtp.tls, "isDev": %isDev, "dbPath": %dbPath } @@ -294,6 +295,7 @@ These can be changed later in the generated forum.json file. let smtpUser = question("SMTP user: ") let smtpPassword = readPasswordFromStdin("SMTP pass: ") let smtpFromAddr = question("SMTP sending email address (eg: mail@mail.hostname.com): ") + let smtpTls = parseBool(question("Enable TLS for SMTP: ")) echo("The following is optional. You can specify your Google Analytics ID " & "if you wish. Otherwise just leave it blank.") @@ -303,7 +305,7 @@ These can be changed later in the generated forum.json file. let dbPath = "nimforum.db" initialiseConfig( name, title, hostname, (recaptchaSiteKey, recaptchaSecretKey), - (smtpAddress, smtpUser, smtpPassword, smtpFromAddr), isDev=false, + (smtpAddress, smtpUser, smtpPassword, smtpFromAddr, smtpTls), isDev=false, dbPath, ga ) @@ -338,7 +340,7 @@ when isMainModule: "Development Forum", "localhost", recaptcha=("", ""), - smtp=("", "", "", ""), + smtp=("", "", "", "", false), isDev=true, dbPath ) @@ -355,7 +357,7 @@ when isMainModule: "Test Forum", "localhost", recaptcha=("", ""), - smtp=("", "", "", ""), + smtp=("", "", "", "", false), isDev=true, dbPath ) diff --git a/src/utils.nim b/src/utils.nim index 1be058b..b915a41 100644 --- a/src/utils.nim +++ b/src/utils.nim @@ -17,6 +17,7 @@ type smtpUser*: string smtpPassword*: string smtpFromAddr*: string + smtpTls*: bool mlistAddress*: string recaptchaSecretKey*: string recaptchaSiteKey*: string From 6c6ed08ec96368e05143bec15e693d370a0e986c Mon Sep 17 00:00:00 2001 From: Danil Yarantsev <21169548+Yardanico@users.noreply.github.com> Date: Sun, 10 May 2020 12:56:13 +0300 Subject: [PATCH 38/80] startTls should be after connection of course --- src/email.nim | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/email.nim b/src/email.nim index fb9d5a3..f89f010 100644 --- a/src/email.nim +++ b/src/email.nim @@ -45,9 +45,9 @@ proc sendMail( return var client = newAsyncSmtp() + await client.connect(mailer.config.smtpAddress, Port(mailer.config.smtpPort)) if mailer.config.smtpTls: await client.startTls() - await client.connect(mailer.config.smtpAddress, Port(mailer.config.smtpPort)) if mailer.config.smtpUser.len > 0: await client.auth(mailer.config.smtpUser, mailer.config.smtpPassword) From a3052efd781dc24b9d896fa70d9f477e05c8a2ae Mon Sep 17 00:00:00 2001 From: Danil Yarantsev <21169548+Yardanico@users.noreply.github.com> Date: Sun, 10 May 2020 12:59:31 +0300 Subject: [PATCH 39/80] Add smptTls to config loading --- src/utils.nim | 1 + 1 file changed, 1 insertion(+) diff --git a/src/utils.nim b/src/utils.nim index b915a41..5c71b28 100644 --- a/src/utils.nim +++ b/src/utils.nim @@ -56,6 +56,7 @@ proc loadConfig*(filename = getCurrentDir() / "forum.json"): Config = result.smtpUser = root{"smtpUser"}.getStr("") result.smtpPassword = root{"smtpPassword"}.getStr("") result.smtpFromAddr = root{"smtpFromAddr"}.getStr("") + result.smtpTls = root{"smtpTls"}.getBool(false) result.mlistAddress = root{"mlistAddress"}.getStr("") result.recaptchaSecretKey = root{"recaptchaSecretKey"}.getStr("") result.recaptchaSiteKey = root{"recaptchaSiteKey"}.getStr("") From e7495128181262d881096326e88115d4e70386b5 Mon Sep 17 00:00:00 2001 From: Danil Yarantsev <21169548+Yardanico@users.noreply.github.com> Date: Sun, 10 May 2020 13:09:12 +0300 Subject: [PATCH 40/80] Update Karax and Jester dependencies --- nimforum.nimble | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/nimforum.nimble b/nimforum.nimble index 3d21367..e978fa7 100644 --- a/nimforum.nimble +++ b/nimforum.nimble @@ -13,13 +13,13 @@ skipExt = @["nim"] # Dependencies requires "nim >= 1.0.6" -requires "jester#d8a03aa" +requires "jester#405be2e" requires "bcrypt#head" requires "hmac#9c61ebe2fd134cf97" requires "recaptcha#d06488e" requires "sass#649e0701fa5c" -requires "karax#f6bda9a" +requires "karax#6b75300" requires "webdriver#429933a" From 55c94768100bbeefe04824d55ed4b68c7375f671 Mon Sep 17 00:00:00 2001 From: Danil Yarantsev <21169548+Yardanico@users.noreply.github.com> Date: Sun, 10 May 2020 13:11:46 +0300 Subject: [PATCH 41/80] Karax to 1.1.2 --- nimforum.nimble | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nimforum.nimble b/nimforum.nimble index e978fa7..a879012 100644 --- a/nimforum.nimble +++ b/nimforum.nimble @@ -19,7 +19,7 @@ requires "hmac#9c61ebe2fd134cf97" requires "recaptcha#d06488e" requires "sass#649e0701fa5c" -requires "karax#6b75300" +requires "karax#5f21dcd" requires "webdriver#429933a" From f5e1a71e6eb678425f05e465682fb0b875fc4378 Mon Sep 17 00:00:00 2001 From: hlaaftana <10591326+hlaaftana@users.noreply.github.com> Date: Sun, 10 May 2020 17:00:39 +0300 Subject: [PATCH 42/80] add clear variables --- src/forum.nim | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/forum.nim b/src/forum.nim index 88a6d5e..5d55670 100644 --- a/src/forum.nim +++ b/src/forum.nim @@ -714,9 +714,12 @@ proc executeDeletePost(c: TForumData, postId: int) = select p.author, p.id from post p where p.author = ? and p.id = ? """ - let postRow = getRow(db, postQuery, c.username, postId) + let + row = getRow(db, postQuery, c.username, postId) + author = row[0] + id = row[1] - if postRow[1].len == 0 and not (c.rank == Admin or c.userid == postRow[0]): + if id.len == 0 and not (c.rank == Admin or c.userid == author): raise newForumError("You cannot delete this post") # Set the `isDeleted` flag. From 16abee059633622bd1cff90ac7cd75320046edaa Mon Sep 17 00:00:00 2001 From: hlaaftana <10591326+hlaaftana@users.noreply.github.com> Date: Sun, 10 May 2020 17:54:32 +0300 Subject: [PATCH 43/80] add user delete thread test --- tests/browsertests/threads.nim | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/tests/browsertests/threads.nim b/tests/browsertests/threads.nim index b79d93a..f5b502d 100644 --- a/tests/browsertests/threads.nim +++ b/tests/browsertests/threads.nim @@ -40,6 +40,18 @@ proc userTests(session: Session, baseUrl: string) = checkText "#thread-title .title-text", userTitleStr checkText ".original-post div.post-content", userContentStr + test "can delete thread": + with session: + click userTitleStr, LinkTextSelector + + click ".post-buttons .delete-button" + + # click delete confirmation + click "#delete-modal .delete-btn" + + # Make sure the forum post is gone + checkIsNone userTitleStr, LinkTextSelector + session.logout() proc anonymousTests(session: Session, baseUrl: string) = @@ -158,4 +170,4 @@ proc test*(session: Session, baseUrl: string) = unBanUser(session, baseUrl) - session.navigate(baseUrl) \ No newline at end of file + session.navigate(baseUrl) From 2987955e8a8ca3b1a7d24a4f8b0c74f04c197495 Mon Sep 17 00:00:00 2001 From: hlaaftana <10591326+hlaaftana@users.noreply.github.com> Date: Sun, 10 May 2020 19:15:02 +0300 Subject: [PATCH 44/80] fix delete thread test --- tests/browsertests/threads.nim | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/tests/browsertests/threads.nim b/tests/browsertests/threads.nim index f5b502d..e80b9c0 100644 --- a/tests/browsertests/threads.nim +++ b/tests/browsertests/threads.nim @@ -42,7 +42,13 @@ proc userTests(session: Session, baseUrl: string) = test "can delete thread": with session: - click userTitleStr, LinkTextSelector + # create thread to be deleted + click "#new-thread-btn" + + sendKeys "#thread-title", "To be deleted" + sendKeys "#reply-textarea", "This thread is to be deleted" + + click "#create-thread-btn" click ".post-buttons .delete-button" @@ -50,7 +56,7 @@ proc userTests(session: Session, baseUrl: string) = click "#delete-modal .delete-btn" # Make sure the forum post is gone - checkIsNone userTitleStr, LinkTextSelector + checkIsNone "To be deleted", LinkTextSelector session.logout() From 418bb3fe47411777c566fed202412780f0b318be Mon Sep 17 00:00:00 2001 From: Andinus Date: Fri, 22 May 2020 12:12:11 +0530 Subject: [PATCH 45/80] Add Date header to emails UTC time is used because we cannot format time as "Fri, 22 May 2020 06:33:00 +0000" with the times package. "zz" returns +00 & "zzz" returns +00:00, note that the former doesn't return minutes value so it'll return +05 for systems in timezone +0530 & "zzz" will return +05:30 for the same. Instead of parsing it again & removing ':' manually we use UTC time & add "+0000". --- src/email.nim | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/email.nim b/src/email.nim index f89f010..e29dc54 100644 --- a/src/email.nim +++ b/src/email.nim @@ -56,6 +56,9 @@ proc sendMail( var headers = otherHeaders headers.add(("From", mailer.config.smtpFromAddr)) + let dateHeader = now().utc().format("ddd, dd MMM yyyy hh:mm:ss") & " +0000" + headers.add(("Date", dateHeader)) + let encoded = createMessage(subject, message, toList, @[], headers) From 474fa6398529051e759426081c182aa08394657f Mon Sep 17 00:00:00 2001 From: Viet Hung Nguyen Date: Sat, 6 Jun 2020 14:37:18 +0700 Subject: [PATCH 46/80] Add SSL support for sending email --- src/email.nim | 12 ++++++++++-- src/utils.nim | 2 ++ 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/src/email.nim b/src/email.nim index f89f010..aa9963c 100644 --- a/src/email.nim +++ b/src/email.nim @@ -44,10 +44,18 @@ proc sendMail( warn("Cannot send mail: no smtp from address configured (smtpFromAddr).") return - var client = newAsyncSmtp() - await client.connect(mailer.config.smtpAddress, Port(mailer.config.smtpPort)) + var client: AsyncSmtp if mailer.config.smtpTls: + client = newAsyncSmtp(useSsl=false) + await client.connect(mailer.config.smtpAddress, Port(mailer.config.smtpPort)) await client.startTls() + elif mailer.config.smtpSsl: + client = newAsyncSmtp(useSsl=true) + await client.connect(mailer.config.smtpAddress, Port(mailer.config.smtpPort)) + else: + client = newAsyncSmtp(useSsl=false) + await client.connect(mailer.config.smtpAddress, Port(mailer.config.smtpPort)) + if mailer.config.smtpUser.len > 0: await client.auth(mailer.config.smtpUser, mailer.config.smtpPassword) diff --git a/src/utils.nim b/src/utils.nim index 5c71b28..ae36c89 100644 --- a/src/utils.nim +++ b/src/utils.nim @@ -18,6 +18,7 @@ type smtpPassword*: string smtpFromAddr*: string smtpTls*: bool + smtpSsl*: bool mlistAddress*: string recaptchaSecretKey*: string recaptchaSiteKey*: string @@ -57,6 +58,7 @@ proc loadConfig*(filename = getCurrentDir() / "forum.json"): Config = result.smtpPassword = root{"smtpPassword"}.getStr("") result.smtpFromAddr = root{"smtpFromAddr"}.getStr("") result.smtpTls = root{"smtpTls"}.getBool(false) + result.smtpSsl = root{"smtpSsl"}.getBool(false) result.mlistAddress = root{"mlistAddress"}.getStr("") result.recaptchaSecretKey = root{"recaptchaSecretKey"}.getStr("") result.recaptchaSiteKey = root{"recaptchaSiteKey"}.getStr("") From b933a9b2e803e9c610d1f67e69fa41b9ab0a9718 Mon Sep 17 00:00:00 2001 From: Viet Hung Nguyen Date: Sat, 6 Jun 2020 21:36:14 +0700 Subject: [PATCH 47/80] Gives note that only v2 work at the moment --- src/setup_nimforum.nim | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/setup_nimforum.nim b/src/setup_nimforum.nim index c1f8473..34511fa 100644 --- a/src/setup_nimforum.nim +++ b/src/setup_nimforum.nim @@ -283,7 +283,7 @@ These can be changed later in the generated forum.json file. echo("") echo("The following question are related to recaptcha. \nYou must set up a " & - "recaptcha for your forum before answering them. \nPlease do so now " & + "recaptcha v2 for your forum before answering them. \nPlease do so now " & "and then answer these questions: https://www.google.com/recaptcha/admin") let recaptchaSiteKey = question("Recaptcha site key: ") let recaptchaSecretKey = question("Recaptcha secret key: ") From ebbfa265d56186a8933d0131c58b6d1d58b7dfec Mon Sep 17 00:00:00 2001 From: Andinus Date: Fri, 12 Jun 2020 22:21:45 +0530 Subject: [PATCH 48/80] Send confirmation email to updated address This will fix https://github.com/nim-lang/nimforum/issues/155. Currently nimforum sends the confirmation email to the address in database but it should've sent it to the new address. Activity: User changes email Issue: Confirmation email is sent to old address Fix: Send the confirmation email to updated address --- src/forum.nim | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/forum.nim b/src/forum.nim index 5d55670..a880610 100644 --- a/src/forum.nim +++ b/src/forum.nim @@ -774,7 +774,7 @@ proc updateProfile( raise newForumError("Rank needs a change when setting new email.") await sendSecureEmail( - mailer, ActivateEmail, c.req, row[0], row[1], row[2], row[3] + mailer, ActivateEmail, c.req, row[0], row[1], email, row[3] ) validateEmail(email, checkDuplicated=wasEmailChanged) From 5c4d9b36d383baeb0d98f28fd12dc02f2359260c Mon Sep 17 00:00:00 2001 From: Andinus Date: Sat, 13 Jun 2020 00:06:38 +0530 Subject: [PATCH 49/80] Add additional setup information to README The build failed for me & was fixed after installing "karax" with nimble. --- README.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/README.md b/README.md index 05d4667..53c0c28 100644 --- a/README.md +++ b/README.md @@ -80,6 +80,9 @@ nimble frontend nimble backend ``` +Note: You might have to run `nimble install karax@#5f21dcd`, if setup +fails with [this error](https://paste.debian.net/1151756/). The hash +needs to be replaced with the one specified in output. Development typically involves running `nimble devdb` which sets up the database for development and testing, then `nimble backend` From 8bad518e4ba322456ca62cc66c375008dfb4921d Mon Sep 17 00:00:00 2001 From: Andinus Date: Sat, 13 Jun 2020 00:55:32 +0530 Subject: [PATCH 50/80] Move setup fail information to Troubleshooting section This also pastes the output instead of linking to pastebin. --- README.md | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 53c0c28..0346c61 100644 --- a/README.md +++ b/README.md @@ -80,10 +80,6 @@ nimble frontend nimble backend ``` -Note: You might have to run `nimble install karax@#5f21dcd`, if setup -fails with [this error](https://paste.debian.net/1151756/). The hash -needs to be replaced with the one specified in output. - Development typically involves running `nimble devdb` which sets up the database for development and testing, then `nimble backend` which compiles and runs the forum's backend, and `nimble frontend` @@ -91,6 +87,22 @@ separately to build the frontend. When making changes to the frontend it should be enough to simply run `nimble frontend` again to rebuild. This command will also build the SASS ``nimforum.scss`` file in the `public/css` directory. +# Troubleshooting + +You might have to run `nimble install karax@#5f21dcd`, if setup fails +with: + +``` +andinus@circinus ~/projects/forks/nimforum> nimble --verbose devdb +[...] + Installing karax@#5f21dcd + Tip: 24 messages have been suppressed, use --verbose to show them. + Error: No binaries built, did you specify a valid binary name? +[...] + Error: Exception raised during nimble script execution +``` + +The hash needs to be replaced with the one specified in output. # Copyright From 16c9daea52d714798d0e675ed42ca15396e3ffb4 Mon Sep 17 00:00:00 2001 From: Miran Date: Thu, 6 Aug 2020 14:07:31 +0200 Subject: [PATCH 51/80] fix deprecated import (#254) --- src/utils.nim | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/utils.nim b/src/utils.nim index ae36c89..f092dc6 100644 --- a/src/utils.nim +++ b/src/utils.nim @@ -1,6 +1,6 @@ import asyncdispatch, smtp, strutils, json, os, rst, rstgen, xmltree, strtabs, htmlparser, streams, parseutils, options, logging -from times import getTime, getGMTime, format +from times import getTime, utc, format # Used to be: # {'A'..'Z', 'a'..'z', '0'..'9', '_', '\128'..'\255'} From 030c52020ed04db005c4ae50570b83520c70486e Mon Sep 17 00:00:00 2001 From: jiro Date: Sun, 23 Aug 2020 22:30:06 +0900 Subject: [PATCH 52/80] Add docker environment for local development (#257) * Add docker environment * Add document of docker * Move docker files * Change context * Move git submodule command to entrypoint.sh * Update README --- README.md | 16 ++++++++++++++++ docker/Dockerfile | 14 ++++++++++++++ docker/docker-compose.yml | 12 ++++++++++++ docker/entrypoint.sh | 19 +++++++++++++++++++ 4 files changed, 61 insertions(+) create mode 100644 docker/Dockerfile create mode 100644 docker/docker-compose.yml create mode 100755 docker/entrypoint.sh diff --git a/README.md b/README.md index 0346c61..d7dedb4 100644 --- a/README.md +++ b/README.md @@ -87,6 +87,22 @@ separately to build the frontend. When making changes to the frontend it should be enough to simply run `nimble frontend` again to rebuild. This command will also build the SASS ``nimforum.scss`` file in the `public/css` directory. +### With docker + +You can easily launch site on localhost if you have `docker` and `docker-compose`. +You don't have to setup dependencies (libsass, sglite, pcre, etc...) on you host PC. + +To get up and running: + +```bash +cd docker +docker-compose build +docker-compose up +``` + +And you can access local NimForum site. +Open http://localhost:5000 . + # Troubleshooting You might have to run `nimble install karax@#5f21dcd`, if setup fails diff --git a/docker/Dockerfile b/docker/Dockerfile new file mode 100644 index 0000000..cb3191a --- /dev/null +++ b/docker/Dockerfile @@ -0,0 +1,14 @@ +FROM nimlang/nim:1.2.6-ubuntu + +RUN apt-get update -yqq \ + && apt-get install -y --no-install-recommends \ + libsass-dev \ + sqlite3 \ + && apt-get clean \ + && rm -rf /var/lib/apt/lists/* + +WORKDIR /app +COPY . /app + +# install dependencies +RUN nimble install -Y diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml new file mode 100644 index 0000000..8657235 --- /dev/null +++ b/docker/docker-compose.yml @@ -0,0 +1,12 @@ +version: "3.7" + +services: + forum: + build: + context: ../ + dockerfile: ./docker/Dockerfile + volumes: + - "../:/app" + ports: + - "5000:5000" + entrypoint: "/app/docker/entrypoint.sh" diff --git a/docker/entrypoint.sh b/docker/entrypoint.sh new file mode 100755 index 0000000..d8f5923 --- /dev/null +++ b/docker/entrypoint.sh @@ -0,0 +1,19 @@ +#!/bin/sh + +set -eu + +git submodule update --init --recursive + +# setup +nimble c -d:release src/setup_nimforum.nim +./src/setup_nimforum --dev + +# build frontend +nimble c -r src/buildcss +nimble js -d:release src/frontend/forum.nim +mkdir -p public/js +cp src/frontend/forum.js public/js/forum.js + +# build backend +nimble c src/forum.nim +./src/forum From 6e32ec27b4ac1fc07f513f94089a0e0cedf31454 Mon Sep 17 00:00:00 2001 From: Miran Date: Mon, 24 Aug 2020 13:48:23 +0200 Subject: [PATCH 53/80] moderators should be able to edit categories (#255) --- src/forum.nim | 2 +- src/frontend/user.nim | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/forum.nim b/src/forum.nim index a880610..563db57 100644 --- a/src/forum.nim +++ b/src/forum.nim @@ -532,7 +532,7 @@ proc updateThread(c: TForumData, threadId: string, queryKeys: seq[string], query let threadAuthor = selectThreadAuthor(threadId.parseInt) # Verify that the current user has permissions to edit the specified thread. - let canEdit = c.rank == Admin or c.userid == threadAuthor.name + let canEdit = c.rank in {Admin, Moderator} or c.userid == threadAuthor.name if not canEdit: raise newForumError("You cannot edit this thread") diff --git a/src/frontend/user.nim b/src/frontend/user.nim index fe4efa7..4428aa4 100644 --- a/src/frontend/user.nim +++ b/src/frontend/user.nim @@ -75,4 +75,4 @@ when defined(js): title="User is a moderator") of Admin: italic(class="fas fa-chess-knight", - title="User is an admin") \ No newline at end of file + title="User is an admin") From e62ae672b30de239830526ce032f1a1f10427387 Mon Sep 17 00:00:00 2001 From: Dominik Picheta Date: Mon, 24 Aug 2020 20:47:01 +0100 Subject: [PATCH 54/80] Optimise threads.json SQL query. --- src/forum.nim | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/forum.nim b/src/forum.nim index 563db57..796edaa 100644 --- a/src/forum.nim +++ b/src/forum.nim @@ -839,10 +839,10 @@ routes: from thread t, category c, person u where t.isDeleted = 0 and category = c.id and $# u.status <> 'Spammer' and u.status <> 'Troll' and - u.id in ( - select u.id from post p, person u - where p.author = u.id and p.thread = t.id - order by u.id + u.id = ( + select p.author from post p + where p.thread = t.id + order by p.author limit 1 ) order by modified desc limit ?, ?;""" From 77fd9af1cd5ea20a8768725d1a70afa384ff8e3b Mon Sep 17 00:00:00 2001 From: Dominik Picheta Date: Mon, 24 Aug 2020 21:02:23 +0100 Subject: [PATCH 55/80] Version 2.1.0. --- nimforum.nimble | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/nimforum.nimble b/nimforum.nimble index a879012..58a22f7 100644 --- a/nimforum.nimble +++ b/nimforum.nimble @@ -1,5 +1,5 @@ # Package -version = "2.0.2" +version = "2.1.0" author = "Dominik Picheta" description = "The Nim forum" license = "MIT" @@ -14,7 +14,7 @@ skipExt = @["nim"] requires "nim >= 1.0.6" requires "jester#405be2e" -requires "bcrypt#head" +requires "bcrypt#440c5676ff6" requires "hmac#9c61ebe2fd134cf97" requires "recaptcha#d06488e" requires "sass#649e0701fa5c" From dc80ef022ec4911acaf5a825c07cb2dae7f14e9f Mon Sep 17 00:00:00 2001 From: Miran Date: Fri, 28 Aug 2020 11:10:17 +0200 Subject: [PATCH 56/80] properly do #255 - moderators can change categories (#258) --- src/frontend/postlist.nim | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/frontend/postlist.nim b/src/frontend/postlist.nim index 305e059..7da57c1 100644 --- a/src/frontend/postlist.nim +++ b/src/frontend/postlist.nim @@ -209,12 +209,12 @@ when defined(js): let loggedIn = currentUser.isSome() let authoredByUser = loggedIn and currentUser.get().name == thread.author.name - let currentAdmin = - currentUser.isSome() and currentUser.get().rank == Admin + let canChangeCategory = + loggedIn and currentUser.get().rank in {Admin, Moderator} result = buildHtml(): tdiv(): - if authoredByUser or currentAdmin: + if authoredByUser or canChangeCategory: render(state.categoryPicker, currentUser, compact=false) else: render(thread.category) From 4821746c5de151d7c223b75049ba285dd8b1d191 Mon Sep 17 00:00:00 2001 From: Joey Yakimowich-Payne Date: Sat, 29 Aug 2020 11:25:40 -0600 Subject: [PATCH 57/80] Add user id to the user object and fix thread user check --- src/forum.nim | 39 ++++++++++++++++++++------------------- src/frontend/user.nim | 1 + src/fts.sql | 1 + 3 files changed, 22 insertions(+), 19 deletions(-) diff --git a/src/forum.nim b/src/forum.nim index 796edaa..09f6180 100644 --- a/src/forum.nim +++ b/src/forum.nim @@ -283,12 +283,13 @@ template createTFD() = proc selectUser(userRow: seq[string], avatarSize: int=80): User = result = User( - name: userRow[0], - avatarUrl: userRow[1].getGravatarUrl(avatarSize), - lastOnline: userRow[2].parseInt, - previousVisitAt: userRow[3].parseInt, - rank: parseEnum[Rank](userRow[4]), - isDeleted: userRow[5] == "1" + id: userRow[0], + name: userRow[1], + avatarUrl: userRow[2].getGravatarUrl(avatarSize), + lastOnline: userRow[3].parseInt, + previousVisitAt: userRow[4].parseInt, + rank: parseEnum[Rank](userRow[5]), + isDeleted: userRow[6] == "1" ) # Don't give data about a deleted user. @@ -302,7 +303,7 @@ proc selectPost(postRow: seq[string], skippedPosts: seq[int], return Post( id: postRow[0].parseInt, replyingTo: replyingTo, - author: selectUser(postRow[5..10]), + author: selectUser(postRow[5..11]), likes: likes, seen: false, # TODO: history: history, @@ -318,7 +319,7 @@ proc selectReplyingTo(replyingTo: string): Option[PostLink] = const replyingToQuery = sql""" select p.id, strftime('%s', p.creation), p.thread, - 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, t.name @@ -334,7 +335,7 @@ proc selectReplyingTo(replyingTo: string): Option[PostLink] = topic: row[^1], threadId: row[2].parseInt(), postId: row[0].parseInt(), - author: some(selectUser(row[3..8])) + author: some(selectUser(row[3..9])) )) proc selectHistory(postId: int): seq[PostInfo] = @@ -353,7 +354,7 @@ proc selectHistory(postId: int): seq[PostInfo] = proc selectLikes(postId: int): seq[User] = const likeQuery = sql""" - select u.name, u.email, strftime('%s', u.lastOnline), + select u.id, u.name, u.email, strftime('%s', u.lastOnline), strftime('%s', u.previousVisitAt), u.status, u.isDeleted from like h, person u @@ -368,9 +369,9 @@ proc selectLikes(postId: int): seq[User] = proc selectThreadAuthor(threadId: int): User = const authorQuery = sql""" - select name, email, strftime('%s', lastOnline), + select u.id, name, email, strftime('%s', lastOnline), strftime('%s', previousVisitAt), status, isDeleted - from person where id in ( + from person u where id in ( select author from post where thread = ? order by id @@ -386,7 +387,7 @@ proc selectThread(threadRow: seq[string], author: User): Thread = where thread = ?;""" const usersListQuery = sql""" - select name, email, strftime('%s', lastOnline), + select u.id, name, email, strftime('%s', lastOnline), strftime('%s', previousVisitAt), status, u.isDeleted, count(*) from person u, post p where p.author = u.id and p.thread = ? @@ -532,7 +533,7 @@ proc updateThread(c: TForumData, threadId: string, queryKeys: seq[string], query let threadAuthor = selectThreadAuthor(threadId.parseInt) # Verify that the current user has permissions to edit the specified thread. - let canEdit = c.rank in {Admin, Moderator} or c.userid == threadAuthor.name + let canEdit = c.rank in {Admin, Moderator} or c.userid == threadAuthor.id if not canEdit: raise newForumError("You cannot edit this thread") @@ -834,7 +835,7 @@ routes: const threadsQuery = """select t.id, t.name, views, strftime('%s', modified), isLocked, c.id, c.name, c.description, c.color, - 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 from thread t, category c, person u where t.isDeleted = 0 and category = c.id and $# @@ -879,7 +880,7 @@ routes: sql( """select p.id, p.content, strftime('%s', p.creation), p.author, p.replyingTo, - 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 from post p, person u @@ -926,7 +927,7 @@ routes: let postsQuery = sql(""" select p.id, p.content, strftime('%s', p.creation), p.author, p.replyingTo, - 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 from post p, person u @@ -995,7 +996,7 @@ routes: """ % postsFrom) let userQuery = sql(""" - select name, email, strftime('%s', lastOnline), + select id, name, email, strftime('%s', lastOnline), strftime('%s', previousVisitAt), status, isDeleted, strftime('%s', creation), id from person @@ -1570,7 +1571,7 @@ routes: postId: rowFT[2].parseInt(), postContent: content, creation: rowFT[4].parseInt(), - author: selectUser(rowFT[5 .. 10]), + author: selectUser(rowFT[5 .. 11]), ) ) diff --git a/src/frontend/user.nim b/src/frontend/user.nim index 4428aa4..db874c3 100644 --- a/src/frontend/user.nim +++ b/src/frontend/user.nim @@ -17,6 +17,7 @@ type Admin ## Admin: can do everything User* = object + id*: string name*: string avatarUrl*: string lastOnline*: int64 diff --git a/src/fts.sql b/src/fts.sql index e5490f2..ee6f1af 100644 --- a/src/fts.sql +++ b/src/fts.sql @@ -46,6 +46,7 @@ SELECT THEN snippet(post_fts, '**', '**', '...', what, -45) ELSE SUBSTR(post_fts.content, 1, 200) END AS content, cdate, + person.id, person.name AS author, person.email AS email, strftime('%s', person.lastOnline) AS lastOnline, From 9739c34abd322b4bf37847361041ea76257c6ba8 Mon Sep 17 00:00:00 2001 From: Joey Yakimowich-Payne Date: Sat, 29 Aug 2020 11:49:00 -0600 Subject: [PATCH 58/80] Add test for category change --- tests/browsertests/categories.nim | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/tests/browsertests/categories.nim b/tests/browsertests/categories.nim index c299218..8c27a87 100644 --- a/tests/browsertests/categories.nim +++ b/tests/browsertests/categories.nim @@ -69,6 +69,29 @@ proc categoriesUserTests(session: Session, baseUrl: string) = ensureExists title, LinkTextSelector + test "can create category thread and change category": + with session: + let newTitle = title & " Selection" + click "#new-thread-btn" + sendKeys "#thread-title", newTitle + + selectCategory "fun" + sendKeys "#reply-textarea", content + + click "#create-thread-btn" + checkText "#thread-title .category", "Fun" + + selectCategory "announcements" + + checkText "#thread-title .category", "Announcements" + + # Make sure there is no error + checkIsNone "#thread-title .text-error" + + navigate baseUrl + + ensureExists newTitle, LinkTextSelector + test "can navigate to categories page": with session: click "#categories-btn" From ce3de27fb923c8095baf87898323da04354d2107 Mon Sep 17 00:00:00 2001 From: Joey Yakimowich-Payne Date: Sat, 29 Aug 2020 12:48:57 -0600 Subject: [PATCH 59/80] Fix user row index --- src/forum.nim | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/forum.nim b/src/forum.nim index 09f6180..36126a7 100644 --- a/src/forum.nim +++ b/src/forum.nim @@ -295,7 +295,7 @@ proc selectUser(userRow: seq[string], avatarSize: int=80): User = # Don't give data about a deleted user. if result.isDeleted: result.name = "DeletedUser" - result.avatarUrl = getGravatarUrl(result.name & userRow[1], avatarSize) + result.avatarUrl = getGravatarUrl(result.name & userRow[2], avatarSize) proc selectPost(postRow: seq[string], skippedPosts: seq[int], replyingTo: Option[PostLink], history: seq[PostInfo], @@ -1022,7 +1022,7 @@ routes: getValue(db, sql("select count(*) " & threadsFrom), userID).parseInt() if c.rank >= Admin or c.username == username: - profile.email = some(userRow[1]) + profile.email = some(userRow[2]) for row in db.getAllRows(postsQuery, username): profile.posts.add( From b27096ff752d42880a6af3ec0aa506aef60259ab Mon Sep 17 00:00:00 2001 From: narimiran Date: Fri, 28 Aug 2020 07:30:57 +0200 Subject: [PATCH 60/80] allowed editing time is now much shorter Other forums usually have allowed editing times measured in *minutes*, we had it in weeks. Two hours should be plenty of time to edit a post, but more importantly it should prevent spamming mis-usages that sometimes happened before: You read a perfectly normal post (usually copy-pasted from somewhere) and then much later on (when most of us regular forum users don't notice anymore because we frequently read new threads/posts) it is edited to contain spammy links and content. Admins must be able to always edit a post, no matter of its age. --- src/forum.nim | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/forum.nim b/src/forum.nim index 796edaa..f3b401e 100644 --- a/src/forum.nim +++ b/src/forum.nim @@ -498,10 +498,10 @@ proc updatePost(c: TForumData, postId: int, content: string, # Verify that the current user has permissions to edit the specified post. let creation = fromUnix(postRow[1].parseInt) - let isArchived = (getTime() - creation).inWeeks > 8 + let isArchived = (getTime() - creation).inHours >= 2 let canEdit = c.rank == Admin or c.userid == postRow[0] - if isArchived: - raise newForumError("This post is archived and can no longer be edited") + if isArchived and c.rank < Admin: + raise newForumError("This post is too old and can no longer be edited") if not canEdit: raise newForumError("You cannot edit this post") From 4f8a585049b1efd42a2ef5c92bafed718b2dc828 Mon Sep 17 00:00:00 2001 From: Joey Yakimowich-Payne Date: Mon, 31 Aug 2020 15:35:17 -0600 Subject: [PATCH 61/80] Remove unnecessary table identifier --- src/forum.nim | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/forum.nim b/src/forum.nim index 36126a7..dacac75 100644 --- a/src/forum.nim +++ b/src/forum.nim @@ -369,9 +369,9 @@ proc selectLikes(postId: int): seq[User] = proc selectThreadAuthor(threadId: int): User = const authorQuery = sql""" - select u.id, name, email, strftime('%s', lastOnline), + select id, name, email, strftime('%s', lastOnline), strftime('%s', previousVisitAt), status, isDeleted - from person u where id in ( + from person where id in ( select author from post where thread = ? order by id From 3d975e8386b72d02bd3281243ad3a698f9d7c06c Mon Sep 17 00:00:00 2001 From: Joey Yakimowich-Payne Date: Sat, 29 Aug 2020 13:54:16 -0600 Subject: [PATCH 62/80] Replace webdriver with halonium --- nimforum.nimble | 2 +- tests/browsertester.nim | 13 +----- tests/browsertests/categories.nim | 18 ++++---- tests/browsertests/common.nim | 71 ++++--------------------------- tests/browsertests/issue181.nim | 2 +- tests/browsertests/scenario1.nim | 4 +- tests/browsertests/threads.nim | 2 +- 7 files changed, 25 insertions(+), 87 deletions(-) diff --git a/nimforum.nimble b/nimforum.nimble index 58a22f7..86dfc77 100644 --- a/nimforum.nimble +++ b/nimforum.nimble @@ -21,7 +21,7 @@ requires "sass#649e0701fa5c" requires "karax#5f21dcd" -requires "webdriver#429933a" +requires "halonium#f54c83f" # Tasks diff --git a/tests/browsertester.nim b/tests/browsertester.nim index 8b6c046..a840f40 100644 --- a/tests/browsertester.nim +++ b/tests/browsertester.nim @@ -1,6 +1,6 @@ import options, osproc, streams, threadpool, os, strformat, httpclient -import webdriver +import halonium proc runProcess(cmd: string) = let p = startProcess( @@ -46,22 +46,13 @@ template withBackend(body: untyped): untyped = import browsertests/[scenario1, threads, issue181, categories] proc main() = - # Kill any already running instances - discard execCmd("killall geckodriver") - spawn runProcess("geckodriver -p 4444 --log config") - defer: - discard execCmd("killall geckodriver") - # Create a fresh DB for the tester. doAssert(execCmd("nimble testdb") == QuitSuccess) doAssert(execCmd("nimble -y frontend") == QuitSuccess) - echo("Waiting for geckodriver to startup...") - sleep(5000) try: - let driver = newWebDriver() - let session = driver.createSession() + let session = createSession(Firefox) withBackend: scenario1.test(session, baseUrl) diff --git a/tests/browsertests/categories.nim b/tests/browsertests/categories.nim index 8c27a87..2acc6f3 100644 --- a/tests/browsertests/categories.nim +++ b/tests/browsertests/categories.nim @@ -1,5 +1,5 @@ import unittest, common -import webdriver +import halonium import karaxutils @@ -47,12 +47,12 @@ proc categoriesUserTests(session: Session, baseUrl: string) = with session: click "#new-thread-btn" - checkIsNone "#add-category" + checkIsNone "#add-category .plus-btn" test "no category add available category page": with session: click "#categories-btn" - checkIsNone "#add-category" + checkIsNone "#add-category .plus-btn" test "can create category thread": with session: @@ -139,17 +139,17 @@ proc categoriesUserTests(session: Session, baseUrl: string) = checkText "#threads-list .thread-title a", "Post 3" for element in session.waitForElements("#threads-list .category-name"): # Have to user "innerText" because elements are hidden on this page - assert element.getProperty("innerText") == "Unsorted" + assert element.text == "Unsorted" selectCategory "announcements" checkText "#threads-list .thread-title a", "Post 2" for element in session.waitForElements("#threads-list .category-name"): - assert element.getProperty("innerText") == "Announcements" + assert element.text == "Announcements" selectCategory "fun" checkText "#threads-list .thread-title a", "Post 1" for element in session.waitForElements("#threads-list .category-name"): - assert element.getProperty("innerText") == "Fun" + assert element.text == "Fun" session.logout() @@ -168,7 +168,7 @@ proc categoriesAdminTests(session: Session, baseUrl: string) = with session: click "#new-thread-btn" - ensureExists "#add-category" + ensureExists "#add-category .plus-btn" click "#add-category .plus-btn" @@ -195,10 +195,10 @@ proc categoriesAdminTests(session: Session, baseUrl: string) = test "category adding disabled on admin logout": with session: navigate(baseUrl & "c/0") - ensureExists "#add-category" + ensureExists "#add-category .plus-btn" logout() - checkIsNone "#add-category" + checkIsNone "#add-category .plus-btn" navigate baseUrl login "admin", "admin" diff --git a/tests/browsertests/common.nim b/tests/browsertests/common.nim index d906675..54ad254 100644 --- a/tests/browsertests/common.nim +++ b/tests/browsertests/common.nim @@ -1,9 +1,9 @@ import os, options, unittest, strutils -import webdriver +import halonium import macros -const actionDelayMs {.intdefine.} = 0 -## Inserts a delay in milliseconds between automated actions. Useful for debugging tests +export waitForElement +export waitForElements macro with*(obj: typed, code: untyped): untyped = ## Execute a set of statements with an object @@ -24,14 +24,6 @@ macro with*(obj: typed, code: untyped): untyped = result = getAst(checkCompiles(result, code)) -proc elementIsSome(element: Option[Element]): bool = - return element.isSome - -proc elementIsNone(element: Option[Element]): bool = - return element.isNone - -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) el.get().click() @@ -44,78 +36,33 @@ proc clear*(session: Session, element: string) = let el = session.waitForElement(element) el.get().clear() -proc sendKeys*(session: Session, element: string, keys: varargs[Key]) = +proc sendKeys*(session: Session, element: string, keys: varargs[string, convertKeyRuneString]) = let el = session.waitForElement(element) - # focus - el.get().click() - for key in keys: - session.press(key) + el.get().sendKeys(keys) proc ensureExists*(session: Session, element: string, strategy=CssSelector) = discard session.waitForElement(element, strategy) template check*(session: Session, element: string, function: untyped) = let el = session.waitForElement(element) - check function(el) + doAssert function(el) template check*(session: Session, element: string, strategy: LocationStrategy, function: untyped) = let el = session.waitForElement(element, strategy) - check function(el) + doAssert function(el) proc setColor*(session: Session, element, color: string, strategy=CssSelector) = let el = session.waitForElement(element, strategy) - discard session.execute("arguments[0].setAttribute('value', '" & color & "')", el.get()) + discard session.executeScript("arguments[0].setAttribute('value', '" & color & "')", el.get()) proc checkIsNone*(session: Session, element: string, strategy=CssSelector) = discard session.waitForElement(element, strategy, waitCondition=elementIsNone) 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=elementIsSome -): Option[Element] = - var waitTime = 0 - - when actionDelayMs > 0: - sleep(actionDelayMs) - - while true: - try: - let loading = session.findElement(selector, strategy) - if waitCondition(loading): - return loading - finally: - discard - sleep(pollTime) - waitTime += pollTime - - if waitTime > timeout: - doAssert false, "Wait for load time exceeded" - -proc waitForElements*( - session: Session, selector: string, strategy=CssSelector, - timeout=20000, pollTime=50 -): seq[Element] = - var waitTime = 0 - - when actionDelayMs > 0: - sleep(actionDelayMs) - - while true: - let loading = session.findElements(selector, strategy) - if loading.len > 0: - return loading - sleep(pollTime) - waitTime += pollTime - - if waitTime > timeout: - doAssert false, "Wait for load time exceeded" + doAssert el.get().text.strip() == expectedValue proc setUserRank*(session: Session, baseUrl, user, rank: string) = with session: diff --git a/tests/browsertests/issue181.nim b/tests/browsertests/issue181.nim index 031cd92..61a5494 100644 --- a/tests/browsertests/issue181.nim +++ b/tests/browsertests/issue181.nim @@ -1,6 +1,6 @@ import unittest, common -import webdriver +import halonium proc test*(session: Session, baseUrl: string) = session.navigate(baseUrl) diff --git a/tests/browsertests/scenario1.nim b/tests/browsertests/scenario1.nim index 6e10e4c..fc51e75 100644 --- a/tests/browsertests/scenario1.nim +++ b/tests/browsertests/scenario1.nim @@ -1,6 +1,6 @@ import unittest, common -import webdriver +import halonium proc test*(session: Session, baseUrl: string) = session.navigate(baseUrl) @@ -40,4 +40,4 @@ proc test*(session: Session, baseUrl: string) = register "TEst1", "test1", verify = false ensureExists "#signup-form .has-error" - navigate baseUrl \ No newline at end of file + navigate baseUrl diff --git a/tests/browsertests/threads.nim b/tests/browsertests/threads.nim index e80b9c0..d4efe57 100644 --- a/tests/browsertests/threads.nim +++ b/tests/browsertests/threads.nim @@ -1,6 +1,6 @@ import unittest, common -import webdriver +import halonium let userTitleStr = "This is a user thread!" From 7d8417ff97adb646a35dbf93d5e81ae8eaaee148 Mon Sep 17 00:00:00 2001 From: Dominik Picheta Date: Tue, 1 Sep 2020 18:27:25 +0100 Subject: [PATCH 63/80] Revert "Replace webdriver with halonium" --- nimforum.nimble | 2 +- tests/browsertester.nim | 13 +++++- tests/browsertests/categories.nim | 18 ++++---- tests/browsertests/common.nim | 71 +++++++++++++++++++++++++++---- tests/browsertests/issue181.nim | 2 +- tests/browsertests/scenario1.nim | 4 +- tests/browsertests/threads.nim | 2 +- 7 files changed, 87 insertions(+), 25 deletions(-) diff --git a/nimforum.nimble b/nimforum.nimble index 86dfc77..58a22f7 100644 --- a/nimforum.nimble +++ b/nimforum.nimble @@ -21,7 +21,7 @@ requires "sass#649e0701fa5c" requires "karax#5f21dcd" -requires "halonium#f54c83f" +requires "webdriver#429933a" # Tasks diff --git a/tests/browsertester.nim b/tests/browsertester.nim index a840f40..8b6c046 100644 --- a/tests/browsertester.nim +++ b/tests/browsertester.nim @@ -1,6 +1,6 @@ import options, osproc, streams, threadpool, os, strformat, httpclient -import halonium +import webdriver proc runProcess(cmd: string) = let p = startProcess( @@ -46,13 +46,22 @@ template withBackend(body: untyped): untyped = import browsertests/[scenario1, threads, issue181, categories] proc main() = + # Kill any already running instances + discard execCmd("killall geckodriver") + spawn runProcess("geckodriver -p 4444 --log config") + defer: + discard execCmd("killall geckodriver") + # Create a fresh DB for the tester. doAssert(execCmd("nimble testdb") == QuitSuccess) doAssert(execCmd("nimble -y frontend") == QuitSuccess) + echo("Waiting for geckodriver to startup...") + sleep(5000) try: - let session = createSession(Firefox) + let driver = newWebDriver() + let session = driver.createSession() withBackend: scenario1.test(session, baseUrl) diff --git a/tests/browsertests/categories.nim b/tests/browsertests/categories.nim index 2acc6f3..8c27a87 100644 --- a/tests/browsertests/categories.nim +++ b/tests/browsertests/categories.nim @@ -1,5 +1,5 @@ import unittest, common -import halonium +import webdriver import karaxutils @@ -47,12 +47,12 @@ proc categoriesUserTests(session: Session, baseUrl: string) = with session: click "#new-thread-btn" - checkIsNone "#add-category .plus-btn" + checkIsNone "#add-category" test "no category add available category page": with session: click "#categories-btn" - checkIsNone "#add-category .plus-btn" + checkIsNone "#add-category" test "can create category thread": with session: @@ -139,17 +139,17 @@ proc categoriesUserTests(session: Session, baseUrl: string) = checkText "#threads-list .thread-title a", "Post 3" for element in session.waitForElements("#threads-list .category-name"): # Have to user "innerText" because elements are hidden on this page - assert element.text == "Unsorted" + assert element.getProperty("innerText") == "Unsorted" selectCategory "announcements" checkText "#threads-list .thread-title a", "Post 2" for element in session.waitForElements("#threads-list .category-name"): - assert element.text == "Announcements" + assert element.getProperty("innerText") == "Announcements" selectCategory "fun" checkText "#threads-list .thread-title a", "Post 1" for element in session.waitForElements("#threads-list .category-name"): - assert element.text == "Fun" + assert element.getProperty("innerText") == "Fun" session.logout() @@ -168,7 +168,7 @@ proc categoriesAdminTests(session: Session, baseUrl: string) = with session: click "#new-thread-btn" - ensureExists "#add-category .plus-btn" + ensureExists "#add-category" click "#add-category .plus-btn" @@ -195,10 +195,10 @@ proc categoriesAdminTests(session: Session, baseUrl: string) = test "category adding disabled on admin logout": with session: navigate(baseUrl & "c/0") - ensureExists "#add-category .plus-btn" + ensureExists "#add-category" logout() - checkIsNone "#add-category .plus-btn" + checkIsNone "#add-category" navigate baseUrl login "admin", "admin" diff --git a/tests/browsertests/common.nim b/tests/browsertests/common.nim index 54ad254..d906675 100644 --- a/tests/browsertests/common.nim +++ b/tests/browsertests/common.nim @@ -1,9 +1,9 @@ import os, options, unittest, strutils -import halonium +import webdriver import macros -export waitForElement -export waitForElements +const actionDelayMs {.intdefine.} = 0 +## Inserts a delay in milliseconds between automated actions. Useful for debugging tests macro with*(obj: typed, code: untyped): untyped = ## Execute a set of statements with an object @@ -24,6 +24,14 @@ macro with*(obj: typed, code: untyped): untyped = result = getAst(checkCompiles(result, code)) +proc elementIsSome(element: Option[Element]): bool = + return element.isSome + +proc elementIsNone(element: Option[Element]): bool = + return element.isNone + +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) el.get().click() @@ -36,33 +44,78 @@ proc clear*(session: Session, element: string) = let el = session.waitForElement(element) el.get().clear() -proc sendKeys*(session: Session, element: string, keys: varargs[string, convertKeyRuneString]) = +proc sendKeys*(session: Session, element: string, keys: varargs[Key]) = let el = session.waitForElement(element) - el.get().sendKeys(keys) + # focus + el.get().click() + for key in keys: + session.press(key) proc ensureExists*(session: Session, element: string, strategy=CssSelector) = discard session.waitForElement(element, strategy) template check*(session: Session, element: string, function: untyped) = let el = session.waitForElement(element) - doAssert function(el) + check function(el) template check*(session: Session, element: string, strategy: LocationStrategy, function: untyped) = let el = session.waitForElement(element, strategy) - doAssert function(el) + check function(el) proc setColor*(session: Session, element, color: string, strategy=CssSelector) = let el = session.waitForElement(element, strategy) - discard session.executeScript("arguments[0].setAttribute('value', '" & color & "')", el.get()) + discard session.execute("arguments[0].setAttribute('value', '" & color & "')", el.get()) proc checkIsNone*(session: Session, element: string, strategy=CssSelector) = discard session.waitForElement(element, strategy, waitCondition=elementIsNone) proc checkText*(session: Session, element, expectedValue: string) = let el = session.waitForElement(element) - doAssert el.get().text.strip() == expectedValue + check el.get().getText() == expectedValue + +proc waitForElement*( + session: Session, selector: string, strategy=CssSelector, + timeout=20000, pollTime=50, + waitCondition=elementIsSome +): Option[Element] = + var waitTime = 0 + + when actionDelayMs > 0: + sleep(actionDelayMs) + + while true: + try: + let loading = session.findElement(selector, strategy) + if waitCondition(loading): + return loading + finally: + discard + sleep(pollTime) + waitTime += pollTime + + if waitTime > timeout: + doAssert false, "Wait for load time exceeded" + +proc waitForElements*( + session: Session, selector: string, strategy=CssSelector, + timeout=20000, pollTime=50 +): seq[Element] = + var waitTime = 0 + + when actionDelayMs > 0: + sleep(actionDelayMs) + + while true: + let loading = session.findElements(selector, strategy) + if loading.len > 0: + return loading + sleep(pollTime) + waitTime += pollTime + + if waitTime > timeout: + doAssert false, "Wait for load time exceeded" proc setUserRank*(session: Session, baseUrl, user, rank: string) = with session: diff --git a/tests/browsertests/issue181.nim b/tests/browsertests/issue181.nim index 61a5494..031cd92 100644 --- a/tests/browsertests/issue181.nim +++ b/tests/browsertests/issue181.nim @@ -1,6 +1,6 @@ import unittest, common -import halonium +import webdriver proc test*(session: Session, baseUrl: string) = session.navigate(baseUrl) diff --git a/tests/browsertests/scenario1.nim b/tests/browsertests/scenario1.nim index fc51e75..6e10e4c 100644 --- a/tests/browsertests/scenario1.nim +++ b/tests/browsertests/scenario1.nim @@ -1,6 +1,6 @@ import unittest, common -import halonium +import webdriver proc test*(session: Session, baseUrl: string) = session.navigate(baseUrl) @@ -40,4 +40,4 @@ proc test*(session: Session, baseUrl: string) = register "TEst1", "test1", verify = false ensureExists "#signup-form .has-error" - navigate baseUrl + navigate baseUrl \ No newline at end of file diff --git a/tests/browsertests/threads.nim b/tests/browsertests/threads.nim index d4efe57..e80b9c0 100644 --- a/tests/browsertests/threads.nim +++ b/tests/browsertests/threads.nim @@ -1,6 +1,6 @@ import unittest, common -import halonium +import webdriver let userTitleStr = "This is a user thread!" From 3ac9ec3ff6e7fab1ceb15da88c7923ab84e3395e Mon Sep 17 00:00:00 2001 From: digitalcraftsman Date: Fri, 18 Sep 2020 23:22:59 +0200 Subject: [PATCH 64/80] Center view count of popular threads Otherwise the counter is misaligned in the corresponding column. --- public/css/nimforum.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/css/nimforum.scss b/public/css/nimforum.scss index 313e844..2daecdb 100644 --- a/public/css/nimforum.scss +++ b/public/css/nimforum.scss @@ -301,7 +301,7 @@ $threads-meta-color: #545d70; } } -.thread-replies, .thread-time, .views-text, .centered-header { +.thread-replies, .thread-time, .views-text, .popular-text, .centered-header { text-align: center; } From 6c6552176a01c6104604ebf2ac43509bffa61d09 Mon Sep 17 00:00:00 2001 From: j-james Date: Sun, 3 Jan 2021 02:31:22 -0800 Subject: [PATCH 65/80] Support dashes in usernames --- src/utils.nim | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/utils.nim b/src/utils.nim index f092dc6..4b1d339 100644 --- a/src/utils.nim +++ b/src/utils.nim @@ -5,7 +5,7 @@ from times import getTime, utc, format # Used to be: # {'A'..'Z', 'a'..'z', '0'..'9', '_', '\128'..'\255'} let - UsernameIdent* = IdentChars # TODO: Double check that everyone follows this. + UsernameIdent* = IdentChars + {'-'} # TODO: Double check that everyone follows this. import frontend/[karaxutils, error] export parseInt From 5b7b2716274052191787603b1ab52294d09dada1 Mon Sep 17 00:00:00 2001 From: Joey Date: Thu, 22 Apr 2021 16:39:25 -0600 Subject: [PATCH 66/80] Add github actions --- .github/workflows/main.yml | 78 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 78 insertions(+) create mode 100644 .github/workflows/main.yml diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml new file mode 100644 index 0000000..3e03fe5 --- /dev/null +++ b/.github/workflows/main.yml @@ -0,0 +1,78 @@ +# 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 From 8cd5c45cda0003b6486be62a90f84e5b6daa9830 Mon Sep 17 00:00:00 2001 From: Joey Yakimowich-Payne Date: Thu, 22 Apr 2021 16:42:32 -0600 Subject: [PATCH 67/80] Remove travis --- .travis.yml | 45 --------------------------------------------- 1 file changed, 45 deletions(-) delete mode 100644 .travis.yml diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index b5c6c9b..0000000 --- a/.travis.yml +++ /dev/null @@ -1,45 +0,0 @@ -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 From 0055a12fc137a07b33e99c38b78b6938dab01fc4 Mon Sep 17 00:00:00 2001 From: Joey Yakimowich-Payne Date: Sat, 24 Apr 2021 16:03:50 -0600 Subject: [PATCH 68/80] Use choosenim instead --- .github/workflows/main.yml | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 3e03fe5..81e0144 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -21,8 +21,7 @@ jobs: matrix: firefox: [ '73.0' ] include: - - nim-version: 'stable' - cache-key: 'stable' + - cache-key: 'stable' steps: - uses: actions/checkout@v2 - name: Checkout submodules @@ -50,10 +49,6 @@ jobs: 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 @@ -73,6 +68,7 @@ jobs: - name: Run tests run: | + export PATH=$HOME/.nimble/bin:$PATH export MOZ_HEADLESS=1 nimble -y install nimble -y test From 8782dff349098e7df8e8573fed2442c36642e2f3 Mon Sep 17 00:00:00 2001 From: Joey Yakimowich-Payne Date: Sat, 24 Apr 2021 16:19:39 -0600 Subject: [PATCH 69/80] Use matrix nim version --- .github/workflows/main.yml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 81e0144..64c51aa 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -21,7 +21,8 @@ jobs: matrix: firefox: [ '73.0' ] include: - - cache-key: 'stable' + - nim-version: 'stable' + cache-key: 'stable' steps: - uses: actions/checkout@v2 - name: Checkout submodules @@ -60,7 +61,7 @@ jobs: - name: Install choosenim run: | - export CHOOSENIM_CHOOSE_VERSION="stable" + export CHOOSENIM_CHOOSE_VERSION="${{ matrix.nim-version }}" curl https://nim-lang.org/choosenim/init.sh -sSf > init.sh sh init.sh -y export PATH=$HOME/.nimble/bin:$PATH From 7954a38601116158b67a996ff154a1abc8973e49 Mon Sep 17 00:00:00 2001 From: zetashift Date: Mon, 26 Apr 2021 02:39:03 +0200 Subject: [PATCH 70/80] Pinned Threads (#278) * Added isSticky field to `Thread` and in the sql query making a Thread - Modified indices in `data` and `selectUser` to support `isSticky` - Add backend procs for initial sticky logic, modeled after locking threads - Fix indices in selectThread - Fixup posts.json's threadquery to match Thread with sticky field * Implement StickyButton for postbutton.nim and add it to postlist.nim * Fix sticky routes * Order sticky in a way that they actually appear at the top * Add border for isSticky on genThread * Rename stickies to pinned, so professional! * Add pinned tests - Add an id to pin button, and add first attempt at useful tests - Improve pin tests, refactored it into adminTests and userTests - Add an id to pin button, and add first attempt at useful tests - Improve pin tests, refactored it into adminTests and userTests * Make tests more reliable Co-authored-by: Joey Yakimowich-Payne --- src/forum.nim | 51 ++++++++++++++++++++++----- src/frontend/post.nim | 2 +- src/frontend/postbutton.nim | 59 +++++++++++++++++++++++++++++++- src/frontend/postlist.nim | 3 ++ src/frontend/threadlist.nim | 10 ++++-- src/setup_nimforum.nim | 1 + tests/browsertests/common.nim | 7 ++-- tests/browsertests/scenario1.nim | 2 +- tests/browsertests/threads.nim | 53 +++++++++++++++++++++++++++- 9 files changed, 170 insertions(+), 18 deletions(-) diff --git a/src/forum.nim b/src/forum.nim index c85ad2e..b87223d 100644 --- a/src/forum.nim +++ b/src/forum.nim @@ -400,10 +400,10 @@ proc selectThread(threadRow: seq[string], author: User): Thread = id: threadRow[0].parseInt, topic: threadRow[1], category: Category( - id: threadRow[5].parseInt, - name: threadRow[6], - description: threadRow[7], - color: threadRow[8] + id: threadRow[6].parseInt, + name: threadRow[7], + description: threadRow[8], + color: threadRow[9] ), users: @[], replies: posts[0].parseInt-1, @@ -412,6 +412,7 @@ 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. @@ -709,6 +710,13 @@ 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""" @@ -833,7 +841,7 @@ routes: categoryArgs.insert($categoryId, 0) const threadsQuery = - """select t.id, t.name, views, strftime('%s', modified), isLocked, + """select t.id, t.name, views, strftime('%s', modified), isLocked, isPinned, 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 @@ -846,14 +854,14 @@ routes: order by p.author limit 1 ) - order by modified desc limit ?, ?;""" + order by isPinned desc, 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 .. 8], selectUser(data[9 .. ^1])) + let thread = selectThread(data[0 .. 9], selectUser(data[10 .. ^1])) list.threads.add(thread) resp $(%list), "application/json" @@ -868,7 +876,7 @@ routes: count = 10 const threadsQuery = - sql"""select t.id, t.name, views, strftime('%s', modified), isLocked, + sql"""select t.id, t.name, views, strftime('%s', modified), isLocked, isPinned, c.id, c.name, c.description, c.color from thread t, category c where t.id = ? and isDeleted = 0 and category = c.id;""" @@ -1339,6 +1347,33 @@ 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(): diff --git a/src/frontend/post.nim b/src/frontend/post.nim index 0530814..dc12e47 100644 --- a/src/frontend/post.nim +++ b/src/frontend/post.nim @@ -64,4 +64,4 @@ when defined(js): renderPostUrl(thread.id, post.id) proc renderPostUrl*(link: PostLink): string = - renderPostUrl(link.threadId, link.postId) \ No newline at end of file + renderPostUrl(link.threadId, link.postId) diff --git a/src/frontend/postbutton.nim b/src/frontend/postbutton.nim index 8bd9c34..98f8d92 100644 --- a/src/frontend/postbutton.nim +++ b/src/frontend/postbutton.nim @@ -201,4 +201,61 @@ when defined(js): text " Unlock Thread" else: italic(class="fas fa-lock") - text " Lock Thread" \ No newline at end of file + 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" + diff --git a/src/frontend/postlist.nim b/src/frontend/postlist.nim index 7da57c1..66b3162 100644 --- a/src/frontend/postlist.nim +++ b/src/frontend/postlist.nim @@ -36,6 +36,7 @@ when defined(js): likeButton: LikeButton deleteModal: DeleteModal lockButton: LockButton + pinButton: PinButton categoryPicker: CategoryPicker proc onReplyPosted(id: int) @@ -56,6 +57,7 @@ when defined(js): likeButton: newLikeButton(), deleteModal: newDeleteModal(onDeletePost, onDeleteThread, nil), lockButton: newLockButton(), + pinButton: newPinButton(), categoryPicker: newCategoryPicker(onCategoryChanged) ) @@ -411,6 +413,7 @@ 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) diff --git a/src/frontend/threadlist.nim b/src/frontend/threadlist.nim index e0b2d36..ecec6da 100644 --- a/src/frontend/threadlist.nim +++ b/src/frontend/threadlist.nim @@ -15,6 +15,7 @@ type creation*: int64 ## Unix timestamp isLocked*: bool isSolved*: bool + isPinned*: bool ThreadList* = ref object threads*: seq[Thread] @@ -96,15 +97,18 @@ when defined(js): else: return $duration.inSeconds & "s" - proc genThread(thread: Thread, isNew: bool, noBorder: bool, displayCategory=true): VNode = + proc genThread(pos: int, 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})): + tr(class=class({"no-border": noBorder, "banned": isBanned, "pinned": thread.isPinned, "thread-" & $pos: true})): 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") @@ -223,7 +227,7 @@ when defined(js): let isLastThread = i+1 == list.threads.len let (isLastUnseen, isNew) = getInfo(list.threads, i, currentUser) - genThread(thread, isNew, + genThread(i+1, thread, isNew, noBorder=isLastUnseen or isLastThread, displayCategory=displayCategory) if isLastUnseen and (not isLastThread): diff --git a/src/setup_nimforum.nim b/src/setup_nimforum.nim index 34511fa..c80ad3b 100644 --- a/src/setup_nimforum.nim +++ b/src/setup_nimforum.nim @@ -81,6 +81,7 @@ 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) diff --git a/tests/browsertests/common.nim b/tests/browsertests/common.nim index d906675..e5924f3 100644 --- a/tests/browsertests/common.nim +++ b/tests/browsertests/common.nim @@ -30,7 +30,8 @@ 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=elementIsSome): Option[Element] +proc waitForElement*(session: Session, selector: string, strategy=CssSelector, timeout=20000, pollTime=50, + waitCondition: proc(element: Option[Element]): bool = elementIsSome): Option[Element] proc click*(session: Session, element: string, strategy=CssSelector) = let el = session.waitForElement(element, strategy) @@ -71,14 +72,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) -proc checkText*(session: Session, element, expectedValue: string) = +template 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=elementIsSome + waitCondition: proc(element: Option[Element]): bool = elementIsSome ): Option[Element] = var waitTime = 0 diff --git a/tests/browsertests/scenario1.nim b/tests/browsertests/scenario1.nim index 6e10e4c..dc78007 100644 --- a/tests/browsertests/scenario1.nim +++ b/tests/browsertests/scenario1.nim @@ -40,4 +40,4 @@ proc test*(session: Session, baseUrl: string) = register "TEst1", "test1", verify = false ensureExists "#signup-form .has-error" - navigate baseUrl \ No newline at end of file + navigate baseUrl diff --git a/tests/browsertests/threads.nim b/tests/browsertests/threads.nim index e80b9c0..2cbecb5 100644 --- a/tests/browsertests/threads.nim +++ b/tests/browsertests/threads.nim @@ -58,10 +58,22 @@ 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" + session.logout() proc anonymousTests(session: Session, baseUrl: string) = - suite "anonymous user tests": with session: navigate baseUrl @@ -161,6 +173,45 @@ 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" + session.logout() proc test*(session: Session, baseUrl: string) = From 35e0de7b91fc76230574b2882005d1eccd12e2ad Mon Sep 17 00:00:00 2001 From: zetashift Date: Tue, 27 Apr 2021 18:23:48 +0200 Subject: [PATCH 71/80] Tests for locking threads (#284) * Initial try at locking threads tests * Uncomment tests * Consist casing * Add correct query * Remove redundant navigate call and add frontpage check * Improve locked thread on frontpage test --- src/frontend/postbutton.nim | 2 +- tests/browsertests/threads.nim | 43 +++++++++++++++++++++++++++++++--- 2 files changed, 41 insertions(+), 4 deletions(-) diff --git a/src/frontend/postbutton.nim b/src/frontend/postbutton.nim index 98f8d92..9fa4ab4 100644 --- a/src/frontend/postbutton.nim +++ b/src/frontend/postbutton.nim @@ -190,7 +190,7 @@ when defined(js): else: "" result = buildHtml(): - button(class="btn btn-secondary", + button(class="btn btn-secondary", id="lock-btn", onClick=(e: Event, n: VNode) => onLockClick(e, n, state, thread), "data-tooltip"=tooltip, diff --git a/tests/browsertests/threads.nim b/tests/browsertests/threads.nim index 2cbecb5..32ce686 100644 --- a/tests/browsertests/threads.nim +++ b/tests/browsertests/threads.nim @@ -1,5 +1,4 @@ import unittest, common - import webdriver let @@ -71,6 +70,19 @@ proc userTests(session: Session, baseUrl: string) = 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) = @@ -173,7 +185,7 @@ proc adminTests(session: Session, baseUrl: string) = # Make sure the forum post is gone checkIsNone adminTitleStr, LinkTextSelector - test "Can pin a thread": + test "can pin a thread": with session: click "#new-thread-btn" sendKeys "#thread-title", "Pinned post" @@ -199,7 +211,7 @@ proc adminTests(session: Session, baseUrl: string) = 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": + test "can unpin a thread": with session: click "Pinned post", LinkTextSelector click "#pin-btn" @@ -212,6 +224,31 @@ proc adminTests(session: Session, baseUrl: string) = 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) = From a1601b4600c7d96dd85e30d05a96d5c41d64b538 Mon Sep 17 00:00:00 2001 From: Juan Carlos Date: Sun, 16 May 2021 20:04:01 -0300 Subject: [PATCH 72/80] Use input type search on search instead of text (#291) --- src/frontend/header.nim | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/frontend/header.nim b/src/frontend/header.nim index cde48de..7cfb133 100644 --- a/src/frontend/header.nim +++ b/src/frontend/header.nim @@ -96,8 +96,8 @@ when defined(js): section(class="navbar-section"): tdiv(class="input-group input-inline"): input(class="search-input input-sm", - `type`="text", placeholder="search", - id="search-box", + `type`="search", placeholder="Search", + id="search-box", required="required", onKeyDown=onKeyDown) if state.loading: tdiv(class="loading") From f940f8186189813eb928d9c895f6c4a972a8c907 Mon Sep 17 00:00:00 2001 From: Dominik Picheta Date: Thu, 11 Nov 2021 22:20:54 +0000 Subject: [PATCH 73/80] Lock CI Nim ver and update to Nim 1.6.0. --- .github/workflows/main.yml | 14 +++++++------- src/forum.nim | 2 +- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 64c51aa..9b02e14 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -2,7 +2,7 @@ name: CI -# Controls when the action will run. +# Controls when the action will run. on: # Triggers the workflow on push or pull request events but only for the master branch push: @@ -21,13 +21,13 @@ jobs: matrix: firefox: [ '73.0' ] include: - - nim-version: 'stable' + - nim-version: '1.6.0' 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: @@ -37,13 +37,13 @@ jobs: 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: @@ -58,7 +58,7 @@ jobs: 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="${{ matrix.nim-version }}" @@ -66,7 +66,7 @@ jobs: sh init.sh -y export PATH=$HOME/.nimble/bin:$PATH nimble refresh -y - + - name: Run tests run: | export PATH=$HOME/.nimble/bin:$PATH diff --git a/src/forum.nim b/src/forum.nim index b87223d..60c047c 100644 --- a/src/forum.nim +++ b/src/forum.nim @@ -276,7 +276,7 @@ template createTFD() = new(c) init(c) c.req = request - if request.cookies.len > 0: + if cookies(request).len > 0: checkLoggedIn(c) #[ DB functions. TODO: Move to another module? ]# From c4684155f5741644c57558882129392c8a2112ec Mon Sep 17 00:00:00 2001 From: Dominik Picheta Date: Thu, 11 Nov 2021 22:24:05 +0000 Subject: [PATCH 74/80] Run CI on all branches and every week. --- .github/workflows/main.yml | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 9b02e14..b2044db 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -4,14 +4,11 @@ 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: + branches: [master] + schedule: + - cron: '0 0 * * 1' # A workflow run is made up of one or more jobs that can run sequentially or in parallel jobs: From 40d7b1cf02aa097aa0c0a56c0d1c1b26e1c01a0a Mon Sep 17 00:00:00 2001 From: Danil Yarantsev Date: Mon, 22 Nov 2021 02:40:04 +0300 Subject: [PATCH 75/80] Fixes a few crashes and search functionality. (#307) * Fixes a few crashes and search functionality. * Use PostError --- src/forum.nim | 14 ++++++++++++-- src/fts.sql | 1 + 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/src/forum.nim b/src/forum.nim index 60c047c..c2682eb 100644 --- a/src/forum.nim +++ b/src/forum.nim @@ -882,6 +882,11 @@ routes: 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 = @@ -927,9 +932,14 @@ routes: get "/specific_posts.json": createTFD() - var + var ids: JsonNode + try: 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(""" diff --git a/src/fts.sql b/src/fts.sql index ee6f1af..1590a05 100644 --- a/src/fts.sql +++ b/src/fts.sql @@ -7,6 +7,7 @@ SELECT post_id, post_content, cdate, + person.id, person.name AS author, person.email AS email, strftime('%s', person.lastOnline) AS lastOnline, From ceb04561cdc2f6c32a1635d221d99564d1896dd1 Mon Sep 17 00:00:00 2001 From: Joey Date: Thu, 22 Apr 2021 16:13:20 -0600 Subject: [PATCH 76/80] Create main github actions file --- .github/workflows/main.yml | 49 +++++++++++++++++++++++++------------- 1 file changed, 32 insertions(+), 17 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index b2044db..47a6a71 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -2,29 +2,30 @@ name: CI -# Controls when the action will run. +# 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] - schedule: - - cron: '0 0 * * 1' + 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: + test_devel: runs-on: ubuntu-latest strategy: matrix: firefox: [ '73.0' ] include: - - nim-version: '1.6.0' + - 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: @@ -34,39 +35,53 @@ jobs: 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 + 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.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="${{ matrix.nim-version }}" + 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 PATH=$HOME/.nimble/bin:$PATH export MOZ_HEADLESS=1 nimble -y install nimble -y test + From 9994fc7840070b4327f54501cdea495c4c19749d Mon Sep 17 00:00:00 2001 From: Joey Date: Thu, 22 Apr 2021 16:14:51 -0600 Subject: [PATCH 77/80] test_devel -> test_stable --- .github/workflows/main.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 47a6a71..0402c51 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -15,7 +15,7 @@ on: # A workflow run is made up of one or more jobs that can run sequentially or in parallel jobs: - test_devel: + test_stable: runs-on: ubuntu-latest strategy: matrix: From 4f00ea5942b0d94e70e00548ca229466b4d0ca55 Mon Sep 17 00:00:00 2001 From: Joey Date: Thu, 22 Apr 2021 16:19:25 -0600 Subject: [PATCH 78/80] Add mkdir --- .github/workflows/main.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 0402c51..fd753de 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -68,6 +68,7 @@ jobs: cd .. 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 From 4a181e9cb19cc44f57871743413c3ff5086f18ec Mon Sep 17 00:00:00 2001 From: Joey Date: Thu, 22 Apr 2021 16:23:39 -0600 Subject: [PATCH 79/80] Install libsass with apt --- .github/workflows/main.yml | 12 +----------- 1 file changed, 1 insertion(+), 11 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index fd753de..77dd1d7 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -55,17 +55,7 @@ jobs: - name: Install geckodriver run: | 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 .. + 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 From 88c20113232f4598dfc5cd4928d2eb3def724a94 Mon Sep 17 00:00:00 2001 From: Joey Date: Thu, 22 Apr 2021 16:29:10 -0600 Subject: [PATCH 80/80] Update submodules --- .github/workflows/main.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 77dd1d7..fde1b09 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -25,6 +25,8 @@ jobs: 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