nodebb-plugin-poll 4.0.14 → 4.1.0

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.
Files changed (41) hide show
  1. package/.editorconfig +7 -7
  2. package/LICENSE +20 -20
  3. package/eslint.config.mjs +7 -0
  4. package/languages/cs/poll.json +47 -47
  5. package/languages/de/poll.json +1 -1
  6. package/languages/en_GB/poll.json +1 -0
  7. package/languages/en_US/poll.json +52 -52
  8. package/languages/es/poll.json +39 -39
  9. package/languages/fr/poll.json +1 -1
  10. package/languages/gl/poll.json +38 -38
  11. package/languages/he/poll.json +1 -1
  12. package/languages/it/poll.json +1 -1
  13. package/languages/pl/poll.json +1 -1
  14. package/languages/pt-PT/poll.json +41 -41
  15. package/languages/ro/poll.json +39 -39
  16. package/languages/ru/poll.json +1 -1
  17. package/languages/tr/poll.json +47 -47
  18. package/lib/config.js +12 -1
  19. package/lib/hooks.js +231 -14
  20. package/lib/nodebb.js +0 -0
  21. package/lib/poll.js +147 -0
  22. package/lib/sockets.js +117 -12
  23. package/lib/vote.js +4 -0
  24. package/library.js +1 -1
  25. package/package-lock.json +1658 -0
  26. package/package.json +2 -2
  27. package/plugin.json +2 -0
  28. package/public/js/admin.js +0 -0
  29. package/public/js/poll/creator.js +26 -4
  30. package/public/js/poll/main.js +19 -8
  31. package/public/js/poll/view.js +31 -18
  32. package/templates/admin/plugins/poll.tpl +10 -11
  33. package/templates/poll/creator.tpl +14 -12
  34. package/templates/poll/manage.tpl +1 -1
  35. package/templates/poll/view/messages.tpl +2 -2
  36. package/templates/poll/view/results.tpl +9 -3
  37. package/templates/poll/view/voting.tpl +5 -5
  38. package/templates/poll/view.tpl +8 -8
  39. package/test/helpers.js +49 -0
  40. package/test/index.js +530 -0
  41. package/upgrades/fix_incorrect_poll_privilege.js +15 -15
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nodebb-plugin-poll",
3
- "version": "4.0.14",
3
+ "version": "4.1.0",
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": "^4.14.0"
29
+ "compatibility": "^4.17.0"
30
30
  },
31
31
  "devDependencies": {
32
32
  "eslint": "10.6.0",
package/plugin.json CHANGED
@@ -17,6 +17,8 @@
17
17
  { "hook": "filter:post.create", "method": "hooks.filter.postCreate" },
18
18
  { "hook": "filter:post.getFields", "method": "hooks.filter.postGetFields" },
19
19
  { "hook": "filter:post.edit", "method": "hooks.filter.postEdit" },
20
+ { "hook": "filter:activitypub.mocks.note", "method": "hooks.filter.activitypubMocksNote" },
21
+ { "hook": "filter:activitypub.create", "method": "hooks.filter.activitypubCreate" },
20
22
 
21
23
  { "hook": "action:post.delete", "method": "hooks.action.postDelete" },
22
24
  { "hook": "action:post.restore", "method": "hooks.action.postRestore" },
File without changes
@@ -46,7 +46,6 @@
46
46
  const html = await Benchpress.render('poll/manage', { post: postData });
47
47
 
48
48
  if (modal) {
49
- // modal.find('.bootbox-body').html(html[0].outerHTML);
50
49
  modal.find('.bootbox-body').html(html);
51
50
  handleSort({ modal, polls: postData.polls });
52
51
  } else {
@@ -158,7 +157,16 @@
158
157
  Creator.show = function (modalTitle, poll, config) {
159
158
  return new Promise((resolve) => {
160
159
  app.require(['benchpressjs', 'modals']).then(([Benchpress, modals]) => {
161
- Benchpress.render('poll/creator', { poll, config }).then(async function (html) {
160
+ const pollForTemplate = { ...poll };
161
+ const end = parseInt(pollForTemplate.end, 10);
162
+ if (end > 0) {
163
+ const d = new Date(end);
164
+ const pad = n => String(n).padStart(2, '0');
165
+ pollForTemplate.end = `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}T${pad(d.getHours())}:${pad(d.getMinutes())}`;
166
+ } else {
167
+ pollForTemplate.end = '';
168
+ }
169
+ Benchpress.render('poll/creator', { poll: pollForTemplate, config }).then(async function (html) {
162
170
  const modal = await modals.dialog({
163
171
  title: modalTitle,
164
172
  message: html,
@@ -190,14 +198,20 @@
190
198
  },
191
199
  });
192
200
 
201
+ setTimezoneLabel(modal);
193
202
  handleOptionSort({ modal });
194
203
 
204
+ // Show/hide the anon-voting federation note as the checkbox is toggled
205
+ modal.find('#allowAnonVoting').on('change', function () {
206
+ modal.find('#pollAnonVotingNote').toggleClass('hidden', !this.checked);
207
+ });
208
+
195
209
  // Add option adder
196
210
  modal.find('#pollAddOption')
197
211
  .off('click')
198
212
  .on('click', async function () {
199
- const optionEls = modal.find('[component="post/poll/option/item"]');
200
- if (config.maxOptions <= optionEls.length) {
213
+ const optionCount = modal.find('#poll-options-container [component="post/poll/option/item"]').length;
214
+ if (config.maxOptions <= optionCount) {
201
215
  clearErrors();
202
216
  error(`[[poll:error.max_options, ${config.maxOptions}]]`);
203
217
  return false;
@@ -230,6 +244,14 @@
230
244
  });
231
245
  }
232
246
 
247
+ function setTimezoneLabel(modal) {
248
+ const offsetMinutes = -new Date().getTimezoneOffset();
249
+ const sign = offsetMinutes >= 0 ? '+' : '-';
250
+ const hours = String(Math.floor(Math.abs(offsetMinutes) / 60)).padStart(2, '0');
251
+ const minutes = String(Math.abs(offsetMinutes) % 60).padStart(2, '0');
252
+ modal.find('[data-timezone-label]').text(`(UTC${sign}${hours}:${minutes})`);
253
+ }
254
+
233
255
  async function error(message) {
234
256
  const errorBox = $('#pollErrorBox');
235
257
  const translator = await app.require('translator');
@@ -71,16 +71,27 @@ $(document).ready(function () {
71
71
  });
72
72
 
73
73
  function getPollByPost(post) {
74
- if (!post || !post.hasOwnProperty('pollIds') || !post.pollIds) return;
75
- let pollIds;
76
- try {
77
- pollIds = JSON.parse(post.pollIds || '[]');
78
- } catch (err) {
79
- pollIds = [];
74
+ if (!post) return;
75
+ let pollIds = [];
76
+ if (post.pollIds) {
77
+ try {
78
+ pollIds = JSON.parse(post.pollIds);
79
+ } catch (err) {
80
+ pollIds = [];
81
+ }
80
82
  }
83
+ const postEl = $(`[component="post"][data-pid="${post.pid}"]`);
84
+ const container = postEl.find('[component="post/content"]');
85
+ const validPollIdSet = new Set((pollIds || []).map(id => String(id)));
86
+ container.find('[data-poll-id]').each(function () {
87
+ const existingPollId = $(this).attr('data-poll-id');
88
+ if (!validPollIdSet.has(existingPollId)) {
89
+ $(this).remove();
90
+ delete Poll.view.polls[existingPollId];
91
+ }
92
+ });
81
93
  if (pollIds.length) {
82
- const postEl = $(`[component="post"][data-pid="${post.pid}"]`);
83
- getPolls(pollIds, postEl.find('[component="post/content"]'));
94
+ getPolls(pollIds, container);
84
95
  }
85
96
  }
86
97
 
@@ -3,7 +3,7 @@
3
3
  (function (Poll) {
4
4
  function vote(view, options) {
5
5
  const form = view.dom.votingPanel.find('form');
6
- const votes = form.serializeArray().map(option => parseInt(option.value, 10));
6
+ const votes = form.serializeArray().map(option => option.value);
7
7
 
8
8
  if (votes.length > 0) {
9
9
  const voteData = {
@@ -66,7 +66,7 @@
66
66
  handle: function (view) {
67
67
  const form = view.dom.votingPanel.find('form');
68
68
  const votes = form.serializeArray().map(function (option) {
69
- return parseInt(option.value, 10);
69
+ return option.value;
70
70
  });
71
71
 
72
72
  if (votes.length > 0) {
@@ -129,6 +129,9 @@
129
129
  {
130
130
  // Option details
131
131
  register: function (view) {
132
+ if (!app.user.uid) {
133
+ return;
134
+ }
132
135
  const self = this;
133
136
  view.dom.resultsPanel.off('click').on('click', '.poll-result-votecount', function (e) {
134
137
  self.handle(view, e);
@@ -201,7 +204,11 @@
201
204
  };
202
205
 
203
206
  View.prototype.voteUpdateAllowed = function () {
204
- return parseInt(this.pollData.disallowVoteUpdate, 10) !== 1;
207
+ // Remote polls: vote changes/removals are not federated (no OSW support yet)
208
+ if (parseInt(this.pollData.info.remote, 10) === 1) {
209
+ return false;
210
+ }
211
+ return parseInt(this.pollData.info.disallowVoteUpdate, 10) !== 1;
205
212
  };
206
213
 
207
214
  View.prototype.pollEndedOrDeleted = function () {
@@ -257,20 +264,26 @@
257
264
  this.dom.messages.addClass('hidden');
258
265
  };
259
266
 
260
- View.prototype.showOptionDetails = function (details) {
261
- require(['benchpress', 'modals'], function (benchpress, modals) {
262
- benchpress.render('poll/view/details', details).then(function (html) {
263
- modals.dialog({
264
- title: `[[poll:x-users-voted-for-this-option, ${details.voteCount}]]`,
265
- message: html,
266
- backdrop: true,
267
- buttons: {
268
- close: {
269
- label: 'Close',
270
- },
271
- },
272
- });
273
- });
267
+ View.prototype.showOptionDetails = async function (details) {
268
+ const [modals, Benchpress] = await app.require(['modals', 'benchpressjs']);
269
+ if (details.votes) {
270
+ const translator = await app.require('translator');
271
+ await Promise.all(details.votes.map(async (voter) => {
272
+ if (!parseInt(voter.uid, 10) && voter.displayname) {
273
+ voter.displayname = await translator.translate(voter.displayname);
274
+ }
275
+ }));
276
+ }
277
+ const html = await Benchpress.render('poll/view/details', details);
278
+ await modals.dialog({
279
+ title: `[[poll:x-users-voted-for-this-option, ${details.voteCount}]]`,
280
+ message: html,
281
+ backdrop: true,
282
+ buttons: {
283
+ close: {
284
+ label: '[[global:close]]',
285
+ },
286
+ },
274
287
  });
275
288
  };
276
289
 
@@ -301,7 +314,7 @@
301
314
  }
302
315
  } else {
303
316
  this.showVoteButton();
304
- if (this.pollData?.info?.allowAnonVoting) {
317
+ if (this.pollData.info.allowAnonVoting) {
305
318
  this.showVoteAnonButton();
306
319
  }
307
320
  }
@@ -4,7 +4,7 @@
4
4
  <h4 class="fw-bold tracking-tight mb-0">{title}</h4>
5
5
  </div>
6
6
  <div class="col-12 col-md-4 px-0 px-md-3 text-end">
7
- <button id="save" class="btn btn-primary btn-sm fw-semibold ff-secondary text-center text-nowrap">[[admin/admin:save-changes]]</button>
7
+ <button id="save" class="btn btn-primary btn-sm fw-semibold ff-secondary text-center text-nowrap">{{tx("admin/admin:save-changes")}}</button>
8
8
  </div>
9
9
  </div>
10
10
 
@@ -15,10 +15,10 @@
15
15
  <thead>
16
16
  <tr class="text-sm">
17
17
  <th>ID</th>
18
- <th class="text-nowrap">[[poll:poll_title]]</th>
19
- <th class="text-nowrap">[[poll:created-time]]</th>
20
- <th class="text-nowrap">[[poll:end-time]]</th>
21
- <th class="text-end text-nowrap text-capitalize">[[poll:votes]]</th>
18
+ <th class="text-nowrap">{{tx("poll:poll_title")}}</th>
19
+ <th class="text-nowrap">{{tx("poll:created-time")}}</th>
20
+ <th class="text-nowrap">{{tx("poll:end-time")}}</th>
21
+ <th class="text-end text-nowrap text-capitalize">{{tx("poll:votes")}}</th>
22
22
  </tr>
23
23
  </thead>
24
24
  <tbody class="text-xs text-tabular">
@@ -47,27 +47,26 @@
47
47
  <div id="spy-container" class="col-12 col-lg-3" tabindex="0">
48
48
  <form class="form poll-settings">
49
49
  <div class="form-check mb-3">
50
- <label class="form-check-label" for="allowGuestsToViewResults">[[poll:allow_guests]]</label>
50
+ <label class="form-check-label" for="allowGuestsToViewResults">{{tx("poll:allow_guests")}}</label>
51
51
  <input class="form-check-input" type="checkbox" id="allowGuestsToViewResults" name="allowGuestsToViewResults">
52
52
  </div>
53
53
 
54
54
  <div class="mb-3">
55
- <label class="form-label" for="maxOptions">[[poll:max_options]]</label>
55
+ <label class="form-label" for="maxOptions">{{tx("poll:max_options")}}</label>
56
56
  <input type="number" class="form-control" id="maxOptions" placeholder="10" min="1" max="100" name="maxOptions">
57
57
  </div>
58
58
 
59
59
  <div class="mb-3">
60
- <label class="form-label" for="defaultTitle">[[poll:default_title]]</label>
60
+ <label class="form-label" for="defaultTitle">{{tx("poll:default_title")}}</label>
61
61
  <input type="text" class="form-control" id="defaultTitle" placeholder="Poll" name="defaultTitle">
62
62
  </div>
63
63
  <div>
64
- <label class="form-label" for="maximumVotesPerUser">[[poll:max_votes]]</label>
64
+ <label class="form-label" for="maximumVotesPerUser">{{tx("poll:max_votes")}}</label>
65
65
  <input type="number" class="form-control" id="maximumVotesPerUser" placeholder="1" min="1" max="100" name="maximumVotesPerUser">
66
- <p class="form-text">[[poll:info_choices]]</p>
66
+ <p class="form-text">{{tx("poll:info_choices")}}</p>
67
67
  </div>
68
68
  </form>
69
69
  </div>
70
70
  </div>
71
71
  </div>
72
72
 
73
-
@@ -4,12 +4,12 @@
4
4
  <input type="hidden" name="pollId" value="{poll.pollId}">
5
5
 
6
6
  <div class="mb-3">
7
- <label class="form-label" for="pollInputTitle">[[poll:poll_title]]</label>
8
- <input type="text" name="title" id="pollInputTitle" value="{poll.title}" placeholder="[[poll:poll_title_placeholder]]" class="form-control">
7
+ <label class="form-label" for="pollInputTitle">{{tx("poll:poll_title")}}</label>
8
+ <input type="text" name="title" id="pollInputTitle" value="{poll.title}" placeholder="{{tx("poll:poll_title_placeholder")}}" class="form-control">
9
9
  </div>
10
10
 
11
11
  <div class="mb-3 d-flex flex-column gap-1">
12
- <label class="form-label" for="pollInputOptions">[[poll:options_title]]</label>
12
+ <label class="form-label" for="pollInputOptions">{{tx("poll:options_title")}}</label>
13
13
  <div id="poll-options-container" class="d-flex flex-column gap-1">
14
14
  {{{ if poll.options.length }}}
15
15
  {{{ each poll.options }}}
@@ -17,32 +17,34 @@
17
17
  {{{ end }}}
18
18
  {{{ end }}}
19
19
  </div>
20
- <button type="button" id="pollAddOption" class="btn btn-primary btn-sm btn-block mt-2">[[poll:options_add]]</button>
20
+ <button type="button" id="pollAddOption" class="btn btn-primary btn-sm btn-block mt-2">{{tx("poll:options_add")}}</button>
21
21
  </div>
22
22
 
23
23
  <hr>
24
24
 
25
25
  <div class="mb-3">
26
- <label class="form-label" for="pollInputAmount">[[poll:max_votes]]</label>
27
- <input type="number" name="maximumVotesPerUser" id="pollInputAmount" value="{poll.maximumVotesPerUser}" min="1" max="10" step="1" placeholder="[[poll:max_votes_placeholder]]" class="form-control">
28
- <p class="form-text">[[poll:info_choices]]</p>
26
+ <label class="form-label" for="pollInputAmount">{{tx("poll:max_votes")}}</label>
27
+ <input type="number" name="maximumVotesPerUser" id="pollInputAmount" value="{poll.maximumVotesPerUser}" min="1" max="10" step="1" placeholder="{{tx("poll:max_votes_placeholder")}}" class="form-control">
28
+ <p class="form-text">{{tx("poll:info_choices")}}</p>
29
29
  </div>
30
30
 
31
31
  <div class="form-check mb-3">
32
- <label class="form-check-label" for="pollDisallowVoteUpdate">[[poll:disallow_vote_update]]</label>
32
+ <label class="form-check-label" for="pollDisallowVoteUpdate">{{tx("poll:disallow_vote_update")}}</label>
33
33
  <input class="form-check-input" type="checkbox" name="disallowVoteUpdate" id="pollDisallowVoteUpdate" {{{if poll.disallowVoteUpdate}}}checked{{{end}}}>
34
34
  </div>
35
35
 
36
36
  <div class="form-check mb-3">
37
- <label class="form-check-label" for="allowAnonVoting">[[poll:allow_anon_voting]]</label>
37
+ <label class="form-check-label" for="allowAnonVoting">{{tx("poll:allow_anon_voting")}}</label>
38
38
  <input class="form-check-input" type="checkbox" name="allowAnonVoting" id="allowAnonVoting" {{{if poll.allowAnonVoting}}}checked{{{end}}}>
39
+ <p class="form-text {{{if poll.allowAnonVoting}}}{{{else}}}hidden{{{end}}}" id="pollAnonVotingNote">{{tx("poll:anon_voting_not_federated")}}</p>
39
40
  </div>
40
41
 
41
42
  <div class="mb-3">
42
- <label class="form-label" for="pollInputEnd">[[poll:auto_end_title]]</label>
43
+ <label class="form-label" for="pollInputEnd">{{tx("poll:auto_end_title")}}</label>
43
44
  <div class="input-group date">
44
- <input id="pollInputEnd" placeholder="[[poll:auto_end_placeholder]]" name="end" class="form-control" value="{poll.end}" type="datetime-local">
45
+ <input id="pollInputEnd" placeholder="{{tx("poll:auto_end_placeholder")}}" name="end" class="form-control" value="{poll.end}" type="datetime-local">
46
+ <span class="input-group-text poll-timezone-label" data-timezone-label></span>
45
47
  </div>
46
- <p class="form-text">[[poll:auto_end_help]]</p>
48
+ <p class="form-text">{{tx("poll:auto_end_help")}}</p>
47
49
  </div>
48
50
  </form>
@@ -1,6 +1,6 @@
1
1
  <div class="topic-poll-modal">
2
2
  {{{ if !post.polls.length }}}
3
- <div class="alert alert-info">[[poll:no-polls]]</div>
3
+ <div class="alert alert-info">{{tx("poll:no-polls")}}</div>
4
4
  {{{ end }}}
5
5
  {{{ each post.polls }}}
6
6
  <div component="post/poll/item" data-poll-id="{./pollId}" class="bg-body d-flex align-items-center justify-content-between mb-3 p-1 border rounded-1">
@@ -1,3 +1,3 @@
1
- <h5>{title}</h5>
2
- <p>{content}</p>
1
+ <h5>{{tx(title)}}</h5>
2
+ <p>{{tx(content)}}</p>
3
3
  <hr>
@@ -1,10 +1,16 @@
1
- <!-- BEGIN poll.options -->
1
+ {{{ each poll.options }}}
2
2
  <div class="poll-result" data-poll-option-id="{poll.options.id}">
3
3
  <div class="d-flex justify-content-between mb-1">
4
4
  <strong>{poll.options.title}</strong>
5
+ {{{ if config.loggedIn }}}
5
6
  <a class="poll-result-votecount small" href="#">
6
- <span>[[poll:x-votes, {poll.options.voteCount}]]</span>
7
+ <span>{{tx("poll:x-votes", poll.options.voteCount)}}</span>
7
8
  </a>
9
+ {{{ else }}}
10
+ <span class="poll-result-votecount small">
11
+ <span>{{tx("poll:x-votes", poll.options.voteCount)}}</span>
12
+ </span>
13
+ {{{ end }}}
8
14
  </div>
9
15
  <div class="progress">
10
16
  <div class="progress-bar poll-result-progressbar" role="progressbar" aria-valuenow="{poll.options.percentage}" aria-valuemin="0" aria-valuemax="100" style="width: {poll.options.percentage}%;">
@@ -12,4 +18,4 @@
12
18
  </div>
13
19
  </div>
14
20
  </div>
15
- <!-- END poll.options -->
21
+ {{{ end }}}
@@ -1,16 +1,16 @@
1
1
  <form role="form" class="poll-voting-form mb-3 d-flex flex-column gap-3">
2
- <!-- BEGIN poll.options -->
2
+ {{{ each poll.options }}}
3
3
  <div class="poll-view-option {poll.optionType}" data-poll-option-id="{poll.options.id}">
4
4
  <div class="form-check">
5
5
  <input class="form-check-input" type="{poll.optionType}" name="pollVoteSelection" value="{poll.options.id}" id="poll-{poll.info.pollId}-option-id-{poll.options.id}">
6
6
  <label class="form-check-label" for="poll-{poll.info.pollId}-option-id-{poll.options.id}">{poll.options.title}</label>
7
7
  </div>
8
8
  </div>
9
- <!-- END poll.options -->
9
+ {{{ end }}}
10
10
  </form>
11
11
 
12
- <!-- IF poll.settings.disallowVoteUpdate -->
12
+ {{{ if poll.info.disallowVoteUpdate }}}
13
13
  <div class="alert alert-warning" role="alert">
14
- [[poll:vote_is_final]]
14
+ {{tx("poll:vote_is_final")}}
15
15
  </div>
16
- <!-- ENDIF poll.settings.disallowVoteUpdate -->
16
+ {{{ end }}}
@@ -9,7 +9,7 @@
9
9
  {{{ end }}}
10
10
  </h5>
11
11
  {{{ if (isAdmin && !poll.isWidget) }}}
12
- <span class="text-sm text-secondary">[[poll:poll-id-x, {poll.info.pollId}]]</span>
12
+ <span class="text-sm text-secondary">{{tx("poll:poll-id-x", poll.info.pollId)}}</span>
13
13
  {{{ end }}}
14
14
  </div>
15
15
  <div class="card-body">
@@ -19,16 +19,16 @@
19
19
  </div>
20
20
  <div class="poll-view-results d-flex flex-column gap-3 mb-3 {{{ if !poll.hasVoted }}}hidden{{{ end }}}">
21
21
  <!-- IMPORT poll/view/results.tpl -->
22
- <div class="text-end text-sm text-secondary poll-result-total-votecount">[[poll:total-votes-x, {poll.info.voteCount}]]</div>
22
+ <div class="text-end text-sm text-secondary poll-result-total-votecount">{{tx("poll:total-votes-x", poll.info.voteCount)}}</div>
23
23
  </div>
24
24
 
25
25
  <div class="poll-view-buttons">
26
- <button type="button" class="btn btn-sm btn-primary poll-button-vote hidden">[[poll:vote]]</button>
27
- <button type="button" class="btn btn-sm btn-primary poll-button-vote-anon hidden">[[poll:vote_anonymously]]</button>
28
- <button type="button" class="btn btn-sm btn-primary poll-button-update-vote hidden">[[poll:update_vote]]</button>
29
- <button type="button" class="btn btn-sm btn-danger poll-button-remove-vote hidden">[[poll:remove_vote]]</button>
30
- <button type="button" class="btn btn-sm btn-link poll-button-results hidden">[[poll:to_results]]</button>
31
- <button type="button" class="btn btn-sm btn-link poll-button-voting hidden">[[poll:to_voting]]</button>
26
+ <button type="button" class="btn btn-sm btn-primary poll-button-vote hidden">{{tx("poll:vote")}}</button>
27
+ <button type="button" class="btn btn-sm btn-primary poll-button-vote-anon hidden">{{tx("poll:vote_anonymously")}}</button>
28
+ <button type="button" class="btn btn-sm btn-primary poll-button-update-vote hidden">{{tx("poll:update_vote")}}</button>
29
+ <button type="button" class="btn btn-sm btn-danger poll-button-remove-vote hidden">{{tx("poll:remove_vote")}}</button>
30
+ <button type="button" class="btn btn-sm btn-link poll-button-results hidden">{{tx("poll:to_results")}}</button>
31
+ <button type="button" class="btn btn-sm btn-link poll-button-voting hidden">{{tx("poll:to_voting")}}</button>
32
32
  </div>
33
33
  </div>
34
34
  </div>
@@ -0,0 +1,49 @@
1
+ 'use strict';
2
+
3
+ // Poll-specific test fixtures. Kept out of core so the core test suite has no
4
+ // plugin-specific artifacts. Generic AP fixtures (note/create/mockRequests)
5
+ // still come from `nodebb.require('./test/activitypub/helpers')`.
6
+
7
+ const utils = nodebb.require('./src/utils');
8
+ const activitypub = nodebb.require('./src/activitypub');
9
+
10
+ const baseUrl = 'https://example.org';
11
+
12
+ module.exports = {
13
+ // FEP-9967 `Question` (poll) object. Mirrors the core note() factory with
14
+ // poll fields (oneOf/anyOf, endTime, votersCount). Caches the object in the
15
+ // AP request cache so the inbox flow can fetch it without real requests.
16
+ question: (override = {}) => {
17
+ const uuid = utils.generateUUID();
18
+ const id = `${baseUrl}/object/${uuid}`;
19
+ const question = {
20
+ '@context': 'https://www.w3.org/ns/activitystreams',
21
+ id,
22
+ url: id,
23
+ type: 'Question',
24
+ to: ['https://www.w3.org/ns/activitystreams#Public'],
25
+ cc: [`${baseUrl}/user/foobar/followers`],
26
+ inReplyTo: null,
27
+ attributedTo: `${baseUrl}/user/foobar`,
28
+ name: utils.generateUUID(),
29
+ content: `<p>${utils.generateUUID()}</p>`,
30
+ published: new Date().toISOString(),
31
+ endTime: new Date(Date.now() + (24 * 60 * 60 * 1000)).toISOString(),
32
+ oneOf: [
33
+ { type: 'Note', name: 'Option A', replies: { type: 'Collection', totalItems: 3 } },
34
+ { type: 'Note', name: 'Option B', replies: { type: 'Collection', totalItems: 7 } },
35
+ ],
36
+ votersCount: 10,
37
+ ...override,
38
+ };
39
+
40
+ Object.entries(question).forEach(([key, value]) => {
41
+ if (value === 'remove') {
42
+ delete question[key];
43
+ }
44
+ });
45
+ activitypub._cache.set(`0;${id}`, question);
46
+
47
+ return { id, question };
48
+ },
49
+ };