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 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
@@ -28,6 +28,7 @@ Config.defaults = {
28
28
  title: 'Poll',
29
29
  maxvotes: 1,
30
30
  disallowVoteUpdate: 0,
31
+ allowAnonVoting: 0,
31
32
  end: 0,
32
33
  },
33
34
  };
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
- await Promise.all([
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.sortedSetRemove(`poll:${pollId}:voters`, uid),
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nodebb-plugin-poll",
3
- "version": "3.2.2",
3
+ "version": "3.2.3",
4
4
  "description": "NodeBB Poll Plugin",
5
5
  "main": "library.js",
6
6
  "scripts": {
@@ -70,9 +70,12 @@
70
70
  return alerts.error('[[error:category-not-selected]]');
71
71
  }
72
72
 
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) {
74
77
  if (err) {
75
- return alerts.error(err.message);
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());
@@ -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
- 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,
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
- // 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
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
- <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}}}>
34
37
  </div>
35
38
 
36
39
  <div class="mb-3">
@@ -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-update-vote hidden">[[poll:update_vote]]</button>
23
- <button type="button" class="btn btn-danger poll-button-remove-vote hidden">[[poll:remove_vote]]</button>
24
- <button type="button" class="btn btn-link poll-button-results hidden">[[poll:to_results]]</button>
25
- <button type="button" class="btn btn-link poll-button-voting hidden">[[poll:to_voting]]</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>