From 9d19d70558743c4304fdc8ec84be3eabe82d2879 Mon Sep 17 00:00:00 2001 From: Joey Payne Date: Fri, 25 Jan 2019 16:40:04 -0700 Subject: [PATCH 01/77] 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 02/77] 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 03/77] 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 04/77] 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 05/77] 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 06/77] 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 07/77] 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 08/77] 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 09/77] 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 10/77] 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 11/77] 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 12/77] 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 13/77] 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 14/77] 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 15/77] 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 16/77] 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 17/77] 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 18/77] 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 19/77] 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 20/77] 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 21/77] 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 22/77] 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 23/77] 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 24/77] 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 25/77] 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 26/77] 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 27/77] 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 28/77] 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 29/77] 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 30/77] 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 31/77] 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 32/77] 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 33/77] 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 34/77] 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 35/77] 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 36/77] 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 37/77] 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 38/77] 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 39/77] 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 40/77] 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 41/77] 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 42/77] 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 43/77] 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 44/77] 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 45/77] 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 46/77] 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 47/77] 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 48/77] 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 49/77] 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 50/77] 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 51/77] 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 52/77] 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 53/77] 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 54/77] 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 55/77] 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 56/77] 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 57/77] 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 58/77] 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 59/77] 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 60/77] 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 61/77] 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 62/77] 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 63/77] 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 64/77] 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 65/77] 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 66/77] 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 67/77] 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 68/77] 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 69/77] 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 70/77] 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 71/77] 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 72/77] 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 73/77] 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 74/77] 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 75/77] 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 76/77] 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 77/77] 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