nodebb-plugin-poll 3.2.2 → 3.2.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -0
- package/languages/en_GB/poll.json +3 -0
- package/languages/en_US/poll.json +3 -0
- package/lib/config.js +1 -0
- package/lib/sockets.js +33 -1
- package/lib/vote.js +16 -3
- package/package.json +1 -1
- package/public/js/poll/creator.js +6 -2
- package/public/js/poll/serializer.js +8 -0
- package/public/js/poll/view.js +57 -23
- package/templates/poll/creator.tpl +8 -5
- package/templates/poll/view.tpl +6 -5
package/README.md
CHANGED
|
@@ -11,6 +11,7 @@ Currently supported settings:
|
|
|
11
11
|
|
|
12
12
|
maxvotes="1" //Max number of votes per user. If larger than 1, a multiple choice poll will be created
|
|
13
13
|
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
|
|
14
15
|
title="Poll title" //Poll title
|
|
15
16
|
|
|
16
17
|
There's also a helpful modal available that will allow you to easily create a poll:
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
"default_title": "Default poll title",
|
|
9
9
|
"max_votes": "Maximum number of votes per user",
|
|
10
10
|
"disallow_vote_update": "Don't allow users to update their vote",
|
|
11
|
+
"allow_anon_voting": "Allow anonymous voting",
|
|
11
12
|
"info_choices": "A number greater than 1 will enable multiple choice polls.",
|
|
12
13
|
"settings": "Settings",
|
|
13
14
|
"save": "Save",
|
|
@@ -29,10 +30,12 @@
|
|
|
29
30
|
|
|
30
31
|
"error.not_main": "Can only add poll in main post.",
|
|
31
32
|
"error.privilege.create": "You're not allowed to create a poll",
|
|
33
|
+
"error.anon-voting-not-allowed": "This poll does not allow voting anonymously",
|
|
32
34
|
|
|
33
35
|
"warning.redactor": "You're using Redactor. Do <strong>not</strong> edit the markup manually. Instead, re-open the poll creator.",
|
|
34
36
|
|
|
35
37
|
"vote": "Vote",
|
|
38
|
+
"vote_anonymously": "Vote anonymously",
|
|
36
39
|
"update_vote": "Update vote",
|
|
37
40
|
"remove_vote": "Remove vote",
|
|
38
41
|
"to_voting": "To Voting",
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
"default_title": "Default poll title",
|
|
9
9
|
"max_votes": "Maximum number of votes per user",
|
|
10
10
|
"disallow_vote_update": "Don't allow users to update their vote",
|
|
11
|
+
"allow_anon_voting": "Allow anonymous voting",
|
|
11
12
|
"info_choices": "A number greater than 1 will enable multiple choice polls.",
|
|
12
13
|
"settings": "Settings",
|
|
13
14
|
"save": "Save",
|
|
@@ -29,10 +30,12 @@
|
|
|
29
30
|
|
|
30
31
|
"error.not_main": "Can only add poll in main post.",
|
|
31
32
|
"error.privilege.create": "You're not allowed to create a poll",
|
|
33
|
+
"error.anon-voting-not-allowed": "This poll does not allow voting anonymously",
|
|
32
34
|
|
|
33
35
|
"warning.redactor": "You're using Redactor. Do <strong>not</strong> edit the markup manually. Instead, re-open the poll creator.",
|
|
34
36
|
|
|
35
37
|
"vote": "Vote",
|
|
38
|
+
"vote_anonymously": "Vote anonymously",
|
|
36
39
|
"update_vote": "Update vote",
|
|
37
40
|
"remove_vote": "Remove vote",
|
|
38
41
|
"to_voting": "To Voting",
|
package/lib/config.js
CHANGED
package/lib/sockets.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
+
const _ = require.main.require('lodash');
|
|
3
4
|
const NodeBB = require('./nodebb');
|
|
4
5
|
|
|
5
6
|
const Config = require('./config');
|
|
@@ -31,6 +32,10 @@ Sockets.get = async function (socket, data) {
|
|
|
31
32
|
throw new Error('Legacy polls are not supported');
|
|
32
33
|
}
|
|
33
34
|
|
|
35
|
+
if (parseInt(pollData.settings.allowAnonVoting, 10) === 1) {
|
|
36
|
+
await anonymizeVoters(socket.uid, data.pollId, pollData.options);
|
|
37
|
+
}
|
|
38
|
+
|
|
34
39
|
pollData.optionType = parseInt(pollData.settings.maxvotes, 10) > 1 ? 'checkbox' : 'radio';
|
|
35
40
|
return pollData;
|
|
36
41
|
};
|
|
@@ -59,6 +64,10 @@ Sockets.vote = async function (socket, data) {
|
|
|
59
64
|
throw new Error(`You can only vote for ${settings.maxvotes} options on this poll.`);
|
|
60
65
|
}
|
|
61
66
|
|
|
67
|
+
if (data.voteAnon && parseInt(settings.allowAnonVoting, 10) !== 1) {
|
|
68
|
+
throw new Error('[[poll:error.anon-voting-not-allowed]]');
|
|
69
|
+
}
|
|
70
|
+
|
|
62
71
|
if (!canVote || !data.options.length) {
|
|
63
72
|
throw new Error('Already voted or invalid option');
|
|
64
73
|
}
|
|
@@ -128,14 +137,20 @@ Sockets.getOptionDetails = async function (socket, data) {
|
|
|
128
137
|
if (!socket.uid || !data || isNaN(parseInt(data.pollId, 10)) || isNaN(parseInt(data.optionId, 10))) {
|
|
129
138
|
throw new Error('Invalid request');
|
|
130
139
|
}
|
|
131
|
-
const [poll, option] = await Promise.all([
|
|
140
|
+
const [poll, settings, option] = await Promise.all([
|
|
132
141
|
Poll.getInfo(data.pollId),
|
|
142
|
+
Poll.getSettings(data.pollId),
|
|
133
143
|
Poll.getOption(data.pollId, data.optionId, true),
|
|
134
144
|
]);
|
|
135
145
|
|
|
136
146
|
if (!option.votes || !option.votes.length) {
|
|
137
147
|
return option;
|
|
138
148
|
}
|
|
149
|
+
|
|
150
|
+
if (parseInt(settings.allowAnonVoting, 10) === 1) {
|
|
151
|
+
await anonymizeVoters(socket.uid, data.pollId, [option]);
|
|
152
|
+
}
|
|
153
|
+
|
|
139
154
|
const userData = await NodeBB.User.getUsersFields(option.votes, [
|
|
140
155
|
'uid', 'username', 'userslug', 'picture',
|
|
141
156
|
]);
|
|
@@ -160,6 +175,23 @@ Sockets.canCreate = async function (socket, data) {
|
|
|
160
175
|
return await checkPrivs(data.cid, socket.uid);
|
|
161
176
|
};
|
|
162
177
|
|
|
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
|
+
|
|
163
195
|
async function checkPrivs(cid, socketUid) {
|
|
164
196
|
const can = await NodeBB.Privileges.categories.can('poll:create', cid, socketUid);
|
|
165
197
|
if (!can) {
|
package/lib/vote.js
CHANGED
|
@@ -9,12 +9,19 @@ Vote.add = async function (voteData) {
|
|
|
9
9
|
const { pollId, options, uid } = voteData;
|
|
10
10
|
const timestamp = +new Date();
|
|
11
11
|
|
|
12
|
-
|
|
12
|
+
const promises = [
|
|
13
13
|
NodeBB.db.sortedSetAdd(`poll:${pollId}:voters`, timestamp, uid),
|
|
14
14
|
NodeBB.db.sortedSetAddBulk(
|
|
15
15
|
options.map(option => ([`poll:${pollId}:options:${option}:votes`, timestamp, uid]))
|
|
16
16
|
),
|
|
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);
|
|
18
25
|
};
|
|
19
26
|
|
|
20
27
|
Vote.remove = async function (voteData) {
|
|
@@ -29,7 +36,10 @@ Vote.removeUidVote = async function (uid, pollId) {
|
|
|
29
36
|
const vote = await Vote.getUidVote(uid, pollId);
|
|
30
37
|
const options = vote.options || [];
|
|
31
38
|
await Promise.all([
|
|
32
|
-
NodeBB.db.
|
|
39
|
+
NodeBB.db.sortedSetsRemove([
|
|
40
|
+
`poll:${pollId}:voters`,
|
|
41
|
+
`poll:${pollId}:anon:voters`,
|
|
42
|
+
], uid),
|
|
33
43
|
NodeBB.db.sortedSetsRemove(
|
|
34
44
|
options.map(option => `poll:${pollId}:options:${option}:votes`),
|
|
35
45
|
uid,
|
|
@@ -49,7 +59,10 @@ Vote.getUidVote = async function (uid, pollId) {
|
|
|
49
59
|
};
|
|
50
60
|
|
|
51
61
|
Vote.update = async function (voteData) {
|
|
62
|
+
const { uid, pollId } = voteData;
|
|
63
|
+
const isVotedAnon = await NodeBB.db.isSortedSetMember(`poll:${pollId}:anon:voters`, uid);
|
|
52
64
|
await Vote.remove(voteData);
|
|
65
|
+
voteData.voteAnon = isVotedAnon;
|
|
53
66
|
await Vote.add(voteData);
|
|
54
67
|
};
|
|
55
68
|
|
package/package.json
CHANGED
|
@@ -70,9 +70,12 @@
|
|
|
70
70
|
return alerts.error('[[error:category-not-selected]]');
|
|
71
71
|
}
|
|
72
72
|
|
|
73
|
-
socket.emit('plugins.poll.canCreate', {
|
|
73
|
+
socket.emit('plugins.poll.canCreate', {
|
|
74
|
+
cid: post.cid,
|
|
75
|
+
pid: post.pid,
|
|
76
|
+
}, function (err, canCreate) {
|
|
74
77
|
if (err) {
|
|
75
|
-
return alerts.error(err
|
|
78
|
+
return alerts.error(err);
|
|
76
79
|
}
|
|
77
80
|
if (!canCreate) {
|
|
78
81
|
return alerts.error('[[error:no-privileges]]');
|
|
@@ -216,6 +219,7 @@
|
|
|
216
219
|
title: obj['settings.title'],
|
|
217
220
|
maxvotes: obj['settings.maxvotes'],
|
|
218
221
|
disallowVoteUpdate: obj['settings.disallowVoteUpdate'] === 'on' ? 'true' : 'false',
|
|
222
|
+
allowAnonVoting: obj['settings.allowAnonVoting'] === 'on' ? 'true' : 'false',
|
|
219
223
|
end: obj['settings.end'],
|
|
220
224
|
},
|
|
221
225
|
};
|
|
@@ -30,6 +30,14 @@ module.exports = function (utils) {
|
|
|
30
30
|
return value === 'true' || value === true ? 1 : 0;
|
|
31
31
|
},
|
|
32
32
|
},
|
|
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
|
+
},
|
|
33
41
|
end: {
|
|
34
42
|
test: function (value) {
|
|
35
43
|
return (!isNaN(value) && parseInt(value, 10) > Date.now());
|
package/public/js/poll/view.js
CHANGED
|
@@ -1,6 +1,33 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
(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
|
+
|
|
4
31
|
var Actions = [
|
|
5
32
|
{
|
|
6
33
|
// Voting
|
|
@@ -11,33 +38,27 @@
|
|
|
11
38
|
});
|
|
12
39
|
},
|
|
13
40
|
handle: function (view) {
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
return parseInt(option.value, 10);
|
|
41
|
+
vote(view, {
|
|
42
|
+
voteAnon: false,
|
|
17
43
|
});
|
|
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
|
-
}
|
|
37
44
|
},
|
|
38
45
|
},
|
|
39
46
|
{
|
|
40
|
-
//
|
|
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
|
|
41
62
|
register: function (view) {
|
|
42
63
|
var self = this;
|
|
43
64
|
view.dom.updateVoteButton.off('click').on('click', function () {
|
|
@@ -169,6 +190,7 @@
|
|
|
169
190
|
votingPanel: panel.find('.poll-view-voting'),
|
|
170
191
|
resultsPanel: panel.find('.poll-view-results'),
|
|
171
192
|
voteButton: panel.find('.poll-button-vote'),
|
|
193
|
+
voteAnonButton: panel.find('.poll-button-vote-anon'),
|
|
172
194
|
updateVoteButton: panel.find('.poll-button-update-vote'),
|
|
173
195
|
removeVoteButton: panel.find('.poll-button-remove-vote'),
|
|
174
196
|
votingPanelButton: panel.find('.poll-button-voting'),
|
|
@@ -291,6 +313,9 @@
|
|
|
291
313
|
}
|
|
292
314
|
} else {
|
|
293
315
|
this.showVoteButton();
|
|
316
|
+
if (this.pollData.settings.allowAnonVoting) {
|
|
317
|
+
this.showVoteAnonButton();
|
|
318
|
+
}
|
|
294
319
|
}
|
|
295
320
|
this.dom.votingPanel.removeClass('hidden');
|
|
296
321
|
};
|
|
@@ -301,6 +326,7 @@
|
|
|
301
326
|
this.hideRemoveVoteButton();
|
|
302
327
|
this.resetVotingForm();
|
|
303
328
|
this.hideVoteButton();
|
|
329
|
+
this.hideVoteAnonButton();
|
|
304
330
|
this.dom.votingPanel.addClass('hidden');
|
|
305
331
|
};
|
|
306
332
|
|
|
@@ -327,6 +353,14 @@
|
|
|
327
353
|
this.dom.voteButton.addClass('hidden');
|
|
328
354
|
};
|
|
329
355
|
|
|
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
|
+
|
|
330
364
|
View.prototype.showUpdateVoteButton = function () {
|
|
331
365
|
this.dom.updateVoteButton.removeClass('hidden');
|
|
332
366
|
};
|
|
@@ -26,11 +26,14 @@
|
|
|
26
26
|
min="1" max="10" step="1" placeholder="[[poll:max_votes_placeholder]]" class="form-control">
|
|
27
27
|
</div>
|
|
28
28
|
|
|
29
|
-
<div class="mb-3">
|
|
30
|
-
<
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
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}}}>
|
|
34
37
|
</div>
|
|
35
38
|
|
|
36
39
|
<div class="mb-3">
|
package/templates/poll/view.tpl
CHANGED
|
@@ -18,11 +18,12 @@
|
|
|
18
18
|
</div>
|
|
19
19
|
|
|
20
20
|
<div class="poll-view-buttons">
|
|
21
|
-
<button type="button" class="btn btn-primary poll-button-vote hidden">[[poll:vote]]</button>
|
|
22
|
-
<button type="button" class="btn btn-primary poll-button-
|
|
23
|
-
<button type="button" class="btn btn-
|
|
24
|
-
<button type="button" class="btn btn-
|
|
25
|
-
<button type="button" class="btn btn-link poll-button-
|
|
21
|
+
<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>
|
|
23
|
+
<button type="button" class="btn btn-sm btn-primary poll-button-update-vote hidden">[[poll:update_vote]]</button>
|
|
24
|
+
<button type="button" class="btn btn-sm btn-danger poll-button-remove-vote hidden">[[poll:remove_vote]]</button>
|
|
25
|
+
<button type="button" class="btn btn-sm btn-link poll-button-results hidden">[[poll:to_results]]</button>
|
|
26
|
+
<button type="button" class="btn btn-sm btn-link poll-button-voting hidden">[[poll:to_voting]]</button>
|
|
26
27
|
</div>
|
|
27
28
|
</div>
|
|
28
29
|
</div>
|