- Our NodeBB deployment hosted by Linux can be found here: http://17313-team13.s3d.cmu.edu:4567/
- Feature: Per-post anonymous posting that hides the author's identity from regular users.
- Summary: When posting, a checkbox can be selected to post anonymously. Posts are shown as "Anonymous" to non-privileged viewers (regular users). Administrators and moderators see the original poster information. The plugin implements a two-stage hook flow to mask identities after NodeBB populates user data.
- How it works:
- Backend - Masking Posts on Retrieval: A plugin inspects posts on
filter:post.getFieldsand, whenpost.isAnonymousis true, stores the original uid, setspost.uid = 0for non-privileged callers (forcing guest data to load), and flagspost.isAnonymousPostand_callerIsPrivileged. The plugin uses anANONYMOUS_USERconstant as the masked user object and keeps_originalUid/_originalUser fields on the post for possible later use. - Backend - User Object Replacement: After NodeBB populates user objects,
filter:topics.addPostDatareplaces the guest user object with anANONYMOUS_USERplaceholder (username/displayname = "Anonymous") for non-privileged viewers while preserving original user data for admins/mods. - Frontend - Checkbox UI: The composer UI includes a checkbox labeled "Post Anonymously" injected by the client code in
public/src/app.jswhen the composer is enhanced for topic/post creation. The checkbox element has the attributedata-composer-anonymous; when changed the composer container receivesdata-anonymous-post="1"(or0). The checkbox is inserted after the.title-containerinside the composer (only fortopics.postactions), and the label text is translatable inpublic/language/*/user.jsonfiles. - Frontend-to-Backend - Post Creation Flow: The
filter:composer.submithook inpublic/src/app.jsreads thedata-anonymous-postattribute and setscomposerData.isAnonymousaccordingly. The backend insrc/posts/create.jsthen consumes this flag and persists it aspostData.isAnonymouson the post record in the database.
- Backend - Masking Posts on Retrieval: A plugin inspects posts on
- How to test:
-
Run the plugin's unit tests which cover masking and privilege visibility:
cd vendor/nodebb-plugin-post-fields-logger npm test
Or run the project test runner targeting the plugin tests directly:
npm test -- vendor/nodebb-plugin-post-fields-logger/test/anonymous-mode.js
-
Manual/quick checks:
- Create a post through the UI by clicking "Post Anonymously" checkbox in the composer.
- View the topic as a regular user: the post should show username/displayname "Anonymous".
- View the topic as an admin/mod: the post should show the original username with full user details.
-
- Tests: See the plugin README and tests:
- vendor/nodebb-plugin-post-fields-logger/README.md - Plugin documentation
- vendor/nodebb-plugin-post-fields-logger/index.js - Plugin implementation with hooks
- vendor/nodebb-plugin-post-fields-logger/test/anonymous-mode.js - Comprehensive test suite including stage 1/2 hooks, privilege-based visibility, post creation flow, and edge cases
- Why tests are sufficient:
- The tests provide comprehensive coverage across multiple test suites:
- Stage 1 Hook (
filter:post.getFields) Tests: Validates UID masking for non-privileged viewers, preserves original UID for privileged viewers (admins/mods), and handles privilege detection correctly. - Stage 2 Hook (
filter:topics.addPostData) Tests: Verifies user object replacement withANONYMOUS_USERplaceholder for non-privileged viewers, preserves original user data for privileged viewers, and correctly handles mixed anonymous/non-anonymous posts. - Privilege-Based Visibility Tests: Tests instructor viewing student anonymous post (shows real identity), student viewing another student's anonymous post (shows "Anonymous"), and post author viewing their own anonymous post.
- Post Creation Flow Tests: Validates that the
filter:composer.submithook captures checkbox state andsrc/posts/create.jspersists theisAnonymousflag to the database, covering the complete frontend-to-backend integration. - Edge Cases: String/boolean type coercion for Redis storage ("true" as truthy), false/0 values not triggering masking, double-processing protection (
_originalUid/_originalUser/_originalHandlepreservation), missing caller context defaults to masking, handle field conditional setting, andANONYMOUS_USERconstant immutability.
- Stage 1 Hook (
- Together, the tests ensure that posts created with the anonymous flag are properly stored and retrieved with correct visibility behavior based on viewer privileges throughout the entire system lifecycle.
- The tests provide comprehensive coverage across multiple test suites:
- Feature: LaTeX button in the composer toolbar and MathJax rendering for math equations in posts and topics.
- Summary: Adds a LaTeX button to the NodeBB composer that wraps selected text in
$$delimiters (or inserts$$ $$when nothing is selected) for display math, and injects MathJax from a CDN so that LaTeX/TeX equations render correctly in topic views and composer preview. - How it works:
- Backend - Formatting Registration: The plugin hooks into
filter:composer.formattingin library.js.registerFormattingadds alatexoption to the payload withname: 'latex',className: 'fa fa-superscript', and visibility for mobile/desktop/main/reply. It preserves any existing formatting options. - Backend - MathJax Script Injection: The plugin hooks into
filter:middleware.renderHeader.addMathJaxScriptappends MathJax configuration and the MathJax CDN script (https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-mml-chtml.js) intotemplateData.useCustomHTML, so every page load includes MathJax. It appends to existinguseCustomHTMLwhen present. - Frontend - LaTeX Button: In public/js/client.js, the plugin listens for
action:composer.enhancedand registers a button dispatch viaformatting.addButtonDispatch('latex', ...). On click, it either wraps the selection in$$...$$or inserts$$ $$with the cursor between them. It then triggers the composer preview. - Frontend - MathJax Rendering: public/js/mathjax.js configures MathJax for inline (
$...$,\(...\)) and display ($$...$$,\[...\]) math. It loads MathJax from the CDN on demand, runstypesetPromiseon page load and when the composer preview updates, and handles the case where the header script is already present.
- Backend - Formatting Registration: The plugin hooks into
- How to test:
-
Run the LaTeX plugin unit tests:
npm test -- test/plugins-composer-latex.js
Or run the plugin's tests directly:
cd vendor/nodebb-plugin-composer-latex npm test
-
Manual/quick checks:
- Start NodeBB and open the composer (new topic or reply).
- Confirm a LaTeX (superscript) button appears in the formatting toolbar.
- Click the button with no selection:
$$ $$should be inserted with cursor between. - Select text and click the button: text should be wrapped in
$$...$$. - Submit a post containing
$E = mc^2$or$$\int_0^1 x^2 dx$$. - View the topic: equations should render as math (not raw LaTeX).
- Check the composer preview: equations should render live as you type.
-
- Tests: Links to automated test files:
- test/plugins-composer-latex.js - Entry point that loads the plugin unit tests
- vendor/nodebb-plugin-composer-latex/test/composer-latex.js - Unit tests for
registerFormattingandaddMathJaxScript
- Why tests are sufficient:
- registerFormatting tests: Verify that the latex option is added with correct name, className, title, and visibility (mobile/desktop); that existing options are preserved; and that an empty or missing
optionsarray is handled correctly. - addMathJaxScript tests: Verify that MathJax config and script are injected into
templateData.useCustomHTMLwith the expected content (mathjax, script, cdn.jsdelivr.net); that injection appends to existinguseCustomHTML; and that nothing is done whentemplateDatais missing. - These unit tests cover the backend hooks that power the LaTeX feature. The frontend button dispatch and MathJax client-side behavior are exercised via manual/end-to-end testing, which is appropriate because they depend on the NodeBB composer UI and DOM. Together, the tests ensure the plugin integrates correctly with NodeBB's formatting and header middleware, and that the math rendering pipeline is correctly configured.
- registerFormatting tests: Verify that the latex option is added with correct name, className, title, and visibility (mobile/desktop); that existing options are preserved; and that an empty or missing
- Feature: The "Fuzzy match" search dropdown option supports fuzzy search.
- Summary: "Fuzzy match" enables users to input search queries and receive post results with low edit distances (word similarity). It supports input queries that with insertions, deletions, and substitutions. Relevant posts with high word similarity (minimal edit distance) appear. This feature was added to provide users with searching flexibility. Users can have typos into their search queries and still receive relevant post results.
- How it works: The fuzzy search feature was mainly implemented in public/src/modules/search.js. getLevenshteinDistance(a, b) computes the minimum edit distance (insertions, deletions, substitutions) between two strings using dynamic programming. fuzzyMatches(query, text) tokenizes both the query and the text, then checks if any query token is within an acceptable edit distance of any text token. The tolerance threshold scales with token length via maxFuzzyEdits(len): <= 5 chars for 1 edit, <= 9 chars for 2 edits, 10+ chars for 3 edits. There is also substring matching fallback. getFuzzyMatchRanges(query, text) traces back through the Levenshtein dynamic programming matrix to find which character positions in text align with the query, returning [start, end] ranges. highlightFuzzyInText() wraps these ranges for underlining relevant characters.
- **Data Flow:**User selects "Fuzzy match" from the search dropdown. matchWords=fuzzy is sent as a query parameter. src/controllers/search.js passes it into template data. public/src/client/search.js reads it from the data-match-words attribute on the results container and calls Search.highlightMatches(query, els, 'fuzzy'), which uses the fuzzy highlighting path instead of the exact-regex path.
- **Edge Cases:**Empty queries return no matches. Single-character tokens are skipped during highlighting.
- How to test:
- Start NodeBB locally (./nodebb dev)
- Create a few posts with known words (e.g., "hello world", "test post")
- Go to the search page, select "Fuzzy match" from the match-words dropdown
- Search "helo" → should return the "hello" post, with "hello" underlined (shared characters highlighted)
- Search "tests" → should return the "test" post
- Search "jello" → should not return the "hello" post
- Switch to "Match all words" → confirm highlighting reverts to exact bold+underline behavior
- Automated tests can be run via
npx mocha test/search-fuzzy.js
- Tests: test/fuzzy-search.js
- Test Sufficiency: The unit tests test the two core functions getLevenshteinDistance and fuzzyMatches, which contain all fuzzy matching logic. The unit tests cover correctness of edit distance, acceptance of small typos, rejection of unrelated words beyond threshold edit distance, edge cases, and substring matching fallback.
-
Feature: A list containing all the users that responded to a post.
-
Summary: When someone views a topic page, a small widget in the sidebar lists all unique users who have posted in that topic (the original author and anyone who replied). Each entry shows the user’s avatar and username, as well as links to their profile. This should allow the instructor to, for example, be able to grade the responses quickly.
-
How it works:
-
Backend (data storage): NodeBB already keeps track of participation using a Redis sorted set:
- Key:
tid:{tid}:posters(This set stores user IDs (UIDs) and a score that represents how many posts they have in the topic.) - When a post is created (in
src/topics/posts.js): NodeBB increments the poster’s score usingdb.sortedSetIncrBy. - When a post is deleted: NodeBB decrements the score. If the score becomes 0 or below, that UID is removed from the set. Thus the sorted set always represents the set of users who currently have at least one post in the topic.
- Key:
-
Backend (data retrieval):
Topics.getUids(tid)insrc/topics/user.jsreads from the sorted set and returns all UIDs with at least one post ordered by post count (highest first).- It uses
db.getSortedSetRevRangeByScoreto do this. - Then the topic controller (
src/controllers/topics.js, around lines 140–143):- calls
topics.getUids(tid) - loads basic user info using
user.getUsersFields(fields:uid,username,userslug,picture) - attaches the result to the topic response as
topicData.respondents
- calls
- It uses
-
Frontend (sidebar display): A template partial renders the list in the topic sidebar
templates/partials/topic/respondents.tpl- It shows a "Respondents" header (translated using
[[topic:respondents]]), loops through the respondents array, and renders each user with 24px avatar, username, and link to their profile.
- It shows a "Respondents" header (translated using
-
-
How to test:
- Automated tests: refer to the bulletpoint below
- Manual checks
- Create a new topic as User A and view the sidebar -> User A should appear in the respondents list.
- Reply as User B and User C, refresh the page -> All three users should appear (A, B, C).
- Reply again as User B, refresh -> User B should still appear only once (no duplicates).
- Click a username -> It should go to that user’s profile page.
-
Tests:
- Run the respondents unit tests:
npx mocha test/topics-respondents.js - Test file: test/topics-respondents.js
- Run the respondents unit tests:
-
Why tests are sufficient: The automated tests focus on the backend pipeline that generates the respondents list.
- Creator inclusion: confirms the topic creator is included immediately.
- Reply inclusion: confirms repliers get added after posting.
- No duplicates: confirms the same user replying multiple times still shows up once.
- Correct count: confirms the expected number of unique respondents is returned.
- User data included: confirms the returned user objects include at least
uidandusername. - Username accuracy: confirms the usernames match the correct UIDs.