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.
- package/.editorconfig +7 -7
- package/LICENSE +20 -20
- package/eslint.config.mjs +7 -0
- package/languages/cs/poll.json +47 -47
- package/languages/de/poll.json +1 -1
- package/languages/en_GB/poll.json +1 -0
- package/languages/en_US/poll.json +52 -52
- package/languages/es/poll.json +39 -39
- package/languages/fr/poll.json +1 -1
- package/languages/gl/poll.json +38 -38
- package/languages/he/poll.json +1 -1
- package/languages/it/poll.json +1 -1
- package/languages/pl/poll.json +1 -1
- package/languages/pt-PT/poll.json +41 -41
- package/languages/ro/poll.json +39 -39
- package/languages/ru/poll.json +1 -1
- package/languages/tr/poll.json +47 -47
- package/lib/config.js +12 -1
- package/lib/hooks.js +231 -14
- package/lib/nodebb.js +0 -0
- package/lib/poll.js +147 -0
- package/lib/sockets.js +117 -12
- package/lib/vote.js +4 -0
- package/library.js +1 -1
- package/package-lock.json +1658 -0
- package/package.json +2 -2
- package/plugin.json +2 -0
- package/public/js/admin.js +0 -0
- package/public/js/poll/creator.js +26 -4
- package/public/js/poll/main.js +19 -8
- package/public/js/poll/view.js +31 -18
- package/templates/admin/plugins/poll.tpl +10 -11
- package/templates/poll/creator.tpl +14 -12
- package/templates/poll/manage.tpl +1 -1
- package/templates/poll/view/messages.tpl +2 -2
- package/templates/poll/view/results.tpl +9 -3
- package/templates/poll/view/voting.tpl +5 -5
- package/templates/poll/view.tpl +8 -8
- package/test/helpers.js +49 -0
- package/test/index.js +530 -0
- 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
|
|
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.
|
|
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" },
|
package/public/js/admin.js
CHANGED
|
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
|
-
|
|
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
|
|
200
|
-
if (config.maxOptions <=
|
|
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');
|
package/public/js/poll/main.js
CHANGED
|
@@ -71,16 +71,27 @@ $(document).ready(function () {
|
|
|
71
71
|
});
|
|
72
72
|
|
|
73
73
|
function getPollByPost(post) {
|
|
74
|
-
if (!post
|
|
75
|
-
let pollIds;
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
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
|
-
|
|
83
|
-
getPolls(pollIds, postEl.find('[component="post/content"]'));
|
|
94
|
+
getPolls(pollIds, container);
|
|
84
95
|
}
|
|
85
96
|
}
|
|
86
97
|
|
package/public/js/poll/view.js
CHANGED
|
@@ -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 =>
|
|
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
|
|
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
|
-
|
|
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(['
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
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
|
|
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">
|
|
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">
|
|
19
|
-
<th class="text-nowrap">
|
|
20
|
-
<th class="text-nowrap">
|
|
21
|
-
<th class="text-end text-nowrap text-capitalize">
|
|
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">
|
|
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">
|
|
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">
|
|
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">
|
|
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">
|
|
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">
|
|
8
|
-
<input type="text" name="title" id="pollInputTitle" value="{poll.title}" placeholder="
|
|
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">
|
|
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">
|
|
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">
|
|
27
|
-
<input type="number" name="maximumVotesPerUser" id="pollInputAmount" value="{poll.maximumVotesPerUser}" min="1" max="10" step="1" placeholder="
|
|
28
|
-
<p class="form-text">
|
|
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">
|
|
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">
|
|
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">
|
|
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="
|
|
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">
|
|
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">
|
|
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
|
-
|
|
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>
|
|
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
|
-
|
|
21
|
+
{{{ end }}}
|
|
@@ -1,16 +1,16 @@
|
|
|
1
1
|
<form role="form" class="poll-voting-form mb-3 d-flex flex-column gap-3">
|
|
2
|
-
|
|
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
|
-
|
|
9
|
+
{{{ end }}}
|
|
10
10
|
</form>
|
|
11
11
|
|
|
12
|
-
|
|
12
|
+
{{{ if poll.info.disallowVoteUpdate }}}
|
|
13
13
|
<div class="alert alert-warning" role="alert">
|
|
14
|
-
|
|
14
|
+
{{tx("poll:vote_is_final")}}
|
|
15
15
|
</div>
|
|
16
|
-
|
|
16
|
+
{{{ end }}}
|
package/templates/poll/view.tpl
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
{{{ end }}}
|
|
10
10
|
</h5>
|
|
11
11
|
{{{ if (isAdmin && !poll.isWidget) }}}
|
|
12
|
-
<span class="text-sm text-secondary">
|
|
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">
|
|
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">
|
|
27
|
-
<button type="button" class="btn btn-sm btn-primary poll-button-vote-anon hidden">
|
|
28
|
-
<button type="button" class="btn btn-sm btn-primary poll-button-update-vote hidden">
|
|
29
|
-
<button type="button" class="btn btn-sm btn-danger poll-button-remove-vote hidden">
|
|
30
|
-
<button type="button" class="btn btn-sm btn-link poll-button-results hidden">
|
|
31
|
-
<button type="button" class="btn btn-sm btn-link poll-button-voting hidden">
|
|
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>
|
package/test/helpers.js
ADDED
|
@@ -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
|
+
};
|