nodebb-plugin-poll 3.2.1 → 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.1",
3
+ "version": "3.2.3",
4
4
  "description": "NodeBB Poll Plugin",
5
5
  "main": "library.js",
6
6
  "scripts": {
package/plugin.json CHANGED
@@ -25,7 +25,6 @@
25
25
  ],
26
26
  "scripts": [
27
27
  "public/js/poll/main.js",
28
- "public/js/poll/sockets.js",
29
28
  "public/js/poll/creator.js",
30
29
  "public/js/poll/view.js"
31
30
  ],
@@ -70,15 +70,17 @@
70
70
  return alerts.error('[[error:category-not-selected]]');
71
71
  }
72
72
 
73
- Poll.sockets.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]]');
79
82
  }
80
-
81
- Poll.sockets.getConfig(null, function (err, config) {
83
+ socket.emit('plugins.poll.getConfig', null, function (err, config) {
82
84
  if (err) {
83
85
  return alerts.error(err);
84
86
  }
@@ -217,6 +219,7 @@
217
219
  title: obj['settings.title'],
218
220
  maxvotes: obj['settings.maxvotes'],
219
221
  disallowVoteUpdate: obj['settings.disallowVoteUpdate'] === 'on' ? 'true' : 'false',
222
+ allowAnonVoting: obj['settings.allowAnonVoting'] === 'on' ? 'true' : 'false',
220
223
  end: obj['settings.end'],
221
224
  },
222
225
  };
@@ -1,5 +1,7 @@
1
1
  'use strict';
2
2
 
3
+ /* global Poll */
4
+
3
5
  window.Poll = {};
4
6
 
5
7
  (function () {
@@ -9,7 +11,9 @@ window.Poll = {};
9
11
  });
10
12
  };
11
13
 
14
+ // eslint-disable-next-line import/no-unresolved
12
15
  require('poll/serializer')(window.utils);
16
+
13
17
  $(window).on('action:topic.loading', function () {
14
18
  if (ajaxify.data.posts.length > 0 && ajaxify.data.posts[0].hasOwnProperty('pollId')) {
15
19
  getPoll(ajaxify.data.posts[0].pollId);
@@ -30,11 +34,15 @@ window.Poll = {};
30
34
  }
31
35
  });
32
36
 
37
+ socket.on('event:poll.voteChange', function (data) {
38
+ Poll.view.update(data);
39
+ });
40
+
33
41
  function getPoll(pollId) {
34
42
  pollId = parseInt(pollId, 10);
35
43
 
36
44
  if (!isNaN(pollId)) {
37
- Poll.sockets.getPoll({ pollId: pollId }, function (err, pollData) {
45
+ socket.emit('plugins.poll.get', { pollId }, function (err, pollData) {
38
46
  if (err) {
39
47
  return Poll.alertError(err.message);
40
48
  }
@@ -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
- Poll.sockets.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 () {
@@ -55,8 +76,7 @@
55
76
  pollId: view.pollData.info.pollId,
56
77
  options: votes,
57
78
  };
58
-
59
- Poll.sockets.updateVote(voteData, function (err) {
79
+ socket.emit('plugins.poll.updateVote', voteData, function (err) {
60
80
  if (err) {
61
81
  return Poll.alertError(err.message);
62
82
  }
@@ -76,7 +96,7 @@
76
96
  handle: function (view) {
77
97
  var voteData = { pollId: view.pollData.info.pollId };
78
98
 
79
- Poll.sockets.removeVote(voteData, function (err) {
99
+ socket.emit('plugins.poll.removeVote', voteData, function (err) {
80
100
  if (err) {
81
101
  return Poll.alertError(err.message);
82
102
  }
@@ -119,7 +139,7 @@
119
139
  handle: function (view, e) {
120
140
  var optionId = $(e.currentTarget).parents('[data-poll-option-id]').data('poll-option-id');
121
141
 
122
- Poll.sockets.getOptionDetails({
142
+ socket.emit('plugins.poll.getOptionDetails', {
123
143
  pollId: view.pollData.info.pollId,
124
144
  optionId: optionId,
125
145
  }, function (err, details) {
@@ -140,7 +160,7 @@
140
160
  });
141
161
  },
142
162
  handle: function (view) {
143
- Poll.sockets.getConfig(null, function (err, config) {
163
+ socket.emit('plugins.poll.getConfig', null, function (err, config) {
144
164
  if (err) {
145
165
  console.error(err);
146
166
  }
@@ -170,6 +190,7 @@
170
190
  votingPanel: panel.find('.poll-view-voting'),
171
191
  resultsPanel: panel.find('.poll-view-results'),
172
192
  voteButton: panel.find('.poll-button-vote'),
193
+ voteAnonButton: panel.find('.poll-button-vote-anon'),
173
194
  updateVoteButton: panel.find('.poll-button-update-vote'),
174
195
  removeVoteButton: panel.find('.poll-button-remove-vote'),
175
196
  votingPanelButton: panel.find('.poll-button-voting'),
@@ -250,15 +271,17 @@
250
271
  };
251
272
 
252
273
  View.prototype.showOptionDetails = function (details) {
253
- app.parseAndTranslate('poll/view/details', details, function (html) {
254
- bootbox.dialog({
255
- message: html,
256
- backdrop: true,
257
- buttons: {
258
- close: {
259
- label: 'Close',
274
+ require(['bootbox'], function (bootbox) {
275
+ app.parseAndTranslate('poll/view/details', details, function (html) {
276
+ bootbox.dialog({
277
+ message: html,
278
+ backdrop: true,
279
+ buttons: {
280
+ close: {
281
+ label: 'Close',
282
+ },
260
283
  },
261
- },
284
+ });
262
285
  });
263
286
  });
264
287
  };
@@ -290,6 +313,9 @@
290
313
  }
291
314
  } else {
292
315
  this.showVoteButton();
316
+ if (this.pollData.settings.allowAnonVoting) {
317
+ this.showVoteAnonButton();
318
+ }
293
319
  }
294
320
  this.dom.votingPanel.removeClass('hidden');
295
321
  };
@@ -300,6 +326,7 @@
300
326
  this.hideRemoveVoteButton();
301
327
  this.resetVotingForm();
302
328
  this.hideVoteButton();
329
+ this.hideVoteAnonButton();
303
330
  this.dom.votingPanel.addClass('hidden');
304
331
  };
305
332
 
@@ -326,6 +353,14 @@
326
353
  this.dom.voteButton.addClass('hidden');
327
354
  };
328
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
+
329
364
  View.prototype.showUpdateVoteButton = function () {
330
365
  this.dom.updateVoteButton.removeClass('hidden');
331
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">
@@ -1,4 +1,4 @@
1
- <h2>{voteCount} [[poll:vote_count]]</h2>
1
+ <h4>{voteCount} [[poll:vote_count]]</h4>
2
2
  <!-- BEGIN votes -->
3
3
  <a href="{config.relative_path}/user/{votes.userslug}" class="poll-result-details">
4
4
  {buildAvatar(votes, "24px", true)}
@@ -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>
@@ -1,61 +0,0 @@
1
- 'use strict';
2
-
3
- (function (Poll) {
4
- var messages = [
5
- {
6
- method: 'getPoll',
7
- message: 'plugins.poll.get',
8
- },
9
- {
10
- method: 'vote',
11
- message: 'plugins.poll.vote',
12
- },
13
- {
14
- method: 'updateVote',
15
- message: 'plugins.poll.updateVote',
16
- },
17
- {
18
- method: 'removeVote',
19
- message: 'plugins.poll.removeVote',
20
- },
21
- {
22
- method: 'getOptionDetails',
23
- message: 'plugins.poll.getOptionDetails',
24
- },
25
- {
26
- method: 'getConfig',
27
- message: 'plugins.poll.getConfig',
28
- },
29
- {
30
- method: 'canCreate',
31
- message: 'plugins.poll.canCreate',
32
- },
33
- ];
34
-
35
- var handlers = [
36
- {
37
- event: 'event:poll.voteChange',
38
- handle: function (data) {
39
- Poll.view.update(data);
40
- },
41
- },
42
- ];
43
-
44
- function init() {
45
- handlers.forEach(function (handler) {
46
- if (socket.listeners(handler.event).length === 0) {
47
- socket.on(handler.event, handler.handle);
48
- }
49
- });
50
-
51
- messages.forEach(function (message) {
52
- Poll.sockets[message.method] = function (data, callback) {
53
- socket.emit(message.message, data, callback);
54
- };
55
- });
56
- }
57
-
58
- Poll.sockets = {};
59
-
60
- init();
61
- }(window.Poll));