nodebb-plugin-poll 3.2.2 → 3.2.4

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/.eslintrc CHANGED
@@ -1,116 +1,3 @@
1
1
  {
2
- "extends": "airbnb-base",
3
- "ignorePatterns": ["**/vendor/*.js"],
4
- "parserOptions": {
5
- "sourceType": "script"
6
- },
7
-
8
- "rules": {
9
- // === Configure rules for our style ===
10
- // imports must be resolvable
11
- "import/no-unresolved": "error",
12
- // use single quotes,
13
- // unless a different style allows avoiding escapes
14
- "quotes": ["error", "single", {
15
- "avoidEscape": true,
16
- "allowTemplateLiterals": true
17
- }],
18
- // allow else-if return
19
- "no-else-return": [ "error", { "allowElseIf": true } ],
20
- // expressions split over multiple lines
21
- // should break after the operator
22
- "operator-linebreak": [ "error", "after" ],
23
- // require arrow parens only when needed
24
- // and whenever the body is a block
25
- "arrow-parens": ["error", "as-needed", { "requireForBlockBody": true }],
26
- // what variables are errors in callbacks
27
- "handle-callback-err": [ "error","^(e$|(e|(.*(_e|E)))rr)" ],
28
- // allow dangling commas in functions
29
- // require them everywhere else
30
- "comma-dangle": ["error", {
31
- "arrays": "always-multiline",
32
- "objects": "always-multiline",
33
- "imports": "always-multiline",
34
- "exports": "always-multiline",
35
- "functions": "only-multiline"
36
- }],
37
- // we actually encourage `return await`
38
- "no-return-await": "off",
39
- // allow `while (true)`
40
- "no-constant-condition": ["error", { "checkLoops": false }],
41
- // allow ignoring an error with `catch`
42
- "no-empty": ["error", { "allowEmptyCatch": true }],
43
- // allow `3 + 5 - 1`, but not `3 * 5 - 1`
44
- "no-mixed-operators": ["error", { "allowSamePrecedence": true }],
45
- // require `'use strict';`
46
- "strict": ["error", "global"],
47
- // we actually use tabs for indentation
48
- "indent": ["error", "tab", { "SwitchCase": 1 }],
49
- "no-tabs": "off",
50
- // we want `== null` to also handle undefined
51
- "no-eq-null": "off",
52
- // allow `for (..; i++)`
53
- "no-plusplus": ["error", { "allowForLoopAfterthoughts": true }],
54
- // allow using functions defined later
55
- "no-use-before-define": ["error", "nofunc"],
56
- // require consistent newlines before and after braces
57
- // if contents are multiline
58
- "object-curly-newline": ["error", { "consistent": true, "multiline": true }],
59
- // require consistent linebreaks inline function parenthesis (arguments or params)
60
- "function-paren-newline": ["error", "consistent"],
61
- // only require const if all parts of destructuring can be const
62
- "prefer-const": ["error", { "destructuring": "all" }],
63
- // don't require destructuring for arrays or assignment
64
- "prefer-destructuring": ["error", {
65
- "VariableDeclarator": { "array": false, "object": true },
66
- "AssignmentExpression": { "array": false, "object": false }
67
- }],
68
- // identical to airbnb rule, except for allowing for..of, because we want to use it
69
- "no-restricted-syntax": [
70
- "error",
71
- {
72
- "selector": "ForInStatement",
73
- "message": "for..in loops iterate over the entire prototype chain, which is virtually never what you want. Use Object.{keys,values,entries}, and iterate over the resulting array."
74
- },
75
- {
76
- "selector": "LabeledStatement",
77
- "message": "Labels are a form of GOTO; using them makes code confusing and hard to maintain and understand."
78
- },
79
- {
80
- "selector": "WithStatement",
81
- "message": "`with` is disallowed in strict mode because it makes code impossible to predict and optimize."
82
- }
83
- ],
84
- // allow lines of up to 120 characters
85
- "max-len": ["error", { "code": 120, "tabWidth": 2, "ignoreUrls": true, "ignoreStrings": true, "ignoreTemplateLiterals": true, "ignoreRegExpLiterals": true }],
86
-
87
- // === Disable rules ===
88
- // more liberal naming
89
- "camelcase": "off",
90
- "no-underscore-dangle": "off",
91
- // don't require anonymous function names
92
- "func-names": "off",
93
- // allow console
94
- "no-console": "off",
95
- // allow new for side effects
96
- // allow new with non-capitalized
97
- "no-new": "off",
98
- "new-cap": "off",
99
- // allow shadowing variables (usually callbacks)
100
- "no-shadow": "off",
101
- // allow multiple empty lines in a row
102
- "no-multiple-empty-lines": "off",
103
- // allow not using object shorthand
104
- "object-shorthand": "off",
105
-
106
- // TODO
107
- "consistent-return": "off",
108
- "no-restricted-globals": "off",
109
- "no-prototype-builtins": "off",
110
- "import/no-extraneous-dependencies": "off",
111
- "import/no-dynamic-require": "off",
112
- "global-require": "off",
113
- "no-param-reassign": "off",
114
- "default-case": "off"
115
- }
2
+ "extends": "nodebb"
116
3
  }
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.4",
4
4
  "description": "NodeBB Poll Plugin",
5
5
  "main": "library.js",
6
6
  "scripts": {
@@ -26,7 +26,7 @@
26
26
  "cron": "^1.8.2"
27
27
  },
28
28
  "nbbpm": {
29
- "compatibility": "^3.2.0"
29
+ "compatibility": "^4.0.0"
30
30
  },
31
31
  "devDependencies": {
32
32
  "eslint": "8.57.0",
@@ -1,82 +1,3 @@
1
1
  {
2
- "globals": {
3
- "app": true,
4
- "io": true,
5
- "socket": true,
6
- "ajaxify": true,
7
- "config": true,
8
- "utils": true,
9
- "overrides": true,
10
- "componentHandler": true,
11
- "bootbox": true,
12
- "Visibility": true,
13
- "Tinycon": true,
14
- "Promise": true
15
- },
16
- "env": {
17
- "jquery": true,
18
- "amd": true,
19
- "browser": true,
20
- "es6": true
21
- },
22
- "rules": {
23
- "comma-dangle": ["error", {
24
- "arrays": "always-multiline",
25
- "objects": "always-multiline",
26
- "imports": "always-multiline",
27
- "exports": "always-multiline",
28
- "functions": "never"
29
- }],
30
- "block-scoped-var": "off",
31
- "no-dupe-class-members": "off",
32
- "prefer-object-spread": "off",
33
- "prefer-reflect": "off",
34
-
35
- // ES6
36
- "prefer-rest-params": "off",
37
- "prefer-spread": "off",
38
- "prefer-arrow-callback": "off",
39
- "prefer-template": "off",
40
- "no-var": "off",
41
- "object-shorthand": "off",
42
- "vars-on-top": "off",
43
- "prefer-destructuring": "off",
44
- // identical to airbnb rule
45
- // except for allowing for..in, because for..of is unavailable on some clients
46
- "no-restricted-syntax": [
47
- "error",
48
- {
49
- "selector": "ForOfStatement",
50
- "message": "iterators/generators require regenerator-runtime, which is too heavyweight for this guide to allow them. Separately, loops should be avoided in favor of array iterations."
51
- },
52
- {
53
- "selector": "LabeledStatement",
54
- "message": "Labels are a form of GOTO; using them makes code confusing and hard to maintain and understand."
55
- },
56
- {
57
- "selector": "WithStatement",
58
- "message": "`with` is disallowed in strict mode because it makes code impossible to predict and optimize."
59
- }
60
- ]
61
- },
62
- "parserOptions": {
63
- "ecmaVersion": 2018,
64
- "ecmaFeatures": {
65
- "classes": false,
66
- "defaultParams": false,
67
- "blockBindings": false,
68
- "forOf": false,
69
- "generators": false,
70
- "globalReturn": false,
71
- "jsx": false,
72
- "modules": false,
73
- "objectLiteralComputedProperties": false,
74
- "objectLiteralDuplicateProperties": false,
75
- "objectLiteralShorthandMethods": false,
76
- "objectLiteralShorthandProperties": false,
77
- "impliedStrict": false,
78
- "restParams": false,
79
- "superInFunctions": false
80
- }
81
- }
82
- }
2
+ "extends": "nodebb/public"
3
+ }
@@ -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>