Skip to content

Commit fe5ee0d

Browse files
committed
feat: allow anon voting, closes #140 , closes #7, closes #106
1 parent bc3578a commit fe5ee0d

File tree

11 files changed

+137
-34
lines changed

11 files changed

+137
-34
lines changed

README.md

+1
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ Currently supported settings:
1111

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

1617
There's also a helpful modal available that will allow you to easily create a poll:

languages/en_GB/poll.json

+3
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
"default_title": "Default poll title",
99
"max_votes": "Maximum number of votes per user",
1010
"disallow_vote_update": "Don't allow users to update their vote",
11+
"allow_anon_voting": "Allow anonymous voting",
1112
"info_choices": "A number greater than 1 will enable multiple choice polls.",
1213
"settings": "Settings",
1314
"save": "Save",
@@ -29,10 +30,12 @@
2930

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

3335
"warning.redactor": "You're using Redactor. Do <strong>not</strong> edit the markup manually. Instead, re-open the poll creator.",
3436

3537
"vote": "Vote",
38+
"vote_anonymously": "Vote anonymously",
3639
"update_vote": "Update vote",
3740
"remove_vote": "Remove vote",
3841
"to_voting": "To Voting",

languages/en_US/poll.json

+3
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
"default_title": "Default poll title",
99
"max_votes": "Maximum number of votes per user",
1010
"disallow_vote_update": "Don't allow users to update their vote",
11+
"allow_anon_voting": "Allow anonymous voting",
1112
"info_choices": "A number greater than 1 will enable multiple choice polls.",
1213
"settings": "Settings",
1314
"save": "Save",
@@ -29,10 +30,12 @@
2930

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

3335
"warning.redactor": "You're using Redactor. Do <strong>not</strong> edit the markup manually. Instead, re-open the poll creator.",
3436

3537
"vote": "Vote",
38+
"vote_anonymously": "Vote anonymously",
3639
"update_vote": "Update vote",
3740
"remove_vote": "Remove vote",
3841
"to_voting": "To Voting",

lib/config.js

+1
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ Config.defaults = {
2828
title: 'Poll',
2929
maxvotes: 1,
3030
disallowVoteUpdate: 0,
31+
allowAnonVoting: 0,
3132
end: 0,
3233
},
3334
};

lib/sockets.js

+33-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
'use strict';
22

3+
const _ = require.main.require('lodash');
34
const NodeBB = require('./nodebb');
45

56
const Config = require('./config');
@@ -31,6 +32,10 @@ Sockets.get = async function (socket, data) {
3132
throw new Error('Legacy polls are not supported');
3233
}
3334

35+
if (parseInt(pollData.settings.allowAnonVoting, 10) === 1) {
36+
await anonymizeVoters(socket.uid, data.pollId, pollData.options);
37+
}
38+
3439
pollData.optionType = parseInt(pollData.settings.maxvotes, 10) > 1 ? 'checkbox' : 'radio';
3540
return pollData;
3641
};
@@ -59,6 +64,10 @@ Sockets.vote = async function (socket, data) {
5964
throw new Error(`You can only vote for ${settings.maxvotes} options on this poll.`);
6065
}
6166

67+
if (data.voteAnon && parseInt(settings.allowAnonVoting, 10) !== 1) {
68+
throw new Error('[[poll:error.anon-voting-not-allowed]]');
69+
}
70+
6271
if (!canVote || !data.options.length) {
6372
throw new Error('Already voted or invalid option');
6473
}
@@ -128,14 +137,20 @@ Sockets.getOptionDetails = async function (socket, data) {
128137
if (!socket.uid || !data || isNaN(parseInt(data.pollId, 10)) || isNaN(parseInt(data.optionId, 10))) {
129138
throw new Error('Invalid request');
130139
}
131-
const [poll, option] = await Promise.all([
140+
const [poll, settings, option] = await Promise.all([
132141
Poll.getInfo(data.pollId),
142+
Poll.getSettings(data.pollId),
133143
Poll.getOption(data.pollId, data.optionId, true),
134144
]);
135145

136146
if (!option.votes || !option.votes.length) {
137147
return option;
138148
}
149+
150+
if (parseInt(settings.allowAnonVoting, 10) === 1) {
151+
await anonymizeVoters(socket.uid, data.pollId, [option]);
152+
}
153+
139154
const userData = await NodeBB.User.getUsersFields(option.votes, [
140155
'uid', 'username', 'userslug', 'picture',
141156
]);
@@ -160,6 +175,23 @@ Sockets.canCreate = async function (socket, data) {
160175
return await checkPrivs(data.cid, socket.uid);
161176
};
162177

178+
async function anonymizeVoters(callerUid, pollId, options) {
179+
const uids = _.uniq(options.map(opt => opt.votes).flat());
180+
const [isAnon, isPrivileged] = await Promise.all([
181+
NodeBB.db.isSortedSetMembers(`poll:${pollId}:anon:voters`, uids),
182+
NodeBB.User.isPrivileged(callerUid),
183+
]);
184+
if (isPrivileged) {
185+
return;
186+
}
187+
const uidToIsAnon = _.zipObject(uids, isAnon);
188+
options.forEach((opt) => {
189+
opt.votes = opt.votes.map(
190+
uid => (uidToIsAnon[uid] && String(callerUid) !== uid ? 0 : uid)
191+
);
192+
});
193+
}
194+
163195
async function checkPrivs(cid, socketUid) {
164196
const can = await NodeBB.Privileges.categories.can('poll:create', cid, socketUid);
165197
if (!can) {

lib/vote.js

+16-3
Original file line numberDiff line numberDiff line change
@@ -9,12 +9,19 @@ Vote.add = async function (voteData) {
99
const { pollId, options, uid } = voteData;
1010
const timestamp = +new Date();
1111

12-
await Promise.all([
12+
const promises = [
1313
NodeBB.db.sortedSetAdd(`poll:${pollId}:voters`, timestamp, uid),
1414
NodeBB.db.sortedSetAddBulk(
1515
options.map(option => ([`poll:${pollId}:options:${option}:votes`, timestamp, uid]))
1616
),
17-
]);
17+
];
18+
if (voteData.voteAnon) {
19+
promises.push(
20+
NodeBB.db.sortedSetAdd(`poll:${pollId}:anon:voters`, timestamp, uid)
21+
);
22+
}
23+
24+
await Promise.all(promises);
1825
};
1926

2027
Vote.remove = async function (voteData) {
@@ -29,7 +36,10 @@ Vote.removeUidVote = async function (uid, pollId) {
2936
const vote = await Vote.getUidVote(uid, pollId);
3037
const options = vote.options || [];
3138
await Promise.all([
32-
NodeBB.db.sortedSetRemove(`poll:${pollId}:voters`, uid),
39+
NodeBB.db.sortedSetsRemove([
40+
`poll:${pollId}:voters`,
41+
`poll:${pollId}:anon:voters`,
42+
], uid),
3343
NodeBB.db.sortedSetsRemove(
3444
options.map(option => `poll:${pollId}:options:${option}:votes`),
3545
uid,
@@ -49,7 +59,10 @@ Vote.getUidVote = async function (uid, pollId) {
4959
};
5060

5161
Vote.update = async function (voteData) {
62+
const { uid, pollId } = voteData;
63+
const isVotedAnon = await NodeBB.db.isSortedSetMember(`poll:${pollId}:anon:voters`, uid);
5264
await Vote.remove(voteData);
65+
voteData.voteAnon = isVotedAnon;
5366
await Vote.add(voteData);
5467
};
5568

public/js/poll/creator.js

+6-2
Original file line numberDiff line numberDiff line change
@@ -70,9 +70,12 @@
7070
return alerts.error('[[error:category-not-selected]]');
7171
}
7272

73-
socket.emit('plugins.poll.canCreate', { cid: post.cid, pid: post.pid }, function (err, canCreate) {
73+
socket.emit('plugins.poll.canCreate', {
74+
cid: post.cid,
75+
pid: post.pid,
76+
}, function (err, canCreate) {
7477
if (err) {
75-
return alerts.error(err.message);
78+
return alerts.error(err);
7679
}
7780
if (!canCreate) {
7881
return alerts.error('[[error:no-privileges]]');
@@ -216,6 +219,7 @@
216219
title: obj['settings.title'],
217220
maxvotes: obj['settings.maxvotes'],
218221
disallowVoteUpdate: obj['settings.disallowVoteUpdate'] === 'on' ? 'true' : 'false',
222+
allowAnonVoting: obj['settings.allowAnonVoting'] === 'on' ? 'true' : 'false',
219223
end: obj['settings.end'],
220224
},
221225
};

public/js/poll/serializer.js

+8
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,14 @@ module.exports = function (utils) {
3030
return value === 'true' || value === true ? 1 : 0;
3131
},
3232
},
33+
allowAnonVoting: {
34+
test: function (value) {
35+
return /true|false/.test(value);
36+
},
37+
parse: function (value) {
38+
return value === 'true' || value === true ? 1 : 0;
39+
},
40+
},
3341
end: {
3442
test: function (value) {
3543
return (!isNaN(value) && parseInt(value, 10) > Date.now());

public/js/poll/view.js

+57-23
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,33 @@
11
'use strict';
22

33
(function (Poll) {
4+
function vote(view, options) {
5+
var form = view.dom.votingPanel.find('form');
6+
var votes = form.serializeArray().map(function (option) {
7+
return parseInt(option.value, 10);
8+
});
9+
10+
if (votes.length > 0) {
11+
var voteData = {
12+
pollId: view.pollData.info.pollId,
13+
options: votes,
14+
voteAnon: options.voteAnon,
15+
};
16+
17+
socket.emit('plugins.poll.vote', voteData, function (err) {
18+
if (!config.loggedIn) {
19+
$(window).trigger('action:poll.vote.notloggedin');
20+
}
21+
22+
if (err) {
23+
return Poll.alertError(err.message);
24+
}
25+
26+
view.showResultsPanel();
27+
});
28+
}
29+
}
30+
431
var Actions = [
532
{
633
// Voting
@@ -11,33 +38,27 @@
1138
});
1239
},
1340
handle: function (view) {
14-
var form = view.dom.votingPanel.find('form');
15-
var votes = form.serializeArray().map(function (option) {
16-
return parseInt(option.value, 10);
41+
vote(view, {
42+
voteAnon: false,
1743
});
18-
19-
if (votes.length > 0) {
20-
var voteData = {
21-
pollId: view.pollData.info.pollId,
22-
options: votes,
23-
};
24-
25-
socket.emit('plugins.poll.vote', voteData, function (err) {
26-
if (!config.loggedIn) {
27-
$(window).trigger('action:poll.vote.notloggedin');
28-
}
29-
30-
if (err) {
31-
return Poll.alertError(err.message);
32-
}
33-
34-
view.showResultsPanel();
35-
});
36-
}
3744
},
3845
},
3946
{
40-
// Voting
47+
// vote anon
48+
register: function (view) {
49+
var self = this;
50+
view.dom.voteAnonButton.off('click').on('click', function () {
51+
self.handle(view);
52+
});
53+
},
54+
handle: function (view) {
55+
vote(view, {
56+
voteAnon: true,
57+
});
58+
},
59+
},
60+
{
61+
// update voting
4162
register: function (view) {
4263
var self = this;
4364
view.dom.updateVoteButton.off('click').on('click', function () {
@@ -169,6 +190,7 @@
169190
votingPanel: panel.find('.poll-view-voting'),
170191
resultsPanel: panel.find('.poll-view-results'),
171192
voteButton: panel.find('.poll-button-vote'),
193+
voteAnonButton: panel.find('.poll-button-vote-anon'),
172194
updateVoteButton: panel.find('.poll-button-update-vote'),
173195
removeVoteButton: panel.find('.poll-button-remove-vote'),
174196
votingPanelButton: panel.find('.poll-button-voting'),
@@ -291,6 +313,9 @@
291313
}
292314
} else {
293315
this.showVoteButton();
316+
if (this.pollData.settings.allowAnonVoting) {
317+
this.showVoteAnonButton();
318+
}
294319
}
295320
this.dom.votingPanel.removeClass('hidden');
296321
};
@@ -301,6 +326,7 @@
301326
this.hideRemoveVoteButton();
302327
this.resetVotingForm();
303328
this.hideVoteButton();
329+
this.hideVoteAnonButton();
304330
this.dom.votingPanel.addClass('hidden');
305331
};
306332

@@ -327,6 +353,14 @@
327353
this.dom.voteButton.addClass('hidden');
328354
};
329355

356+
View.prototype.showVoteAnonButton = function () {
357+
this.dom.voteAnonButton.removeClass('hidden');
358+
};
359+
360+
View.prototype.hideVoteAnonButton = function () {
361+
this.dom.voteAnonButton.addClass('hidden');
362+
};
363+
330364
View.prototype.showUpdateVoteButton = function () {
331365
this.dom.updateVoteButton.removeClass('hidden');
332366
};

templates/poll/creator.tpl

+8-5
Original file line numberDiff line numberDiff line change
@@ -26,11 +26,14 @@
2626
min="1" max="10" step="1" placeholder="[[poll:max_votes_placeholder]]" class="form-control">
2727
</div>
2828

29-
<div class="mb-3">
30-
<div class="form-check">
31-
<label class="form-check-label" for="pollDisallowVoteUpdate">[[poll:disallow_vote_update]]</label>
32-
<input class="form-check-input" type="checkbox" name="settings.disallowVoteUpdate" id="pollDisallowVoteUpdate" {{{if poll.settings.disallowVoteUpdate}}}checked{{{end}}}>
33-
</div>
29+
<div class="form-check mb-3">
30+
<label class="form-check-label" for="pollDisallowVoteUpdate">[[poll:disallow_vote_update]]</label>
31+
<input class="form-check-input" type="checkbox" name="settings.disallowVoteUpdate" id="pollDisallowVoteUpdate" {{{if poll.settings.disallowVoteUpdate}}}checked{{{end}}}>
32+
</div>
33+
34+
<div class="form-check mb-3">
35+
<label class="form-check-label" for="allowAnonVoting">[[poll:allow_anon_voting]]</label>
36+
<input class="form-check-input" type="checkbox" name="settings.allowAnonVoting" id="allowAnonVoting" {{{if poll.settings.allowAnonVoting}}}checked{{{end}}}>
3437
</div>
3538

3639
<div class="mb-3">

templates/poll/view.tpl

+1
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919

2020
<div class="poll-view-buttons">
2121
<button type="button" class="btn btn-sm btn-primary poll-button-vote hidden">[[poll:vote]]</button>
22+
<button type="button" class="btn btn-sm btn-primary poll-button-vote-anon hidden">[[poll:vote_anonymously]]</button>
2223
<button type="button" class="btn btn-sm btn-primary poll-button-update-vote hidden">[[poll:update_vote]]</button>
2324
<button type="button" class="btn btn-sm btn-danger poll-button-remove-vote hidden">[[poll:remove_vote]]</button>
2425
<button type="button" class="btn btn-sm btn-link poll-button-results hidden">[[poll:to_results]]</button>

0 commit comments

Comments
 (0)