From c3c3d91a0a137baef737c2c6f5fa52e8221f94af Mon Sep 17 00:00:00 2001 From: Dominik Picheta Date: Sun, 27 May 2018 21:45:08 +0100 Subject: [PATCH 001/144] Improves setup document. --- setup.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/setup.md b/setup.md index 7619e10..ea3dc1b 100644 --- a/setup.md +++ b/setup.md @@ -17,14 +17,14 @@ Extract the downloaded tarball on your server. These steps can be done using the following commands: ``` -wget TODO -tar -xf TODO +wget https://github.com/nim-lang/nimforum/releases/download/v2.0.0/nimforum_2.0.0_linux.tar.xz +tar -xf nimforum_2.0.0_linux.tar.xz ``` Then ``cd`` into the forum's directory: ``` -cd TODO +cd nimforum_2.0.0_linux ``` ### Dependencies From 34f5b3f80aca4d471066929239ef3fc3d47043b1 Mon Sep 17 00:00:00 2001 From: Joey Date: Fri, 6 Jul 2018 21:55:34 +0900 Subject: [PATCH 002/144] Update readme with required devdb call --- README.md | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 9672657..c8057cd 100644 --- a/README.md +++ b/README.md @@ -63,11 +63,12 @@ test Runs tester fasttest Runs tester without recompiling backend ``` -Development typically involves running `nimble backend` which compiles -and runs the forum's backend, and `nimble frontend` 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. +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` +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. # Copyright From 122e27925679f794a1b0c4509e2221cad316dcbc Mon Sep 17 00:00:00 2001 From: Joey Yakimowich-Payne Date: Fri, 13 Jul 2018 09:16:48 +0900 Subject: [PATCH 003/144] Add helpful ids and classes for testing --- src/frontend/newthread.nim | 11 ++++++----- src/frontend/postlist.nim | 9 ++++++--- src/frontend/replybox.nim | 5 +++-- src/frontend/threadlist.nim | 4 ++-- 4 files changed, 17 insertions(+), 12 deletions(-) diff --git a/src/frontend/newthread.nim b/src/frontend/newthread.nim index 8e701a2..e75b6bb 100644 --- a/src/frontend/newthread.nim +++ b/src/frontend/newthread.nim @@ -49,7 +49,7 @@ when defined(js): tdiv(class="title"): p(): text "New Thread" tdiv(class="content"): - input(class="form-input", `type`="text", name="subject", + input(id="thread-title", class="form-input", `type`="text", name="subject", placeholder="Type the title here", oninput=(e: Event, n: VNode) => onSubjectChange(e, n, state)) if state.error.isSome(): @@ -58,10 +58,11 @@ when defined(js): renderContent(state.replyBox, none[Thread](), none[Post]()) tdiv(class="footer"): - button(class=class( - {"loading": state.loading}, - "btn btn-primary" + button(id="create-thread-btn", + class=class( + {"loading": state.loading}, + "btn btn-primary" ), onClick=(ev: Event, n: VNode) => (onCreateClick(ev, n, state))): - text "Create thread" \ No newline at end of file + text "Create thread" diff --git a/src/frontend/postlist.nim b/src/frontend/postlist.nim index 006af52..6a27df3 100644 --- a/src/frontend/postlist.nim +++ b/src/frontend/postlist.nim @@ -220,8 +220,11 @@ when defined(js): ): VNode = let postCopy = post # TODO: Another workaround here, closure capture :( + let originalPost = thread.author == post.author + result = buildHtml(): - tdiv(class=class({"highlight": highlight}, "post"), id = $post.id): + tdiv(class=class({"highlight": highlight, "original-post": originalPost}, "post"), + id = $post.id): tdiv(class="post-icon"): render(post.author, "post-avatar") tdiv(class="post-main"): @@ -326,7 +329,7 @@ when defined(js): let list = state.list.get() result = buildHtml(): section(class="container grid-xl"): - tdiv(class="title"): + tdiv(id="thread-title", class="title"): p(): text list.thread.topic if list.thread.isLocked: italic(class="fas fa-lock fa-xs", @@ -368,4 +371,4 @@ when defined(js): render(state.replyBox, list.thread, state.replyingTo, false) - render(state.deleteModal) \ No newline at end of file + render(state.deleteModal) diff --git a/src/frontend/replybox.nim b/src/frontend/replybox.nim index 8d56e1d..9d815f6 100644 --- a/src/frontend/replybox.nim +++ b/src/frontend/replybox.nim @@ -114,7 +114,8 @@ when defined(js): elif state.rendering.isSome(): verbatim(state.rendering.get()) else: - textarea(class="form-input post-text-area", rows="5", + textarea(id="reply-textarea", + class="form-input post-text-area", rows="5", onChange=(e: Event, n: VNode) => onChange(e, n, state), value=state.text) @@ -162,4 +163,4 @@ when defined(js): button(class="btn"): italic(class="fas fa-arrow-up") tdiv(class="information-content"): - renderContent(state, some(thread), post) \ No newline at end of file + renderContent(state, some(thread), post) diff --git a/src/frontend/threadlist.nim b/src/frontend/threadlist.nim index 710c2c5..cceca03 100644 --- a/src/frontend/threadlist.nim +++ b/src/frontend/threadlist.nim @@ -78,7 +78,7 @@ when defined(js): button(class="btn btn-link"): text "Categories" section(class="navbar-section"): if currentUser.isSome(): - a(href=makeUri("/newthread"), onClick=anchorCB): + a(id="new-thread-btn", href=makeUri("/newthread"), onClick=anchorCB): button(class="btn btn-secondary"): italic(class="fas fa-plus") text " New Thread" @@ -243,4 +243,4 @@ when defined(js): proc renderThreadList*(currentUser: Option[User]): VNode = result = buildHtml(tdiv): genTopButtons(currentUser) - genThreadList(currentUser) \ No newline at end of file + genThreadList(currentUser) From b650a9f401b13910b43fe16799e832dbba00ed48 Mon Sep 17 00:00:00 2001 From: Joey Yakimowich-Payne Date: Fri, 13 Jul 2018 09:18:09 +0900 Subject: [PATCH 004/144] Add basic test for creating a thread --- tests/browsertester.nim | 4 ++- tests/browsertests/common.nim | 42 +++++++++++++++++++++++++++++ tests/browsertests/scenario1.nim | 19 +++---------- tests/browsertests/threads.nim | 46 ++++++++++++++++++++++++++++++++ 4 files changed, 95 insertions(+), 16 deletions(-) create mode 100644 tests/browsertests/common.nim create mode 100644 tests/browsertests/threads.nim diff --git a/tests/browsertester.nim b/tests/browsertester.nim index 77d4ada..646ab79 100644 --- a/tests/browsertester.nim +++ b/tests/browsertester.nim @@ -44,6 +44,7 @@ template withBackend(body: untyped): untyped = body import browsertests/scenario1 +import browsertests/threads when isMainModule: spawn runProcess("geckodriver -p 4444 --log config") @@ -63,8 +64,9 @@ when isMainModule: withBackend: scenario1.test(session, baseUrl) + threads.test(session, baseUrl) session.close() except: sleep(10000) # See if we can grab any more output. - raise \ No newline at end of file + raise diff --git a/tests/browsertests/common.nim b/tests/browsertests/common.nim new file mode 100644 index 0000000..3d651e3 --- /dev/null +++ b/tests/browsertests/common.nim @@ -0,0 +1,42 @@ +import os, options +import webdriver + +proc waitForLoad*(session: Session, timeout=20000) = + var waitTime = 0 + sleep(2000) + + while true: + let loading = session.findElement(".loading") + if loading.isNone: return + sleep(1000) + waitTime += 1000 + + if waitTime > timeout: + doAssert false, "Wait for load time exceeded" + +proc logout*(session: Session) = + # Check whether we can log out. + let logoutLink = session.findElement( + "Logout", + LinkTextSelector + ).get() + logoutLink.click() + +proc login*(session: Session, user, password: string) = + let logIn = session.findElement("#login-btn").get() + logIn.click() + + let usernameField = session.findElement( + "#login-form input[name='username']" + ) + + let passwordField = session.findElement( + "#login-form input[name='password']" + ) + + usernameField.get().sendKeys("admin") + passwordField.get().sendKeys("admin") + passwordField.get().click() # Focus field. + session.press(Key.Enter) + + waitForLoad(session, 5000) \ No newline at end of file diff --git a/tests/browsertests/scenario1.nim b/tests/browsertests/scenario1.nim index e190e03..fe8ef22 100644 --- a/tests/browsertests/scenario1.nim +++ b/tests/browsertests/scenario1.nim @@ -1,20 +1,7 @@ -import unittest, options, os +import unittest, options, os, common import webdriver -proc waitForLoad(session: Session, timeout=20000) = - var waitTime = 0 - sleep(2000) - - while true: - let loading = session.findElement(".loading") - if loading.isNone: return - sleep(1000) - waitTime += 1000 - - if waitTime > timeout: - doAssert false, "Wait for load time exceeded" - proc test*(session: Session, baseUrl: string) = session.navigate(baseUrl) @@ -112,4 +99,6 @@ proc test*(session: Session, baseUrl: string) = "#main-navbar .menu-right div.tile-content" ).get() - check profileName.getText() == "test" \ No newline at end of file + check profileName.getText() == "test" + + logout(session) \ No newline at end of file diff --git a/tests/browsertests/threads.nim b/tests/browsertests/threads.nim new file mode 100644 index 0000000..1474ec6 --- /dev/null +++ b/tests/browsertests/threads.nim @@ -0,0 +1,46 @@ +import unittest, options, os, common + +import webdriver + +proc test*(session: Session, baseUrl: string) = + session.navigate(baseUrl) + + waitForLoad(session) + + login(session, "admin", "admin") + + test "can create thread": + let newThreadBtn = session.findElement("#new-thread-btn").get() + newThreadBtn.click() + + waitForLoad(session) + + let newThread = session.findElement("#new-thread") + check newThread.isSome() + + let createThreadBtn = session.findElement("#create-thread-btn") + check createThreadBtn.isSome() + + + let threadTitle = session.findElement("#thread-title") + check threadTitle.isSome() + + let replyBox = session.findElement("#reply-textarea") + check replyBox.isSome() + + threadTitle.get().sendKeys("This is a thread title!") + replyBox.get().sendKeys("This is content.") + + createThreadBtn.get().click() + + waitForLoad(session) + + let newThreadTitle = session.findElement("#thread-title") + check newThreadTitle.isSome() + + check newThreadTitle.get().getText() == "This is a thread title!" + + let content = session.findElement(".original-post div.post-content") + check content.isSome() + + check content.get().getText() == "This is content." From d422b07394de1259c7c09088b7e8ab05375fffeb Mon Sep 17 00:00:00 2001 From: Joey Yakimowich-Payne Date: Mon, 16 Jul 2018 14:49:06 +0900 Subject: [PATCH 005/144] Add more helpful classes and ids --- src/frontend/editbox.nim | 4 ++-- src/frontend/forum.nim | 2 +- src/frontend/signup.nim | 9 +++++---- src/frontend/usermenu.nim | 12 +++++++----- 4 files changed, 15 insertions(+), 12 deletions(-) diff --git a/src/frontend/editbox.nim b/src/frontend/editbox.nim index 3f8753e..d461f90 100644 --- a/src/frontend/editbox.nim +++ b/src/frontend/editbox.nim @@ -87,7 +87,7 @@ when defined(js): text state.error.get().message tdiv(class="edit-buttons"): - tdiv(class="reply-button"): + tdiv(class="cancel-button"): button(class="btn btn-link", onClick=(e: Event, n: VNode) => (state.onEditCancel())): text " Cancel" @@ -95,4 +95,4 @@ when defined(js): button(class=class({"loading": state.loading}, "btn btn-primary"), onClick=(e: Event, n: VNode) => state.save()): italic(class="fas fa-check") - text " Save" \ No newline at end of file + text " Save" diff --git a/src/frontend/forum.nim b/src/frontend/forum.nim index cfc4151..4dea047 100644 --- a/src/frontend/forum.nim +++ b/src/frontend/forum.nim @@ -149,4 +149,4 @@ proc render(): VNode = ]) window.onPopState = onPopState -setRenderer render \ No newline at end of file +setRenderer render diff --git a/src/frontend/signup.nim b/src/frontend/signup.nim index 20a14ae..1a4b47a 100644 --- a/src/frontend/signup.nim +++ b/src/frontend/signup.nim @@ -78,10 +78,11 @@ when defined(js): "data-sitekey"=recaptchaSiteKey.get()) script(src="https://www.google.com/recaptcha/api.js") tdiv(class="modal-footer"): - button(class=class({"loading": state.loading}, "btn btn-primary"), - onClick=(ev: Event, n: VNode) => onSignUpClick(ev, n, state)): + button(class=class({"loading": state.loading}, + "btn btn-primary create-account-btn"), + onClick=(ev: Event, n: VNode) => onSignUpClick(ev, n, state)): text "Create account" - button(class="btn", + button(class="btn login-btn", onClick=(ev: Event, n: VNode) => (state.onLogIn(); state.shown = false)): text "Log in" @@ -92,4 +93,4 @@ when defined(js): onClick=(ev: Event, n: VNode) => (state.shown = false; anchorCB(ev, n))): text "content license" - text "." \ No newline at end of file + text "." diff --git a/src/frontend/usermenu.nim b/src/frontend/usermenu.nim index 65ea45c..89954a9 100644 --- a/src/frontend/usermenu.nim +++ b/src/frontend/usermenu.nim @@ -24,7 +24,7 @@ when defined(js): proc render*(state: UserMenu, user: User): VNode = result = buildHtml(): - tdiv(): + tdiv(id="profile-btn"): figure(class="avatar c-hand", onClick=(e: Event, n: VNode) => onClick(e, n, state)): img(src=user.avatarUrl, title=user.name) @@ -52,13 +52,15 @@ when defined(js): tdiv(class="tile-icon"): img(class="avatar", src=user.avatarUrl, title=user.name) - tdiv(class="tile-content"): + tdiv(id="profile-name", class="tile-content"): text user.name li(class="divider") li(class="menu-item"): - a(href=makeUri("/profile/" & user.name)): + a(id="myprofile-btn", + href=makeUri("/profile/" & user.name)): text "My profile" li(class="menu-item c-hand"): - a(onClick = (e: Event, n: VNode) => + a(id="logout-btn", + onClick = (e: Event, n: VNode) => (state.shown=false; state.onLogout())): - text "Logout" \ No newline at end of file + text "Logout" From cb5923d9f8caf97f19aee09e7e243d582cd9c904 Mon Sep 17 00:00:00 2001 From: Joey Yakimowich-Payne Date: Mon, 16 Jul 2018 14:49:56 +0900 Subject: [PATCH 006/144] Add helpful macro and procs for testing --- tests/browsertests/common.nim | 80 ++++++++++++++++++++++++++--------- 1 file changed, 59 insertions(+), 21 deletions(-) diff --git a/tests/browsertests/common.nim b/tests/browsertests/common.nim index 3d651e3..7100116 100644 --- a/tests/browsertests/common.nim +++ b/tests/browsertests/common.nim @@ -1,5 +1,48 @@ -import os, options +import os, options, unittest import webdriver +import macros + +macro with*(obj: typed, code: untyped): untyped = + ## Execute a set of statements with an object + expectKind code, nnkStmtList + result = code + + # Simply inject obj into call + for i in 0 ..< result.len: + if result[i].kind in {nnkCommand, nnkCall}: + result[i].insert(1, obj) + +template click*(session: Session, element: string, strategy=CssSelector) = + let el = session.findElement(element, strategy) + check el.isSome() + el.get().click() + +template sendKeys*(session: Session, element, keys: string) = + let el = session.findElement(element) + check el.isSome() + el.get().sendKeys(keys) + +template sendKeys*(session: Session, element: string, keys: varargs[Key]) = + let el = session.findElement(element) + check el.isSome() + + # focus + el.get().click() + for key in keys: + session.press(key) + +template ensureExists*(session: Session, element: string) = + let el = session.findElement(element) + check el.isSome() + +template check*(session: Session, element: string, function: untyped) = + let el = session.findElement(element) + check function(el) + +template checkText*(session: Session, element, expectedValue: string) = + let el = session.findElement(element) + check el.isSome() + check el.get().getText() == expectedValue proc waitForLoad*(session: Session, timeout=20000) = var waitTime = 0 @@ -14,29 +57,24 @@ proc waitForLoad*(session: Session, timeout=20000) = if waitTime > timeout: doAssert false, "Wait for load time exceeded" +proc wait*(session: Session) = + session.waitForLoad() + +proc wait*(session: Session, msTimeout: int) = + session.waitForLoad(msTimeout) + proc logout*(session: Session) = - # Check whether we can log out. - let logoutLink = session.findElement( - "Logout", - LinkTextSelector - ).get() - logoutLink.click() + with session: + click "#profile-btn" + click "#logout-btn" proc login*(session: Session, user, password: string) = - let logIn = session.findElement("#login-btn").get() - logIn.click() + with session: + click "#login-btn" - let usernameField = session.findElement( - "#login-form input[name='username']" - ) + sendKeys "#login-form input[name='username']", "admin" + sendKeys "#login-form input[name='password']", "admin" - let passwordField = session.findElement( - "#login-form input[name='password']" - ) + sendKeys "#login-form input[name='password']", Key.Enter - usernameField.get().sendKeys("admin") - passwordField.get().sendKeys("admin") - passwordField.get().click() # Focus field. - session.press(Key.Enter) - - waitForLoad(session, 5000) \ No newline at end of file + wait(5000) From 1b55aec5d26195852c8f9e96809d35d7a5590d54 Mon Sep 17 00:00:00 2001 From: Joey Yakimowich-Payne Date: Mon, 16 Jul 2018 14:50:33 +0900 Subject: [PATCH 007/144] Rewrite scenario1 with new macro --- tests/browsertests/scenario1.nim | 108 +++++++++---------------------- 1 file changed, 32 insertions(+), 76 deletions(-) diff --git a/tests/browsertests/scenario1.nim b/tests/browsertests/scenario1.nim index fe8ef22..1674130 100644 --- a/tests/browsertests/scenario1.nim +++ b/tests/browsertests/scenario1.nim @@ -9,96 +9,52 @@ proc test*(session: Session, baseUrl: string) = # Sanity checks test "shows sign up": - let signUp = session.findElement("#signup-btn") - check signUp.get().getText() == "Sign up" + with session: + checkText "#signup-btn", "Sign up" test "shows log in": - let logIn = session.findElement("#login-btn") - check logIn.get().getText() == "Log in" + with session: + checkText "#login-btn", "Log in" test "is empty": - let thread = session.findElement("tr > td.thread-title") - check thread.isNone() + with session: + check "tr > td.thread-title", isNone # Logging in test "can login/logout": - let logIn = session.findElement("#login-btn").get() - logIn.click() + with session: + click "#login-btn" - let usernameField = session.findElement( - "#login-form input[name='username']" - ) - check usernameField.isSome() - let passwordField = session.findElement( - "#login-form input[name='password']" - ) - check passwordField.isSome() + sendKeys "#login-form input[name='username']", "admin" + sendKeys "#login-form input[name='password']", "admin" - usernameField.get().sendKeys("admin") - passwordField.get().sendKeys("admin") - passwordField.get().click() # Focus field. - session.press(Key.Enter) + sendKeys "#login-form input[name='password']", Key.Enter + wait(5000) - waitForLoad(session, 5000) + # Verify that the user menu has been initialised properly. + click "#profile-btn" + checkText "#profile-btn #profile-name", "admin" - # Verify that the user menu has been initialised properly. - let profileButton = session.findElement( - "#main-navbar figure.avatar" - ).get() - profileButton.click() - - let profileName = session.findElement( - "#main-navbar .menu-right div.tile-content" - ).get() - - check profileName.getText() == "admin" - - # Check whether we can log out. - let logoutLink = session.findElement( - "Logout", - LinkTextSelector - ).get() - logoutLink.click() - - # Verify we have logged out by looking for the log in button. - check session.findElement("#login-btn").isSome() + # Check whether we can log out. + click "#logout-btn" + # Verify we have logged out by looking for the log in button. + ensureExists "#login-btn" test "can register": - let signup = session.findElement("#signup-btn").get() - signup.click() + with session: + click "#signup-btn" - let emailField = session.findElement( - "#signup-form input[name='email']" - ).get() - let usernameField = session.findElement( - "#signup-form input[name='username']" - ).get() - let passwordField = session.findElement( - "#signup-form input[name='password']" - ).get() + sendKeys "#signup-form input[name='email']", "test@test.com" + sendKeys "#signup-form input[name='username']", "test" + sendKeys "#signup-form input[name='password']", "test" - emailField.sendKeys("test@test.com") - usernameField.sendKeys("test") - passwordField.sendKeys("test") + click "#signup-modal .create-account-btn" + wait(5000) - let createAccount = session.findElement( - "#signup-modal .modal-footer .btn-primary" - ).get() + # Verify that the user menu has been initialised properly. + click "#profile-btn" + checkText "#profile-btn #profile-name", "test" + # close menu + click "#profile-btn" - createAccount.click() - - waitForLoad(session, 5000) - - # Verify that the user menu has been initialised properly. - let profileButton = session.findElement( - "#main-navbar figure.avatar" - ).get() - profileButton.click() - - let profileName = session.findElement( - "#main-navbar .menu-right div.tile-content" - ).get() - - check profileName.getText() == "test" - - logout(session) \ No newline at end of file + logout(session) From d7f3a038a95bc9e2e90499c8e10d73bc5228e846 Mon Sep 17 00:00:00 2001 From: Joey Yakimowich-Payne Date: Mon, 16 Jul 2018 14:51:21 +0900 Subject: [PATCH 008/144] Rewrite threads tests with new macro --- tests/browsertests/threads.nim | 71 +++++++++++++++++++++------------- 1 file changed, 44 insertions(+), 27 deletions(-) diff --git a/tests/browsertests/threads.nim b/tests/browsertests/threads.nim index 1474ec6..23ffce2 100644 --- a/tests/browsertests/threads.nim +++ b/tests/browsertests/threads.nim @@ -3,44 +3,61 @@ import unittest, options, os, common import webdriver proc test*(session: Session, baseUrl: string) = - session.navigate(baseUrl) - - waitForLoad(session) - - login(session, "admin", "admin") - - test "can create thread": - let newThreadBtn = session.findElement("#new-thread-btn").get() - newThreadBtn.click() + let + titleStr = "This is a thread title!" + contentStr = "This is content" + suite "thread tests": + session.navigate(baseUrl) waitForLoad(session) + login(session, "admin", "admin") - let newThread = session.findElement("#new-thread") - check newThread.isSome() + setup: + session.navigate(baseUrl) + waitForLoad(session) - let createThreadBtn = session.findElement("#create-thread-btn") - check createThreadBtn.isSome() + test "can create thread": + with session: + click "#new-thread-btn" + wait() + sendKeys "#thread-title", titleStr + sendKeys "#reply-textarea", contentStr - let threadTitle = session.findElement("#thread-title") - check threadTitle.isSome() + click "#create-thread-btn" + wait() - let replyBox = session.findElement("#reply-textarea") - check replyBox.isSome() + checkText "#thread-title", titleStr + checkText ".original-post div.post-content", contentStr - threadTitle.get().sendKeys("This is a thread title!") - replyBox.get().sendKeys("This is content.") + test "try create duplicate thread": + with session: + click "#new-thread-btn" + wait() + ensureExists "#new-thread" - createThreadBtn.get().click() + sendKeys "#thread-title", titleStr + sendKeys "#reply-textarea", contentStr - waitForLoad(session) + click "#create-thread-btn" - let newThreadTitle = session.findElement("#thread-title") - check newThreadTitle.isSome() + wait() - check newThreadTitle.get().getText() == "This is a thread title!" + ensureExists "#new-thread p.text-error" - let content = session.findElement(".original-post div.post-content") - check content.isSome() + test "can edit post": + let modificationText = " and I edited it!" + with session: + click titleStr, LinkTextSelector + wait() - check content.get().getText() == "This is content." + click ".post-buttons .edit-button" + wait() + + sendKeys ".original-post #reply-textarea", modificationText + click ".edit-buttons .save-button" + wait() + + checkText ".original-post div.post-content", contentStr & modificationText + + logout(session) From 3c932248173080cee0c62de97d773d942ac28491 Mon Sep 17 00:00:00 2001 From: Joey Yakimowich-Payne Date: Mon, 16 Jul 2018 19:05:26 +0900 Subject: [PATCH 009/144] Fix user/password combo --- tests/browsertests/common.nim | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/tests/browsertests/common.nim b/tests/browsertests/common.nim index 7100116..cb16831 100644 --- a/tests/browsertests/common.nim +++ b/tests/browsertests/common.nim @@ -39,6 +39,11 @@ template check*(session: Session, element: string, function: untyped) = let el = session.findElement(element) check function(el) +template check*(session: Session, element: string, + strategy: LocationStrategy, function: untyped) = + let el = session.findElement(element, strategy) + check function(el) + template checkText*(session: Session, element, expectedValue: string) = let el = session.findElement(element) check el.isSome() @@ -67,13 +72,14 @@ proc logout*(session: Session) = with session: click "#profile-btn" click "#logout-btn" + wait(5000) proc login*(session: Session, user, password: string) = with session: click "#login-btn" - sendKeys "#login-form input[name='username']", "admin" - sendKeys "#login-form input[name='password']", "admin" + sendKeys "#login-form input[name='username']", user + sendKeys "#login-form input[name='password']", password sendKeys "#login-form input[name='password']", Key.Enter From 0d67eab62642bcab571cdfffe4358281f6464b10 Mon Sep 17 00:00:00 2001 From: Joey Yakimowich-Payne Date: Mon, 16 Jul 2018 19:06:00 +0900 Subject: [PATCH 010/144] Create test and dev users for testing --- src/setup_nimforum.nim | 37 ++++++++++++++++++++++++++++--------- 1 file changed, 28 insertions(+), 9 deletions(-) diff --git a/src/setup_nimforum.nim b/src/setup_nimforum.nim index a015f2d..9c93080 100644 --- a/src/setup_nimforum.nim +++ b/src/setup_nimforum.nim @@ -22,10 +22,26 @@ proc backup(path: string, contents: Option[string]=none[string]()) = echo(path, " already exists. Moving to ", backupPath) moveFile(path, backupPath) +proc createUser(db: DbConn, user: tuple[username, password, email: string], + rank: Rank) = + + if user.username.len != 0: + let salt = makeSalt() + let password = makePassword(user.password, salt) + + exec(db, sql""" + INSERT INTO person(name, password, email, salt, status, lastOnline) + VALUES (?, ?, ?, ?, ?, DATETIME('now')) + """, user.username, password, user.email, salt, $rank) + proc initialiseDb(admin: tuple[username, password, email: string], filename="nimforum.db") = - let path = getCurrentDir() / filename - if "-dev" notin filename and "-test" notin filename: + let + path = getCurrentDir() / filename + isTest = "-test" in filename + isDev = "-dev" in filename + + if not isDev and not isTest: backup(path) removeFile(path) @@ -98,13 +114,16 @@ proc initialiseDb(admin: tuple[username, password, email: string], db.exec sql"create index PersonStatusIdx on person(status);" # Create default user. - if admin.username.len != 0: - let salt = makeSalt() - let password = makePassword(admin.password, salt) - db.exec(sql""" - insert into person (id, name, password, email, salt, status) - values (1, ?, ?, ?, ?, ?); - """, admin.username, password, admin.email, salt, $Admin) + db.createUser(admin, Admin) + + # Create test users if test or development + if isTest or isDev: + for rank in Spammer..Moderator: + let rankLower = toLowerAscii($rank) + let user = (username: $rankLower, + password: $rankLower, + email: $rankLower & "@localhost.local") + db.createUser(user, rank) # -- Post From 6fb5cfbfa236c0c627a03184784569a4142df6d2 Mon Sep 17 00:00:00 2001 From: Joey Yakimowich-Payne Date: Mon, 16 Jul 2018 21:09:24 +0900 Subject: [PATCH 011/144] Add more friendly classes --- src/frontend/delete.nim | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/frontend/delete.nim b/src/frontend/delete.nim index 789aebc..37446e5 100644 --- a/src/frontend/delete.nim +++ b/src/frontend/delete.nim @@ -94,7 +94,7 @@ when defined(js): proc render*(state: DeleteModal): VNode = result = buildHtml(): tdiv(class=class({"active": state.shown}, "modal modal-sm"), - id="login-modal"): + id="delete-modal"): a(href="", class="modal-overlay", "aria-label"="close", onClick=(ev: Event, n: VNode) => onClose(ev, n, state)) tdiv(class="modal-container"): @@ -122,11 +122,11 @@ when defined(js): button(class=class( {"loading": state.loading}, - "btn btn-primary" + "btn btn-primary delete-btn" ), onClick=(ev: Event, n: VNode) => onDelete(ev, n, state)): italic(class="fas fa-trash-alt") text " Delete" - button(class="btn", + button(class="btn cancel-btn", onClick=(ev: Event, n: VNode) => (state.shown = false)): - text "Cancel" \ No newline at end of file + text "Cancel" From 3324f37faa31512f8a5a29e453379f1ba7e5fadd Mon Sep 17 00:00:00 2001 From: Joey Yakimowich-Payne Date: Mon, 16 Jul 2018 21:09:55 +0900 Subject: [PATCH 012/144] Cleanup in scenario1 --- tests/browsertests/scenario1.nim | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/tests/browsertests/scenario1.nim b/tests/browsertests/scenario1.nim index 1674130..d5c2fe5 100644 --- a/tests/browsertests/scenario1.nim +++ b/tests/browsertests/scenario1.nim @@ -9,16 +9,13 @@ proc test*(session: Session, baseUrl: string) = # Sanity checks test "shows sign up": - with session: - checkText "#signup-btn", "Sign up" + session.checkText("#signup-btn", "Sign up") test "shows log in": - with session: - checkText "#login-btn", "Log in" + session.checkText("#login-btn", "Log in") test "is empty": - with session: - check "tr > td.thread-title", isNone + session.check("tr > td.thread-title", isNone) # Logging in test "can login/logout": @@ -29,7 +26,7 @@ proc test*(session: Session, baseUrl: string) = sendKeys "#login-form input[name='password']", "admin" sendKeys "#login-form input[name='password']", Key.Enter - wait(5000) + wait 5000 # Verify that the user menu has been initialised properly. click "#profile-btn" @@ -49,7 +46,7 @@ proc test*(session: Session, baseUrl: string) = sendKeys "#signup-form input[name='password']", "test" click "#signup-modal .create-account-btn" - wait(5000) + wait 5000 # Verify that the user menu has been initialised properly. click "#profile-btn" From f315be7361a3078c747f0783ab3fd680fc4676dd Mon Sep 17 00:00:00 2001 From: Joey Yakimowich-Payne Date: Mon, 16 Jul 2018 21:10:46 +0900 Subject: [PATCH 013/144] Add tests for like/delete thread --- tests/browsertests/threads.nim | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/tests/browsertests/threads.nim b/tests/browsertests/threads.nim index 23ffce2..4a26148 100644 --- a/tests/browsertests/threads.nim +++ b/tests/browsertests/threads.nim @@ -60,4 +60,36 @@ proc test*(session: Session, baseUrl: string) = checkText ".original-post div.post-content", contentStr & modificationText + test "can like thread": + # logout admin and login to regular user + logout(session) + login(session, "user", "user") + + with session: + click titleStr, LinkTextSelector + wait() + + click ".post-buttons .like-button" + + checkText ".post-buttons .like-button .like-count", "1" + + logout(session) + session.navigate(baseUrl) + waitForLoad(session) + login(session, "admin", "admin") + + test "can delete thread": + with session: + click titleStr, LinkTextSelector + wait() + + click ".post-buttons .delete-button" + wait() + + # click delete confirmation + click "#delete-modal .delete-btn" + + # Make sure the forum post is gone + check titleStr, LinkTextSelector, isNone + logout(session) From b2fc4dfbe060eac87caec14bd340204704da09af Mon Sep 17 00:00:00 2001 From: Joey Yakimowich-Payne Date: Tue, 17 Jul 2018 12:50:23 +0900 Subject: [PATCH 014/144] Add license id --- src/frontend/signup.nim | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/frontend/signup.nim b/src/frontend/signup.nim index 1a4b47a..6a422d6 100644 --- a/src/frontend/signup.nim +++ b/src/frontend/signup.nim @@ -89,7 +89,7 @@ when defined(js): p(class="license-text text-gray"): text "By registering, you agree to the " - a(href=makeUri("/about/license"), + a(id="license", href=makeUri("/about/license"), onClick=(ev: Event, n: VNode) => (state.shown = false; anchorCB(ev, n))): text "content license" From 0050ad42f5cb7971fd9b91b263b50b5736a2100d Mon Sep 17 00:00:00 2001 From: Joey Yakimowich-Payne Date: Tue, 17 Jul 2018 19:55:17 +0900 Subject: [PATCH 015/144] Try to appease travis --- tests/browsertests/common.nim | 7 ++++++- tests/browsertests/scenario1.nim | 2 +- tests/browsertests/threads.nim | 5 ++++- 3 files changed, 11 insertions(+), 3 deletions(-) diff --git a/tests/browsertests/common.nim b/tests/browsertests/common.nim index cb16831..340492c 100644 --- a/tests/browsertests/common.nim +++ b/tests/browsertests/common.nim @@ -44,6 +44,10 @@ template check*(session: Session, element: string, let el = session.findElement(element, strategy) check function(el) +template checkIsNone*(session: Session, element: string, strategy=CssSelector) = + let el = session.findElement(element, strategy) + check el.isNone() + template checkText*(session: Session, element, expectedValue: string) = let el = session.findElement(element) check el.isSome() @@ -70,8 +74,9 @@ proc wait*(session: Session, msTimeout: int) = proc logout*(session: Session) = with session: + wait(5000) click "#profile-btn" - click "#logout-btn" + click "#profile-btn #logout-btn" wait(5000) proc login*(session: Session, user, password: string) = diff --git a/tests/browsertests/scenario1.nim b/tests/browsertests/scenario1.nim index d5c2fe5..89dc76b 100644 --- a/tests/browsertests/scenario1.nim +++ b/tests/browsertests/scenario1.nim @@ -15,7 +15,7 @@ proc test*(session: Session, baseUrl: string) = session.checkText("#login-btn", "Log in") test "is empty": - session.check("tr > td.thread-title", isNone) + session.checkIsNone("tr > td.thread-title") # Logging in test "can login/logout": diff --git a/tests/browsertests/threads.nim b/tests/browsertests/threads.nim index 4a26148..5181b11 100644 --- a/tests/browsertests/threads.nim +++ b/tests/browsertests/threads.nim @@ -80,6 +80,7 @@ proc test*(session: Session, baseUrl: string) = test "can delete thread": with session: + wait() click titleStr, LinkTextSelector wait() @@ -90,6 +91,8 @@ proc test*(session: Session, baseUrl: string) = click "#delete-modal .delete-btn" # Make sure the forum post is gone - check titleStr, LinkTextSelector, isNone + checkIsNone titleStr, LinkTextSelector + session.navigate(baseUrl) + waitForLoad(session) logout(session) From c361fda523b4ce0fe9ea9f6c33ab5fae3e8badac Mon Sep 17 00:00:00 2001 From: Joey Yakimowich-Payne Date: Tue, 17 Jul 2018 22:29:51 +0900 Subject: [PATCH 016/144] Replace len check with assert --- src/setup_nimforum.nim | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/src/setup_nimforum.nim b/src/setup_nimforum.nim index 9c93080..3c5cf5b 100644 --- a/src/setup_nimforum.nim +++ b/src/setup_nimforum.nim @@ -24,15 +24,14 @@ proc backup(path: string, contents: Option[string]=none[string]()) = proc createUser(db: DbConn, user: tuple[username, password, email: string], rank: Rank) = + assert user.username.len != 0 + let salt = makeSalt() + let password = makePassword(user.password, salt) - if user.username.len != 0: - let salt = makeSalt() - let password = makePassword(user.password, salt) - - exec(db, sql""" - INSERT INTO person(name, password, email, salt, status, lastOnline) - VALUES (?, ?, ?, ?, ?, DATETIME('now')) - """, user.username, password, user.email, salt, $rank) + exec(db, sql""" + INSERT INTO person(name, password, email, salt, status, lastOnline) + VALUES (?, ?, ?, ?, ?, DATETIME('now')) + """, user.username, password, user.email, salt, $rank) proc initialiseDb(admin: tuple[username, password, email: string], filename="nimforum.db") = From 80558b6bfb5364c8745639b7e17b6f05d6df8ca8 Mon Sep 17 00:00:00 2001 From: Joey Yakimowich-Payne Date: Tue, 17 Jul 2018 22:32:38 +0900 Subject: [PATCH 017/144] Make 5000 ms default wait --- tests/browsertests/common.nim | 11 ++++------- tests/browsertests/scenario1.nim | 4 ++-- tests/browsertests/threads.nim | 4 ++-- 3 files changed, 8 insertions(+), 11 deletions(-) diff --git a/tests/browsertests/common.nim b/tests/browsertests/common.nim index 340492c..a2fb09b 100644 --- a/tests/browsertests/common.nim +++ b/tests/browsertests/common.nim @@ -66,18 +66,15 @@ proc waitForLoad*(session: Session, timeout=20000) = if waitTime > timeout: doAssert false, "Wait for load time exceeded" -proc wait*(session: Session) = - session.waitForLoad() - -proc wait*(session: Session, msTimeout: int) = +proc wait*(session: Session, msTimeout: int = 5000) = session.waitForLoad(msTimeout) proc logout*(session: Session) = with session: - wait(5000) + wait() click "#profile-btn" click "#profile-btn #logout-btn" - wait(5000) + wait() proc login*(session: Session, user, password: string) = with session: @@ -88,4 +85,4 @@ proc login*(session: Session, user, password: string) = sendKeys "#login-form input[name='password']", Key.Enter - wait(5000) + wait() diff --git a/tests/browsertests/scenario1.nim b/tests/browsertests/scenario1.nim index 89dc76b..f8efe48 100644 --- a/tests/browsertests/scenario1.nim +++ b/tests/browsertests/scenario1.nim @@ -26,7 +26,7 @@ proc test*(session: Session, baseUrl: string) = sendKeys "#login-form input[name='password']", "admin" sendKeys "#login-form input[name='password']", Key.Enter - wait 5000 + wait() # Verify that the user menu has been initialised properly. click "#profile-btn" @@ -46,7 +46,7 @@ proc test*(session: Session, baseUrl: string) = sendKeys "#signup-form input[name='password']", "test" click "#signup-modal .create-account-btn" - wait 5000 + wait() # Verify that the user menu has been initialised properly. click "#profile-btn" diff --git a/tests/browsertests/threads.nim b/tests/browsertests/threads.nim index 5181b11..a74d59e 100644 --- a/tests/browsertests/threads.nim +++ b/tests/browsertests/threads.nim @@ -80,7 +80,6 @@ proc test*(session: Session, baseUrl: string) = test "can delete thread": with session: - wait() click titleStr, LinkTextSelector wait() @@ -89,10 +88,11 @@ proc test*(session: Session, baseUrl: string) = # click delete confirmation click "#delete-modal .delete-btn" + wait() # Make sure the forum post is gone checkIsNone titleStr, LinkTextSelector session.navigate(baseUrl) - waitForLoad(session) + session.wait() logout(session) From b405f63a32ed5c99bf7efeeebf4c7f28fea0b121 Mon Sep 17 00:00:00 2001 From: Joey Yakimowich-Payne Date: Tue, 17 Jul 2018 22:51:01 +0900 Subject: [PATCH 018/144] Separate user and admin tests --- tests/browsertests/scenario1.nim | 2 + tests/browsertests/threads.nim | 73 +++++++++++++++++++++----------- 2 files changed, 50 insertions(+), 25 deletions(-) diff --git a/tests/browsertests/scenario1.nim b/tests/browsertests/scenario1.nim index f8efe48..18d1bc7 100644 --- a/tests/browsertests/scenario1.nim +++ b/tests/browsertests/scenario1.nim @@ -54,4 +54,6 @@ proc test*(session: Session, baseUrl: string) = # close menu click "#profile-btn" + session.navigate(baseUrl) + session.wait() logout(session) diff --git a/tests/browsertests/threads.nim b/tests/browsertests/threads.nim index a74d59e..74729a3 100644 --- a/tests/browsertests/threads.nim +++ b/tests/browsertests/threads.nim @@ -4,31 +4,61 @@ import webdriver proc test*(session: Session, baseUrl: string) = let - titleStr = "This is a thread title!" - contentStr = "This is content" + userTitleStr = "This is a user thread!" + userContentStr = "A user has filled this out" - suite "thread tests": + adminTitleStr = "This is a thread title!" + adminContentStr = "This is content" + + suite "user thread tests": session.navigate(baseUrl) - waitForLoad(session) - login(session, "admin", "admin") + session.wait() + login(session, "user", "user") setup: session.navigate(baseUrl) - waitForLoad(session) + session.wait() test "can create thread": with session: click "#new-thread-btn" wait() - sendKeys "#thread-title", titleStr - sendKeys "#reply-textarea", contentStr + sendKeys "#thread-title", userTitleStr + sendKeys "#reply-textarea", userContentStr click "#create-thread-btn" wait() - checkText "#thread-title", titleStr - checkText ".original-post div.post-content", contentStr + checkText "#thread-title", userTitleStr + checkText ".original-post div.post-content", userContentStr + + session.navigate(baseUrl) + session.wait() + logout(session) + + suite "admin thread tests": + session.navigate(baseUrl) + session.wait() + login(session, "admin", "admin") + + setup: + session.navigate(baseUrl) + session.wait() + + 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", adminTitleStr + checkText ".original-post div.post-content", adminContentStr test "try create duplicate thread": with session: @@ -36,8 +66,8 @@ proc test*(session: Session, baseUrl: string) = wait() ensureExists "#new-thread" - sendKeys "#thread-title", titleStr - sendKeys "#reply-textarea", contentStr + sendKeys "#thread-title", adminTitleStr + sendKeys "#reply-textarea", adminContentStr click "#create-thread-btn" @@ -48,7 +78,7 @@ proc test*(session: Session, baseUrl: string) = test "can edit post": let modificationText = " and I edited it!" with session: - click titleStr, LinkTextSelector + click adminTitleStr, LinkTextSelector wait() click ".post-buttons .edit-button" @@ -58,29 +88,22 @@ proc test*(session: Session, baseUrl: string) = click ".edit-buttons .save-button" wait() - checkText ".original-post div.post-content", contentStr & modificationText + checkText ".original-post div.post-content", adminContentStr & modificationText test "can like thread": - # logout admin and login to regular user - logout(session) - login(session, "user", "user") + # Try to like the user thread above with session: - click titleStr, LinkTextSelector + click userTitleStr, LinkTextSelector wait() click ".post-buttons .like-button" checkText ".post-buttons .like-button .like-count", "1" - logout(session) - session.navigate(baseUrl) - waitForLoad(session) - login(session, "admin", "admin") - test "can delete thread": with session: - click titleStr, LinkTextSelector + click adminTitleStr, LinkTextSelector wait() click ".post-buttons .delete-button" @@ -91,7 +114,7 @@ proc test*(session: Session, baseUrl: string) = wait() # Make sure the forum post is gone - checkIsNone titleStr, LinkTextSelector + checkIsNone adminTitleStr, LinkTextSelector session.navigate(baseUrl) session.wait() From 22ec94590c3cbed650fba9162929610ab6aa15d0 Mon Sep 17 00:00:00 2001 From: Dominik Picheta Date: Thu, 19 Jul 2018 19:21:55 +0100 Subject: [PATCH 019/144] Update Jester to 0.4.0. --- .travis.yml | 2 +- nimforum.nimble | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.travis.yml b/.travis.yml index b80127e..b6177c4 100644 --- a/.travis.yml +++ b/.travis.yml @@ -32,7 +32,7 @@ before_install: - export PATH=$PATH:$PWD/geckodriver install: - - export CHOOSENIM_CHOOSE_VERSION="#987bf13" + - export CHOOSENIM_CHOOSE_VERSION="#f92d61b1f4e193bd" - | curl https://nim-lang.org/choosenim/init.sh -sSf > init.sh sh init.sh -y diff --git a/nimforum.nimble b/nimforum.nimble index 64484b1..e0d0f83 100644 --- a/nimforum.nimble +++ b/nimforum.nimble @@ -12,8 +12,8 @@ skipExt = @["nim"] # Dependencies -requires "nim >= 0.14.0" -requires "jester#64295c8" +requires "nim >= 0.18.1" +requires "jester 0.4.0" requires "bcrypt#head" requires "hmac#9c61ebe2fd134cf97" requires "recaptcha 1.0.2" From 954fe7b05a613f3db9e67665bd5f26707b2aff87 Mon Sep 17 00:00:00 2001 From: markprocess Date: Thu, 19 Jul 2018 16:09:36 -0400 Subject: [PATCH 020/144] Refactoring exception handling --- src/forum.nim | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/src/forum.nim b/src/forum.nim index e55aa06..ca9e54b 100644 --- a/src/forum.nim +++ b/src/forum.nim @@ -1023,8 +1023,7 @@ routes: let session = executeLogin(c, username, password) setCookie("sid", session) resp Http200, "{}", "application/json" - except ForumError: - let exc = (ref ForumError)(getCurrentException()) + except ForumError as exc: resp Http400, $(%exc.data), "application/json" get "/status.json": @@ -1282,8 +1281,7 @@ routes: try: await updateProfile(c, username, email, rank) resp Http200, "{}", "application/json" - except ForumError: - let exc = (ref ForumError)(getCurrentException()) + except ForumError as exc: resp Http400, $(%exc.data), "application/json" post "/sendResetPassword": @@ -1311,8 +1309,7 @@ routes: c, formData["email"].body, recaptcha, request.host ) resp Http200, "{}", "application/json" - except ForumError: - let exc = (ref ForumError)(getCurrentException()) + except ForumError as exc: resp Http400, $(%exc.data), "application/json" post "/resetPassword": @@ -1350,7 +1347,7 @@ routes: ) resp Http200, "{}", "application/json" except ForumError as exc: - resp Http400, $(%exc.data),"application/json" + resp Http400, $(%exc.data), "application/json" post "/activateEmail": createTFD() @@ -1371,7 +1368,7 @@ routes: ) resp Http200, "{}", "application/json" except ForumError as exc: - resp Http400, $(%exc.data),"application/json" + resp Http400, $(%exc.data), "application/json" get "/t/@id": cond "id" in request.params @@ -1456,4 +1453,4 @@ routes: get re"/(.*)": cond request.matches[0].splitFile.ext == "" - resp karaxHtml \ No newline at end of file + resp karaxHtml From 9ee0ddf1768f5387b30c2cdeb20d839ea1c0cc0b Mon Sep 17 00:00:00 2001 From: Dominik Picheta Date: Fri, 20 Jul 2018 13:52:27 +0100 Subject: [PATCH 021/144] Sessions now persist across different IP addresses. --- src/forum.nim | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/forum.nim b/src/forum.nim index ca9e54b..9d7c7eb 100644 --- a/src/forum.nim +++ b/src/forum.nim @@ -115,21 +115,21 @@ proc sendResetPassword( ) proc logout(c: TForumData) = - const query = sql"delete from session where ip = ? and key = ?" + const query = sql"delete from session where key = ?" c.username = "" c.userpass = "" - exec(db, query, c.req.ip, c.req.cookies["sid"]) + exec(db, query, c.req.cookies["sid"]) proc checkLoggedIn(c: TForumData) = if not c.req.cookies.hasKey("sid"): return let sid = c.req.cookies["sid"] if execAffectedRows(db, sql("update session set lastModified = DATETIME('now') " & - "where ip = ? and key = ?"), - c.req.ip, sid) > 0: + "where key = ?"), + sid) > 0: c.userid = getValue(db, - sql"select userid from session where ip = ? and key = ?", - c.req.ip, sid) + sql"select userid from session where key = ?", + sid) let row = getRow(db, sql"select name, email, status from person where id = ?", c.userid) From 9f9d16467f8866104023ee34866db8d495373e64 Mon Sep 17 00:00:00 2001 From: Dominik Picheta Date: Fri, 20 Jul 2018 14:14:59 +0100 Subject: [PATCH 022/144] Fixes #181. --- src/forum.nim | 1 - src/frontend/post.nim | 3 ++- src/frontend/threadlist.nim | 16 +++++++++------- src/frontend/user.nim | 7 +++++-- 4 files changed, 16 insertions(+), 11 deletions(-) diff --git a/src/forum.nim b/src/forum.nim index 9d7c7eb..399a64e 100644 --- a/src/forum.nim +++ b/src/forum.nim @@ -769,7 +769,6 @@ 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.status <> 'Banned' and u.id in ( select u.id from post p, person u where p.author = u.id and p.thread = t.id diff --git a/src/frontend/post.nim b/src/frontend/post.nim index c32b490..5ca0d5f 100644 --- a/src/frontend/post.nim +++ b/src/frontend/post.nim @@ -32,7 +32,8 @@ proc lastEdit*(post: Post): PostInfo = post.history[^1] proc isModerated*(post: Post): bool = - ## Determines whether the specified thread is under moderation. + ## Determines whether the specified post is under moderation + ## (i.e. whether the post is invisible to ordinary users). post.author.rank <= Moderated proc isLikedBy*(post: Post, user: Option[User]): bool = diff --git a/src/frontend/threadlist.nim b/src/frontend/threadlist.nim index cceca03..0516d74 100644 --- a/src/frontend/threadlist.nim +++ b/src/frontend/threadlist.nim @@ -22,6 +22,7 @@ type proc isModerated*(thread: Thread): bool = ## Determines whether the specified thread is under moderation. + ## (i.e. whether the specified thread is invisible to ordinary users). thread.author.rank <= Moderated when defined(js): @@ -53,6 +54,8 @@ when defined(js): ## ## The rules for this are determined by the rank of the user, their ## settings (TODO), and whether the thread's creator is moderated or not. + ## + ## The ``user`` argument refers to the currently logged in user. mixin isModerated if user.isNone(): return not thread.isModerated @@ -108,20 +111,19 @@ when defined(js): proc genThread(thread: Thread, isNew: bool, noBorder: bool): VNode = let isOld = (getTime() - thread.creation.fromUnix).weeks > 2 - let isBanned = thread.author.rank < Moderated + let isBanned = thread.author.rank.isBanned() result = buildHtml(): tr(class=class({"no-border": noBorder, "banned": isBanned})): td(class="thread-title"): if thread.isLocked: italic(class="fas fa-lock fa-xs", title="Thread cannot be replied to") + if isBanned: + italic(class="fas fa-ban fa-xs", + title="Thread author is banned") if thread.isModerated: - if isBanned: - italic(class="fas fa-ban fa-xs", - title="Thread author is banned") - else: - italic(class="fas fa-eye-slash fa-xs", - title="Thread is moderated") + italic(class="fas fa-eye-slash fa-xs", + title="Thread is moderated") if thread.isSolved: italic(class="fas fa-check-square fa-xs", title="Thread has a solution") diff --git a/src/frontend/user.nim b/src/frontend/user.nim index bbf5782..ea0624b 100644 --- a/src/frontend/user.nim +++ b/src/frontend/user.nim @@ -4,10 +4,10 @@ type # If you add more "Banned" states, be sure to modify forum's threadsQuery too. Rank* {.pure.} = enum ## serialized as 'status' Spammer ## spammer: every post is invisible - Troll ## troll: cannot write new posts - Banned ## A non-specific ban Moderated ## new member: posts manually reviewed before everybody ## can see them + Troll ## troll: cannot write new posts + Banned ## A non-specific ban EmailUnconfirmed ## member with unconfirmed email address. Their posts ## are visible, but cannot make new posts. This is so that ## when a user with existing posts changes their email, @@ -34,6 +34,9 @@ proc canPost*(rank: Rank): bool = ## Determines whether the specified rank can make new posts. rank >= Rank.User or rank == Moderated +proc isBanned*(rank: Rank): bool = + rank in {Spammer, Troll, Banned} + when defined(js): include karax/prelude import karaxutils From 5714ad0c6aafcd7b95a433d97f7b4069d5cc791b Mon Sep 17 00:00:00 2001 From: Dominik Picheta Date: Fri, 20 Jul 2018 15:16:39 +0100 Subject: [PATCH 023/144] Adds attempt to test issue #181. Moves procs to common too. Sadly the attempt wasn't a success. I decided to commit what I've got so far and come back to it later. Out of time for this now. --- src/frontend/profilesettings.nim | 5 +-- tests/browsertester.nim | 5 +-- tests/browsertests/common.nim | 54 +++++++++++++++++++++++++++++++- tests/browsertests/issue181.nim | 46 +++++++++++++++++++++++++++ tests/browsertests/scenario1.nim | 29 ++--------------- 5 files changed, 108 insertions(+), 31 deletions(-) create mode 100644 tests/browsertests/issue181.nim diff --git a/src/frontend/profilesettings.nim b/src/frontend/profilesettings.nim index 07862b7..ca96361 100644 --- a/src/frontend/profilesettings.nim +++ b/src/frontend/profilesettings.nim @@ -88,7 +88,7 @@ when defined(js): class="form-select", value = $state.rank, onchange=(e: Event, n: VNode) => onRankChange(e, n, state)): for r in Rank: - option(text $r) + option(text $r, id="rank-" & toLowerAscii($r)) p(class="form-input-hint text-warning"): text "You can modify anyone's rank. Remember: with " & "great power comes great responsibility." @@ -185,7 +185,8 @@ when defined(js): button(class=class( {"disabled": not needsSave(state)}, "btn btn-primary" ), - onClick=(e: Event, n: VNode) => save(state)): + onClick=(e: Event, n: VNode) => save(state), + id="save-btn"): italic(class="fas fa-save") text " Save" diff --git a/tests/browsertester.nim b/tests/browsertester.nim index 646ab79..15a3f68 100644 --- a/tests/browsertester.nim +++ b/tests/browsertester.nim @@ -43,8 +43,7 @@ template withBackend(body: untyped): untyped = body -import browsertests/scenario1 -import browsertests/threads +import browsertests/[scenario1, threads, issue181] when isMainModule: spawn runProcess("geckodriver -p 4444 --log config") @@ -65,6 +64,8 @@ when isMainModule: withBackend: scenario1.test(session, baseUrl) threads.test(session, baseUrl) + # TODO: Fix the issue181 test. + # issue181.test(session, baseUrl) session.close() except: diff --git a/tests/browsertests/common.nim b/tests/browsertests/common.nim index a2fb09b..9a0c17a 100644 --- a/tests/browsertests/common.nim +++ b/tests/browsertests/common.nim @@ -1,4 +1,4 @@ -import os, options, unittest +import os, options, unittest, strutils import webdriver import macros @@ -76,6 +76,9 @@ proc logout*(session: Session) = click "#profile-btn #logout-btn" wait() + # Verify we have logged out by looking for the log in button. + ensureExists "#login-btn" + proc login*(session: Session, user, password: string) = with session: click "#login-btn" @@ -86,3 +89,52 @@ 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 + click "#profile-btn" + +proc register*(session: Session, user, password: string) = + with session: + click "#signup-btn" + + sendKeys "#signup-form input[name='email']", user & "@" & user & ".com" + sendKeys "#signup-form input[name='username']", user + sendKeys "#signup-form input[name='password']", password + + click "#signup-modal .create-account-btn" + wait() + + # Verify that the user menu has been initialised properly. + click "#profile-btn" + checkText "#profile-btn #profile-name", user + # close menu + click "#profile-btn" + +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 + checkText ".original-post div.post-content", content + +proc changeRank*(session: Session, rank: string) = + with session: + # Make sure the "Settings" tab is selected. + click ".profile-tabs li:nth-child(2)" + + click "#rank-field" + click "#rank-field option#rank-" & rank.toLowerAscii() + + wait() + + # TODO: Getting an "element click intercepted" error here. + click "#save-btn" diff --git a/tests/browsertests/issue181.nim b/tests/browsertests/issue181.nim new file mode 100644 index 0000000..3e28737 --- /dev/null +++ b/tests/browsertests/issue181.nim @@ -0,0 +1,46 @@ +import unittest, options, os, common + +import webdriver + +proc test*(session: Session, baseUrl: string) = + session.navigate(baseUrl) + + waitForLoad(session) + + test "can see banned posts": + with session: + register("issue181", "issue181") + logout() + + # Change rank to `user` so they can post. + login("admin", "admin") + + navigate(baseUrl & "profile/user") + wait() + changeRank("user") + logout() + + login("issue181", "issue181") + + const title = "Testing issue 181." + createThread(title, "Test for issue #181") + + logout() + wait() + + login("admin", "admin") + + # Ban our user. + navigate(baseUrl & "profile/issue181") + changeRank("banned") + + # Make sure the banned user's thread is still visible. + navigate(baseUrl) + ensureExists("tr.banned") + checkText("tr.banned .thread-title > a", title) + logout() + checkText("tr.banned .thread-title > a", title) + + session.navigate(baseUrl) + session.wait() + logout(session) diff --git a/tests/browsertests/scenario1.nim b/tests/browsertests/scenario1.nim index 18d1bc7..5dbc8b6 100644 --- a/tests/browsertests/scenario1.nim +++ b/tests/browsertests/scenario1.nim @@ -20,39 +20,16 @@ proc test*(session: Session, baseUrl: string) = # Logging in test "can login/logout": with session: - click "#login-btn" - - sendKeys "#login-form input[name='username']", "admin" - sendKeys "#login-form input[name='password']", "admin" - - 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", "admin" + login("admin", "admin") # Check whether we can log out. - click "#logout-btn" + logout() # Verify we have logged out by looking for the log in button. ensureExists "#login-btn" test "can register": with session: - click "#signup-btn" - - sendKeys "#signup-form input[name='email']", "test@test.com" - sendKeys "#signup-form input[name='username']", "test" - sendKeys "#signup-form input[name='password']", "test" - - click "#signup-modal .create-account-btn" - wait() - - # Verify that the user menu has been initialised properly. - click "#profile-btn" - checkText "#profile-btn #profile-name", "test" - # close menu - click "#profile-btn" + register("test", "test") session.navigate(baseUrl) session.wait() From 8c16a776b631db4cb1de06fe98bcd3fd97d3dcdb Mon Sep 17 00:00:00 2001 From: Dominik Picheta Date: Fri, 20 Jul 2018 15:45:11 +0100 Subject: [PATCH 024/144] Bump version to 2.0.1. --- nimforum.nimble | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nimforum.nimble b/nimforum.nimble index e0d0f83..d267497 100644 --- a/nimforum.nimble +++ b/nimforum.nimble @@ -1,5 +1,5 @@ # Package -version = "2.0.0" +version = "2.0.1" author = "Dominik Picheta" description = "The Nim forum" license = "MIT" From a6c0fe691cbe1124f2f250975e951ccbc5ba6601 Mon Sep 17 00:00:00 2001 From: Joey Yakimowich-Payne Date: Sun, 22 Jul 2018 18:44:16 +0900 Subject: [PATCH 025/144] Add test for banned user --- src/frontend/profile.nim | 6 ++-- src/frontend/profilesettings.nim | 11 +++--- tests/browsertests/common.nim | 30 +++++++++++++--- tests/browsertests/scenario1.nim | 4 +-- tests/browsertests/threads.nim | 61 ++++++++++++++++++++++---------- 5 files changed, 80 insertions(+), 32 deletions(-) diff --git a/src/frontend/profile.nim b/src/frontend/profile.nim index 48f519a..60f9daf 100644 --- a/src/frontend/profile.nim +++ b/src/frontend/profile.nim @@ -119,7 +119,7 @@ when defined(js): ), onClick=(e: Event, n: VNode) => (state.currentTab = Overview) ): - a(class="c-hand"): + a(id="overview-tab", class="c-hand"): text "Overview" li(class=class( {"active": state.currentTab == Settings}, @@ -127,7 +127,7 @@ when defined(js): ), onClick=(e: Event, n: VNode) => (state.currentTab = Settings) ): - a(class="c-hand"): + a(id="settings-tab", class="c-hand"): italic(class="fas fa-cog") text " Settings" @@ -147,4 +147,4 @@ when defined(js): genPostLink(thread) of Settings: if state.settings.isSome(): - render(state.settings.get(), currentUser) \ No newline at end of file + render(state.settings.get(), currentUser) diff --git a/src/frontend/profilesettings.nim b/src/frontend/profilesettings.nim index ca96361..56ea7b2 100644 --- a/src/frontend/profilesettings.nim +++ b/src/frontend/profilesettings.nim @@ -165,7 +165,8 @@ when defined(js): label(class="form-label"): text "Account" tdiv(class="col-9 col-sm-12"): - button(class="btn btn-secondary", `type`="button", + button(id="delete-account-btn", + class="btn btn-secondary", `type`="button", onClick=(e: Event, n: VNode) => (state.deleteModal.show(state.profile.user))): italic(class="fas fa-times") @@ -176,13 +177,15 @@ when defined(js): span(class="text-error"): text state.error.get().message - button(class=class( + button(id="cancel-btn", + class=class( {"disabled": not needsSave(state)}, "btn btn-link" ), onClick=(e: Event, n: VNode) => (resetSettings(state))): text "Cancel" - button(class=class( + button(id="save-btn", + class=class( {"disabled": not needsSave(state)}, "btn btn-primary" ), onClick=(e: Event, n: VNode) => save(state), @@ -199,4 +202,4 @@ when defined(js): rankField.setInputText($state.rank) let emailField = getVNodeById("email-field") if not emailField.isNil: - emailField.setInputText($state.email) \ No newline at end of file + emailField.setInputText($state.email) diff --git a/tests/browsertests/common.nim b/tests/browsertests/common.nim index 9a0c17a..e8948fa 100644 --- a/tests/browsertests/common.nim +++ b/tests/browsertests/common.nim @@ -31,8 +31,8 @@ template sendKeys*(session: Session, element: string, keys: varargs[Key]) = for key in keys: session.press(key) -template ensureExists*(session: Session, element: string) = - let el = session.findElement(element) +template ensureExists*(session: Session, element: string, strategy=CssSelector) = + let el = session.findElement(element, strategy) check el.isSome() template check*(session: Session, element: string, function: untyped) = @@ -69,8 +69,22 @@ proc waitForLoad*(session: Session, timeout=20000) = proc wait*(session: Session, msTimeout: int = 5000) = session.waitForLoad(msTimeout) -proc logout*(session: Session) = +proc setUserRank*(session: Session, user, rank, baseUrl: string) = with session: + navigate(baseUrl & "profile/" & user) + wait() + + click "#settings-tab" + + click "#rank-field" + click("#rank-field-" & rank.toLowerAscii) + + click "#save-btn" + wait() + +proc logout*(session: Session, baseUrl: string) = + with session: + navigate baseUrl wait() click "#profile-btn" click "#profile-btn #logout-btn" @@ -79,8 +93,10 @@ proc logout*(session: Session) = # Verify we have logged out by looking for the log in button. ensureExists "#login-btn" -proc login*(session: Session, user, password: string) = +proc login*(session: Session, baseUrl, user, password: string) = with session: + navigate baseUrl + wait() click "#login-btn" sendKeys "#login-form input[name='username']", user @@ -138,3 +154,9 @@ proc changeRank*(session: Session, rank: string) = # TODO: Getting an "element click intercepted" error here. click "#save-btn" + +proc banUser*(session: Session, baseUrl: string) = + with session: + login baseUrl, "admin", "admin" + setUserRank "user", "banned", baseUrl + logout baseUrl diff --git a/tests/browsertests/scenario1.nim b/tests/browsertests/scenario1.nim index 5dbc8b6..4a73676 100644 --- a/tests/browsertests/scenario1.nim +++ b/tests/browsertests/scenario1.nim @@ -31,6 +31,4 @@ proc test*(session: Session, baseUrl: string) = with session: register("test", "test") - session.navigate(baseUrl) - session.wait() - logout(session) + session.logout(baseUrl) diff --git a/tests/browsertests/threads.nim b/tests/browsertests/threads.nim index 74729a3..a844720 100644 --- a/tests/browsertests/threads.nim +++ b/tests/browsertests/threads.nim @@ -2,18 +2,17 @@ import unittest, options, os, common import webdriver -proc test*(session: Session, baseUrl: string) = - let - userTitleStr = "This is a user thread!" - userContentStr = "A user has filled this out" +let + userTitleStr = "This is a user thread!" + userContentStr = "A user has filled this out" - adminTitleStr = "This is a thread title!" - adminContentStr = "This is content" + adminTitleStr = "This is a thread title!" + adminContentStr = "This is content" + +proc userTests(session: Session, baseUrl: string) = suite "user thread tests": - session.navigate(baseUrl) - session.wait() - login(session, "user", "user") + session.login(baseUrl, "user", "user") setup: session.navigate(baseUrl) @@ -33,19 +32,39 @@ proc test*(session: Session, baseUrl: string) = checkText "#thread-title", userTitleStr checkText ".original-post div.post-content", userContentStr - session.navigate(baseUrl) - session.wait() - logout(session) + session.logout(baseUrl) +proc bannedTests(session: Session, baseUrl: string) = + suite "banned user thread tests": + session.login(baseUrl, "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" + + session.logout(baseUrl) + +proc adminTests(session: Session, baseUrl: string) = suite "admin thread tests": - session.navigate(baseUrl) - session.wait() - login(session, "admin", "admin") + session.login(baseUrl, "admin", "admin") setup: session.navigate(baseUrl) session.wait() + test "can view banned thread": + with session: + ensureExists userTitleStr, LinkTextSelector + test "can create thread": with session: click "#new-thread-btn" @@ -116,6 +135,12 @@ proc test*(session: Session, baseUrl: string) = # Make sure the forum post is gone checkIsNone adminTitleStr, LinkTextSelector - session.navigate(baseUrl) - session.wait() - logout(session) + session.logout(baseUrl) + +proc test*(session: Session, baseUrl: string) = + userTests(session, baseUrl) + + banUser(session, baseUrl) + + bannedTests(session, baseUrl) + adminTests(session, baseUrl) From 465ba1e02498e1c3542419da10314091f1ffd64e Mon Sep 17 00:00:00 2001 From: Joey Yakimowich-Payne Date: Sun, 22 Jul 2018 20:10:41 +0900 Subject: [PATCH 026/144] Fix tests and merge --- tests/browsertester.nim | 3 +-- tests/browsertests/common.nim | 23 ++--------------------- tests/browsertests/issue181.nim | 28 ++++++++++------------------ tests/browsertests/scenario1.nim | 4 ++-- tests/browsertests/threads.nim | 21 +++++++++++++++++++++ 5 files changed, 36 insertions(+), 43 deletions(-) diff --git a/tests/browsertester.nim b/tests/browsertester.nim index 15a3f68..0f4efe9 100644 --- a/tests/browsertester.nim +++ b/tests/browsertester.nim @@ -64,8 +64,7 @@ when isMainModule: withBackend: scenario1.test(session, baseUrl) threads.test(session, baseUrl) - # TODO: Fix the issue181 test. - # issue181.test(session, baseUrl) + issue181.test(session, baseUrl) session.close() except: diff --git a/tests/browsertests/common.nim b/tests/browsertests/common.nim index e8948fa..96649ca 100644 --- a/tests/browsertests/common.nim +++ b/tests/browsertests/common.nim @@ -69,7 +69,7 @@ proc waitForLoad*(session: Session, timeout=20000) = proc wait*(session: Session, msTimeout: int = 5000) = session.waitForLoad(msTimeout) -proc setUserRank*(session: Session, user, rank, baseUrl: string) = +proc setUserRank*(session: Session, baseUrl, user, rank: string) = with session: navigate(baseUrl & "profile/" & user) wait() @@ -77,7 +77,7 @@ proc setUserRank*(session: Session, user, rank, baseUrl: string) = click "#settings-tab" click "#rank-field" - click("#rank-field-" & rank.toLowerAscii) + click("#rank-field option#rank-" & rank.toLowerAscii) click "#save-btn" wait() @@ -141,22 +141,3 @@ proc createThread*(session: Session, title, content: string) = checkText "#thread-title", title checkText ".original-post div.post-content", content - -proc changeRank*(session: Session, rank: string) = - with session: - # Make sure the "Settings" tab is selected. - click ".profile-tabs li:nth-child(2)" - - click "#rank-field" - click "#rank-field option#rank-" & rank.toLowerAscii() - - wait() - - # TODO: Getting an "element click intercepted" error here. - click "#save-btn" - -proc banUser*(session: Session, baseUrl: string) = - with session: - login baseUrl, "admin", "admin" - setUserRank "user", "banned", baseUrl - logout baseUrl diff --git a/tests/browsertests/issue181.nim b/tests/browsertests/issue181.nim index 3e28737..c7fe98c 100644 --- a/tests/browsertests/issue181.nim +++ b/tests/browsertests/issue181.nim @@ -10,37 +10,29 @@ proc test*(session: Session, baseUrl: string) = test "can see banned posts": with session: register("issue181", "issue181") - logout() + logout(baseUrl) # Change rank to `user` so they can post. - login("admin", "admin") + login(baseUrl, "admin", "admin") + setUserRank(baseUrl, "issue181", "user") + logout(baseUrl) - navigate(baseUrl & "profile/user") - wait() - changeRank("user") - logout() - - login("issue181", "issue181") + login(baseUrl, "issue181", "issue181") const title = "Testing issue 181." createThread(title, "Test for issue #181") - logout() - wait() + logout(baseUrl) - login("admin", "admin") + login(baseUrl, "admin", "admin") # Ban our user. - navigate(baseUrl & "profile/issue181") - changeRank("banned") + setUserRank(baseUrl, "issue181", "banned") # Make sure the banned user's thread is still visible. navigate(baseUrl) + wait() ensureExists("tr.banned") checkText("tr.banned .thread-title > a", title) - logout() + logout(baseUrl) checkText("tr.banned .thread-title > a", title) - - session.navigate(baseUrl) - session.wait() - logout(session) diff --git a/tests/browsertests/scenario1.nim b/tests/browsertests/scenario1.nim index 4a73676..d0d8aee 100644 --- a/tests/browsertests/scenario1.nim +++ b/tests/browsertests/scenario1.nim @@ -20,10 +20,10 @@ proc test*(session: Session, baseUrl: string) = # Logging in test "can login/logout": with session: - login("admin", "admin") + login(baseUrl, "admin", "admin") # Check whether we can log out. - logout() + logout(baseUrl) # Verify we have logged out by looking for the log in button. ensureExists "#login-btn" diff --git a/tests/browsertests/threads.nim b/tests/browsertests/threads.nim index a844720..fe6b4e0 100644 --- a/tests/browsertests/threads.nim +++ b/tests/browsertests/threads.nim @@ -9,6 +9,11 @@ let adminTitleStr = "This is a thread title!" adminContentStr = "This is content" +proc banUser(session: Session, baseUrl: string) = + with session: + login baseUrl, "admin", "admin" + setUserRank baseUrl, "user", "banned" + logout baseUrl proc userTests(session: Session, baseUrl: string) = suite "user thread tests": @@ -34,6 +39,21 @@ proc userTests(session: Session, baseUrl: string) = session.logout(baseUrl) +proc anonymousTests(session: Session, baseUrl: string) = + + suite "anonymous user tests": + with session: + navigate baseUrl + wait() + + test "can view banned thread": + with session: + ensureExists userTitleStr, LinkTextSelector + + with session: + navigate baseUrl + wait() + proc bannedTests(session: Session, baseUrl: string) = suite "banned user thread tests": session.login(baseUrl, "banned", "banned") @@ -143,4 +163,5 @@ proc test*(session: Session, baseUrl: string) = banUser(session, baseUrl) bannedTests(session, baseUrl) + anonymousTests(session, baseUrl) adminTests(session, baseUrl) From 7925c4b8b128c3e11803a8b57fe53eb73b49af16 Mon Sep 17 00:00:00 2001 From: Joey Yakimowich-Payne Date: Mon, 23 Jul 2018 10:13:06 +0900 Subject: [PATCH 027/144] Remove navigate from login/logout and add clear command --- tests/browsertests/common.nim | 20 ++++++++++++++------ tests/browsertests/issue181.nim | 16 +++++++++------- tests/browsertests/scenario1.nim | 6 +++--- tests/browsertests/threads.nim | 25 +++++++++++++++++-------- 4 files changed, 43 insertions(+), 24 deletions(-) diff --git a/tests/browsertests/common.nim b/tests/browsertests/common.nim index 96649ca..2c60840 100644 --- a/tests/browsertests/common.nim +++ b/tests/browsertests/common.nim @@ -22,6 +22,11 @@ template sendKeys*(session: Session, element, keys: string) = check el.isSome() el.get().sendKeys(keys) +template clear*(session: Session, element: string) = + let el = session.findElement(element) + check el.isSome() + el.get().clear() + template sendKeys*(session: Session, element: string, keys: varargs[Key]) = let el = session.findElement(element) check el.isSome() @@ -82,10 +87,8 @@ proc setUserRank*(session: Session, baseUrl, user, rank: string) = click "#save-btn" wait() -proc logout*(session: Session, baseUrl: string) = +proc logout*(session: Session) = with session: - navigate baseUrl - wait() click "#profile-btn" click "#profile-btn #logout-btn" wait() @@ -93,12 +96,13 @@ proc logout*(session: Session, baseUrl: string) = # Verify we have logged out by looking for the log in button. ensureExists "#login-btn" -proc login*(session: Session, baseUrl, user, password: string) = +proc login*(session: Session, user, password: string) = with session: - navigate baseUrl - wait() click "#login-btn" + clear "#login-form input[name='username']" + clear "#login-form input[name='password']" + sendKeys "#login-form input[name='username']", user sendKeys "#login-form input[name='password']", password @@ -115,6 +119,10 @@ proc register*(session: Session, user, password: string) = with session: click "#signup-btn" + clear "#signup-form input[name='email']" + clear "#signup-form input[name='username']" + clear "#signup-form input[name='password']" + sendKeys "#signup-form input[name='email']", user & "@" & user & ".com" sendKeys "#signup-form input[name='username']", user sendKeys "#signup-form input[name='password']", password diff --git a/tests/browsertests/issue181.nim b/tests/browsertests/issue181.nim index c7fe98c..7a646c2 100644 --- a/tests/browsertests/issue181.nim +++ b/tests/browsertests/issue181.nim @@ -10,21 +10,23 @@ proc test*(session: Session, baseUrl: string) = test "can see banned posts": with session: register("issue181", "issue181") - logout(baseUrl) + logout() # Change rank to `user` so they can post. - login(baseUrl, "admin", "admin") + login("admin", "admin") setUserRank(baseUrl, "issue181", "user") - logout(baseUrl) + logout() - login(baseUrl, "issue181", "issue181") + login("issue181", "issue181") + navigate(baseUrl) + wait() const title = "Testing issue 181." createThread(title, "Test for issue #181") - logout(baseUrl) + logout() - login(baseUrl, "admin", "admin") + login("admin", "admin") # Ban our user. setUserRank(baseUrl, "issue181", "banned") @@ -34,5 +36,5 @@ proc test*(session: Session, baseUrl: string) = wait() ensureExists("tr.banned") checkText("tr.banned .thread-title > a", title) - logout(baseUrl) + logout() checkText("tr.banned .thread-title > a", title) diff --git a/tests/browsertests/scenario1.nim b/tests/browsertests/scenario1.nim index d0d8aee..34132e5 100644 --- a/tests/browsertests/scenario1.nim +++ b/tests/browsertests/scenario1.nim @@ -20,10 +20,10 @@ proc test*(session: Session, baseUrl: string) = # Logging in test "can login/logout": with session: - login(baseUrl, "admin", "admin") + login("admin", "admin") # Check whether we can log out. - logout(baseUrl) + logout() # Verify we have logged out by looking for the log in button. ensureExists "#login-btn" @@ -31,4 +31,4 @@ proc test*(session: Session, baseUrl: string) = with session: register("test", "test") - session.logout(baseUrl) + session.logout() diff --git a/tests/browsertests/threads.nim b/tests/browsertests/threads.nim index fe6b4e0..f0b1b32 100644 --- a/tests/browsertests/threads.nim +++ b/tests/browsertests/threads.nim @@ -11,13 +11,13 @@ let proc banUser(session: Session, baseUrl: string) = with session: - login baseUrl, "admin", "admin" + login "admin", "admin" setUserRank baseUrl, "user", "banned" - logout baseUrl + logout() proc userTests(session: Session, baseUrl: string) = suite "user thread tests": - session.login(baseUrl, "user", "user") + session.login("user", "user") setup: session.navigate(baseUrl) @@ -37,7 +37,7 @@ proc userTests(session: Session, baseUrl: string) = checkText "#thread-title", userTitleStr checkText ".original-post div.post-content", userContentStr - session.logout(baseUrl) + session.logout() proc anonymousTests(session: Session, baseUrl: string) = @@ -56,7 +56,10 @@ proc anonymousTests(session: Session, baseUrl: string) = proc bannedTests(session: Session, baseUrl: string) = suite "banned user thread tests": - session.login(baseUrl, "banned", "banned") + with session: + navigate baseUrl + wait() + login "banned", "banned" test "can't start thread": with session: @@ -71,11 +74,11 @@ proc bannedTests(session: Session, baseUrl: string) = ensureExists "#new-thread p.text-error" - session.logout(baseUrl) + session.logout() proc adminTests(session: Session, baseUrl: string) = suite "admin thread tests": - session.login(baseUrl, "admin", "admin") + session.login("admin", "admin") setup: session.navigate(baseUrl) @@ -155,9 +158,12 @@ proc adminTests(session: Session, baseUrl: string) = # Make sure the forum post is gone checkIsNone adminTitleStr, LinkTextSelector - session.logout(baseUrl) + session.logout() proc test*(session: Session, baseUrl: string) = + session.navigate(baseUrl) + session.wait() + userTests(session, baseUrl) banUser(session, baseUrl) @@ -165,3 +171,6 @@ proc test*(session: Session, baseUrl: string) = bannedTests(session, baseUrl) anonymousTests(session, baseUrl) adminTests(session, baseUrl) + + session.navigate(baseUrl) + session.wait() From 85f31aaf6bf6663f69ca1477b7c8db3bc8bffc96 Mon Sep 17 00:00:00 2001 From: Joey Yakimowich-Payne Date: Mon, 23 Jul 2018 19:59:05 +0900 Subject: [PATCH 028/144] Update webdriver commit hash --- nimforum.nimble | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nimforum.nimble b/nimforum.nimble index d267497..1ea21bd 100644 --- a/nimforum.nimble +++ b/nimforum.nimble @@ -21,7 +21,7 @@ requires "sass#649e0701fa5c" requires "karax#d8df257dd" -requires "webdriver#a2be578" +requires "webdriver#20f3c1b" # Tasks From 7e424792284003e88d013407e1997f2b85866d8e Mon Sep 17 00:00:00 2001 From: "Mr.Chun" Date: Sun, 29 Jul 2018 23:04:36 +0800 Subject: [PATCH 029/144] make send mail from address configurable (#191) * feat(email): make send mail from address configurable * Exit sendMail early if there is no smtpFromAddr. * Small adjustment to setup_nimforum --- src/email.nim | 10 ++++++---- src/setup_nimforum.nim | 10 ++++++---- src/utils.nim | 2 ++ 3 files changed, 14 insertions(+), 8 deletions(-) diff --git a/src/email.nim b/src/email.nim index 60b4527..8cf7b36 100644 --- a/src/email.nim +++ b/src/email.nim @@ -30,7 +30,6 @@ proc rateCheck(mailer: Mailer, address: string): bool = proc sendMail( mailer: Mailer, subject, message, recipient: string, - fromAddr = "forum@nim-lang.org", otherHeaders:seq[(string, string)] = @[] ) {.async.} = # Ensure we aren't emailing this address too much. @@ -41,6 +40,9 @@ proc sendMail( if mailer.config.smtpAddress.len == 0: warn("Cannot send mail: no smtp server configured (smtpAddress).") return + if mailer.config.smtpFromAddr.len == 0: + warn("Cannot send mail: no smtp from address configured (smtpFromAddr).") + return var client = newAsyncSmtp() await client.connect(mailer.config.smtpAddress, Port(mailer.config.smtpPort)) @@ -50,12 +52,12 @@ proc sendMail( let toList = @[recipient] var headers = otherHeaders - headers.add(("From", fromAddr)) + headers.add(("From", mailer.config.smtpFromAddr)) let encoded = createMessage(subject, message, toList, @[], headers) - await client.sendMail(fromAddr, toList, $encoded) + await client.sendMail(mailer.config.smtpFromAddr, toList, $encoded) proc sendPassReset(mailer: Mailer, email, user, resetUrl: string) {.async.} = let message = """Hello $1, @@ -133,4 +135,4 @@ proc sendSecureEmail*( if emailSentFut.error of ForumError: raise emailSentFut.error else: - raise newForumError("Couldn't send email", @["email"]) \ No newline at end of file + raise newForumError("Couldn't send email", @["email"]) diff --git a/src/setup_nimforum.nim b/src/setup_nimforum.nim index 3c5cf5b..5647e48 100644 --- a/src/setup_nimforum.nim +++ b/src/setup_nimforum.nim @@ -226,7 +226,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: string], + smtp: tuple[address, user, password, fromAddr: string], isDev: bool, dbPath: string, ga: string="" @@ -242,6 +242,7 @@ proc initialiseConfig( "smtpAddress": %smtp.address, "smtpUser": %smtp.user, "smtpPassword": %smtp.password, + "smtpFromAddr": %smtp.fromAddr, "isDev": %isDev, "dbPath": %dbPath } @@ -284,6 +285,7 @@ These can be changed later in the generated forum.json file. let smtpAddress = question("SMTP address (eg: mail.hostname.com): ") let smtpUser = question("SMTP user: ") let smtpPassword = readPasswordFromStdin("SMTP pass: ") + let smtpFromAddr = question("SMTP sending email address (eg: mail@mail.hostname.com): ") echo("The following is optional. You can specify your Google Analytics ID " & "if you wish. Otherwise just leave it blank.") @@ -293,7 +295,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), isDev=false, + (smtpAddress, smtpUser, smtpPassword, smtpFromAddr), isDev=false, dbPath, ga ) @@ -328,7 +330,7 @@ when isMainModule: "Development Forum", "localhost", recaptcha=("", ""), - smtp=("", "", ""), + smtp=("", "", "", ""), isDev=true, dbPath ) @@ -345,7 +347,7 @@ when isMainModule: "Test Forum", "localhost", recaptcha=("", ""), - smtp=("", "", ""), + smtp=("", "", "", ""), isDev=true, dbPath ) diff --git a/src/utils.nim b/src/utils.nim index 82b1e51..2676ab8 100644 --- a/src/utils.nim +++ b/src/utils.nim @@ -21,6 +21,7 @@ type smtpPort*: int smtpUser*: string smtpPassword*: string + smtpFromAddr*: string mlistAddress*: string recaptchaSecretKey*: string recaptchaSiteKey*: string @@ -58,6 +59,7 @@ proc loadConfig*(filename = getCurrentDir() / "forum.json"): Config = result.smtpPort = root{"smtpPort"}.getNum(25).int result.smtpUser = root{"smtpUser"}.getStr("") result.smtpPassword = root{"smtpPassword"}.getStr("") + result.smtpFromAddr = root{"smtpFromAddr"}.getStr("") result.mlistAddress = root{"mlistAddress"}.getStr("") result.recaptchaSecretKey = root{"recaptchaSecretKey"}.getStr("") result.recaptchaSiteKey = root{"recaptchaSiteKey"}.getStr("") From 7cda14e9feacad0ce7b577172618a8c05917b573 Mon Sep 17 00:00:00 2001 From: Joey Yakimowich-Payne Date: Mon, 6 Aug 2018 16:22:32 +0900 Subject: [PATCH 030/144] Add frontend category picker and enable categories in threadlist --- public/css/nimforum.scss | 12 +++-- src/frontend/category.nim | 11 +++-- src/frontend/categorypicker.nim | 88 +++++++++++++++++++++++++++++++++ src/frontend/newthread.nim | 12 +++-- 4 files changed, 113 insertions(+), 10 deletions(-) create mode 100644 src/frontend/categorypicker.nim diff --git a/public/css/nimforum.scss b/public/css/nimforum.scss index e970e8e..2aa3dc6 100644 --- a/public/css/nimforum.scss +++ b/public/css/nimforum.scss @@ -206,6 +206,14 @@ $threads-meta-color: #545d70; display: inline-block; } +.square { + width: 0; + height: 0; + border: 0.3rem solid #98c766; + display: inline-block; + margin-right: 5px; +} + .load-more-separator { text-align: center; color: darken($label-color, 35%); @@ -711,10 +719,8 @@ hr { #threads-list.table { tr > th:nth-child(2), tr > td:nth-child(2) { - display: none; } } .category, div.flag-button { - display: none; -} \ No newline at end of file +} diff --git a/src/frontend/category.nim b/src/frontend/category.nim index 6c20f94..41efd95 100644 --- a/src/frontend/category.nim +++ b/src/frontend/category.nim @@ -6,6 +6,8 @@ type description*: string color*: string + CategoryList* = ref object + categories*: seq[Category] when defined(js): include karax/prelude @@ -17,12 +19,13 @@ when defined(js): result = buildHtml(): if category.name.len >= 0: tdiv(class="category", + title=category.description, "data-color"="#" & category.color): - tdiv(class="triangle", + tdiv(class="square", style=style( - (StyleAttr.borderBottom, - kstring"0.6rem solid #" & category.color) + (StyleAttr.border, + kstring"0.3rem solid #" & category.color) )) text category.name else: - span() \ No newline at end of file + span() diff --git a/src/frontend/categorypicker.nim b/src/frontend/categorypicker.nim new file mode 100644 index 0000000..d2414e6 --- /dev/null +++ b/src/frontend/categorypicker.nim @@ -0,0 +1,88 @@ +when defined(js): + import sugar, httpcore, options, json + import dom except Event + + include karax/prelude + import karax / [kajax, kdom, vstyles, vdom] + + import error, replybox, threadlist, post, category + import category, karaxutils + + type + CategoryPicker* = ref object of VComponent + list: Option[CategoryList] + selectedCategoryID*: int + loading: bool + status: HttpCode + + proc getSelectedCategory*(state: CategoryPicker): Option[Category] = + if state.list.isSome: + return some(state.list.get().categories[state.selectedCategoryID]) + return none[Category]() + + proc onCategoryList(state: CategoryPicker): proc (httpStatus: int, response: kstring) = + return proc (httpStatus: int, response: kstring) = + state.loading = false + state.status = httpStatus.HttpCode + if state.status != Http200: return + + let parsed = parseJson($response) + let list = parsed.to(CategoryList) + + if state.list.isSome: + state.list.get().categories.add(list.categories) + else: + state.list = some(list) + + if state.selectedCategoryID > state.list.get().categories.len(): + state.selectedCategoryID = 0 + + proc loadCategories(state: CategoryPicker) = + if not state.loading: + state.loading = true + ajaxGet(makeUri("categories.json"), @[], onCategoryList(state)) + + proc newCategoryPicker*(): CategoryPicker = + result = CategoryPicker( + list: none[CategoryList](), + selectedCategoryID: 0, + loading: false, + status: Http200 + ) + + proc onCategoryClick(state: CategoryPicker, category: Category): proc (ev: Event, n: VNode) = + # this is necessary to capture the right value + let cat = category + return proc (ev: Event, n: VNode) = + state.selectedCategoryID = cat.id + state.markDirty() + + proc render*(state: CategoryPicker): VNode = + if state.status != Http200: + return renderError("Couldn't retrieve categories.", state.status) + + if state.list.isNone: + state.loadCategories() + return buildHtml(tdiv(class="loading loading-lg")) + + let list = state.list.get().categories + let selectedCategory = list[state.selectedCategoryID] + + result = buildHtml(): + tdiv(id="category-selection", class="input-group"): + label(class="d-inline-block form-label"): + text "Category" + tdiv(class="dropdown"): + a(class="btn btn-link dropdown-toggle", tabindex="0"): + tdiv(class="d-inline-block"): + render(selectedCategory) + text " " + italic(class="fas fa-caret-down") + ul(class="menu"): + for category in list: + li(class="menu-item"): + a(onClick=onCategoryClick(state, category)): + render(category) + + + \ No newline at end of file diff --git a/src/frontend/newthread.nim b/src/frontend/newthread.nim index e75b6bb..9c6a79b 100644 --- a/src/frontend/newthread.nim +++ b/src/frontend/newthread.nim @@ -5,8 +5,8 @@ when defined(js): include karax/prelude import karax / [kajax, kdom] - import error, replybox, threadlist, post - import karaxutils + import error, replybox, threadlist, post, category + import karaxutils, categorypicker type NewThread* = ref object @@ -14,11 +14,13 @@ when defined(js): error: Option[PostError] replyBox: ReplyBox subject: kstring + categoryPicker: CategoryPicker proc newNewThread*(): NewThread = NewThread( replyBox: newReplyBox(nil), - subject: "" + subject: "", + categoryPicker: newCategoryPicker() ) proc onSubjectChange(e: Event, n: VNode, state: NewThread) = @@ -37,8 +39,11 @@ when defined(js): let uri = makeUri("newthread") # TODO: This is a hack, karax should support this. let formData = newFormData() + let categoryID = state.categoryPicker.selectedCategoryID + formData.append("subject", state.subject) formData.append("msg", state.replyBox.getText()) + formData.append("categoryId", $categoryID) ajaxPost(uri, @[], cast[cstring](formData), (s: int, r: kstring) => onCreatePost(s, r, state)) @@ -55,6 +60,7 @@ when defined(js): if state.error.isSome(): p(class="text-error"): text state.error.get().message + render(state.categoryPicker) renderContent(state.replyBox, none[Thread](), none[Post]()) tdiv(class="footer"): From f0bcb9abfd7e0a0fe5eafed53bbe86e04a9f0515 Mon Sep 17 00:00:00 2001 From: Joey Yakimowich-Payne Date: Mon, 6 Aug 2018 16:32:20 +0900 Subject: [PATCH 031/144] Add backend for categories --- src/forum.nim | 27 ++++++++++++++++++++++----- 1 file changed, 22 insertions(+), 5 deletions(-) diff --git a/src/forum.nim b/src/forum.nim index 399a64e..e2f7804 100644 --- a/src/forum.nim +++ b/src/forum.nim @@ -520,10 +520,10 @@ proc updatePost(c: TForumData, postId: int, content: string, if row[0] == $postId: exec(db, crud(crUpdate, "thread", "name"), subject.get(), threadId) -proc executeNewThread(c: TForumData, subject, msg: string): (int64, int64) = +proc executeNewThread(c: TForumData, subject, msg, categoryID: string): (int64, int64) = const query = sql""" - insert into thread(name, views, modified) values (?, 0, DATETIME('now')) + insert into thread(name, views, modified, category) values (?, 0, DATETIME('now'), ?) """ assert c.loggedIn() @@ -543,13 +543,17 @@ proc executeNewThread(c: TForumData, subject, msg: string): (int64, int64) = if msg.len == 0: raise newForumError("Message is empty", @["msg"]) + let catID = getInt(categoryID, -1) + if catID == -1: + raise newForumError("CategoryID is invalid", @["categoryId"]) + if not validateRst(c, msg): raise newForumError("Message needs to be valid RST", @["msg"]) if rateLimitCheck(c): raise newForumError("You're posting too fast!") - result[0] = tryInsertID(db, query, subject).int + result[0] = tryInsertID(db, query, subject, categoryID).int if result[0] < 0: raise newForumError("Subject already exists", @["subject"]) @@ -756,6 +760,18 @@ settings: routes: + get "/categories.json": + const categoriesQuery = sql"""select * from category;""" + + 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] + ) + list.categories.add(category) + + resp $(%list), "application/json" + get "/threads.json": var start = getInt(@"start", 0) @@ -1147,13 +1163,14 @@ routes: let formData = request.formData cond "msg" in formData cond "subject" in formData + cond "categoryId" in formData let msg = formData["msg"].body let subject = formData["subject"].body - # TODO: category + let categoryID = formData["categoryId"].body try: - let res = executeNewThread(c, subject, msg) + let res = executeNewThread(c, subject, msg, categoryID) resp Http200, $(%[res[0], res[1]]), "application/json" except ForumError as exc: resp Http400, $(%exc.data), "application/json" From 5ed17333f9fa4e21e5c813b189d5144a395334a4 Mon Sep 17 00:00:00 2001 From: Joey Yakimowich-Payne Date: Mon, 6 Aug 2018 16:46:04 +0900 Subject: [PATCH 032/144] Add category id to picker --- src/frontend/categorypicker.nim | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/frontend/categorypicker.nim b/src/frontend/categorypicker.nim index d2414e6..97a2e02 100644 --- a/src/frontend/categorypicker.nim +++ b/src/frontend/categorypicker.nim @@ -81,7 +81,8 @@ when defined(js): ul(class="menu"): for category in list: li(class="menu-item"): - a(onClick=onCategoryClick(state, category)): + a(class="category-" & $category.id, + onClick=onCategoryClick(state, category)): render(category) From 3df30386d945c7e259a1ee37dab94833560cc48a Mon Sep 17 00:00:00 2001 From: Joey Yakimowich-Payne Date: Mon, 6 Aug 2018 16:54:16 +0900 Subject: [PATCH 033/144] Fix misc compiler warnings --- src/frontend/error.nim | 2 +- src/frontend/forum.nim | 2 +- src/frontend/replybox.nim | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/frontend/error.nim b/src/frontend/error.nim index 4a23c44..54d495b 100644 --- a/src/frontend/error.nim +++ b/src/frontend/error.nim @@ -86,7 +86,7 @@ when defined(js): state.error = some(error) except: - kout(getCurrentExceptionMsg().cstring) + echo getCurrentExceptionMsg() state.error = some(PostError( errorFields: @[], message: "Unknown error occurred." diff --git a/src/frontend/forum.nim b/src/frontend/forum.nim index 4dea047..5fb11b4 100644 --- a/src/frontend/forum.nim +++ b/src/frontend/forum.nim @@ -49,7 +49,7 @@ proc onPopState(event: dom.Event) = # This event is usually only called when the user moves back in their # history. I fire it in karaxutils.anchorCB as well to ensure the URL is # always updated. This should be moved into Karax in the future. - kout(kstring"New URL: ", window.location.href, " ", state.url.href) + echo "New URL: ", window.location.href, " ", state.url.href document.title = state.originalTitle if state.url.href != window.location.href: state = newState() # Reload the state to remove stale data. diff --git a/src/frontend/replybox.nim b/src/frontend/replybox.nim index 9d815f6..c7e61cc 100644 --- a/src/frontend/replybox.nim +++ b/src/frontend/replybox.nim @@ -44,7 +44,7 @@ when defined(js): proc onPreviewPost(httpStatus: int, response: kstring, state: ReplyBox) = postFinished: - kout(response) + echo response state.rendering = some[kstring](response) proc onPreviewClick(e: Event, n: VNode, state: ReplyBox) = From 495ddf5d93478b0a62d707a65ed3907e090570c4 Mon Sep 17 00:00:00 2001 From: Joey Yakimowich-Payne Date: Mon, 6 Aug 2018 16:54:55 +0900 Subject: [PATCH 034/144] Add test data for categories --- src/setup_nimforum.nim | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/src/setup_nimforum.nim b/src/setup_nimforum.nim index 5647e48..b235415 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', '', ''); + values (0, 'Default', 'The default category', ''); """) # -- Thread @@ -115,7 +115,7 @@ proc initialiseDb(admin: tuple[username, password, email: string], # Create default user. db.createUser(admin, Admin) - # Create test users if test or development + # Create some test data for development if isTest or isDev: for rank in Spammer..Moderator: let rankLower = toLowerAscii($rank) @@ -124,6 +124,14 @@ proc initialiseDb(admin: tuple[username, password, email: string], email: $rankLower & "@localhost.local") db.createUser(user, rank) + db.exec(sql""" + insert into category (name, description, color) + values ('Libraries', 'Libraries and library development', '0198E1'), + ('Announcements', 'Announcements by Nim core devs', 'FFEB3B'), + ('Fun', 'Posts that are just for fun', '00897B'), + ('Potential Issues', 'Potential Nim compiler issues', 'E53935'); + """) + # -- Post db.exec(sql""" From d35f1e90cfe1cdbafd10b74ce26e75cd1dd28491 Mon Sep 17 00:00:00 2001 From: Joey Yakimowich-Payne Date: Mon, 6 Aug 2018 17:21:18 +0900 Subject: [PATCH 035/144] Add name slug for categories and remove unused function --- src/frontend/categorypicker.nim | 15 +++++---------- 1 file changed, 5 insertions(+), 10 deletions(-) diff --git a/src/frontend/categorypicker.nim b/src/frontend/categorypicker.nim index 97a2e02..03abbd3 100644 --- a/src/frontend/categorypicker.nim +++ b/src/frontend/categorypicker.nim @@ -1,5 +1,5 @@ when defined(js): - import sugar, httpcore, options, json + import sugar, httpcore, options, json, strutils import dom except Event include karax/prelude @@ -15,10 +15,8 @@ when defined(js): loading: bool status: HttpCode - proc getSelectedCategory*(state: CategoryPicker): Option[Category] = - if state.list.isSome: - return some(state.list.get().categories[state.selectedCategoryID]) - return none[Category]() + proc slug(name: string): string = + name.strip().replace(" ", "-").toLowerAscii proc onCategoryList(state: CategoryPicker): proc (httpStatus: int, response: kstring) = return proc (httpStatus: int, response: kstring) = @@ -81,9 +79,6 @@ when defined(js): ul(class="menu"): for category in list: li(class="menu-item"): - a(class="category-" & $category.id, + a(class="category-" & $category.id & " " & category.name.slug, onClick=onCategoryClick(state, category)): - render(category) - - - \ No newline at end of file + render(category) \ No newline at end of file From 2e42ede2adaee4cb46d7bb6cf5768080f9cf4909 Mon Sep 17 00:00:00 2001 From: Joey Yakimowich-Payne Date: Mon, 6 Aug 2018 17:30:07 +0900 Subject: [PATCH 036/144] Add basic categories test --- tests/browsertester.nim | 3 +- tests/browsertests/categories.nim | 52 +++++++++++++++++++++++++++++++ 2 files changed, 54 insertions(+), 1 deletion(-) create mode 100644 tests/browsertests/categories.nim diff --git a/tests/browsertester.nim b/tests/browsertester.nim index 0f4efe9..82bb5cb 100644 --- a/tests/browsertester.nim +++ b/tests/browsertester.nim @@ -43,7 +43,7 @@ template withBackend(body: untyped): untyped = body -import browsertests/[scenario1, threads, issue181] +import browsertests/[scenario1, threads, issue181, categories] when isMainModule: spawn runProcess("geckodriver -p 4444 --log config") @@ -64,6 +64,7 @@ when isMainModule: withBackend: scenario1.test(session, baseUrl) threads.test(session, baseUrl) + categories.test(session, baseUrl) issue181.test(session, baseUrl) session.close() diff --git a/tests/browsertests/categories.nim b/tests/browsertests/categories.nim new file mode 100644 index 0000000..6f91f9d --- /dev/null +++ b/tests/browsertests/categories.nim @@ -0,0 +1,52 @@ +import unittest, options, os, common + +import webdriver + +proc selectCategory(session: Session, name: string) = + with session: + click "#category-selection .dropdown-toggle" + + click "#category-selection ." & name + + +proc categoriesTests(session: Session, baseUrl: string) = + let + title = "Category Test" + content = "Choosing category test" + + with session: + navigate baseUrl + wait() + login "user", "user" + + test "can create category thread": + with session: + click "#new-thread-btn" + wait() + + sendKeys "#thread-title", title + + selectCategory "fun" + + sendKeys "#reply-textarea", content + + click "#create-thread-btn" + wait() + + checkText "#thread-title .category", "Fun" + + navigate baseUrl + wait() + + ensureExists title, LinkTextSelector + + session.logout() + +proc test*(session: Session, baseUrl: string) = + session.navigate(baseUrl) + session.wait() + + categoriesTests(session, baseUrl) + + session.navigate(baseUrl) + session.wait() \ No newline at end of file From b0639c4da2de5e9dc1a8a4a5972edc9e3f3ec33f Mon Sep 17 00:00:00 2001 From: Joey Yakimowich-Payne Date: Tue, 7 Aug 2018 20:25:35 +0900 Subject: [PATCH 037/144] Add backend for updating thread --- src/forum.nim | 35 +++++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/src/forum.nim b/src/forum.nim index e2f7804..3cd6bdc 100644 --- a/src/forum.nim +++ b/src/forum.nim @@ -1151,6 +1151,41 @@ routes: except ForumError as exc: resp Http400, $(%exc.data), "application/json" + post "/updateThread": + createTFD() + if not c.loggedIn(): + let err = PostError( + errorFields: @[], + message: "Not logged in." + ) + resp Http401, $(%err), "application/json" + + let formData = request.formData + + cond "threadId" in formData + + let threadId = formData["threadId"].body + + let keys = ["name", "views", "modified", "category", "isLocked", "solution", "isDeleted"] + + # optional parameters + var + queryValues: seq[string] = @[] + queryKeys: seq[string] = @[] + + for key in keys: + if key in formData: + queryKeys.add(key) + queryValues.add(formData[key].body) + + if queryKeys.len() > 0: + queryValues.add(threadId) + try: + exec(db, crud(crUpdate, "thread", queryKeys), queryValues) + resp Http200, "{}", "application/json" + except ForumError as exc: + resp Http400, $(%exc.data), "application/json" + post "/newthread": createTFD() if not c.loggedIn(): From cb7418f8251cf723fa4bb915aa8787eeb607abe1 Mon Sep 17 00:00:00 2001 From: Joey Yakimowich-Payne Date: Tue, 7 Aug 2018 20:26:48 +0900 Subject: [PATCH 038/144] Add frontend for category picker when admin or thread author --- src/frontend/categorypicker.nim | 21 ++++++++++++--- src/frontend/postlist.nim | 46 +++++++++++++++++++++++++++++++-- 2 files changed, 61 insertions(+), 6 deletions(-) diff --git a/src/frontend/categorypicker.nim b/src/frontend/categorypicker.nim index 03abbd3..fd9063f 100644 --- a/src/frontend/categorypicker.nim +++ b/src/frontend/categorypicker.nim @@ -14,6 +14,7 @@ when defined(js): selectedCategoryID*: int loading: bool status: HttpCode + onCategoryChange: proc (oldCategory: Category, newCategory: Category) proc slug(name: string): string = name.strip().replace(" ", "-").toLowerAscii @@ -40,20 +41,32 @@ when defined(js): state.loading = true ajaxGet(makeUri("categories.json"), @[], onCategoryList(state)) - proc newCategoryPicker*(): CategoryPicker = + proc `[]`*(state: CategoryPicker, id: int): Category = + state.list.get().categories[id] + + proc newCategoryPicker*( + onCategoryChange: proc (oldCategory: Category, newCategory: Category) = + proc (oldCategory: Category, newCategory: Category) = discard + ): CategoryPicker = result = CategoryPicker( list: none[CategoryList](), selectedCategoryID: 0, loading: false, - status: Http200 + status: Http200, + onCategoryChange: onCategoryChange ) + 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 return proc (ev: Event, n: VNode) = - state.selectedCategoryID = cat.id - state.markDirty() + let oldCategory = state[state.selectedCategoryID] + state.select(cat.id) + state.onCategoryChange(oldCategory, cat) proc render*(state: CategoryPicker): VNode = if state.status != Http200: diff --git a/src/frontend/postlist.nim b/src/frontend/postlist.nim index 6a27df3..610e245 100644 --- a/src/frontend/postlist.nim +++ b/src/frontend/postlist.nim @@ -20,12 +20,14 @@ when defined(js): import karax / [vstyles, kajax, kdom] import karaxutils, error, replybox, editbox, postbutton, delete + import categorypicker type State = ref object list: Option[PostList] loading: bool status: HttpCode + error: Option[PostError] replyingTo: Option[Post] replyBox: ReplyBox editing: Option[Post] ## If in edit mode, this contains the post. @@ -33,8 +35,10 @@ when defined(js): likeButton: LikeButton deleteModal: DeleteModal lockButton: LockButton + categoryPicker: CategoryPicker proc onReplyPosted(id: int) + proc onCategoryChanged(oldCategory: Category, newCategory: Category) proc onEditPosted(id: int, content: string, subject: Option[string]) proc onEditCancelled() proc onDeletePost(post: Post) @@ -44,17 +48,37 @@ when defined(js): list: none[PostList](), loading: false, status: Http200, + error: none[PostError](), replyingTo: none[Post](), replyBox: newReplyBox(onReplyPosted), editBox: newEditBox(onEditPosted, onEditCancelled), likeButton: newLikeButton(), deleteModal: newDeleteModal(onDeletePost, onDeleteThread, nil), - lockButton: newLockButton() + lockButton: newLockButton(), + categoryPicker: newCategoryPicker(onCategoryChanged) ) var state = newState() + proc onCategoryPost(httpStatus: int, response: kstring, state: State) = + state.loading = false + postFinished: + discard + # TODO: show success message + + proc onCategoryChanged(oldCategory: Category, newCategory: Category) = + let uri = makeUri("/updateThread") + + let formData = newFormData() + formData.append("threadId", $state.list.get().thread.id) + formData.append("category", $newCategory.id) + + state.loading = true + + ajaxPost(uri, @[], cast[cstring](formData), + (s: int, r: kstring) => onCategoryPost(s, r, state)) + proc onPostList(httpStatus: int, response: kstring, postId: Option[int]) = state.loading = false state.status = httpStatus.HttpCode @@ -66,6 +90,7 @@ when defined(js): state.list = some(list) dom.document.title = list.thread.topic & " - " & dom.document.title + state.categoryPicker.select(list.thread.category.id) # The anchor should be jumped to once all the posts have been loaded. if postId.isSome(): @@ -179,6 +204,20 @@ when defined(js): span(class="more-post-count"): text "(" & $post.moreBefore.len & ")" + proc genCategories(thread: Thread, currentUser: Option[User]): VNode = + let loggedIn = currentUser.isSome() + let authoredByUser = + loggedIn and currentUser.get().name == thread.author.name + let currentAdmin = + currentUser.isSome() and currentUser.get().rank == Admin + + result = buildHtml(): + tdiv(): + if authoredByUser or currentAdmin: + render(state.categoryPicker) + else: + render(thread.category) + proc genPostButtons(post: Post, currentUser: Option[User]): Vnode = let loggedIn = currentUser.isSome() let authoredByUser = @@ -330,6 +369,9 @@ when defined(js): result = buildHtml(): section(class="container grid-xl"): tdiv(id="thread-title", class="title"): + if state.error.isSome(): + span(class="text-error"): + text state.error.get().message p(): text list.thread.topic if list.thread.isLocked: italic(class="fas fa-lock fa-xs", @@ -343,7 +385,7 @@ when defined(js): italic(class="fas fa-check-square fa-xs", title="Thread has a solution") text "Solved" - render(list.thread.category) + genCategories(list.thread, currentUser) tdiv(class="posts"): var prevPost: Option[Post] = none[Post]() for i, post in list.posts: From a05667ef78587119aa0205b81f6a293d0dc018e9 Mon Sep 17 00:00:00 2001 From: Joey Yakimowich-Payne Date: Tue, 7 Aug 2018 21:03:05 +0900 Subject: [PATCH 039/144] Add server side checking for user permissions --- src/forum.nim | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/src/forum.nim b/src/forum.nim index 3cd6bdc..fc6e81a 100644 --- a/src/forum.nim +++ b/src/forum.nim @@ -520,6 +520,16 @@ proc updatePost(c: TForumData, postId: int, content: string, if row[0] == $postId: exec(db, crud(crUpdate, "thread", "name"), subject.get(), threadId) +proc updateThread(c: TForumData, threadId: string, queryKeys: seq[string], queryValues: seq[string]) = + 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 + if not canEdit: + raise newForumError("You cannot edit this thread") + + exec(db, crud(crUpdate, "thread", queryKeys), queryValues) + proc executeNewThread(c: TForumData, subject, msg, categoryID: string): (int64, int64) = const query = sql""" @@ -1181,7 +1191,7 @@ routes: if queryKeys.len() > 0: queryValues.add(threadId) try: - exec(db, crud(crUpdate, "thread", queryKeys), queryValues) + updateThread(c, threadId, queryKeys, queryValues) resp Http200, "{}", "application/json" except ForumError as exc: resp Http400, $(%exc.data), "application/json" From 3b681e32f6de19298d424d01e473ffe5e0aaa874 Mon Sep 17 00:00:00 2001 From: Joey Yakimowich-Payne Date: Wed, 8 Aug 2018 18:22:41 +0900 Subject: [PATCH 040/144] Add backend for creating a category --- src/forum.nim | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/src/forum.nim b/src/forum.nim index fc6e81a..ac67c51 100644 --- a/src/forum.nim +++ b/src/forum.nim @@ -665,6 +665,12 @@ proc executeLike(c: TForumData, postId: int) = # Save the like. exec(db, crud(crCreate, "like", "author", "post"), c.userid, postId) +proc executeNewCategory(c: TForumData, name, color, description: string): int64 = + if name.len == 0: + raise newForumError("Category name must not be empty!", @["name"]) + + result = insertID(db, crud(crCreate, "category", "name", "color", "description"), name, color, description) + proc executeUnlike(c: TForumData, postId: int) = # Verify the post and like exists for the current user. const likeQuery = sql""" @@ -1051,6 +1057,21 @@ routes: except ForumError as exc: resp Http400, $(%exc.data), "application/json" + post "/createCategory": + createTFD() + let formData = request.formData + + let name = formData["name"].body + let color = formData["color"].body.replace("#", "") + let description = formData["description"].body + + try: + let id = executeNewCategory(c, name, color, description) + let category = Category(id: id.int, name: name, color: color, description: description) + resp Http200, $(%category), "application/json" + except ForumError as exc: + resp Http400, $(%exc.data), "application/json" + get "/status.json": createTFD() From f1c5db2cedda43d1f66430c80abeb3d667453759 Mon Sep 17 00:00:00 2001 From: Joey Yakimowich-Payne Date: Wed, 8 Aug 2018 18:25:37 +0900 Subject: [PATCH 041/144] Add frontend for adding a category with the category picker --- public/css/nimforum.scss | 28 +++++--- src/frontend/category.nim | 3 + src/frontend/categorypicker.nim | 111 ++++++++++++++++++++++++++++---- src/frontend/newthread.nim | 5 +- src/frontend/postlist.nim | 3 + 5 files changed, 128 insertions(+), 22 deletions(-) diff --git a/public/css/nimforum.scss b/public/css/nimforum.scss index 2aa3dc6..98b4a04 100644 --- a/public/css/nimforum.scss +++ b/public/css/nimforum.scss @@ -107,6 +107,24 @@ $logo-height: $navbar-height - 20px; } } +#category-selection { + .dropdown { + .btn { + margin-right: 0px; + } + } + .plus-btn { + margin-right: 0px; + i { + margin-right: 0px; + } + } +} + +.category { + white-space: nowrap; +} + #new-thread { .modal-container .modal-body { max-height: none; @@ -715,12 +733,4 @@ hr { // - Hide features that have not been implemented yet. #main-buttons > section.navbar-section:nth-child(1) { display: none; -} - -#threads-list.table { - tr > th:nth-child(2), tr > td:nth-child(2) { - } -} - -.category, div.flag-button { -} +} \ No newline at end of file diff --git a/src/frontend/category.nim b/src/frontend/category.nim index 41efd95..1d18c97 100644 --- a/src/frontend/category.nim +++ b/src/frontend/category.nim @@ -9,6 +9,9 @@ type CategoryList* = ref object categories*: seq[Category] +proc cmpNames*(cat1: Category, cat2: Category): int = + cat1.name.cmp(cat2.name) + when defined(js): include karax/prelude import karax / [vstyles, kajax, kdom] diff --git a/src/frontend/categorypicker.nim b/src/frontend/categorypicker.nim index fd9063f..799eab9 100644 --- a/src/frontend/categorypicker.nim +++ b/src/frontend/categorypicker.nim @@ -1,5 +1,5 @@ when defined(js): - import sugar, httpcore, options, json, strutils + import sugar, httpcore, options, json, strutils, algorithm import dom except Event include karax/prelude @@ -13,13 +13,17 @@ when defined(js): 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) proc slug(name: string): string = name.strip().replace(" ", "-").toLowerAscii - proc onCategoryList(state: CategoryPicker): proc (httpStatus: int, response: kstring) = + proc onCategoryLoad(state: CategoryPicker): proc (httpStatus: int, response: kstring) = return proc (httpStatus: int, response: kstring) = state.loading = false state.status = httpStatus.HttpCode @@ -27,6 +31,7 @@ when defined(js): let parsed = parseJson($response) let list = parsed.to(CategoryList) + list.categories.sort(cmpNames) if state.list.isSome: state.list.get().categories.add(list.categories) @@ -39,23 +44,36 @@ when defined(js): proc loadCategories(state: CategoryPicker) = if not state.loading: state.loading = true - ajaxGet(makeUri("categories.json"), @[], onCategoryList(state)) + ajaxGet(makeUri("categories.json"), @[], onCategoryLoad(state)) proc `[]`*(state: CategoryPicker, id: int): Category = - state.list.get().categories[id] + for cat in state.list.get().categories: + if cat.id == id: + return cat + raise newException(IndexError, "Category at " & $id & " not found!") + + proc nullAddCategory(category: Category) = discard + proc nullCategoryChange(oldCategory: Category, newCategory: Category) = discard proc newCategoryPicker*( - onCategoryChange: proc (oldCategory: Category, newCategory: Category) = - proc (oldCategory: Category, newCategory: Category) = discard + onCategoryChange: proc(oldCategory: Category, newCategory: Category) = nullCategoryChange, + onAddCategory: proc(category: Category) = nullAddCategory ): CategoryPicker = result = CategoryPicker( list: none[CategoryList](), selectedCategoryID: 0, loading: false, + modalShown: false, + addEnabled: false, status: Http200, - onCategoryChange: onCategoryChange + error: none[PostError](), + onCategoryChange: onCategoryChange, + onAddCategory: onAddCategory ) + proc setAddEnabled*(state: CategoryPicker, enabled: bool) = + state.addEnabled = enabled + proc select*(state: CategoryPicker, id: int) = state.selectedCategoryID = id state.markDirty() @@ -68,6 +86,75 @@ 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, @[], cast[cstring](formData), + (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() + )): + 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" + proc render*(state: CategoryPicker): VNode = if state.status != Http200: return renderError("Couldn't retrieve categories.", state.status) @@ -77,15 +164,13 @@ when defined(js): return buildHtml(tdiv(class="loading loading-lg")) let list = state.list.get().categories - let selectedCategory = list[state.selectedCategoryID] + let selectedCategory = state[state.selectedCategoryID] result = buildHtml(): tdiv(id="category-selection", class="input-group"): - label(class="d-inline-block form-label"): - text "Category" tdiv(class="dropdown"): a(class="btn btn-link dropdown-toggle", tabindex="0"): - tdiv(class="d-inline-block"): + tdiv(class="selected-category d-inline-block"): render(selectedCategory) text " " italic(class="fas fa-caret-down") @@ -94,4 +179,6 @@ when defined(js): li(class="menu-item"): a(class="category-" & $category.id & " " & category.name.slug, onClick=onCategoryClick(state, category)): - render(category) \ No newline at end of file + render(category) + if state.addEnabled: + genAddCategory(state) \ No newline at end of file diff --git a/src/frontend/newthread.nim b/src/frontend/newthread.nim index 9c6a79b..8ef7066 100644 --- a/src/frontend/newthread.nim +++ b/src/frontend/newthread.nim @@ -60,7 +60,10 @@ when defined(js): if state.error.isSome(): p(class="text-error"): text state.error.get().message - render(state.categoryPicker) + tdiv(): + label(class="d-inline-block form-label"): + text "Category" + render(state.categoryPicker) renderContent(state.replyBox, none[Thread](), none[Post]()) tdiv(class="footer"): diff --git a/src/frontend/postlist.nim b/src/frontend/postlist.nim index 610e245..6349dab 100644 --- a/src/frontend/postlist.nim +++ b/src/frontend/postlist.nim @@ -211,6 +211,9 @@ when defined(js): let currentAdmin = currentUser.isSome() and currentUser.get().rank == Admin + if currentAdmin: + state.categoryPicker.setAddEnabled(true) + result = buildHtml(): tdiv(): if authoredByUser or currentAdmin: From 82463ea42370aed7fc1edc0b648e337f11d4dd1b Mon Sep 17 00:00:00 2001 From: Joey Yakimowich-Payne Date: Wed, 8 Aug 2018 19:09:54 +0900 Subject: [PATCH 042/144] Add server check for category adding --- src/forum.nim | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/forum.nim b/src/forum.nim index ac67c51..6e35d03 100644 --- a/src/forum.nim +++ b/src/forum.nim @@ -666,6 +666,12 @@ proc executeLike(c: TForumData, postId: int) = exec(db, crud(crCreate, "like", "author", "post"), c.userid, postId) proc executeNewCategory(c: TForumData, name, color, description: string): int64 = + + let canAdd = c.rank == Admin + + if not canAdd: + raise newForumError("You do not have permissions to add a category.") + if name.len == 0: raise newForumError("Category name must not be empty!", @["name"]) From d5df46823a8ebbcc3f5f990d7d3dc8ce7d0241f1 Mon Sep 17 00:00:00 2001 From: Joey Yakimowich-Payne Date: Wed, 8 Aug 2018 19:10:46 +0900 Subject: [PATCH 043/144] Add category add button on new thread --- src/frontend/forum.nim | 2 +- src/frontend/newthread.nim | 12 ++++++++++-- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/src/frontend/forum.nim b/src/frontend/forum.nim index 5fb11b4..efb072e 100644 --- a/src/frontend/forum.nim +++ b/src/frontend/forum.nim @@ -83,7 +83,7 @@ proc render(): VNode = route([ r("/newthread", (params: Params) => - (render(state.newThread)) + (render(state.newThread, getLoggedInUser())) ), r("/profile/@username", (params: Params) => diff --git a/src/frontend/newthread.nim b/src/frontend/newthread.nim index 8ef7066..6d44381 100644 --- a/src/frontend/newthread.nim +++ b/src/frontend/newthread.nim @@ -5,7 +5,7 @@ when defined(js): include karax/prelude import karax / [kajax, kdom] - import error, replybox, threadlist, post, category + import error, replybox, threadlist, post, category, user import karaxutils, categorypicker type @@ -47,7 +47,15 @@ when defined(js): ajaxPost(uri, @[], cast[cstring](formData), (s: int, r: kstring) => onCreatePost(s, r, state)) - proc render*(state: NewThread): VNode = + proc render*(state: NewThread, currentUser: Option[User]): VNode = + + let loggedIn = currentUser.isSome() + let currentAdmin = + currentUser.isSome() and currentUser.get().rank == Admin + + if currentAdmin: + state.categoryPicker.setAddEnabled(true) + result = buildHtml(): section(class="container grid-xl"): tdiv(id="new-thread"): From 0af291dc102969da8d230a7d23202f972d9caf1f Mon Sep 17 00:00:00 2001 From: Joey Yakimowich-Payne Date: Wed, 8 Aug 2018 19:12:28 +0900 Subject: [PATCH 044/144] Add test for category adding --- tests/browsertests/categories.nim | 99 ++++++++++++++++++++++++------- 1 file changed, 76 insertions(+), 23 deletions(-) diff --git a/tests/browsertests/categories.nim b/tests/browsertests/categories.nim index 6f91f9d..7924ac4 100644 --- a/tests/browsertests/categories.nim +++ b/tests/browsertests/categories.nim @@ -9,44 +9,97 @@ proc selectCategory(session: Session, name: string) = click "#category-selection ." & name -proc categoriesTests(session: Session, baseUrl: string) = +proc categoriesUserTests(session: Session, baseUrl: string) = let title = "Category Test" content = "Choosing category test" - with session: - navigate baseUrl - wait() - login "user", "user" + suite "user tests": - test "can create category thread": with session: - click "#new-thread-btn" - wait() - - sendKeys "#thread-title", title - - selectCategory "fun" - - sendKeys "#reply-textarea", content - - click "#create-thread-btn" - wait() - - checkText "#thread-title .category", "Fun" - navigate baseUrl wait() + login "user", "user" - ensureExists title, LinkTextSelector + setup: + with session: + navigate baseUrl + wait() - session.logout() + 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 + + selectCategory "fun" + + sendKeys "#reply-textarea", content + + click "#create-thread-btn" + wait() + + checkText "#thread-title .category", "Fun" + + navigate baseUrl + wait() + + ensureExists title, LinkTextSelector + + 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 + 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']" + 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 + + click "#add-category #add-category-btn" + wait() + + checkText "#category-selection .selected-category", name + + session.logout() proc test*(session: Session, baseUrl: string) = session.navigate(baseUrl) session.wait() - categoriesTests(session, baseUrl) + categoriesUserTests(session, baseUrl) + categoriesAdminTests(session, baseUrl) session.navigate(baseUrl) session.wait() \ No newline at end of file From 796d8ee20c60a2b6f5f200cb845741f23b8e50c5 Mon Sep 17 00:00:00 2001 From: Joey Yakimowich-Payne Date: Wed, 8 Aug 2018 21:41:20 +0900 Subject: [PATCH 045/144] Fix tests --- src/frontend/postlist.nim | 2 +- tests/browsertests/threads.nim | 12 ++++++++++-- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/src/frontend/postlist.nim b/src/frontend/postlist.nim index 6349dab..3076a2a 100644 --- a/src/frontend/postlist.nim +++ b/src/frontend/postlist.nim @@ -375,7 +375,7 @@ when defined(js): if state.error.isSome(): span(class="text-error"): text state.error.get().message - p(): text list.thread.topic + p(class="title-text"): text list.thread.topic if list.thread.isLocked: italic(class="fas fa-lock fa-xs", title="Thread cannot be replied to") diff --git a/tests/browsertests/threads.nim b/tests/browsertests/threads.nim index f0b1b32..45b971b 100644 --- a/tests/browsertests/threads.nim +++ b/tests/browsertests/threads.nim @@ -15,6 +15,12 @@ proc banUser(session: Session, baseUrl: string) = setUserRank baseUrl, "user", "banned" logout() +proc unBanUser(session: Session, baseUrl: string) = + with session: + login "admin", "admin" + setUserRank baseUrl, "user", "user" + logout() + proc userTests(session: Session, baseUrl: string) = suite "user thread tests": session.login("user", "user") @@ -34,7 +40,7 @@ proc userTests(session: Session, baseUrl: string) = click "#create-thread-btn" wait() - checkText "#thread-title", userTitleStr + checkText "#thread-title .title-text", userTitleStr checkText ".original-post div.post-content", userContentStr session.logout() @@ -99,7 +105,7 @@ proc adminTests(session: Session, baseUrl: string) = click "#create-thread-btn" wait() - checkText "#thread-title", adminTitleStr + checkText "#thread-title .title-text", adminTitleStr checkText ".original-post div.post-content", adminContentStr test "try create duplicate thread": @@ -172,5 +178,7 @@ proc test*(session: Session, baseUrl: string) = anonymousTests(session, baseUrl) adminTests(session, baseUrl) + unBanUser(session, baseUrl) + session.navigate(baseUrl) session.wait() From 416d2601fb3ae605a11b0a919fc16e385ef63b39 Mon Sep 17 00:00:00 2001 From: Joey Yakimowich-Payne Date: Thu, 9 Aug 2018 09:51:37 +0900 Subject: [PATCH 046/144] Fix another test issue --- 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 2c60840..b3a093c 100644 --- a/tests/browsertests/common.nim +++ b/tests/browsertests/common.nim @@ -147,5 +147,5 @@ proc createThread*(session: Session, title, content: string) = click "#create-thread-btn" wait() - checkText "#thread-title", title + checkText "#thread-title .title-text", title checkText ".original-post div.post-content", content From 4e1b906b4988f7193e72d4914e75788cd0f41ea9 Mon Sep 17 00:00:00 2001 From: Joey Yakimowich-Payne Date: Thu, 9 Aug 2018 10:14:03 +0900 Subject: [PATCH 047/144] Rename square to category --- public/css/nimforum.scss | 12 +----------- src/frontend/category.nim | 2 +- 2 files changed, 2 insertions(+), 12 deletions(-) diff --git a/public/css/nimforum.scss b/public/css/nimforum.scss index 98b4a04..ead7b0e 100644 --- a/public/css/nimforum.scss +++ b/public/css/nimforum.scss @@ -214,17 +214,7 @@ $threads-meta-color: #545d70; } } -.triangle { - // TODO: Abstract this into a "category" class. - width: 0; - height: 0; - border-left: 0.3rem solid transparent; - border-right: 0.3rem solid transparent; - border-bottom: 0.6rem solid #98c766; - display: inline-block; -} - -.square { +.category-color { width: 0; height: 0; border: 0.3rem solid #98c766; diff --git a/src/frontend/category.nim b/src/frontend/category.nim index 1d18c97..de2d248 100644 --- a/src/frontend/category.nim +++ b/src/frontend/category.nim @@ -24,7 +24,7 @@ when defined(js): tdiv(class="category", title=category.description, "data-color"="#" & category.color): - tdiv(class="square", + tdiv(class="category-color", style=style( (StyleAttr.border, kstring"0.3rem solid #" & category.color) From f8781ba5f35632194f3542ee3e2bc7f6a6c9e441 Mon Sep 17 00:00:00 2001 From: Joey Date: Fri, 10 Aug 2018 16:51:22 +0900 Subject: [PATCH 048/144] Add better RST preview error (#196) * Add better RST preview error * Add exception information back --- src/forum.nim | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/forum.nim b/src/forum.nim index 399a64e..da38305 100644 --- a/src/forum.nim +++ b/src/forum.nim @@ -1071,7 +1071,8 @@ routes: except EParseError: let err = PostError( errorFields: @[], - message: getCurrentExceptionMsg() + message: "Message needs to be valid RST! Error: " & + getCurrentExceptionMsg() ) resp Http400, $(%err), "application/json" From 7321ee6f6125bac0c8091baeda2ef572e0820ff7 Mon Sep 17 00:00:00 2001 From: Joey Yakimowich-Payne Date: Thu, 16 Aug 2018 11:10:49 +0900 Subject: [PATCH 049/144] Add case insensitive check to username validation --- src/forum.nim | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/forum.nim b/src/forum.nim index da38305..ad1ab09 100644 --- a/src/forum.nim +++ b/src/forum.nim @@ -608,7 +608,7 @@ proc executeRegister(c: TForumData, name, pass, antibot, userIp, raise newForumError("Invalid username", @["username"]) if getValue( db, - sql"select name from person where name = ? and isDeleted = 0", + sql"select name from person where name = ? collate nocase and isDeleted = 0", name ).len > 0: raise newForumError("Username already exists", @["username"]) From 2b88a54f5475174b1b8c38634dcd08c99e2fa7b2 Mon Sep 17 00:00:00 2001 From: Joey Yakimowich-Payne Date: Thu, 16 Aug 2018 11:29:57 +0900 Subject: [PATCH 050/144] Add test for case insensitive name check --- tests/browsertests/common.nim | 14 ++++++++------ tests/browsertests/scenario1.nim | 15 ++++++++++++++- 2 files changed, 22 insertions(+), 7 deletions(-) diff --git a/tests/browsertests/common.nim b/tests/browsertests/common.nim index 2c60840..a2c9b51 100644 --- a/tests/browsertests/common.nim +++ b/tests/browsertests/common.nim @@ -115,7 +115,7 @@ proc login*(session: Session, user, password: string) = checkText "#profile-btn #profile-name", user click "#profile-btn" -proc register*(session: Session, user, password: string) = +proc register*(session: Session, user, password: string, verify = true) = with session: click "#signup-btn" @@ -130,11 +130,13 @@ proc register*(session: Session, user, password: string) = click "#signup-modal .create-account-btn" wait() - # Verify that the user menu has been initialised properly. - click "#profile-btn" - checkText "#profile-btn #profile-name", user - # close menu - click "#profile-btn" + if verify: + with session: + # Verify that the user menu has been initialised properly. + click "#profile-btn" + checkText "#profile-btn #profile-name", user + # close menu + click "#profile-btn" proc createThread*(session: Session, title, content: string) = with session: diff --git a/tests/browsertests/scenario1.nim b/tests/browsertests/scenario1.nim index 34132e5..b5912ec 100644 --- a/tests/browsertests/scenario1.nim +++ b/tests/browsertests/scenario1.nim @@ -30,5 +30,18 @@ proc test*(session: Session, baseUrl: string) = test "can register": with session: register("test", "test") + logout() - session.logout() + test "can't register same username with different case": + with session: + register "test1", "test1", verify = false + logout() + + navigate baseUrl + wait() + + register "TEst1", "test1", verify = false + + ensureExists "#signup-form .has-error" + navigate baseUrl + wait() \ No newline at end of file From 30dc09f453235bbb2f8509f60c6a57f80b402294 Mon Sep 17 00:00:00 2001 From: LemonBoy Date: Tue, 21 Aug 2018 23:59:42 +0200 Subject: [PATCH 051/144] Hide all the avatars but the first on small screens --- public/css/nimforum.scss | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/public/css/nimforum.scss b/public/css/nimforum.scss index e970e8e..530180b 100644 --- a/public/css/nimforum.scss +++ b/public/css/nimforum.scss @@ -253,6 +253,13 @@ $threads-meta-color: #545d70; } +// Hide all the avatars but the first on small screens. +@media screen and (max-width: 600px) { + #threads-list a:not(:first-child) > .avatar { + display: none; + } +} + .posts, .about { @extend .grid-md; @extend .container; From 41a1a36dbfea56476ac66ae7aa6e8b848b7c0f85 Mon Sep 17 00:00:00 2001 From: Joey Payne Date: Thu, 24 Jan 2019 20:37:03 -0700 Subject: [PATCH 052/144] Fix issues compiling and testing --- README.md | 20 ++++++++++++++++++++ nimforum.nimble | 6 +++--- src/auth.nim | 8 ++++---- src/forum.nim | 11 +++++------ src/frontend/threadlist.nim | 2 +- tests/browsertester.nim | 5 ++++- 6 files changed, 37 insertions(+), 15 deletions(-) diff --git a/README.md b/README.md index c8057cd..37d7881 100644 --- a/README.md +++ b/README.md @@ -63,6 +63,26 @@ test Runs tester fasttest Runs tester without recompiling backend ``` +To get up and running: + +```bash +git clone https://github.com/nim-lang/nimforum +cd nimforum +git submodule update --init --recursive + +nimble install + +# Setup the db with user: admin, pass: admin and some other users +nimble devdb + +# Run this again if frontend code changes +nimble frontend + +# Will start a server at localhost:5000 +nimble backend +``` + + 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` diff --git a/nimforum.nimble b/nimforum.nimble index 1ea21bd..79c43af 100644 --- a/nimforum.nimble +++ b/nimforum.nimble @@ -1,5 +1,5 @@ # Package -version = "2.0.1" +version = "2.1.0" author = "Dominik Picheta" description = "The Nim forum" license = "MIT" @@ -13,13 +13,13 @@ skipExt = @["nim"] # Dependencies requires "nim >= 0.18.1" -requires "jester 0.4.0" +requires "jester#22f8240" requires "bcrypt#head" requires "hmac#9c61ebe2fd134cf97" requires "recaptcha 1.0.2" requires "sass#649e0701fa5c" -requires "karax#d8df257dd" +requires "karax#c8c7b13" requires "webdriver#20f3c1b" diff --git a/src/auth.nim b/src/auth.nim index 0b08bfe..381b666 100644 --- a/src/auth.nim +++ b/src/auth.nim @@ -71,13 +71,13 @@ when isMainModule: "test", "$2a$08$bY85AhoD1e9u0IsD9sM7Ee6kFSLeXRLxJ6rMgfb1wDnU9liaymoTG", 1526908753, - "*B2a] IL\"~sh)q-GBd/i$^>.TL]PR~>1IX>Fp-:M3pCm^cFD\um" + "*B2a] IL\"~sh)q-GBd/i$^>.TL]PR~>1IX>Fp-:M3pCm^cFD\\um" ) let ident2 = makeIdentHash( "test", "$2a$08$bY85AhoD1e9u0IsD9sM7Ee6kFSLeXRLxJ6rMgfb1wDnU9liaymoTG", 1526908753, - "*B2a] IL\"~sh)q-GBd/i$^>.TL]PR~>1IX>Fp-:M3pCm^cFD\um" + "*B2a] IL\"~sh)q-GBd/i$^>.TL]PR~>1IX>Fp-:M3pCm^cFD\\um" ) doAssert ident == ident2 @@ -85,6 +85,6 @@ when isMainModule: "test", "$2a$08$bY85AhoD1e9u0IsD9sM7Ee6kFSLeXRLxJ6rMgfb1wDnU9liaymoTG", 1526908754, - "*B2a] IL\"~sh)q-GBd/i$^>.TL]PR~>1IX>Fp-:M3pCm^cFD\um" + "*B2a] IL\"~sh)q-GBd/i$^>.TL]PR~>1IX>Fp-:M3pCm^cFD\\um" ) - doAssert ident != invalid \ No newline at end of file + doAssert ident != invalid diff --git a/src/forum.nim b/src/forum.nim index 6e35d03..303d801 100644 --- a/src/forum.nim +++ b/src/forum.nim @@ -8,7 +8,7 @@ import system except Thread import os, strutils, times, md5, strtabs, math, db_sqlite, - scgi, jester, asyncdispatch, asyncnet, sequtils, + jester, asyncdispatch, asyncnet, sequtils, parseutils, random, rst, recaptcha, json, re, sugar, strformat, logging import cgi except setCookie @@ -76,7 +76,6 @@ proc getGravatarUrl(email: string, size = 80): string = # ----------------------------------------------------------------------------- -template `||`(x: untyped): untyped = (if not isNil(x): x else: "") proc validateCaptcha(recaptchaResp, ip: string) {.async.} = # captcha validation: @@ -133,9 +132,9 @@ proc checkLoggedIn(c: TForumData) = let row = getRow(db, sql"select name, email, status from person where id = ?", c.userid) - c.username = ||row[0] - c.email = ||row[1] - c.rank = parseEnum[Rank](||row[2]) + c.username = row[0] + c.email = row[1] + c.rank = parseEnum[Rank](row[2]) # In order to handle the "last visit" line appropriately, i.e. # it shouldn't disappear after a refresh, we need to manage a @@ -463,7 +462,7 @@ proc executeReply(c: TForumData, threadId: int, content: string, crud(crCreate, "post", "author", "ip", "content", "thread", "replyingTo"), c.userId, c.req.ip, content, $threadId, if replyingTo.isSome(): $replyingTo.get() - else: nil + else: "" ) discard tryExec( db, diff --git a/src/frontend/threadlist.nim b/src/frontend/threadlist.nim index 0516d74..b3b3d81 100644 --- a/src/frontend/threadlist.nim +++ b/src/frontend/threadlist.nim @@ -60,7 +60,7 @@ when defined(js): if user.isNone(): return not thread.isModerated let rank = user.get().rank - if rank < Moderator and thread.isModerated: + if rank < Rank.Moderator and thread.isModerated: return thread.author == user.get() return true diff --git a/tests/browsertester.nim b/tests/browsertester.nim index 82bb5cb..6a5c374 100644 --- a/tests/browsertester.nim +++ b/tests/browsertester.nim @@ -45,7 +45,7 @@ template withBackend(body: untyped): untyped = import browsertests/[scenario1, threads, issue181, categories] -when isMainModule: +proc main() = spawn runProcess("geckodriver -p 4444 --log config") defer: discard execCmd("killall geckodriver") @@ -71,3 +71,6 @@ when isMainModule: except: sleep(10000) # See if we can grab any more output. raise + +when isMainModule: + main() From f4af965a2e0c146c8d5e33e203cde3dd1c5e22b0 Mon Sep 17 00:00:00 2001 From: lallulli Date: Thu, 31 Oct 2019 11:36:05 +0100 Subject: [PATCH 053/144] Better way of updating nginx configuration Using `nginx -s reload` is better than restarting the server on live systems, because if there is any problem with new config files, nginx will warn you, refuse to reload and continue to work with old configuration. Moreover, this command will minimize downtime. --- setup.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/setup.md b/setup.md index ea3dc1b..d0f2d3e 100644 --- a/setup.md +++ b/setup.md @@ -100,7 +100,7 @@ You should then create a symlink to this file inside ``/etc/nginx/sites-enabled/ ln -s /etc/nginx/sites-available/ /etc/nginx/sites-enabled/ ``` -Then restart nginx by running ``sudo systemctl restart nginx``. +Then reload nginx configuration by running ``sudo nginx -s reload``. ### Supervisor @@ -168,4 +168,4 @@ You should see something like this: ## Conclusion That should be all you need to get started. Your forum should now be accessible -via your hostname, assuming that it points to your VPS' IP address. \ No newline at end of file +via your hostname, assuming that it points to your VPS' IP address. From f93bd87316bd12fa3bf8091f5ca4bb3bd64f939d Mon Sep 17 00:00:00 2001 From: Joey Yakimowich-Payne Date: Fri, 14 Feb 2020 06:41:09 -0700 Subject: [PATCH 054/144] Update for Nim 1.0.6 --- nimforum.nimble | 12 ++++++------ src/auth.nim | 8 ++++---- src/forum.nim | 11 +++++------ src/frontend/error.nim | 4 ++-- src/frontend/forum.nim | 2 +- src/frontend/karaxutils.nim | 10 ++-------- src/frontend/login.nim | 4 ++-- src/frontend/replybox.nim | 4 ++-- src/frontend/resetpassword.nim | 4 ++-- src/frontend/threadlist.nim | 2 +- src/utils.nim | 9 ++------- tests/browsertester.nim | 5 ++++- 12 files changed, 33 insertions(+), 42 deletions(-) diff --git a/nimforum.nimble b/nimforum.nimble index 1ea21bd..d6c08f8 100644 --- a/nimforum.nimble +++ b/nimforum.nimble @@ -12,16 +12,16 @@ skipExt = @["nim"] # Dependencies -requires "nim >= 0.18.1" -requires "jester 0.4.0" +requires "nim >= 1.0.6" +requires "jester#d8a03aa" requires "bcrypt#head" requires "hmac#9c61ebe2fd134cf97" -requires "recaptcha 1.0.2" +requires "recaptcha#d06488e" requires "sass#649e0701fa5c" -requires "karax#d8df257dd" +requires "karax#f6bda9a" -requires "webdriver#20f3c1b" +requires "webdriver#c2fee57" # Tasks @@ -36,7 +36,7 @@ task frontend, "Builds the necessary JS frontend (with CSS)": exec "nimble c -r src/buildcss" exec "nimble js -d:release src/frontend/forum.nim" mkDir "public/js" - cpFile "src/frontend/nimcache/forum.js", "public/js/forum.js" + cpFile "src/frontend/forum.js", "public/js/forum.js" task minify, "Minifies the JS using Google's closure compiler": exec "closure-compiler public/js/forum.js --js_output_file public/js/forum.js.opt" diff --git a/src/auth.nim b/src/auth.nim index 0b08bfe..381b666 100644 --- a/src/auth.nim +++ b/src/auth.nim @@ -71,13 +71,13 @@ when isMainModule: "test", "$2a$08$bY85AhoD1e9u0IsD9sM7Ee6kFSLeXRLxJ6rMgfb1wDnU9liaymoTG", 1526908753, - "*B2a] IL\"~sh)q-GBd/i$^>.TL]PR~>1IX>Fp-:M3pCm^cFD\um" + "*B2a] IL\"~sh)q-GBd/i$^>.TL]PR~>1IX>Fp-:M3pCm^cFD\\um" ) let ident2 = makeIdentHash( "test", "$2a$08$bY85AhoD1e9u0IsD9sM7Ee6kFSLeXRLxJ6rMgfb1wDnU9liaymoTG", 1526908753, - "*B2a] IL\"~sh)q-GBd/i$^>.TL]PR~>1IX>Fp-:M3pCm^cFD\um" + "*B2a] IL\"~sh)q-GBd/i$^>.TL]PR~>1IX>Fp-:M3pCm^cFD\\um" ) doAssert ident == ident2 @@ -85,6 +85,6 @@ when isMainModule: "test", "$2a$08$bY85AhoD1e9u0IsD9sM7Ee6kFSLeXRLxJ6rMgfb1wDnU9liaymoTG", 1526908754, - "*B2a] IL\"~sh)q-GBd/i$^>.TL]PR~>1IX>Fp-:M3pCm^cFD\um" + "*B2a] IL\"~sh)q-GBd/i$^>.TL]PR~>1IX>Fp-:M3pCm^cFD\\um" ) - doAssert ident != invalid \ No newline at end of file + doAssert ident != invalid diff --git a/src/forum.nim b/src/forum.nim index ad1ab09..10d3511 100644 --- a/src/forum.nim +++ b/src/forum.nim @@ -8,7 +8,7 @@ import system except Thread import os, strutils, times, md5, strtabs, math, db_sqlite, - scgi, jester, asyncdispatch, asyncnet, sequtils, + jester, asyncdispatch, asyncnet, sequtils, parseutils, random, rst, recaptcha, json, re, sugar, strformat, logging import cgi except setCookie @@ -76,7 +76,6 @@ proc getGravatarUrl(email: string, size = 80): string = # ----------------------------------------------------------------------------- -template `||`(x: untyped): untyped = (if not isNil(x): x else: "") proc validateCaptcha(recaptchaResp, ip: string) {.async.} = # captcha validation: @@ -133,9 +132,9 @@ proc checkLoggedIn(c: TForumData) = let row = getRow(db, sql"select name, email, status from person where id = ?", c.userid) - c.username = ||row[0] - c.email = ||row[1] - c.rank = parseEnum[Rank](||row[2]) + c.username = row[0] + c.email = row[1] + c.rank = parseEnum[Rank](row[2]) # In order to handle the "last visit" line appropriately, i.e. # it shouldn't disappear after a refresh, we need to manage a @@ -463,7 +462,7 @@ proc executeReply(c: TForumData, threadId: int, content: string, crud(crCreate, "post", "author", "ip", "content", "thread", "replyingTo"), c.userId, c.req.ip, content, $threadId, if replyingTo.isSome(): $replyingTo.get() - else: nil + else: "-1" ) discard tryExec( db, diff --git a/src/frontend/error.nim b/src/frontend/error.nim index 4a23c44..06a8d07 100644 --- a/src/frontend/error.nim +++ b/src/frontend/error.nim @@ -86,8 +86,8 @@ when defined(js): state.error = some(error) except: - kout(getCurrentExceptionMsg().cstring) + echo getCurrentExceptionMsg() state.error = some(PostError( errorFields: @[], message: "Unknown error occurred." - )) \ No newline at end of file + )) diff --git a/src/frontend/forum.nim b/src/frontend/forum.nim index 4dea047..5fb11b4 100644 --- a/src/frontend/forum.nim +++ b/src/frontend/forum.nim @@ -49,7 +49,7 @@ proc onPopState(event: dom.Event) = # This event is usually only called when the user moves back in their # history. I fire it in karaxutils.anchorCB as well to ensure the URL is # always updated. This should be moved into Karax in the future. - kout(kstring"New URL: ", window.location.href, " ", state.url.href) + echo "New URL: ", window.location.href, " ", state.url.href document.title = state.originalTitle if state.url.href != window.location.href: state = newState() # Reload the state to remove stale data. diff --git a/src/frontend/karaxutils.nim b/src/frontend/karaxutils.nim index 462d000..2aa9d39 100644 --- a/src/frontend/karaxutils.nim +++ b/src/frontend/karaxutils.nim @@ -25,7 +25,7 @@ proc getInt64*(s: string, default = 0): int64 = when defined(js): include karax/prelude - import karax / [kdom] + import karax / [kdom, kajax] from dom import nil @@ -87,16 +87,10 @@ when defined(js): navigateTo(url) - type - FormData* = ref object - proc newFormData*(): FormData - {.importcpp: "new FormData()", constructor.} proc newFormData*(form: dom.Element): FormData {.importcpp: "new FormData(@)", constructor.} proc get*(form: FormData, key: cstring): cstring {.importcpp: "#.get(@)".} - proc append*(form: FormData, key, val: cstring) - {.importcpp: "#.append(@)".} proc renderProfileUrl*(username: string): string = makeUri(fmt"/profile/{username}") @@ -120,4 +114,4 @@ when defined(js): inc(i) # Skip = i += query.parseUntil(val, '&', i) inc(i) # Skip & - result[$decodeUri(key)] = $decodeUri(val) \ No newline at end of file + result[$decodeUri(key)] = $decodeUri(val) diff --git a/src/frontend/login.nim b/src/frontend/login.nim index f0779c7..c19088e 100644 --- a/src/frontend/login.nim +++ b/src/frontend/login.nim @@ -1,6 +1,6 @@ when defined(js): import sugar, httpcore, options, json - import dom except Event + import dom except Event, KeyboardEvent include karax/prelude import karax / [kajax, kdom] @@ -93,4 +93,4 @@ when defined(js): (state.onSignUp(); state.shown = false)): text "Create account" - render(state.resetPasswordModal, recaptchaSiteKey) \ No newline at end of file + render(state.resetPasswordModal, recaptchaSiteKey) diff --git a/src/frontend/replybox.nim b/src/frontend/replybox.nim index 9d815f6..e386dcb 100644 --- a/src/frontend/replybox.nim +++ b/src/frontend/replybox.nim @@ -26,7 +26,7 @@ when defined(js): proc performScroll() = let replyBox = dom.document.getElementById("reply-box") - replyBox.scrollIntoView(false) + replyBox.scrollIntoView() proc show*(state: ReplyBox) = # Scroll to the reply box. @@ -44,7 +44,7 @@ when defined(js): proc onPreviewPost(httpStatus: int, response: kstring, state: ReplyBox) = postFinished: - kout(response) + echo response state.rendering = some[kstring](response) proc onPreviewClick(e: Event, n: VNode, state: ReplyBox) = diff --git a/src/frontend/resetpassword.nim b/src/frontend/resetpassword.nim index c2f01a1..51b467b 100644 --- a/src/frontend/resetpassword.nim +++ b/src/frontend/resetpassword.nim @@ -1,6 +1,6 @@ when defined(js): import sugar, httpcore, options, json - import dom except Event + import dom except Event, KeyboardEvent include karax/prelude import karax / [kajax, kdom] @@ -152,4 +152,4 @@ when defined(js): ), `type`="button", onClick=(ev: Event, n: VNode) => onClick(ev, n, state)): - text "Reset password" \ No newline at end of file + text "Reset password" diff --git a/src/frontend/threadlist.nim b/src/frontend/threadlist.nim index 0516d74..b3b3d81 100644 --- a/src/frontend/threadlist.nim +++ b/src/frontend/threadlist.nim @@ -60,7 +60,7 @@ when defined(js): if user.isNone(): return not thread.isModerated let rank = user.get().rank - if rank < Moderator and thread.isModerated: + if rank < Rank.Moderator and thread.isModerated: return thread.author == user.get() return true diff --git a/src/utils.nim b/src/utils.nim index 2676ab8..1be058b 100644 --- a/src/utils.nim +++ b/src/utils.nim @@ -10,11 +10,6 @@ let import frontend/[karaxutils, error] export parseInt -proc `%`*[T](opt: Option[T]): JsonNode = - ## Generic constructor for JSON data. Creates a new ``JNull JsonNode`` - ## if ``opt`` is empty, otherwise it delegates to the underlying value. - if opt.isSome: %opt.get else: newJNull() - type Config* = object smtpAddress*: string @@ -56,7 +51,7 @@ proc loadConfig*(filename = getCurrentDir() / "forum.json"): Config = smtpPassword: "", mlistAddress: "") let root = parseFile(filename) result.smtpAddress = root{"smtpAddress"}.getStr("") - result.smtpPort = root{"smtpPort"}.getNum(25).int + result.smtpPort = root{"smtpPort"}.getInt(25) result.smtpUser = root{"smtpUser"}.getStr("") result.smtpPassword = root{"smtpPassword"}.getStr("") result.smtpFromAddr = root{"smtpFromAddr"}.getStr("") @@ -69,7 +64,7 @@ proc loadConfig*(filename = getCurrentDir() / "forum.json"): Config = result.name = root["name"].getStr() result.title = root["title"].getStr() result.ga = root{"ga"}.getStr() - result.port = root{"port"}.getNum(5000).int + result.port = root{"port"}.getInt(5000) proc processGT(n: XmlNode, tag: string): (int, XmlNode, string) = result = (0, newElement(tag), tag) diff --git a/tests/browsertester.nim b/tests/browsertester.nim index 0f4efe9..4e03081 100644 --- a/tests/browsertester.nim +++ b/tests/browsertester.nim @@ -45,7 +45,7 @@ template withBackend(body: untyped): untyped = import browsertests/[scenario1, threads, issue181] -when isMainModule: +proc main() = spawn runProcess("geckodriver -p 4444 --log config") defer: discard execCmd("killall geckodriver") @@ -70,3 +70,6 @@ when isMainModule: except: sleep(10000) # See if we can grab any more output. raise + +when isMainModule: + main() From 64262978dbe427b218f6ba615f9e2e9959d47b69 Mon Sep 17 00:00:00 2001 From: Joey Yakimowich-Payne Date: Fri, 14 Feb 2020 07:39:55 -0700 Subject: [PATCH 055/144] Fix travis --- .travis.yml | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/.travis.yml b/.travis.yml index b6177c4..b5c6c9b 100644 --- a/.travis.yml +++ b/.travis.yml @@ -9,7 +9,7 @@ cache: - "$HOME/.choosenim" addons: - firefox: "60.0.1" + firefox: "73.0" before_install: - sudo apt-get -qq update @@ -26,13 +26,13 @@ before_install: - sudo make -j5 install - cd .. - - wget https://github.com/mozilla/geckodriver/releases/download/v0.20.1/geckodriver-v0.20.1-linux64.tar.gz + - wget https://github.com/mozilla/geckodriver/releases/download/v0.26.0/geckodriver-v0.26.0-linux64.tar.gz - mkdir geckodriver - - tar -xzf geckodriver-v0.20.1-linux64.tar.gz -C geckodriver + - tar -xzf geckodriver-v0.26.0-linux64.tar.gz -C geckodriver - export PATH=$PATH:$PWD/geckodriver install: - - export CHOOSENIM_CHOOSE_VERSION="#f92d61b1f4e193bd" + - export CHOOSENIM_CHOOSE_VERSION="stable" - | curl https://nim-lang.org/choosenim/init.sh -sSf > init.sh sh init.sh -y @@ -41,4 +41,5 @@ install: script: - export MOZ_HEADLESS=1 - - nimble -y test \ No newline at end of file + - nimble -y install + - nimble -y test From 14a0864d867c16bc6d6ac1e5d9877795992e9cb5 Mon Sep 17 00:00:00 2001 From: Joey Yakimowich-Payne Date: Fri, 14 Feb 2020 08:42:27 -0700 Subject: [PATCH 056/144] Version bump --- nimforum.nimble | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nimforum.nimble b/nimforum.nimble index d6c08f8..97581b0 100644 --- a/nimforum.nimble +++ b/nimforum.nimble @@ -1,5 +1,5 @@ # Package -version = "2.0.1" +version = "2.0.2" author = "Dominik Picheta" description = "The Nim forum" license = "MIT" From 2717496bb5f2841243e5b92003a375cd681186bd Mon Sep 17 00:00:00 2001 From: Joey Yakimowich-Payne Date: Fri, 14 Feb 2020 09:27:05 -0700 Subject: [PATCH 057/144] Handle no replyingTo option better --- src/forum.nim | 22 +++++++++++++++------- 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/src/forum.nim b/src/forum.nim index 10d3511..9ca8abf 100644 --- a/src/forum.nim +++ b/src/forum.nim @@ -457,13 +457,21 @@ proc executeReply(c: TForumData, threadId: int, content: string, if isLocked == "1": raise newForumError("Cannot reply to a locked thread.") - let retID = insertID( - db, - crud(crCreate, "post", "author", "ip", "content", "thread", "replyingTo"), - c.userId, c.req.ip, content, $threadId, - if replyingTo.isSome(): $replyingTo.get() - else: "-1" - ) + var retID: int64 + + if replyingTo.isSome(): + retID = insertID( + db, + crud(crCreate, "post", "author", "ip", "content", "thread", "replyingTo"), + c.userId, c.req.ip, content, $threadId, $replyingTo.get() + ) + else: + retID = insertID( + db, + crud(crCreate, "post", "author", "ip", "content", "thread"), + c.userId, c.req.ip, content, $threadId + ) + discard tryExec( db, crud(crCreate, "post_fts", "id", "content"), From de7b391d114172202b3e38aa3991ab17334e6f2c Mon Sep 17 00:00:00 2001 From: Joey Yakimowich-Payne Date: Sat, 15 Feb 2020 08:36:08 -0700 Subject: [PATCH 058/144] Remove unnecessary readme line --- README.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/README.md b/README.md index 37d7881..05d4667 100644 --- a/README.md +++ b/README.md @@ -70,8 +70,6 @@ git clone https://github.com/nim-lang/nimforum cd nimforum git submodule update --init --recursive -nimble install - # Setup the db with user: admin, pass: admin and some other users nimble devdb From 46d6a3b6bb45681caf4ff9bfcde3c4de48308f71 Mon Sep 17 00:00:00 2001 From: Joey Yakimowich-Payne Date: Sat, 15 Feb 2020 08:37:28 -0700 Subject: [PATCH 059/144] Fix indentation --- src/forum.nim | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/forum.nim b/src/forum.nim index a551b73..6c6f55a 100644 --- a/src/forum.nim +++ b/src/forum.nim @@ -528,14 +528,14 @@ proc updatePost(c: TForumData, postId: int, content: string, exec(db, crud(crUpdate, "thread", "name"), subject.get(), threadId) proc updateThread(c: TForumData, threadId: string, queryKeys: seq[string], queryValues: seq[string]) = - let threadAuthor = selectThreadAuthor(threadId.parseInt) + 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 - if not canEdit: - raise newForumError("You cannot edit this thread") + # Verify that the current user has permissions to edit the specified thread. + let canEdit = c.rank == Admin or c.userid == threadAuthor.name + if not canEdit: + raise newForumError("You cannot edit this thread") - exec(db, crud(crUpdate, "thread", queryKeys), queryValues) + exec(db, crud(crUpdate, "thread", queryKeys), queryValues) proc executeNewThread(c: TForumData, subject, msg, categoryID: string): (int64, int64) = const From d9335ee0f0a9282dc373cd8a7fd8e1f2749d7799 Mon Sep 17 00:00:00 2001 From: Joey Yakimowich-Payne Date: Sat, 15 Feb 2020 08:52:13 -0700 Subject: [PATCH 060/144] Tighten updateThread fields and add todos --- src/forum.nim | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/forum.nim b/src/forum.nim index 6c6f55a..cb069af 100644 --- a/src/forum.nim +++ b/src/forum.nim @@ -1197,6 +1197,8 @@ routes: resp Http400, $(%exc.data), "application/json" post "/updateThread": + # TODO: Add some way of keeping track of modifications for historical + # purposes createTFD() if not c.loggedIn(): let err = PostError( @@ -1211,7 +1213,9 @@ routes: let threadId = formData["threadId"].body - let keys = ["name", "views", "modified", "category", "isLocked", "solution", "isDeleted"] + # TODO: might want to add more properties here under a tighter permissions + # model + let keys = ["name", "category", "solution"] # optional parameters var From 918cda96cf91bb881f6ec1599df57d4489af6c15 Mon Sep 17 00:00:00 2001 From: Joey Yakimowich-Payne Date: Sat, 15 Feb 2020 08:53:56 -0700 Subject: [PATCH 061/144] Minor cleanup --- src/frontend/categorypicker.nim | 38 +++++++++++++++++---------------- 1 file changed, 20 insertions(+), 18 deletions(-) diff --git a/src/frontend/categorypicker.nim b/src/frontend/categorypicker.nim index 799eab9..7834538 100644 --- a/src/frontend/categorypicker.nim +++ b/src/frontend/categorypicker.nim @@ -24,22 +24,23 @@ when defined(js): name.strip().replace(" ", "-").toLowerAscii proc onCategoryLoad(state: CategoryPicker): proc (httpStatus: int, response: kstring) = - return proc (httpStatus: int, response: kstring) = - state.loading = false - state.status = httpStatus.HttpCode - if state.status != Http200: return + return + proc (httpStatus: int, response: kstring) = + state.loading = false + state.status = httpStatus.HttpCode + if state.status != Http200: return - let parsed = parseJson($response) - let list = parsed.to(CategoryList) - list.categories.sort(cmpNames) + let parsed = parseJson($response) + let list = parsed.to(CategoryList) + list.categories.sort(cmpNames) - if state.list.isSome: - state.list.get().categories.add(list.categories) - else: - state.list = some(list) + if state.list.isSome: + state.list.get().categories.add(list.categories) + else: + state.list = some(list) - if state.selectedCategoryID > state.list.get().categories.len(): - state.selectedCategoryID = 0 + if state.selectedCategoryID > state.list.get().categories.len(): + state.selectedCategoryID = 0 proc loadCategories(state: CategoryPicker) = if not state.loading: @@ -81,10 +82,11 @@ when defined(js): proc onCategoryClick(state: CategoryPicker, category: Category): proc (ev: Event, n: VNode) = # this is necessary to capture the right value let cat = category - return proc (ev: Event, n: VNode) = - let oldCategory = state[state.selectedCategoryID] - state.select(cat.id) - state.onCategoryChange(oldCategory, cat) + return + proc (ev: Event, n: VNode) = + let oldCategory = state[state.selectedCategoryID] + state.select(cat.id) + state.onCategoryChange(oldCategory, cat) proc onAddCategoryPost(httpStatus: int, response: kstring, state: CategoryPicker) = postFinished: @@ -181,4 +183,4 @@ when defined(js): onClick=onCategoryClick(state, category)): render(category) if state.addEnabled: - genAddCategory(state) \ No newline at end of file + genAddCategory(state) From ce9cde4a0da4100ae114bcbd4cb00deda36352a5 Mon Sep 17 00:00:00 2001 From: Joey Yakimowich-Payne Date: Sat, 15 Feb 2020 09:02:00 -0700 Subject: [PATCH 062/144] Refactor category picker --- src/frontend/categorypicker.nim | 11 +++++++++-- src/frontend/newthread.nim | 10 +--------- src/frontend/postlist.nim | 5 +---- 3 files changed, 11 insertions(+), 15 deletions(-) diff --git a/src/frontend/categorypicker.nim b/src/frontend/categorypicker.nim index 7834538..0ea771f 100644 --- a/src/frontend/categorypicker.nim +++ b/src/frontend/categorypicker.nim @@ -5,7 +5,7 @@ when defined(js): include karax/prelude import karax / [kajax, kdom, vstyles, vdom] - import error, replybox, threadlist, post, category + import error, replybox, threadlist, post, category, user import category, karaxutils type @@ -157,7 +157,14 @@ when defined(js): state.onAddCategoryClick()): text "Add" - proc render*(state: CategoryPicker): VNode = + proc render*(state: CategoryPicker, currentUser: Option[User]): VNode = + let loggedIn = currentUser.isSome() + let currentAdmin = + loggedIn and currentUser.get().rank == Admin + + if currentAdmin: + state.setAddEnabled(true) + if state.status != Http200: return renderError("Couldn't retrieve categories.", state.status) diff --git a/src/frontend/newthread.nim b/src/frontend/newthread.nim index 6d44381..1d1be24 100644 --- a/src/frontend/newthread.nim +++ b/src/frontend/newthread.nim @@ -48,14 +48,6 @@ when defined(js): (s: int, r: kstring) => onCreatePost(s, r, state)) proc render*(state: NewThread, currentUser: Option[User]): VNode = - - let loggedIn = currentUser.isSome() - let currentAdmin = - currentUser.isSome() and currentUser.get().rank == Admin - - if currentAdmin: - state.categoryPicker.setAddEnabled(true) - result = buildHtml(): section(class="container grid-xl"): tdiv(id="new-thread"): @@ -71,7 +63,7 @@ when defined(js): tdiv(): label(class="d-inline-block form-label"): text "Category" - render(state.categoryPicker) + render(state.categoryPicker, currentUser) renderContent(state.replyBox, none[Thread](), none[Post]()) tdiv(class="footer"): diff --git a/src/frontend/postlist.nim b/src/frontend/postlist.nim index 3076a2a..a259343 100644 --- a/src/frontend/postlist.nim +++ b/src/frontend/postlist.nim @@ -211,13 +211,10 @@ when defined(js): let currentAdmin = currentUser.isSome() and currentUser.get().rank == Admin - if currentAdmin: - state.categoryPicker.setAddEnabled(true) - result = buildHtml(): tdiv(): if authoredByUser or currentAdmin: - render(state.categoryPicker) + render(state.categoryPicker, currentUser) else: render(thread.category) From 7337bceff3ec8b5950dc9b3dc46da302d9e41711 Mon Sep 17 00:00:00 2001 From: Joey Yakimowich-Payne Date: Sat, 15 Feb 2020 09:35:05 -0700 Subject: [PATCH 063/144] Remove casting from formdata --- src/frontend/newthread.nim | 4 +++- src/frontend/postlist.nim | 3 ++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/src/frontend/newthread.nim b/src/frontend/newthread.nim index 1d1be24..fe4e619 100644 --- a/src/frontend/newthread.nim +++ b/src/frontend/newthread.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] @@ -44,7 +45,8 @@ when defined(js): formData.append("subject", state.subject) formData.append("msg", state.replyBox.getText()) formData.append("categoryId", $categoryID) - ajaxPost(uri, @[], cast[cstring](formData), + + ajaxPost(uri, @[], formData.to(cstring), (s: int, r: kstring) => onCreatePost(s, r, state)) proc render*(state: NewThread, currentUser: Option[User]): VNode = diff --git a/src/frontend/postlist.nim b/src/frontend/postlist.nim index a259343..46dee9a 100644 --- a/src/frontend/postlist.nim +++ b/src/frontend/postlist.nim @@ -15,6 +15,7 @@ type when defined(js): from dom import document + import jsffi except `&` include karax/prelude import karax / [vstyles, kajax, kdom] @@ -76,7 +77,7 @@ when defined(js): state.loading = true - ajaxPost(uri, @[], cast[cstring](formData), + ajaxPost(uri, @[], formData.to(cstring), (s: int, r: kstring) => onCategoryPost(s, r, state)) proc onPostList(httpStatus: int, response: kstring, postId: Option[int]) = From 01d13aa0f36881c5bcddb45513ea3b3e179a8308 Mon Sep 17 00:00:00 2001 From: Joey Yakimowich-Payne Date: Sat, 15 Feb 2020 09:36:50 -0700 Subject: [PATCH 064/144] Add todo about categories query --- src/forum.nim | 1 + 1 file changed, 1 insertion(+) diff --git a/src/forum.nim b/src/forum.nim index cb069af..c635f1a 100644 --- a/src/forum.nim +++ b/src/forum.nim @@ -790,6 +790,7 @@ settings: routes: get "/categories.json": + # TODO: Limit this query in the case of many many categories const categoriesQuery = sql"""select * from category;""" var list = CategoryList(categories: @[]) From 8d317ae0e3fc773609b85c35dbce15145947d0fd Mon Sep 17 00:00:00 2001 From: Joey Yakimowich-Payne Date: Sat, 15 Feb 2020 18:41:59 -0700 Subject: [PATCH 065/144] 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 066/144] 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 067/144] 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 068/144] 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 069/144] 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 070/144] 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 071/144] 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 072/144] 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 073/144] 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 074/144] 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 075/144] 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 076/144] 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 077/144] 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 078/144] 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 079/144] 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 080/144] 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 081/144] 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 082/144] 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 083/144] 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 084/144] 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 085/144] 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 086/144] 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 087/144] 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 088/144] 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 089/144] 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 090/144] 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 091/144] 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 092/144] 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 093/144] 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 094/144] 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 095/144] 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 096/144] 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 097/144] 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 098/144] 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 099/144] 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 100/144] 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 101/144] 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 102/144] 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 103/144] 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 104/144] 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 105/144] 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 106/144] 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 107/144] 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 108/144] 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 109/144] 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 110/144] 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 111/144] 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 112/144] 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 113/144] 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 114/144] 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 115/144] 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 116/144] 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 117/144] 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 118/144] 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 119/144] 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 120/144] 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 121/144] 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 122/144] 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 123/144] 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 124/144] 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 125/144] 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 126/144] 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 127/144] 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 128/144] 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 129/144] 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 130/144] 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 131/144] 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 132/144] 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 133/144] 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 134/144] 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 135/144] 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 136/144] 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 137/144] 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 138/144] 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 139/144] 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 140/144] 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 141/144] 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 142/144] 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 143/144] 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 144/144] 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