From 7cda14e9feacad0ce7b577172618a8c05917b573 Mon Sep 17 00:00:00 2001 From: Joey Yakimowich-Payne Date: Mon, 6 Aug 2018 16:22:32 +0900 Subject: [PATCH 01/26] Add frontend category picker and enable categories in threadlist --- public/css/nimforum.scss | 12 +++-- src/frontend/category.nim | 11 +++-- src/frontend/categorypicker.nim | 88 +++++++++++++++++++++++++++++++++ src/frontend/newthread.nim | 12 +++-- 4 files changed, 113 insertions(+), 10 deletions(-) create mode 100644 src/frontend/categorypicker.nim diff --git a/public/css/nimforum.scss b/public/css/nimforum.scss index e970e8e..2aa3dc6 100644 --- a/public/css/nimforum.scss +++ b/public/css/nimforum.scss @@ -206,6 +206,14 @@ $threads-meta-color: #545d70; display: inline-block; } +.square { + width: 0; + height: 0; + border: 0.3rem solid #98c766; + display: inline-block; + margin-right: 5px; +} + .load-more-separator { text-align: center; color: darken($label-color, 35%); @@ -711,10 +719,8 @@ hr { #threads-list.table { tr > th:nth-child(2), tr > td:nth-child(2) { - display: none; } } .category, div.flag-button { - display: none; -} \ No newline at end of file +} diff --git a/src/frontend/category.nim b/src/frontend/category.nim index 6c20f94..41efd95 100644 --- a/src/frontend/category.nim +++ b/src/frontend/category.nim @@ -6,6 +6,8 @@ type description*: string color*: string + CategoryList* = ref object + categories*: seq[Category] when defined(js): include karax/prelude @@ -17,12 +19,13 @@ when defined(js): result = buildHtml(): if category.name.len >= 0: tdiv(class="category", + title=category.description, "data-color"="#" & category.color): - tdiv(class="triangle", + tdiv(class="square", style=style( - (StyleAttr.borderBottom, - kstring"0.6rem solid #" & category.color) + (StyleAttr.border, + kstring"0.3rem solid #" & category.color) )) text category.name else: - span() \ No newline at end of file + span() diff --git a/src/frontend/categorypicker.nim b/src/frontend/categorypicker.nim new file mode 100644 index 0000000..d2414e6 --- /dev/null +++ b/src/frontend/categorypicker.nim @@ -0,0 +1,88 @@ +when defined(js): + import sugar, httpcore, options, json + import dom except Event + + include karax/prelude + import karax / [kajax, kdom, vstyles, vdom] + + import error, replybox, threadlist, post, category + import category, karaxutils + + type + CategoryPicker* = ref object of VComponent + list: Option[CategoryList] + selectedCategoryID*: int + loading: bool + status: HttpCode + + proc getSelectedCategory*(state: CategoryPicker): Option[Category] = + if state.list.isSome: + return some(state.list.get().categories[state.selectedCategoryID]) + return none[Category]() + + proc onCategoryList(state: CategoryPicker): proc (httpStatus: int, response: kstring) = + return proc (httpStatus: int, response: kstring) = + state.loading = false + state.status = httpStatus.HttpCode + if state.status != Http200: return + + let parsed = parseJson($response) + let list = parsed.to(CategoryList) + + if state.list.isSome: + state.list.get().categories.add(list.categories) + else: + state.list = some(list) + + if state.selectedCategoryID > state.list.get().categories.len(): + state.selectedCategoryID = 0 + + proc loadCategories(state: CategoryPicker) = + if not state.loading: + state.loading = true + ajaxGet(makeUri("categories.json"), @[], onCategoryList(state)) + + proc newCategoryPicker*(): CategoryPicker = + result = CategoryPicker( + list: none[CategoryList](), + selectedCategoryID: 0, + loading: false, + status: Http200 + ) + + proc onCategoryClick(state: CategoryPicker, category: Category): proc (ev: Event, n: VNode) = + # this is necessary to capture the right value + let cat = category + return proc (ev: Event, n: VNode) = + state.selectedCategoryID = cat.id + state.markDirty() + + proc render*(state: CategoryPicker): VNode = + if state.status != Http200: + return renderError("Couldn't retrieve categories.", state.status) + + if state.list.isNone: + state.loadCategories() + return buildHtml(tdiv(class="loading loading-lg")) + + let list = state.list.get().categories + let selectedCategory = list[state.selectedCategoryID] + + result = buildHtml(): + tdiv(id="category-selection", class="input-group"): + label(class="d-inline-block form-label"): + text "Category" + tdiv(class="dropdown"): + a(class="btn btn-link dropdown-toggle", tabindex="0"): + tdiv(class="d-inline-block"): + render(selectedCategory) + text " " + italic(class="fas fa-caret-down") + ul(class="menu"): + for category in list: + li(class="menu-item"): + a(onClick=onCategoryClick(state, category)): + render(category) + + + \ No newline at end of file diff --git a/src/frontend/newthread.nim b/src/frontend/newthread.nim index e75b6bb..9c6a79b 100644 --- a/src/frontend/newthread.nim +++ b/src/frontend/newthread.nim @@ -5,8 +5,8 @@ when defined(js): include karax/prelude import karax / [kajax, kdom] - import error, replybox, threadlist, post - import karaxutils + import error, replybox, threadlist, post, category + import karaxutils, categorypicker type NewThread* = ref object @@ -14,11 +14,13 @@ when defined(js): error: Option[PostError] replyBox: ReplyBox subject: kstring + categoryPicker: CategoryPicker proc newNewThread*(): NewThread = NewThread( replyBox: newReplyBox(nil), - subject: "" + subject: "", + categoryPicker: newCategoryPicker() ) proc onSubjectChange(e: Event, n: VNode, state: NewThread) = @@ -37,8 +39,11 @@ when defined(js): let uri = makeUri("newthread") # TODO: This is a hack, karax should support this. let formData = newFormData() + let categoryID = state.categoryPicker.selectedCategoryID + formData.append("subject", state.subject) formData.append("msg", state.replyBox.getText()) + formData.append("categoryId", $categoryID) ajaxPost(uri, @[], cast[cstring](formData), (s: int, r: kstring) => onCreatePost(s, r, state)) @@ -55,6 +60,7 @@ when defined(js): if state.error.isSome(): p(class="text-error"): text state.error.get().message + render(state.categoryPicker) renderContent(state.replyBox, none[Thread](), none[Post]()) tdiv(class="footer"): From f0bcb9abfd7e0a0fe5eafed53bbe86e04a9f0515 Mon Sep 17 00:00:00 2001 From: Joey Yakimowich-Payne Date: Mon, 6 Aug 2018 16:32:20 +0900 Subject: [PATCH 02/26] Add backend for categories --- src/forum.nim | 27 ++++++++++++++++++++++----- 1 file changed, 22 insertions(+), 5 deletions(-) diff --git a/src/forum.nim b/src/forum.nim index 399a64e..e2f7804 100644 --- a/src/forum.nim +++ b/src/forum.nim @@ -520,10 +520,10 @@ proc updatePost(c: TForumData, postId: int, content: string, if row[0] == $postId: exec(db, crud(crUpdate, "thread", "name"), subject.get(), threadId) -proc executeNewThread(c: TForumData, subject, msg: string): (int64, int64) = +proc executeNewThread(c: TForumData, subject, msg, categoryID: string): (int64, int64) = const query = sql""" - insert into thread(name, views, modified) values (?, 0, DATETIME('now')) + insert into thread(name, views, modified, category) values (?, 0, DATETIME('now'), ?) """ assert c.loggedIn() @@ -543,13 +543,17 @@ proc executeNewThread(c: TForumData, subject, msg: string): (int64, int64) = if msg.len == 0: raise newForumError("Message is empty", @["msg"]) + let catID = getInt(categoryID, -1) + if catID == -1: + raise newForumError("CategoryID is invalid", @["categoryId"]) + if not validateRst(c, msg): raise newForumError("Message needs to be valid RST", @["msg"]) if rateLimitCheck(c): raise newForumError("You're posting too fast!") - result[0] = tryInsertID(db, query, subject).int + result[0] = tryInsertID(db, query, subject, categoryID).int if result[0] < 0: raise newForumError("Subject already exists", @["subject"]) @@ -756,6 +760,18 @@ settings: routes: + get "/categories.json": + const categoriesQuery = sql"""select * from category;""" + + var list = CategoryList(categories: @[]) + for data in getAllRows(db, categoriesQuery): + let category = Category( + id: data[0].getInt, name: data[1], description: data[2], color: data[3] + ) + list.categories.add(category) + + resp $(%list), "application/json" + get "/threads.json": var start = getInt(@"start", 0) @@ -1147,13 +1163,14 @@ routes: let formData = request.formData cond "msg" in formData cond "subject" in formData + cond "categoryId" in formData let msg = formData["msg"].body let subject = formData["subject"].body - # TODO: category + let categoryID = formData["categoryId"].body try: - let res = executeNewThread(c, subject, msg) + let res = executeNewThread(c, subject, msg, categoryID) resp Http200, $(%[res[0], res[1]]), "application/json" except ForumError as exc: resp Http400, $(%exc.data), "application/json" From 5ed17333f9fa4e21e5c813b189d5144a395334a4 Mon Sep 17 00:00:00 2001 From: Joey Yakimowich-Payne Date: Mon, 6 Aug 2018 16:46:04 +0900 Subject: [PATCH 03/26] Add category id to picker --- src/frontend/categorypicker.nim | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/frontend/categorypicker.nim b/src/frontend/categorypicker.nim index d2414e6..97a2e02 100644 --- a/src/frontend/categorypicker.nim +++ b/src/frontend/categorypicker.nim @@ -81,7 +81,8 @@ when defined(js): ul(class="menu"): for category in list: li(class="menu-item"): - a(onClick=onCategoryClick(state, category)): + a(class="category-" & $category.id, + onClick=onCategoryClick(state, category)): render(category) From 3df30386d945c7e259a1ee37dab94833560cc48a Mon Sep 17 00:00:00 2001 From: Joey Yakimowich-Payne Date: Mon, 6 Aug 2018 16:54:16 +0900 Subject: [PATCH 04/26] Fix misc compiler warnings --- src/frontend/error.nim | 2 +- src/frontend/forum.nim | 2 +- src/frontend/replybox.nim | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/frontend/error.nim b/src/frontend/error.nim index 4a23c44..54d495b 100644 --- a/src/frontend/error.nim +++ b/src/frontend/error.nim @@ -86,7 +86,7 @@ when defined(js): state.error = some(error) except: - kout(getCurrentExceptionMsg().cstring) + echo getCurrentExceptionMsg() state.error = some(PostError( errorFields: @[], message: "Unknown error occurred." diff --git a/src/frontend/forum.nim b/src/frontend/forum.nim index 4dea047..5fb11b4 100644 --- a/src/frontend/forum.nim +++ b/src/frontend/forum.nim @@ -49,7 +49,7 @@ proc onPopState(event: dom.Event) = # This event is usually only called when the user moves back in their # history. I fire it in karaxutils.anchorCB as well to ensure the URL is # always updated. This should be moved into Karax in the future. - kout(kstring"New URL: ", window.location.href, " ", state.url.href) + echo "New URL: ", window.location.href, " ", state.url.href document.title = state.originalTitle if state.url.href != window.location.href: state = newState() # Reload the state to remove stale data. diff --git a/src/frontend/replybox.nim b/src/frontend/replybox.nim index 9d815f6..c7e61cc 100644 --- a/src/frontend/replybox.nim +++ b/src/frontend/replybox.nim @@ -44,7 +44,7 @@ when defined(js): proc onPreviewPost(httpStatus: int, response: kstring, state: ReplyBox) = postFinished: - kout(response) + echo response state.rendering = some[kstring](response) proc onPreviewClick(e: Event, n: VNode, state: ReplyBox) = From 495ddf5d93478b0a62d707a65ed3907e090570c4 Mon Sep 17 00:00:00 2001 From: Joey Yakimowich-Payne Date: Mon, 6 Aug 2018 16:54:55 +0900 Subject: [PATCH 05/26] Add test data for categories --- src/setup_nimforum.nim | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/src/setup_nimforum.nim b/src/setup_nimforum.nim index 5647e48..b235415 100644 --- a/src/setup_nimforum.nim +++ b/src/setup_nimforum.nim @@ -66,7 +66,7 @@ proc initialiseDb(admin: tuple[username, password, email: string], db.exec(sql""" insert into category (id, name, description, color) - values (0, 'Default', '', ''); + values (0, 'Default', 'The default category', ''); """) # -- Thread @@ -115,7 +115,7 @@ proc initialiseDb(admin: tuple[username, password, email: string], # Create default user. db.createUser(admin, Admin) - # Create test users if test or development + # Create some test data for development if isTest or isDev: for rank in Spammer..Moderator: let rankLower = toLowerAscii($rank) @@ -124,6 +124,14 @@ proc initialiseDb(admin: tuple[username, password, email: string], email: $rankLower & "@localhost.local") db.createUser(user, rank) + db.exec(sql""" + insert into category (name, description, color) + values ('Libraries', 'Libraries and library development', '0198E1'), + ('Announcements', 'Announcements by Nim core devs', 'FFEB3B'), + ('Fun', 'Posts that are just for fun', '00897B'), + ('Potential Issues', 'Potential Nim compiler issues', 'E53935'); + """) + # -- Post db.exec(sql""" From d35f1e90cfe1cdbafd10b74ce26e75cd1dd28491 Mon Sep 17 00:00:00 2001 From: Joey Yakimowich-Payne Date: Mon, 6 Aug 2018 17:21:18 +0900 Subject: [PATCH 06/26] Add name slug for categories and remove unused function --- src/frontend/categorypicker.nim | 15 +++++---------- 1 file changed, 5 insertions(+), 10 deletions(-) diff --git a/src/frontend/categorypicker.nim b/src/frontend/categorypicker.nim index 97a2e02..03abbd3 100644 --- a/src/frontend/categorypicker.nim +++ b/src/frontend/categorypicker.nim @@ -1,5 +1,5 @@ when defined(js): - import sugar, httpcore, options, json + import sugar, httpcore, options, json, strutils import dom except Event include karax/prelude @@ -15,10 +15,8 @@ when defined(js): loading: bool status: HttpCode - proc getSelectedCategory*(state: CategoryPicker): Option[Category] = - if state.list.isSome: - return some(state.list.get().categories[state.selectedCategoryID]) - return none[Category]() + proc slug(name: string): string = + name.strip().replace(" ", "-").toLowerAscii proc onCategoryList(state: CategoryPicker): proc (httpStatus: int, response: kstring) = return proc (httpStatus: int, response: kstring) = @@ -81,9 +79,6 @@ when defined(js): ul(class="menu"): for category in list: li(class="menu-item"): - a(class="category-" & $category.id, + a(class="category-" & $category.id & " " & category.name.slug, onClick=onCategoryClick(state, category)): - render(category) - - - \ No newline at end of file + render(category) \ No newline at end of file From 2e42ede2adaee4cb46d7bb6cf5768080f9cf4909 Mon Sep 17 00:00:00 2001 From: Joey Yakimowich-Payne Date: Mon, 6 Aug 2018 17:30:07 +0900 Subject: [PATCH 07/26] Add basic categories test --- tests/browsertester.nim | 3 +- tests/browsertests/categories.nim | 52 +++++++++++++++++++++++++++++++ 2 files changed, 54 insertions(+), 1 deletion(-) create mode 100644 tests/browsertests/categories.nim diff --git a/tests/browsertester.nim b/tests/browsertester.nim index 0f4efe9..82bb5cb 100644 --- a/tests/browsertester.nim +++ b/tests/browsertester.nim @@ -43,7 +43,7 @@ template withBackend(body: untyped): untyped = body -import browsertests/[scenario1, threads, issue181] +import browsertests/[scenario1, threads, issue181, categories] when isMainModule: spawn runProcess("geckodriver -p 4444 --log config") @@ -64,6 +64,7 @@ when isMainModule: withBackend: scenario1.test(session, baseUrl) threads.test(session, baseUrl) + categories.test(session, baseUrl) issue181.test(session, baseUrl) session.close() diff --git a/tests/browsertests/categories.nim b/tests/browsertests/categories.nim new file mode 100644 index 0000000..6f91f9d --- /dev/null +++ b/tests/browsertests/categories.nim @@ -0,0 +1,52 @@ +import unittest, options, os, common + +import webdriver + +proc selectCategory(session: Session, name: string) = + with session: + click "#category-selection .dropdown-toggle" + + click "#category-selection ." & name + + +proc categoriesTests(session: Session, baseUrl: string) = + let + title = "Category Test" + content = "Choosing category test" + + with session: + navigate baseUrl + wait() + login "user", "user" + + test "can create category thread": + with session: + click "#new-thread-btn" + wait() + + sendKeys "#thread-title", title + + selectCategory "fun" + + sendKeys "#reply-textarea", content + + click "#create-thread-btn" + wait() + + checkText "#thread-title .category", "Fun" + + navigate baseUrl + wait() + + ensureExists title, LinkTextSelector + + session.logout() + +proc test*(session: Session, baseUrl: string) = + session.navigate(baseUrl) + session.wait() + + categoriesTests(session, baseUrl) + + session.navigate(baseUrl) + session.wait() \ No newline at end of file From b0639c4da2de5e9dc1a8a4a5972edc9e3f3ec33f Mon Sep 17 00:00:00 2001 From: Joey Yakimowich-Payne Date: Tue, 7 Aug 2018 20:25:35 +0900 Subject: [PATCH 08/26] Add backend for updating thread --- src/forum.nim | 35 +++++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/src/forum.nim b/src/forum.nim index e2f7804..3cd6bdc 100644 --- a/src/forum.nim +++ b/src/forum.nim @@ -1151,6 +1151,41 @@ routes: except ForumError as exc: resp Http400, $(%exc.data), "application/json" + post "/updateThread": + createTFD() + if not c.loggedIn(): + let err = PostError( + errorFields: @[], + message: "Not logged in." + ) + resp Http401, $(%err), "application/json" + + let formData = request.formData + + cond "threadId" in formData + + let threadId = formData["threadId"].body + + let keys = ["name", "views", "modified", "category", "isLocked", "solution", "isDeleted"] + + # optional parameters + var + queryValues: seq[string] = @[] + queryKeys: seq[string] = @[] + + for key in keys: + if key in formData: + queryKeys.add(key) + queryValues.add(formData[key].body) + + if queryKeys.len() > 0: + queryValues.add(threadId) + try: + exec(db, crud(crUpdate, "thread", queryKeys), queryValues) + resp Http200, "{}", "application/json" + except ForumError as exc: + resp Http400, $(%exc.data), "application/json" + post "/newthread": createTFD() if not c.loggedIn(): From cb7418f8251cf723fa4bb915aa8787eeb607abe1 Mon Sep 17 00:00:00 2001 From: Joey Yakimowich-Payne Date: Tue, 7 Aug 2018 20:26:48 +0900 Subject: [PATCH 09/26] Add frontend for category picker when admin or thread author --- src/frontend/categorypicker.nim | 21 ++++++++++++--- src/frontend/postlist.nim | 46 +++++++++++++++++++++++++++++++-- 2 files changed, 61 insertions(+), 6 deletions(-) diff --git a/src/frontend/categorypicker.nim b/src/frontend/categorypicker.nim index 03abbd3..fd9063f 100644 --- a/src/frontend/categorypicker.nim +++ b/src/frontend/categorypicker.nim @@ -14,6 +14,7 @@ when defined(js): selectedCategoryID*: int loading: bool status: HttpCode + onCategoryChange: proc (oldCategory: Category, newCategory: Category) proc slug(name: string): string = name.strip().replace(" ", "-").toLowerAscii @@ -40,20 +41,32 @@ when defined(js): state.loading = true ajaxGet(makeUri("categories.json"), @[], onCategoryList(state)) - proc newCategoryPicker*(): CategoryPicker = + proc `[]`*(state: CategoryPicker, id: int): Category = + state.list.get().categories[id] + + proc newCategoryPicker*( + onCategoryChange: proc (oldCategory: Category, newCategory: Category) = + proc (oldCategory: Category, newCategory: Category) = discard + ): CategoryPicker = result = CategoryPicker( list: none[CategoryList](), selectedCategoryID: 0, loading: false, - status: Http200 + status: Http200, + onCategoryChange: onCategoryChange ) + proc select*(state: CategoryPicker, id: int) = + state.selectedCategoryID = id + state.markDirty() + proc onCategoryClick(state: CategoryPicker, category: Category): proc (ev: Event, n: VNode) = # this is necessary to capture the right value let cat = category return proc (ev: Event, n: VNode) = - state.selectedCategoryID = cat.id - state.markDirty() + let oldCategory = state[state.selectedCategoryID] + state.select(cat.id) + state.onCategoryChange(oldCategory, cat) proc render*(state: CategoryPicker): VNode = if state.status != Http200: diff --git a/src/frontend/postlist.nim b/src/frontend/postlist.nim index 6a27df3..610e245 100644 --- a/src/frontend/postlist.nim +++ b/src/frontend/postlist.nim @@ -20,12 +20,14 @@ when defined(js): import karax / [vstyles, kajax, kdom] import karaxutils, error, replybox, editbox, postbutton, delete + import categorypicker type State = ref object list: Option[PostList] loading: bool status: HttpCode + error: Option[PostError] replyingTo: Option[Post] replyBox: ReplyBox editing: Option[Post] ## If in edit mode, this contains the post. @@ -33,8 +35,10 @@ when defined(js): likeButton: LikeButton deleteModal: DeleteModal lockButton: LockButton + categoryPicker: CategoryPicker proc onReplyPosted(id: int) + proc onCategoryChanged(oldCategory: Category, newCategory: Category) proc onEditPosted(id: int, content: string, subject: Option[string]) proc onEditCancelled() proc onDeletePost(post: Post) @@ -44,17 +48,37 @@ when defined(js): list: none[PostList](), loading: false, status: Http200, + error: none[PostError](), replyingTo: none[Post](), replyBox: newReplyBox(onReplyPosted), editBox: newEditBox(onEditPosted, onEditCancelled), likeButton: newLikeButton(), deleteModal: newDeleteModal(onDeletePost, onDeleteThread, nil), - lockButton: newLockButton() + lockButton: newLockButton(), + categoryPicker: newCategoryPicker(onCategoryChanged) ) var state = newState() + proc onCategoryPost(httpStatus: int, response: kstring, state: State) = + state.loading = false + postFinished: + discard + # TODO: show success message + + proc onCategoryChanged(oldCategory: Category, newCategory: Category) = + let uri = makeUri("/updateThread") + + let formData = newFormData() + formData.append("threadId", $state.list.get().thread.id) + formData.append("category", $newCategory.id) + + state.loading = true + + ajaxPost(uri, @[], cast[cstring](formData), + (s: int, r: kstring) => onCategoryPost(s, r, state)) + proc onPostList(httpStatus: int, response: kstring, postId: Option[int]) = state.loading = false state.status = httpStatus.HttpCode @@ -66,6 +90,7 @@ when defined(js): state.list = some(list) dom.document.title = list.thread.topic & " - " & dom.document.title + state.categoryPicker.select(list.thread.category.id) # The anchor should be jumped to once all the posts have been loaded. if postId.isSome(): @@ -179,6 +204,20 @@ when defined(js): span(class="more-post-count"): text "(" & $post.moreBefore.len & ")" + proc genCategories(thread: Thread, currentUser: Option[User]): VNode = + let loggedIn = currentUser.isSome() + let authoredByUser = + loggedIn and currentUser.get().name == thread.author.name + let currentAdmin = + currentUser.isSome() and currentUser.get().rank == Admin + + result = buildHtml(): + tdiv(): + if authoredByUser or currentAdmin: + render(state.categoryPicker) + else: + render(thread.category) + proc genPostButtons(post: Post, currentUser: Option[User]): Vnode = let loggedIn = currentUser.isSome() let authoredByUser = @@ -330,6 +369,9 @@ when defined(js): result = buildHtml(): section(class="container grid-xl"): tdiv(id="thread-title", class="title"): + if state.error.isSome(): + span(class="text-error"): + text state.error.get().message p(): text list.thread.topic if list.thread.isLocked: italic(class="fas fa-lock fa-xs", @@ -343,7 +385,7 @@ when defined(js): italic(class="fas fa-check-square fa-xs", title="Thread has a solution") text "Solved" - render(list.thread.category) + genCategories(list.thread, currentUser) tdiv(class="posts"): var prevPost: Option[Post] = none[Post]() for i, post in list.posts: From a05667ef78587119aa0205b81f6a293d0dc018e9 Mon Sep 17 00:00:00 2001 From: Joey Yakimowich-Payne Date: Tue, 7 Aug 2018 21:03:05 +0900 Subject: [PATCH 10/26] Add server side checking for user permissions --- src/forum.nim | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/src/forum.nim b/src/forum.nim index 3cd6bdc..fc6e81a 100644 --- a/src/forum.nim +++ b/src/forum.nim @@ -520,6 +520,16 @@ proc updatePost(c: TForumData, postId: int, content: string, if row[0] == $postId: exec(db, crud(crUpdate, "thread", "name"), subject.get(), threadId) +proc updateThread(c: TForumData, threadId: string, queryKeys: seq[string], queryValues: seq[string]) = + let threadAuthor = selectThreadAuthor(threadId.parseInt) + + # Verify that the current user has permissions to edit the specified thread. + let canEdit = c.rank == Admin or c.userid == threadAuthor.name + if not canEdit: + raise newForumError("You cannot edit this thread") + + exec(db, crud(crUpdate, "thread", queryKeys), queryValues) + proc executeNewThread(c: TForumData, subject, msg, categoryID: string): (int64, int64) = const query = sql""" @@ -1181,7 +1191,7 @@ routes: if queryKeys.len() > 0: queryValues.add(threadId) try: - exec(db, crud(crUpdate, "thread", queryKeys), queryValues) + updateThread(c, threadId, queryKeys, queryValues) resp Http200, "{}", "application/json" except ForumError as exc: resp Http400, $(%exc.data), "application/json" From 3b681e32f6de19298d424d01e473ffe5e0aaa874 Mon Sep 17 00:00:00 2001 From: Joey Yakimowich-Payne Date: Wed, 8 Aug 2018 18:22:41 +0900 Subject: [PATCH 11/26] Add backend for creating a category --- src/forum.nim | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/src/forum.nim b/src/forum.nim index fc6e81a..ac67c51 100644 --- a/src/forum.nim +++ b/src/forum.nim @@ -665,6 +665,12 @@ proc executeLike(c: TForumData, postId: int) = # Save the like. exec(db, crud(crCreate, "like", "author", "post"), c.userid, postId) +proc executeNewCategory(c: TForumData, name, color, description: string): int64 = + if name.len == 0: + raise newForumError("Category name must not be empty!", @["name"]) + + result = insertID(db, crud(crCreate, "category", "name", "color", "description"), name, color, description) + proc executeUnlike(c: TForumData, postId: int) = # Verify the post and like exists for the current user. const likeQuery = sql""" @@ -1051,6 +1057,21 @@ routes: except ForumError as exc: resp Http400, $(%exc.data), "application/json" + post "/createCategory": + createTFD() + let formData = request.formData + + let name = formData["name"].body + let color = formData["color"].body.replace("#", "") + let description = formData["description"].body + + try: + let id = executeNewCategory(c, name, color, description) + let category = Category(id: id.int, name: name, color: color, description: description) + resp Http200, $(%category), "application/json" + except ForumError as exc: + resp Http400, $(%exc.data), "application/json" + get "/status.json": createTFD() From f1c5db2cedda43d1f66430c80abeb3d667453759 Mon Sep 17 00:00:00 2001 From: Joey Yakimowich-Payne Date: Wed, 8 Aug 2018 18:25:37 +0900 Subject: [PATCH 12/26] Add frontend for adding a category with the category picker --- public/css/nimforum.scss | 28 +++++--- src/frontend/category.nim | 3 + src/frontend/categorypicker.nim | 111 ++++++++++++++++++++++++++++---- src/frontend/newthread.nim | 5 +- src/frontend/postlist.nim | 3 + 5 files changed, 128 insertions(+), 22 deletions(-) diff --git a/public/css/nimforum.scss b/public/css/nimforum.scss index 2aa3dc6..98b4a04 100644 --- a/public/css/nimforum.scss +++ b/public/css/nimforum.scss @@ -107,6 +107,24 @@ $logo-height: $navbar-height - 20px; } } +#category-selection { + .dropdown { + .btn { + margin-right: 0px; + } + } + .plus-btn { + margin-right: 0px; + i { + margin-right: 0px; + } + } +} + +.category { + white-space: nowrap; +} + #new-thread { .modal-container .modal-body { max-height: none; @@ -715,12 +733,4 @@ hr { // - Hide features that have not been implemented yet. #main-buttons > section.navbar-section:nth-child(1) { display: none; -} - -#threads-list.table { - tr > th:nth-child(2), tr > td:nth-child(2) { - } -} - -.category, div.flag-button { -} +} \ No newline at end of file diff --git a/src/frontend/category.nim b/src/frontend/category.nim index 41efd95..1d18c97 100644 --- a/src/frontend/category.nim +++ b/src/frontend/category.nim @@ -9,6 +9,9 @@ type CategoryList* = ref object categories*: seq[Category] +proc cmpNames*(cat1: Category, cat2: Category): int = + cat1.name.cmp(cat2.name) + when defined(js): include karax/prelude import karax / [vstyles, kajax, kdom] diff --git a/src/frontend/categorypicker.nim b/src/frontend/categorypicker.nim index fd9063f..799eab9 100644 --- a/src/frontend/categorypicker.nim +++ b/src/frontend/categorypicker.nim @@ -1,5 +1,5 @@ when defined(js): - import sugar, httpcore, options, json, strutils + import sugar, httpcore, options, json, strutils, algorithm import dom except Event include karax/prelude @@ -13,13 +13,17 @@ when defined(js): list: Option[CategoryList] selectedCategoryID*: int loading: bool + modalShown: bool + addEnabled: bool status: HttpCode + error: Option[PostError] onCategoryChange: proc (oldCategory: Category, newCategory: Category) + onAddCategory: proc (category: Category) proc slug(name: string): string = name.strip().replace(" ", "-").toLowerAscii - proc onCategoryList(state: CategoryPicker): proc (httpStatus: int, response: kstring) = + proc onCategoryLoad(state: CategoryPicker): proc (httpStatus: int, response: kstring) = return proc (httpStatus: int, response: kstring) = state.loading = false state.status = httpStatus.HttpCode @@ -27,6 +31,7 @@ when defined(js): let parsed = parseJson($response) let list = parsed.to(CategoryList) + list.categories.sort(cmpNames) if state.list.isSome: state.list.get().categories.add(list.categories) @@ -39,23 +44,36 @@ when defined(js): proc loadCategories(state: CategoryPicker) = if not state.loading: state.loading = true - ajaxGet(makeUri("categories.json"), @[], onCategoryList(state)) + ajaxGet(makeUri("categories.json"), @[], onCategoryLoad(state)) proc `[]`*(state: CategoryPicker, id: int): Category = - state.list.get().categories[id] + for cat in state.list.get().categories: + if cat.id == id: + return cat + raise newException(IndexError, "Category at " & $id & " not found!") + + proc nullAddCategory(category: Category) = discard + proc nullCategoryChange(oldCategory: Category, newCategory: Category) = discard proc newCategoryPicker*( - onCategoryChange: proc (oldCategory: Category, newCategory: Category) = - proc (oldCategory: Category, newCategory: Category) = discard + onCategoryChange: proc(oldCategory: Category, newCategory: Category) = nullCategoryChange, + onAddCategory: proc(category: Category) = nullAddCategory ): CategoryPicker = result = CategoryPicker( list: none[CategoryList](), selectedCategoryID: 0, loading: false, + modalShown: false, + addEnabled: false, status: Http200, - onCategoryChange: onCategoryChange + error: none[PostError](), + onCategoryChange: onCategoryChange, + onAddCategory: onAddCategory ) + proc setAddEnabled*(state: CategoryPicker, enabled: bool) = + state.addEnabled = enabled + proc select*(state: CategoryPicker, id: int) = state.selectedCategoryID = id state.markDirty() @@ -68,6 +86,75 @@ when defined(js): state.select(cat.id) state.onCategoryChange(oldCategory, cat) + proc onAddCategoryPost(httpStatus: int, response: kstring, state: CategoryPicker) = + postFinished: + state.modalShown = false + let j = parseJson($response) + let category = j.to(Category) + + state.list.get().categories.add(category) + state.list.get().categories.sort(cmpNames) + state.select(category.id) + + state.onAddCategory(category) + + proc onAddCategoryClick(state: CategoryPicker) = + state.loading = true + state.error = none[PostError]() + + let uri = makeUri("createCategory") + let form = dom.document.getElementById("add-category-form") + let formData = newFormData(form) + + ajaxPost(uri, @[], cast[cstring](formData), + (s: int, r: kstring) => onAddCategoryPost(s, r, state)) + + proc onClose(ev: Event, n: VNode, state: CategoryPicker) = + state.modalShown = false + state.markDirty() + ev.preventDefault() + + proc genAddCategory(state: CategoryPicker): VNode = + result = buildHtml(): + tdiv(id="add-category"): + button(class="plus-btn btn btn-link", + onClick=(ev: Event, n: VNode) => ( + state.modalShown = true; + state.markDirty() + )): + italic(class="fas fa-plus") + tdiv(class=class({"active": state.modalShown}, "modal modal-sm")): + a(href="", class="modal-overlay", "aria-label"="close", + onClick=(ev: Event, n: VNode) => onClose(ev, n, state)) + tdiv(class="modal-container"): + tdiv(class="modal-header"): + tdiv(class="card-title h5"): + text "Add New Category" + tdiv(class="modal-body"): + form(id="add-category-form"): + genFormField( + state.error, "name", "Name", "text", false, + placeholder="Category Name") + genFormField( + state.error, "color", "Color", "color", false, + placeholder="#XXYYZZ" + ) + genFormField( + state.error, + "description", + "Description", + "text", + true, + placeholder="Description" + ) + tdiv(class="modal-footer"): + button( + id="add-category-btn", + class="btn btn-primary", + onClick=(ev: Event, n: VNode) => + state.onAddCategoryClick()): + text "Add" + proc render*(state: CategoryPicker): VNode = if state.status != Http200: return renderError("Couldn't retrieve categories.", state.status) @@ -77,15 +164,13 @@ when defined(js): return buildHtml(tdiv(class="loading loading-lg")) let list = state.list.get().categories - let selectedCategory = list[state.selectedCategoryID] + let selectedCategory = state[state.selectedCategoryID] result = buildHtml(): tdiv(id="category-selection", class="input-group"): - label(class="d-inline-block form-label"): - text "Category" tdiv(class="dropdown"): a(class="btn btn-link dropdown-toggle", tabindex="0"): - tdiv(class="d-inline-block"): + tdiv(class="selected-category d-inline-block"): render(selectedCategory) text " " italic(class="fas fa-caret-down") @@ -94,4 +179,6 @@ when defined(js): li(class="menu-item"): a(class="category-" & $category.id & " " & category.name.slug, onClick=onCategoryClick(state, category)): - render(category) \ No newline at end of file + render(category) + if state.addEnabled: + genAddCategory(state) \ No newline at end of file diff --git a/src/frontend/newthread.nim b/src/frontend/newthread.nim index 9c6a79b..8ef7066 100644 --- a/src/frontend/newthread.nim +++ b/src/frontend/newthread.nim @@ -60,7 +60,10 @@ when defined(js): if state.error.isSome(): p(class="text-error"): text state.error.get().message - render(state.categoryPicker) + tdiv(): + label(class="d-inline-block form-label"): + text "Category" + render(state.categoryPicker) renderContent(state.replyBox, none[Thread](), none[Post]()) tdiv(class="footer"): diff --git a/src/frontend/postlist.nim b/src/frontend/postlist.nim index 610e245..6349dab 100644 --- a/src/frontend/postlist.nim +++ b/src/frontend/postlist.nim @@ -211,6 +211,9 @@ when defined(js): let currentAdmin = currentUser.isSome() and currentUser.get().rank == Admin + if currentAdmin: + state.categoryPicker.setAddEnabled(true) + result = buildHtml(): tdiv(): if authoredByUser or currentAdmin: From 82463ea42370aed7fc1edc0b648e337f11d4dd1b Mon Sep 17 00:00:00 2001 From: Joey Yakimowich-Payne Date: Wed, 8 Aug 2018 19:09:54 +0900 Subject: [PATCH 13/26] Add server check for category adding --- src/forum.nim | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/forum.nim b/src/forum.nim index ac67c51..6e35d03 100644 --- a/src/forum.nim +++ b/src/forum.nim @@ -666,6 +666,12 @@ proc executeLike(c: TForumData, postId: int) = exec(db, crud(crCreate, "like", "author", "post"), c.userid, postId) proc executeNewCategory(c: TForumData, name, color, description: string): int64 = + + let canAdd = c.rank == Admin + + if not canAdd: + raise newForumError("You do not have permissions to add a category.") + if name.len == 0: raise newForumError("Category name must not be empty!", @["name"]) From d5df46823a8ebbcc3f5f990d7d3dc8ce7d0241f1 Mon Sep 17 00:00:00 2001 From: Joey Yakimowich-Payne Date: Wed, 8 Aug 2018 19:10:46 +0900 Subject: [PATCH 14/26] Add category add button on new thread --- src/frontend/forum.nim | 2 +- src/frontend/newthread.nim | 12 ++++++++++-- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/src/frontend/forum.nim b/src/frontend/forum.nim index 5fb11b4..efb072e 100644 --- a/src/frontend/forum.nim +++ b/src/frontend/forum.nim @@ -83,7 +83,7 @@ proc render(): VNode = route([ r("/newthread", (params: Params) => - (render(state.newThread)) + (render(state.newThread, getLoggedInUser())) ), r("/profile/@username", (params: Params) => diff --git a/src/frontend/newthread.nim b/src/frontend/newthread.nim index 8ef7066..6d44381 100644 --- a/src/frontend/newthread.nim +++ b/src/frontend/newthread.nim @@ -5,7 +5,7 @@ when defined(js): include karax/prelude import karax / [kajax, kdom] - import error, replybox, threadlist, post, category + import error, replybox, threadlist, post, category, user import karaxutils, categorypicker type @@ -47,7 +47,15 @@ when defined(js): ajaxPost(uri, @[], cast[cstring](formData), (s: int, r: kstring) => onCreatePost(s, r, state)) - proc render*(state: NewThread): VNode = + proc render*(state: NewThread, currentUser: Option[User]): VNode = + + let loggedIn = currentUser.isSome() + let currentAdmin = + currentUser.isSome() and currentUser.get().rank == Admin + + if currentAdmin: + state.categoryPicker.setAddEnabled(true) + result = buildHtml(): section(class="container grid-xl"): tdiv(id="new-thread"): From 0af291dc102969da8d230a7d23202f972d9caf1f Mon Sep 17 00:00:00 2001 From: Joey Yakimowich-Payne Date: Wed, 8 Aug 2018 19:12:28 +0900 Subject: [PATCH 15/26] Add test for category adding --- tests/browsertests/categories.nim | 99 ++++++++++++++++++++++++------- 1 file changed, 76 insertions(+), 23 deletions(-) diff --git a/tests/browsertests/categories.nim b/tests/browsertests/categories.nim index 6f91f9d..7924ac4 100644 --- a/tests/browsertests/categories.nim +++ b/tests/browsertests/categories.nim @@ -9,44 +9,97 @@ proc selectCategory(session: Session, name: string) = click "#category-selection ." & name -proc categoriesTests(session: Session, baseUrl: string) = +proc categoriesUserTests(session: Session, baseUrl: string) = let title = "Category Test" content = "Choosing category test" - with session: - navigate baseUrl - wait() - login "user", "user" + suite "user tests": - test "can create category thread": with session: - click "#new-thread-btn" - wait() - - sendKeys "#thread-title", title - - selectCategory "fun" - - sendKeys "#reply-textarea", content - - click "#create-thread-btn" - wait() - - checkText "#thread-title .category", "Fun" - navigate baseUrl wait() + login "user", "user" - ensureExists title, LinkTextSelector + setup: + with session: + navigate baseUrl + wait() - session.logout() + test "no category add available": + with session: + click "#new-thread-btn" + wait() + + checkIsNone "#add-category" + + test "can create category thread": + with session: + click "#new-thread-btn" + wait() + + sendKeys "#thread-title", title + + selectCategory "fun" + + sendKeys "#reply-textarea", content + + click "#create-thread-btn" + wait() + + checkText "#thread-title .category", "Fun" + + navigate baseUrl + wait() + + ensureExists title, LinkTextSelector + + session.logout() + +proc categoriesAdminTests(session: Session, baseUrl: string) = + let + name = "Category Test" + color = "Creating category test" + description = "This is a description" + + suite "admin tests": + with session: + navigate baseUrl + wait() + login "admin", "admin" + + test "can create category": + with session: + click "#new-thread-btn" + wait() + + ensureExists "#add-category" + + click "#add-category .plus-btn" + wait() + + clear "#add-category input[name='name']" + clear "#add-category input[name='color']" + clear "#add-category input[name='description']" + + + sendKeys "#add-category input[name='name']", name + sendKeys "#add-category input[name='color']", color + sendKeys "#add-category input[name='description']", description + + click "#add-category #add-category-btn" + wait() + + checkText "#category-selection .selected-category", name + + session.logout() proc test*(session: Session, baseUrl: string) = session.navigate(baseUrl) session.wait() - categoriesTests(session, baseUrl) + categoriesUserTests(session, baseUrl) + categoriesAdminTests(session, baseUrl) session.navigate(baseUrl) session.wait() \ No newline at end of file From 796d8ee20c60a2b6f5f200cb845741f23b8e50c5 Mon Sep 17 00:00:00 2001 From: Joey Yakimowich-Payne Date: Wed, 8 Aug 2018 21:41:20 +0900 Subject: [PATCH 16/26] Fix tests --- src/frontend/postlist.nim | 2 +- tests/browsertests/threads.nim | 12 ++++++++++-- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/src/frontend/postlist.nim b/src/frontend/postlist.nim index 6349dab..3076a2a 100644 --- a/src/frontend/postlist.nim +++ b/src/frontend/postlist.nim @@ -375,7 +375,7 @@ when defined(js): if state.error.isSome(): span(class="text-error"): text state.error.get().message - p(): text list.thread.topic + p(class="title-text"): text list.thread.topic if list.thread.isLocked: italic(class="fas fa-lock fa-xs", title="Thread cannot be replied to") diff --git a/tests/browsertests/threads.nim b/tests/browsertests/threads.nim index f0b1b32..45b971b 100644 --- a/tests/browsertests/threads.nim +++ b/tests/browsertests/threads.nim @@ -15,6 +15,12 @@ proc banUser(session: Session, baseUrl: string) = setUserRank baseUrl, "user", "banned" logout() +proc unBanUser(session: Session, baseUrl: string) = + with session: + login "admin", "admin" + setUserRank baseUrl, "user", "user" + logout() + proc userTests(session: Session, baseUrl: string) = suite "user thread tests": session.login("user", "user") @@ -34,7 +40,7 @@ proc userTests(session: Session, baseUrl: string) = click "#create-thread-btn" wait() - checkText "#thread-title", userTitleStr + checkText "#thread-title .title-text", userTitleStr checkText ".original-post div.post-content", userContentStr session.logout() @@ -99,7 +105,7 @@ proc adminTests(session: Session, baseUrl: string) = click "#create-thread-btn" wait() - checkText "#thread-title", adminTitleStr + checkText "#thread-title .title-text", adminTitleStr checkText ".original-post div.post-content", adminContentStr test "try create duplicate thread": @@ -172,5 +178,7 @@ proc test*(session: Session, baseUrl: string) = anonymousTests(session, baseUrl) adminTests(session, baseUrl) + unBanUser(session, baseUrl) + session.navigate(baseUrl) session.wait() From 416d2601fb3ae605a11b0a919fc16e385ef63b39 Mon Sep 17 00:00:00 2001 From: Joey Yakimowich-Payne Date: Thu, 9 Aug 2018 09:51:37 +0900 Subject: [PATCH 17/26] Fix another test issue --- tests/browsertests/common.nim | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/browsertests/common.nim b/tests/browsertests/common.nim index 2c60840..b3a093c 100644 --- a/tests/browsertests/common.nim +++ b/tests/browsertests/common.nim @@ -147,5 +147,5 @@ proc createThread*(session: Session, title, content: string) = click "#create-thread-btn" wait() - checkText "#thread-title", title + checkText "#thread-title .title-text", title checkText ".original-post div.post-content", content From 4e1b906b4988f7193e72d4914e75788cd0f41ea9 Mon Sep 17 00:00:00 2001 From: Joey Yakimowich-Payne Date: Thu, 9 Aug 2018 10:14:03 +0900 Subject: [PATCH 18/26] Rename square to category --- public/css/nimforum.scss | 12 +----------- src/frontend/category.nim | 2 +- 2 files changed, 2 insertions(+), 12 deletions(-) diff --git a/public/css/nimforum.scss b/public/css/nimforum.scss index 98b4a04..ead7b0e 100644 --- a/public/css/nimforum.scss +++ b/public/css/nimforum.scss @@ -214,17 +214,7 @@ $threads-meta-color: #545d70; } } -.triangle { - // TODO: Abstract this into a "category" class. - width: 0; - height: 0; - border-left: 0.3rem solid transparent; - border-right: 0.3rem solid transparent; - border-bottom: 0.6rem solid #98c766; - display: inline-block; -} - -.square { +.category-color { width: 0; height: 0; border: 0.3rem solid #98c766; diff --git a/src/frontend/category.nim b/src/frontend/category.nim index 1d18c97..de2d248 100644 --- a/src/frontend/category.nim +++ b/src/frontend/category.nim @@ -24,7 +24,7 @@ when defined(js): tdiv(class="category", title=category.description, "data-color"="#" & category.color): - tdiv(class="square", + tdiv(class="category-color", style=style( (StyleAttr.border, kstring"0.3rem solid #" & category.color) From 41a1a36dbfea56476ac66ae7aa6e8b848b7c0f85 Mon Sep 17 00:00:00 2001 From: Joey Payne Date: Thu, 24 Jan 2019 20:37:03 -0700 Subject: [PATCH 19/26] Fix issues compiling and testing --- README.md | 20 ++++++++++++++++++++ nimforum.nimble | 6 +++--- src/auth.nim | 8 ++++---- src/forum.nim | 11 +++++------ src/frontend/threadlist.nim | 2 +- tests/browsertester.nim | 5 ++++- 6 files changed, 37 insertions(+), 15 deletions(-) diff --git a/README.md b/README.md index c8057cd..37d7881 100644 --- a/README.md +++ b/README.md @@ -63,6 +63,26 @@ test Runs tester fasttest Runs tester without recompiling backend ``` +To get up and running: + +```bash +git clone https://github.com/nim-lang/nimforum +cd nimforum +git submodule update --init --recursive + +nimble install + +# Setup the db with user: admin, pass: admin and some other users +nimble devdb + +# Run this again if frontend code changes +nimble frontend + +# Will start a server at localhost:5000 +nimble backend +``` + + Development typically involves running `nimble devdb` which sets up the database for development and testing, then `nimble backend` which compiles and runs the forum's backend, and `nimble frontend` diff --git a/nimforum.nimble b/nimforum.nimble index 1ea21bd..79c43af 100644 --- a/nimforum.nimble +++ b/nimforum.nimble @@ -1,5 +1,5 @@ # Package -version = "2.0.1" +version = "2.1.0" author = "Dominik Picheta" description = "The Nim forum" license = "MIT" @@ -13,13 +13,13 @@ skipExt = @["nim"] # Dependencies requires "nim >= 0.18.1" -requires "jester 0.4.0" +requires "jester#22f8240" requires "bcrypt#head" requires "hmac#9c61ebe2fd134cf97" requires "recaptcha 1.0.2" requires "sass#649e0701fa5c" -requires "karax#d8df257dd" +requires "karax#c8c7b13" requires "webdriver#20f3c1b" diff --git a/src/auth.nim b/src/auth.nim index 0b08bfe..381b666 100644 --- a/src/auth.nim +++ b/src/auth.nim @@ -71,13 +71,13 @@ when isMainModule: "test", "$2a$08$bY85AhoD1e9u0IsD9sM7Ee6kFSLeXRLxJ6rMgfb1wDnU9liaymoTG", 1526908753, - "*B2a] IL\"~sh)q-GBd/i$^>.TL]PR~>1IX>Fp-:M3pCm^cFD\um" + "*B2a] IL\"~sh)q-GBd/i$^>.TL]PR~>1IX>Fp-:M3pCm^cFD\\um" ) let ident2 = makeIdentHash( "test", "$2a$08$bY85AhoD1e9u0IsD9sM7Ee6kFSLeXRLxJ6rMgfb1wDnU9liaymoTG", 1526908753, - "*B2a] IL\"~sh)q-GBd/i$^>.TL]PR~>1IX>Fp-:M3pCm^cFD\um" + "*B2a] IL\"~sh)q-GBd/i$^>.TL]PR~>1IX>Fp-:M3pCm^cFD\\um" ) doAssert ident == ident2 @@ -85,6 +85,6 @@ when isMainModule: "test", "$2a$08$bY85AhoD1e9u0IsD9sM7Ee6kFSLeXRLxJ6rMgfb1wDnU9liaymoTG", 1526908754, - "*B2a] IL\"~sh)q-GBd/i$^>.TL]PR~>1IX>Fp-:M3pCm^cFD\um" + "*B2a] IL\"~sh)q-GBd/i$^>.TL]PR~>1IX>Fp-:M3pCm^cFD\\um" ) - doAssert ident != invalid \ No newline at end of file + doAssert ident != invalid diff --git a/src/forum.nim b/src/forum.nim index 6e35d03..303d801 100644 --- a/src/forum.nim +++ b/src/forum.nim @@ -8,7 +8,7 @@ import system except Thread import os, strutils, times, md5, strtabs, math, db_sqlite, - scgi, jester, asyncdispatch, asyncnet, sequtils, + jester, asyncdispatch, asyncnet, sequtils, parseutils, random, rst, recaptcha, json, re, sugar, strformat, logging import cgi except setCookie @@ -76,7 +76,6 @@ proc getGravatarUrl(email: string, size = 80): string = # ----------------------------------------------------------------------------- -template `||`(x: untyped): untyped = (if not isNil(x): x else: "") proc validateCaptcha(recaptchaResp, ip: string) {.async.} = # captcha validation: @@ -133,9 +132,9 @@ proc checkLoggedIn(c: TForumData) = let row = getRow(db, sql"select name, email, status from person where id = ?", c.userid) - c.username = ||row[0] - c.email = ||row[1] - c.rank = parseEnum[Rank](||row[2]) + c.username = row[0] + c.email = row[1] + c.rank = parseEnum[Rank](row[2]) # In order to handle the "last visit" line appropriately, i.e. # it shouldn't disappear after a refresh, we need to manage a @@ -463,7 +462,7 @@ proc executeReply(c: TForumData, threadId: int, content: string, crud(crCreate, "post", "author", "ip", "content", "thread", "replyingTo"), c.userId, c.req.ip, content, $threadId, if replyingTo.isSome(): $replyingTo.get() - else: nil + else: "" ) discard tryExec( db, diff --git a/src/frontend/threadlist.nim b/src/frontend/threadlist.nim index 0516d74..b3b3d81 100644 --- a/src/frontend/threadlist.nim +++ b/src/frontend/threadlist.nim @@ -60,7 +60,7 @@ when defined(js): if user.isNone(): return not thread.isModerated let rank = user.get().rank - if rank < Moderator and thread.isModerated: + if rank < Rank.Moderator and thread.isModerated: return thread.author == user.get() return true diff --git a/tests/browsertester.nim b/tests/browsertester.nim index 82bb5cb..6a5c374 100644 --- a/tests/browsertester.nim +++ b/tests/browsertester.nim @@ -45,7 +45,7 @@ template withBackend(body: untyped): untyped = import browsertests/[scenario1, threads, issue181, categories] -when isMainModule: +proc main() = spawn runProcess("geckodriver -p 4444 --log config") defer: discard execCmd("killall geckodriver") @@ -71,3 +71,6 @@ when isMainModule: except: sleep(10000) # See if we can grab any more output. raise + +when isMainModule: + main() From de7b391d114172202b3e38aa3991ab17334e6f2c Mon Sep 17 00:00:00 2001 From: Joey Yakimowich-Payne Date: Sat, 15 Feb 2020 08:36:08 -0700 Subject: [PATCH 20/26] Remove unnecessary readme line --- README.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/README.md b/README.md index 37d7881..05d4667 100644 --- a/README.md +++ b/README.md @@ -70,8 +70,6 @@ git clone https://github.com/nim-lang/nimforum cd nimforum git submodule update --init --recursive -nimble install - # Setup the db with user: admin, pass: admin and some other users nimble devdb From 46d6a3b6bb45681caf4ff9bfcde3c4de48308f71 Mon Sep 17 00:00:00 2001 From: Joey Yakimowich-Payne Date: Sat, 15 Feb 2020 08:37:28 -0700 Subject: [PATCH 21/26] Fix indentation --- src/forum.nim | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/forum.nim b/src/forum.nim index a551b73..6c6f55a 100644 --- a/src/forum.nim +++ b/src/forum.nim @@ -528,14 +528,14 @@ proc updatePost(c: TForumData, postId: int, content: string, exec(db, crud(crUpdate, "thread", "name"), subject.get(), threadId) proc updateThread(c: TForumData, threadId: string, queryKeys: seq[string], queryValues: seq[string]) = - let threadAuthor = selectThreadAuthor(threadId.parseInt) + let threadAuthor = selectThreadAuthor(threadId.parseInt) - # Verify that the current user has permissions to edit the specified thread. - let canEdit = c.rank == Admin or c.userid == threadAuthor.name - if not canEdit: - raise newForumError("You cannot edit this thread") + # Verify that the current user has permissions to edit the specified thread. + let canEdit = c.rank == Admin or c.userid == threadAuthor.name + if not canEdit: + raise newForumError("You cannot edit this thread") - exec(db, crud(crUpdate, "thread", queryKeys), queryValues) + exec(db, crud(crUpdate, "thread", queryKeys), queryValues) proc executeNewThread(c: TForumData, subject, msg, categoryID: string): (int64, int64) = const From d9335ee0f0a9282dc373cd8a7fd8e1f2749d7799 Mon Sep 17 00:00:00 2001 From: Joey Yakimowich-Payne Date: Sat, 15 Feb 2020 08:52:13 -0700 Subject: [PATCH 22/26] Tighten updateThread fields and add todos --- src/forum.nim | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/forum.nim b/src/forum.nim index 6c6f55a..cb069af 100644 --- a/src/forum.nim +++ b/src/forum.nim @@ -1197,6 +1197,8 @@ routes: resp Http400, $(%exc.data), "application/json" post "/updateThread": + # TODO: Add some way of keeping track of modifications for historical + # purposes createTFD() if not c.loggedIn(): let err = PostError( @@ -1211,7 +1213,9 @@ routes: let threadId = formData["threadId"].body - let keys = ["name", "views", "modified", "category", "isLocked", "solution", "isDeleted"] + # TODO: might want to add more properties here under a tighter permissions + # model + let keys = ["name", "category", "solution"] # optional parameters var From 918cda96cf91bb881f6ec1599df57d4489af6c15 Mon Sep 17 00:00:00 2001 From: Joey Yakimowich-Payne Date: Sat, 15 Feb 2020 08:53:56 -0700 Subject: [PATCH 23/26] Minor cleanup --- src/frontend/categorypicker.nim | 38 +++++++++++++++++---------------- 1 file changed, 20 insertions(+), 18 deletions(-) diff --git a/src/frontend/categorypicker.nim b/src/frontend/categorypicker.nim index 799eab9..7834538 100644 --- a/src/frontend/categorypicker.nim +++ b/src/frontend/categorypicker.nim @@ -24,22 +24,23 @@ when defined(js): name.strip().replace(" ", "-").toLowerAscii proc onCategoryLoad(state: CategoryPicker): proc (httpStatus: int, response: kstring) = - return proc (httpStatus: int, response: kstring) = - state.loading = false - state.status = httpStatus.HttpCode - if state.status != Http200: return + return + proc (httpStatus: int, response: kstring) = + state.loading = false + state.status = httpStatus.HttpCode + if state.status != Http200: return - let parsed = parseJson($response) - let list = parsed.to(CategoryList) - list.categories.sort(cmpNames) + let parsed = parseJson($response) + let list = parsed.to(CategoryList) + list.categories.sort(cmpNames) - if state.list.isSome: - state.list.get().categories.add(list.categories) - else: - state.list = some(list) + if state.list.isSome: + state.list.get().categories.add(list.categories) + else: + state.list = some(list) - if state.selectedCategoryID > state.list.get().categories.len(): - state.selectedCategoryID = 0 + if state.selectedCategoryID > state.list.get().categories.len(): + state.selectedCategoryID = 0 proc loadCategories(state: CategoryPicker) = if not state.loading: @@ -81,10 +82,11 @@ when defined(js): proc onCategoryClick(state: CategoryPicker, category: Category): proc (ev: Event, n: VNode) = # this is necessary to capture the right value let cat = category - return proc (ev: Event, n: VNode) = - let oldCategory = state[state.selectedCategoryID] - state.select(cat.id) - state.onCategoryChange(oldCategory, cat) + return + proc (ev: Event, n: VNode) = + let oldCategory = state[state.selectedCategoryID] + state.select(cat.id) + state.onCategoryChange(oldCategory, cat) proc onAddCategoryPost(httpStatus: int, response: kstring, state: CategoryPicker) = postFinished: @@ -181,4 +183,4 @@ when defined(js): onClick=onCategoryClick(state, category)): render(category) if state.addEnabled: - genAddCategory(state) \ No newline at end of file + genAddCategory(state) From ce9cde4a0da4100ae114bcbd4cb00deda36352a5 Mon Sep 17 00:00:00 2001 From: Joey Yakimowich-Payne Date: Sat, 15 Feb 2020 09:02:00 -0700 Subject: [PATCH 24/26] Refactor category picker --- src/frontend/categorypicker.nim | 11 +++++++++-- src/frontend/newthread.nim | 10 +--------- src/frontend/postlist.nim | 5 +---- 3 files changed, 11 insertions(+), 15 deletions(-) diff --git a/src/frontend/categorypicker.nim b/src/frontend/categorypicker.nim index 7834538..0ea771f 100644 --- a/src/frontend/categorypicker.nim +++ b/src/frontend/categorypicker.nim @@ -5,7 +5,7 @@ when defined(js): include karax/prelude import karax / [kajax, kdom, vstyles, vdom] - import error, replybox, threadlist, post, category + import error, replybox, threadlist, post, category, user import category, karaxutils type @@ -157,7 +157,14 @@ when defined(js): state.onAddCategoryClick()): text "Add" - proc render*(state: CategoryPicker): VNode = + proc render*(state: CategoryPicker, currentUser: Option[User]): VNode = + let loggedIn = currentUser.isSome() + let currentAdmin = + loggedIn and currentUser.get().rank == Admin + + if currentAdmin: + state.setAddEnabled(true) + if state.status != Http200: return renderError("Couldn't retrieve categories.", state.status) diff --git a/src/frontend/newthread.nim b/src/frontend/newthread.nim index 6d44381..1d1be24 100644 --- a/src/frontend/newthread.nim +++ b/src/frontend/newthread.nim @@ -48,14 +48,6 @@ when defined(js): (s: int, r: kstring) => onCreatePost(s, r, state)) proc render*(state: NewThread, currentUser: Option[User]): VNode = - - let loggedIn = currentUser.isSome() - let currentAdmin = - currentUser.isSome() and currentUser.get().rank == Admin - - if currentAdmin: - state.categoryPicker.setAddEnabled(true) - result = buildHtml(): section(class="container grid-xl"): tdiv(id="new-thread"): @@ -71,7 +63,7 @@ when defined(js): tdiv(): label(class="d-inline-block form-label"): text "Category" - render(state.categoryPicker) + render(state.categoryPicker, currentUser) renderContent(state.replyBox, none[Thread](), none[Post]()) tdiv(class="footer"): diff --git a/src/frontend/postlist.nim b/src/frontend/postlist.nim index 3076a2a..a259343 100644 --- a/src/frontend/postlist.nim +++ b/src/frontend/postlist.nim @@ -211,13 +211,10 @@ when defined(js): let currentAdmin = currentUser.isSome() and currentUser.get().rank == Admin - if currentAdmin: - state.categoryPicker.setAddEnabled(true) - result = buildHtml(): tdiv(): if authoredByUser or currentAdmin: - render(state.categoryPicker) + render(state.categoryPicker, currentUser) else: render(thread.category) From 7337bceff3ec8b5950dc9b3dc46da302d9e41711 Mon Sep 17 00:00:00 2001 From: Joey Yakimowich-Payne Date: Sat, 15 Feb 2020 09:35:05 -0700 Subject: [PATCH 25/26] Remove casting from formdata --- src/frontend/newthread.nim | 4 +++- src/frontend/postlist.nim | 3 ++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/src/frontend/newthread.nim b/src/frontend/newthread.nim index 1d1be24..fe4e619 100644 --- a/src/frontend/newthread.nim +++ b/src/frontend/newthread.nim @@ -1,6 +1,7 @@ when defined(js): import sugar, httpcore, options, json import dom except Event + import jsffi except `&` include karax/prelude import karax / [kajax, kdom] @@ -44,7 +45,8 @@ when defined(js): formData.append("subject", state.subject) formData.append("msg", state.replyBox.getText()) formData.append("categoryId", $categoryID) - ajaxPost(uri, @[], cast[cstring](formData), + + ajaxPost(uri, @[], formData.to(cstring), (s: int, r: kstring) => onCreatePost(s, r, state)) proc render*(state: NewThread, currentUser: Option[User]): VNode = diff --git a/src/frontend/postlist.nim b/src/frontend/postlist.nim index a259343..46dee9a 100644 --- a/src/frontend/postlist.nim +++ b/src/frontend/postlist.nim @@ -15,6 +15,7 @@ type when defined(js): from dom import document + import jsffi except `&` include karax/prelude import karax / [vstyles, kajax, kdom] @@ -76,7 +77,7 @@ when defined(js): state.loading = true - ajaxPost(uri, @[], cast[cstring](formData), + ajaxPost(uri, @[], formData.to(cstring), (s: int, r: kstring) => onCategoryPost(s, r, state)) proc onPostList(httpStatus: int, response: kstring, postId: Option[int]) = From 01d13aa0f36881c5bcddb45513ea3b3e179a8308 Mon Sep 17 00:00:00 2001 From: Joey Yakimowich-Payne Date: Sat, 15 Feb 2020 09:36:50 -0700 Subject: [PATCH 26/26] Add todo about categories query --- src/forum.nim | 1 + 1 file changed, 1 insertion(+) diff --git a/src/forum.nim b/src/forum.nim index cb069af..c635f1a 100644 --- a/src/forum.nim +++ b/src/forum.nim @@ -790,6 +790,7 @@ settings: routes: get "/categories.json": + # TODO: Limit this query in the case of many many categories const categoriesQuery = sql"""select * from category;""" var list = CategoryList(categories: @[])