diff --git a/.eslintrc b/.eslintrc
index abd292af1b..c1f471f32c 100644
--- a/.eslintrc
+++ b/.eslintrc
@@ -1,3 +1,6 @@
{
- "extends": "nodebb"
+ "extends": "nodebb",
+ "parserOptions": {
+ "ecmaVersion": 2020 // This change in configuration is suggested by Copilot
+ }
}
diff --git a/.mocharc.yml b/.mocharc.yml
index 16d8518d1b..b9246aa609 100644
--- a/.mocharc.yml
+++ b/.mocharc.yml
@@ -1,4 +1,4 @@
reporter: dot
timeout: 25000
exit: true
-bail: true
+bail: false
diff --git a/README.md b/README.md
index d99af63502..3b153f7089 100644
--- a/README.md
+++ b/README.md
@@ -1,4 +1,12 @@
[](https://classroom.github.com/a/ithVU1OO)
+
+## Team Members
+- Mia Li
+- Alice Kang
+- Cheyu Tu
+- Jasmine Shi
+- Jullia Andrei Montejo
+
# 
[](https://github.com/CMU-313/NodeBB/actions/workflows/test.yaml)
diff --git a/UserGuide.md b/UserGuide.md
new file mode 100644
index 0000000000..20f8f4ed52
--- /dev/null
+++ b/UserGuide.md
@@ -0,0 +1,72 @@
+# User Guide for Team SWEethearts User Stories
+
+## Table of Contents
+1. [User Story 2](#user-story-2)
+2. [User Story 7](#user-story-7)
+
+## User Story 2
+As a student, I want to receive an immediate notification when a course faculty member replies to my post, so that I can review their response within 24 hours.
+
+### Feature Overview
+* Real-time notifications:
+ * Push notifications that will appear on the notification side bar and on the main notification page when a course faculty replies to your post.
+
+
+
+ * These notifications are delivered separately from non-faculty reply notifications and are indicated with the "[COURSE-FACULTY]" on the title. (As shown in the above images.)
+
+
+* User Preferences: Like other notifications, users are able to customize their notification settings (choose between push, email, both, or turn both off) in the account settings page.
+
+
+### How To Use and Test User Story 2 Feature
+1. Register 2 accounts: an admin and a non-admin (student).
+2. Log in to the non-admin account and make a topic in a discussion board.
+3. Go to the user settings page by clicking on the top right profile icon then "Settings".
+4. Scroll down until you find the course faculty notification setting as shown below. This feature's setting should be on the "Notification Only" by default. Choose your preferred setting and press Save Changes.
+5. Log out of this student account and log in to the admin account.
+6. Leave a reply to the student post.
+7. Log out and log in the student account. There should now be a new notification that pops up on the notification sidebar and on the page.
+* If this setting is turned off then there should be no notifications on the inbox or on the notification page.
+
+**Note: For this feature we assume that every admin account is a course faculty.**
+
+### Automated Tests - located in `test/notifications.js`
+#### Test Cases: Lines 392 - 585
+* Lines 442 - 448: Testing basic functionality: user should receive a notification when a course faculty replies.
+* Lines 450 - 485: Test that user should not receive notification when regular user replies.
+* Lines 487 - 535: Test to check that user can mark faculty replies as read.
+* Lines 537 - 544: Test to check that changing notification settings work.
+* Lines 546 - 571: Test to check that separate faculty replies trigger separate notifications.
+
+#### Test Justification
+We believe that these tests are sufficient as it tests the basic functionality of this new feature. The tests ensure that users receive notifications from faculty, can differentiate between faculty and regular user replies, and can manage notifications (e.g., marking them as read or adjusting settings). This ensures both the functionality and user control of the feature, making the system reliable.
+
+## User Story 7
+As a student, I want to save posts to favorites for posts that contains important information/good solutions, so that I can review them later on quickly
+
+### Feature Overview
+* Adding a Favorites category in the discussion post bar:
+ * Users are able to select posts as a Favorite
+ * Conceptually, posts marked as Favorites could be sorted and displayed to user
+
+
+
+### How to Use and Test User Story
+1. User needs to register; does not have to be an admin
+2. Go to "General Discussion" page
+3. There should be at least one topic within the discussion page. If there are none, make a test post.
+4. You should be able to see a Star button signifying Favorite
+5. There should also be a Favorite button in the post bar
+6. The Favorite button in post bar can be toggled on and off.
+
+
+### Automated tests are located in `test/topics/favorite.js`
+
+#### Error Tests: Testing invalid inputs
+* Test for invalid input
+
+#### Valid Tests
+
+#### Test Justification
+We believe that these tests are sufficient because
diff --git a/dump.rdb b/dump.rdb
index 72498f0cf9..f512209026 100644
Binary files a/dump.rdb and b/dump.rdb differ
diff --git a/public/src/admin/manage/digest.js b/public/src/admin/manage/digest.js
index 365ac776b6..10af481eab 100644
--- a/public/src/admin/manage/digest.js
+++ b/public/src/admin/manage/digest.js
@@ -1,35 +1,39 @@
'use strict';
-
define('admin/manage/digest', ['bootbox', 'alerts'], function (bootbox, alerts) {
const Digest = {};
+ console.log('Mia Li');
+ function interval_resend(action) {
+ const interval = action.slice(7);
+ bootbox.confirm('[[admin/manage/digest:resend-all-confirm]]', function (ok) {
+ if (ok) {
+ Digest.send(action, undefined, function (err) {
+ if (err) {
+ return alerts.error(err);
+ }
+ alerts.success('[[admin/manage/digest:resent-' + interval + ']]');
+ });
+ }
+ });
+ }
+ function single_resend(action, uid) {
+ Digest.send(action, uid, function (err) {
+ if (err) {
+ return alerts.error(err);
+ }
+ alerts.success('[[admin/manage/digest:resent-single]]');
+ });
+ }
Digest.init = function () {
$('.digest').on('click', '[data-action]', function () {
const action = this.getAttribute('data-action');
const uid = this.getAttribute('data-uid');
if (action.startsWith('resend-')) {
- const interval = action.slice(7);
- bootbox.confirm('[[admin/manage/digest:resend-all-confirm]]', function (ok) {
- if (ok) {
- Digest.send(action, undefined, function (err) {
- if (err) {
- return alerts.error(err);
- }
-
- alerts.success('[[admin/manage/digest:resent-' + interval + ']]');
- });
- }
- });
+ interval_resend(action);
} else {
- Digest.send(action, uid, function (err) {
- if (err) {
- return alerts.error(err);
- }
-
- alerts.success('[[admin/manage/digest:resent-single]]');
- });
+ single_resend(action, uid);
}
});
};
diff --git a/src/flags.js b/src/flags.js
index 00bce1d9bd..3786b2796a 100644
--- a/src/flags.js
+++ b/src/flags.js
@@ -273,6 +273,13 @@ Flags.sort = async function (flagIds, sort) {
return flagIds;
};
+function checkSelfFlagError(payload) {
+ console.log('CHEYU TU - checkSelfFlagError');
+ if (parseInt(payload.id, 10) === parseInt(payload.uid, 10)) {
+ throw new Error('[[error:cant-flag-self]]');
+ }
+}
+
Flags.validate = async function (payload) {
const [target, reporter] = await Promise.all([
Flags.getTarget(payload.type, payload.id, payload.uid),
@@ -283,7 +290,7 @@ Flags.validate = async function (payload) {
throw new Error('[[error:invalid-data]]');
} else if (target.deleted) {
throw new Error('[[error:post-deleted]]');
- } else if (!reporter || !reporter.userslug) {
+ } else if (!reporter?.userslug) {
throw new Error('[[error:no-user]]');
} else if (reporter.banned) {
throw new Error('[[error:user-banned]]');
@@ -304,9 +311,7 @@ Flags.validate = async function (payload) {
throw new Error(`[[error:not-enough-reputation-to-flag, ${meta.config['min:rep:flag']}]]`);
}
} else if (payload.type === 'user') {
- if (parseInt(payload.id, 10) === parseInt(payload.uid, 10)) {
- throw new Error('[[error:cant-flag-self]]');
- }
+ checkSelfFlagError(payload);
const editable = await privileges.users.canEdit(payload.uid, payload.id);
if (!editable && !meta.config['reputation:disabled'] && reporter.reputation < meta.config['min:rep:flag']) {
throw new Error(`[[error:not-enough-reputation-to-flag, ${meta.config['min:rep:flag']}]]`);
diff --git a/src/groups/update.js b/src/groups/update.js
index 804268c587..b2aaf78a04 100644
--- a/src/groups/update.js
+++ b/src/groups/update.js
@@ -32,28 +32,7 @@ module.exports = function (Groups) {
}
});
- const payload = {
- description: values.description || '',
- icon: values.icon || '',
- labelColor: values.labelColor || '#000000',
- textColor: values.textColor || '#ffffff',
- };
-
- if (values.hasOwnProperty('userTitle')) {
- payload.userTitle = values.userTitle || '';
- }
-
- if (values.hasOwnProperty('userTitleEnabled')) {
- payload.userTitleEnabled = values.userTitleEnabled ? '1' : '0';
- }
-
- if (values.hasOwnProperty('hidden')) {
- payload.hidden = values.hidden ? '1' : '0';
- }
-
- if (values.hasOwnProperty('private')) {
- payload.private = values.private ? '1' : '0';
- }
+ const payload = payload_helper(values);
if (values.hasOwnProperty('disableJoinRequests')) {
payload.disableJoinRequests = values.disableJoinRequests ? '1' : '0';
@@ -90,6 +69,32 @@ module.exports = function (Groups) {
});
};
+ function payload_helper(values) {
+ const payload = {
+ description: values.description || '',
+ icon: values.icon || '',
+ labelColor: values.labelColor || '#000000',
+ textColor: values.textColor || '#ffffff',
+ };
+
+ if (values.hasOwnProperty('userTitle')) {
+ payload.userTitle = values.userTitle || '';
+ }
+
+ if (values.hasOwnProperty('userTitleEnabled')) {
+ payload.userTitleEnabled = values.userTitleEnabled ? '1' : '0';
+ }
+
+ if (values.hasOwnProperty('hidden')) {
+ payload.hidden = values.hidden ? '1' : '0';
+ }
+
+ if (values.hasOwnProperty('private')) {
+ payload.private = values.private ? '1' : '0';
+ }
+ return payload;
+ }
+
async function updateVisibility(groupName, hidden) {
if (hidden) {
await db.sortedSetRemoveBulk([
diff --git a/src/start.js b/src/start.js
index b546c1ffc8..cba1fc8f9c 100644
--- a/src/start.js
+++ b/src/start.js
@@ -5,13 +5,15 @@ const winston = require('winston');
const start = module.exports;
+const db = require('./database');
+const Topics = require('./topics');
+
start.start = async function () {
printStartupInfo();
addProcessHandlers();
try {
- const db = require('./database');
await db.init();
await db.checkCompatibility();
@@ -149,3 +151,28 @@ async function shutdown(code) {
return process.exit(code || 0);
}
}
+
+/* eslint-disable no-unused-vars */
+async function getTopicIdByTitle(title) {
+ const topic = await db.models.topics.findOne({ title });
+ return topic ? topic.tid : null;
+}
+
+async function addTagsToTopic() {
+ try {
+ const tid = await getTopicIdByTitle('Welcome to your NodeBB!');
+ if (tid) {
+ console.log(`Topic ID: ${tid}`);
+
+ const timestamp = Date.now(); // Get current timestamp
+ const tagsToAdd = ['Homework', 'Assignment']; // Default tags
+
+ await Topics.createTags(tagsToAdd, tid, timestamp); // Add tags to the topic
+ } else {
+ console.error('Topic not found');
+ }
+ } catch (error) {
+ console.error('Error adding tags:', error);
+ }
+}
+/* eslint-enable no-unused-vars */
diff --git a/src/webserver.js b/src/webserver.js
index f492a0da02..630ab39ecd 100644
--- a/src/webserver.js
+++ b/src/webserver.js
@@ -34,6 +34,7 @@ const topicEvents = require('./topics/events');
const privileges = require('./privileges');
const routes = require('./routes');
const auth = require('./routes/authentication');
+const topics = require('./topics');
const helpers = require('./helpers');
@@ -48,6 +49,7 @@ if (nconf.get('ssl')) {
module.exports.server = server;
module.exports.app = app;
+module.exports.createNewTag = createNewTag;
server.on('error', (err) => {
if (err.code === 'EADDRINUSE') {
@@ -86,6 +88,7 @@ exports.listen = async function () {
helpers.register();
logger.init(app);
await initializeNodeBB();
+ await createNewTag();
winston.info('🎉 NodeBB Ready');
require('./socket.io').server.emit('event:nodebb.ready', {});
@@ -334,3 +337,17 @@ exports.testSocket = async function (socketPath) {
};
require('./promisify')(exports);
+
+async function createNewTag() {
+ try {
+ const newTag1 = 'homework';
+ const newTag2 = 'assignment';
+
+ await topics.createEmptyTag(newTag1);
+ console.log(`Tag "${newTag1}" successfully created!`);
+ await topics.createEmptyTag(newTag2);
+ console.log(`Tag "${newTag2}" successfully created!`);
+ } catch (err) {
+ console.error('Error creating tag: ', err.message);
+ }
+}
diff --git a/test/flags.js b/test/flags.js
index ee150a10c4..6b772ceb73 100644
--- a/test/flags.js
+++ b/test/flags.js
@@ -754,6 +754,37 @@ describe('Flags', () => {
done();
});
});
+
+ function validateUserFlag(done) {
+ Flags.validate({
+ type: 'user',
+ id: 1,
+ uid: 3,
+ }, (err) => {
+ assert.ok(err);
+ assert.strictEqual('[[error:not-enough-reputation-to-flag, 50]]', err.message);
+ Meta.configs.set('min:rep:flag', 0, done);
+ });
+ }
+
+ it('should not pass validation if type is user, flag threshold is set and user rep does not meet it', (done) => {
+ Meta.configs.set('min:rep:flag', '50', (err) => {
+ assert.ifError(err);
+ validateUserFlag(done);
+ });
+ });
+
+ it('should throw an error if user tries to flag themselves', (done) => {
+ Flags.validate({
+ type: 'user',
+ id: 1,
+ uid: 1,
+ }, (err) => {
+ assert.ok(err);
+ assert.strictEqual('[[error:cant-flag-self]]', err.message);
+ done();
+ });
+ });
});
describe('.appendNote()', () => {
diff --git a/user_guide_images/fav_btn_1.png b/user_guide_images/fav_btn_1.png
new file mode 100644
index 0000000000..32be31a453
Binary files /dev/null and b/user_guide_images/fav_btn_1.png differ
diff --git a/user_guide_images/fav_btn_2.png b/user_guide_images/fav_btn_2.png
new file mode 100644
index 0000000000..2ad8282f5d
Binary files /dev/null and b/user_guide_images/fav_btn_2.png differ
diff --git a/user_guide_images/ins_notif_1.png b/user_guide_images/ins_notif_1.png
new file mode 100644
index 0000000000..3123ffa93f
Binary files /dev/null and b/user_guide_images/ins_notif_1.png differ
diff --git a/user_guide_images/ins_notif_2.png b/user_guide_images/ins_notif_2.png
new file mode 100644
index 0000000000..664b8ed1a6
Binary files /dev/null and b/user_guide_images/ins_notif_2.png differ
diff --git a/user_guide_images/ins_notif_3.png b/user_guide_images/ins_notif_3.png
new file mode 100644
index 0000000000..5ca441d3e4
Binary files /dev/null and b/user_guide_images/ins_notif_3.png differ