Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 2 additions & 19 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,27 +1,10 @@
# NodeBB Poll plugin

This NodeBB plugin will allow you to add polls to the first post of a topic with the following markup:
This NodeBB plugin will allow you to add polls to posts.

[poll <settings>]
- Poll option
- Another option
[/poll]

Currently supported settings:

maxvotes="1" //Max number of votes per user. If larger than 1, a multiple choice poll will be created
disallowVoteUpdate="0" //if set, users won't be able to update/remove their vote
allowAnonVoting="0" // if set to 1, users will be able to vote anonymously
title="Poll title" //Poll title

There's also a helpful modal available that will allow you to easily create a poll:
There's helpful modal available that will allow you to easily create a poll:
![](https://i.imgur.com/2fPnWLb.png)

## Todo

- Add the ability to edit a poll
- A lot more...

If you're willing to help, please make any improvements you want and submit a PR.

## Installation
Expand Down
16 changes: 12 additions & 4 deletions languages/en_GB/poll.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
{
"poll": "Poll",
"poll-id-x": "Poll ID: %1",
"toggles": "Toggles",
"allow_guests": "Allow guests to view poll results",
"limits": "Limits",
Expand All @@ -13,8 +14,12 @@
"settings": "Settings",
"save": "Save",
"reset": "Reset",

"manage-polls": "Manage polls",
"add-poll": "Add poll",
"no-polls": "No polls found",
"confirm-remove": "Are you sure you want to remove this poll?",
"creator_title": "Create a poll",
"edit-a-poll-title": "Edit a poll",
"poll_title": "Poll Title",
"poll_title_placeholder": "Enter poll title",
"options_title": "Options",
Expand All @@ -24,11 +29,12 @@
"auto_end_placeholder": "Click to enter date and time",
"auto_end_help": "Leaving this empty will never end the poll.",

"error.max_options": "You can only create %d options.",
"error.max_options": "You can only create %1 options.",
"error.no_options": "Create at least one option.",
"error.valid_date": "Please enter a valid date.",

"error.not_main": "Can only add poll in main post.",
"error.invalid-post": "Invalid post.",
"error.privilege.create": "You're not allowed to create a poll",
"error.anon-voting-not-allowed": "This poll does not allow voting anonymously",

Expand All @@ -45,9 +51,11 @@
"voting_update_disallowed_message": "You have already voted and changing your vote is not allowed for this poll",
"vote_is_final": "Changing your vote is not allowed for this poll so your vote is final",

"vote_count": "users voted for this option",
"x-users-voted-for-this-option": "%1 user(s) voted for this option",
"votes": "votes",
"x-votes": "%1 votes",
"total-votes-x": "Total Votes: %1",
"admin.create-poll": "Create Poll"
"admin.create-poll": "Create Poll",
"created-time": "Created",
"end-time": "End"
}
42 changes: 13 additions & 29 deletions lib/config.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
'use strict';

const NodeBB = require('./nodebb');
const meta = require.main.require('./src/meta');

const packageInfo = require('../package.json');
const pluginInfo = require('../plugin.json');
Expand All @@ -18,35 +18,19 @@ Config.plugin = {
};

Config.defaults = {
toggles: {
allowAnon: false,
},
limits: {
maxOptions: 10,
},
defaults: {
title: 'Poll',
maxvotes: 1,
disallowVoteUpdate: 0,
allowAnonVoting: 0,
end: 0,
},
defaultTitle: 'Poll',
maxOptions: 10,
maximumVotesPerUser: 1,
allowGuestsToViewResults: 0,
disallowVoteUpdate: 0,
allowAnonVoting: 0,
};

Config.settings = {};

Config.init = function () {
return new Promise((resolve) => {
Config.settings = new NodeBB.Settings(Config.plugin.id, Config.plugin.version, Config.defaults, resolve);
});
};

Config.adminSockets = {
sync: function () {
Config.settings.sync();
},
getDefaults: function (socket, data, callback) {
callback(null, Config.settings.createDefaultWrapper());
},
Config.getSettings = async function () {
const settings = await meta.settings.get(Config.plugin.id);
return {
...Config.defaults,
...settings,
};
};

149 changes: 81 additions & 68 deletions lib/hooks.js
Original file line number Diff line number Diff line change
@@ -1,117 +1,130 @@
'use strict';

const NodeBB = require('./nodebb');
const db = require.main.require('./src/database');
const topics = require.main.require('./src/topics');
const posts = require.main.require('./src/posts');
const privileges = require.main.require('./src/privileges');


const Config = require('./config');
const Poll = require('./poll');
const Serializer = require('./serializer');

const Hooks = exports;

Hooks.filter = {};
Hooks.action = {};

Hooks.filter.parseRaw = function (raw) {
return Serializer.removeMarkup(raw, '[Poll]');
Hooks.filter.configGet = async function (config) {
config.poll = await Config.getSettings();
return config;
};

Hooks.filter.postCreate = async function (obj) {
if (Serializer.hasMarkup(obj.post.content) && obj.data.isMain) {
return await savePoll(obj);
}
return obj;
Hooks.filter.registerFormatting = function (payload) {
payload.options.push({
name: 'poll',
className: `fa ${Config.plugin.icon}`,
title: '[[poll:manage-polls]]',
badge: true,
});
return payload;
};

Hooks.filter.postEdit = async function (obj) {
const { tid, pollId } = await NodeBB.Posts.getPostFields(obj.data.pid, ['tid', 'pollId']);
if (pollId || !Serializer.hasMarkup(obj.post.content)) {
return obj;
Hooks.filter.composerPush = async function (hookData) {
// used for editing, add the polls so they are avable in composer
if (hookData.pid) {
const pollIds = await Poll.getPollIdsByPid(hookData.pid);
const polls = await Promise.all(pollIds.map(pollId => Poll.get(pollId, 0)));
hookData.polls = polls.map(p => p && p.info).filter(Boolean);
}
return hookData;
};

const result = await NodeBB.Topics.getTopicFields(tid, ['mainPid', 'cid']);
if (parseInt(result.mainPid, 10) !== parseInt(obj.data.pid, 10)) {
return obj;
Hooks.filter.postCreate = async function (hookData) {
// post is going to be saved to db, data is what is submitted by user
const { post, data } = hookData;
if (Array.isArray(data?.polls) && data.polls.length) {
const savedPolls = await Poll.add(post, data.polls);
if (savedPolls.length) {
post.pollIds = JSON.stringify(savedPolls.map(p => String(p.pollId)));
}
}
return hookData;
};

await canCreate(result.cid, obj.post.editor);
Hooks.filter.postGetFields = async function (hookData) {
if (!hookData.fields.includes('pollIds')) {
// when a new post is created it doesnt send pollIds to the client
// force load pollIds so it is available when the client needs to load the poll data
const postData = await db.getObjectsFields(hookData.pids.map(pid => `post:${pid}`), ['pollIds']);
hookData.posts.forEach((post, index) => {
if (post) {
post.pollIds = postData[index].pollIds || '';
}
});
}
return hookData;
};

const postData = await savePoll({
...obj.post,
uid: obj.data.uid,
pid: obj.data.pid,
tid: tid,
});
delete postData.uid;
delete postData.pid;
delete postData.tid;
obj.post = postData;
Hooks.filter.postEdit = async function (hookData) {
const currentPollIds = await Poll.getPollIdsByPid(hookData.data.pid);
const toAdd = hookData.data.polls.filter(p => !currentPollIds.includes(String(p.pollId)));

if (!postData.pollId) {
return obj;
if (toAdd.length) {
const cid = await posts.getCidByPid(hookData.data.pid, 'cid');
await canCreate(cid, hookData.data.uid);
}

// NodeBB only updates the edited, editor and content fields, so we add the pollId field manually.
await NodeBB.Posts.setPostField(obj.data.pid, 'pollId', postData.pollId);
return obj;
await Poll.edit({
...hookData.post,
uid: hookData.data.uid,
pid: hookData.data.pid,
}, hookData.data.polls);
hookData.post.pollIds = JSON.stringify(hookData.data.polls.map(p => String(p.pollId)));
return hookData;
};

Hooks.filter.topicPost = async function (data) {
if (Serializer.hasMarkup(data.content)) {
if (Array.isArray(data.polls) && data.polls.length) {
await canCreate(data.cid, data.uid);
return data;
}
return data;
};

Hooks.action.postDelete = async function (data) {
const pollId = await Poll.getPollIdByPid(data.post.pid);
if (pollId) {
await Poll.delete(pollId);
Hooks.filter.topicReply = async function (data) {
if (Array.isArray(data.polls) && data.polls.length) {
const cid = await topics.getTopicField(data.tid, 'cid');
await canCreate(cid, data.uid);
}
return data;
};

Hooks.action.postRestore = async function (data) {
const pollId = await Poll.getPollIdByPid(data.post.pid);
if (pollId) {
await Poll.restore(pollId);
Hooks.action.postDelete = async function (data) {
const pollIds = await Poll.getPollIdsByPid(data.post.pid);
if (pollIds.length) {
await Poll.delete(pollIds);
}
};

Hooks.action.topicDelete = async function (data) {
const pollId = await Poll.getPollIdByTid(data.topic.tid);
if (pollId) {
Poll.delete(pollId);
Hooks.action.postRestore = async function (data) {
const pollIds = await Poll.getPollIdsByPid(data.post.pid);
if (pollIds.length) {
await Poll.restore(pollIds);
}
};

Hooks.action.topicRestore = async function (data) {
const pollId = await Poll.getPollIdByTid(data.topic.tid);
if (pollId) {
Poll.restore(pollId);
Hooks.action.postsPurge = async function (data) {
const { posts } = data;
const pollIds = await Poll.getPollIdsByPids(posts.map(p => p.pid));
const toDelete = pollIds.flat();
if (toDelete.length) {
await Poll.deletePolls(toDelete);
}
};

async function canCreate(cid, uid) {
const can = await NodeBB.Privileges.categories.can('poll:create', cid, uid);
const can = await privileges.categories.can('poll:create', cid, uid);
if (!can) {
throw new Error('[[poll:error.privilege.create]]');
}
}

// called from filter:post.edit and filter:post.create
async function savePoll(obj) {
const postobj = obj.post ? obj.post : obj;
const pollData = Serializer.serialize(postobj.content, Config.settings.get());

if (!pollData || !pollData.options.length) {
return obj;
}

const pollId = await Poll.add(pollData, postobj);

postobj.pollId = pollId;
postobj.content = Serializer.removeMarkup(postobj.content);

return obj;
}

1 change: 0 additions & 1 deletion lib/nodebb.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ module.exports = {
Privileges: require.main.require('./src/privileges'),
Plugins: require.main.require('./src/plugins'),
PluginSockets: require.main.require('./src/socket.io/plugins'),
AdminSockets: require.main.require('./src/socket.io/admin').plugins,
SocketIndex: require.main.require('./src/socket.io/index'),
Translator: require.main.require('./src/translator'),
winston: require.main.require('winston'),
Expand Down
Loading