nodebb-plugin-poll 4.0.13 → 4.0.15
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/languages/cs/poll.json +47 -47
- package/languages/de/poll.json +1 -1
- 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/poll.js +45 -0
- package/lib/sockets.js +8 -10
- package/library.js +1 -1
- package/package.json +1 -1
- package/public/js/poll/creator.js +22 -5
- package/public/js/poll/main.js +19 -8
- package/public/js/poll/view.js +25 -16
- package/templates/admin/plugins/poll.tpl +10 -11
- package/templates/poll/creator.tpl +13 -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/languages/tr/poll.json
CHANGED
|
@@ -1,47 +1,47 @@
|
|
|
1
|
-
{
|
|
2
|
-
"poll": "Anket",
|
|
3
|
-
"toggles": "Değişiklikler",
|
|
4
|
-
"allow_guests": "Ziyaretçilerin anket sonucunu görmelerine izin ver",
|
|
5
|
-
"limits": "Sınırlamalar",
|
|
6
|
-
"max_options": "Anketlerdeki maksimum seçenek sayısı",
|
|
7
|
-
"defaults": "Varsayılanlar",
|
|
8
|
-
"default_title": "Varsayılan Anket Başlığı",
|
|
9
|
-
"max_votes": "Kişi başı maksimum oy sayısı",
|
|
10
|
-
"disallow_vote_update": "Kişilerin oylarını değiştirmelerine izin verme",
|
|
11
|
-
"info_choices": "1'den büyük bir sayı çoktan seçmeli anket oluşturacak.",
|
|
12
|
-
"settings": "Ayarlar",
|
|
13
|
-
"save": "Kaydet",
|
|
14
|
-
"reset": "Sıfırla",
|
|
15
|
-
|
|
16
|
-
"creator_title": "Anket Oluştur",
|
|
17
|
-
"poll_title": "Anket Başlığı",
|
|
18
|
-
"poll_title_placeholder": "Anket başlığını buraya girin",
|
|
19
|
-
"options_title": "Seçenekler",
|
|
20
|
-
"options_add": "Seçenek ekle",
|
|
21
|
-
"max_votes_placeholder": "Bir sayı girin",
|
|
22
|
-
"auto_end_title": "Anketi otomatik olarak bitir",
|
|
23
|
-
"auto_end_placeholder": "Tarih ve saat bilgisini buraya girin",
|
|
24
|
-
"auto_end_help": "Burayı boş bırakırsanız anket hiç sonlanmayacak.",
|
|
25
|
-
|
|
26
|
-
"error.max_options": "Sadece %
|
|
27
|
-
"error.no_options": "En az bir seçenek oluşturmalısınız",
|
|
28
|
-
"error.valid_date": "Lütfen geçerli bir tarih girin",
|
|
29
|
-
|
|
30
|
-
"error.not_main": "Anketi sadece ilk iletiye ekleyebilirsiniz.",
|
|
31
|
-
"error.privilege.create": "Anket oluşturmaya yetkiniz yok",
|
|
32
|
-
|
|
33
|
-
"warning.redactor": "Redactor kullanıyorsunuz. Markup'ı manuel olarak <strong>değiştirmeyin</strong>. Bunun yerine anket oluşturucuyu yeniden açın.",
|
|
34
|
-
|
|
35
|
-
"vote": "Oy ver",
|
|
36
|
-
"update_vote": "Oyu güncelle",
|
|
37
|
-
"remove_vote": "Oyu kaldır",
|
|
38
|
-
"to_voting": "Seçeneklere geri dön",
|
|
39
|
-
"to_results": "Sonuçlar",
|
|
40
|
-
"voting_unavailable_title": "Oy vermek artık mümkün değil",
|
|
41
|
-
"voting_unavailable_message": "Bu anket bitti veya silindi. Sonuçları hala görmeniz mümkün",
|
|
42
|
-
"voting_update_disallowed_message": "Daha önce oy verdiniz. Bu anket için oyunuzu değiştiremezsiniz",
|
|
43
|
-
"vote_is_final": "Bu ankette oy değiştirmeniz mümkün değildir, o nedenle seçiminiz son karardır",
|
|
44
|
-
|
|
45
|
-
"vote_count": "kişi bu seçenek için oy kullandı",
|
|
46
|
-
"votes": "oy"
|
|
47
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"poll": "Anket",
|
|
3
|
+
"toggles": "Değişiklikler",
|
|
4
|
+
"allow_guests": "Ziyaretçilerin anket sonucunu görmelerine izin ver",
|
|
5
|
+
"limits": "Sınırlamalar",
|
|
6
|
+
"max_options": "Anketlerdeki maksimum seçenek sayısı",
|
|
7
|
+
"defaults": "Varsayılanlar",
|
|
8
|
+
"default_title": "Varsayılan Anket Başlığı",
|
|
9
|
+
"max_votes": "Kişi başı maksimum oy sayısı",
|
|
10
|
+
"disallow_vote_update": "Kişilerin oylarını değiştirmelerine izin verme",
|
|
11
|
+
"info_choices": "1'den büyük bir sayı çoktan seçmeli anket oluşturacak.",
|
|
12
|
+
"settings": "Ayarlar",
|
|
13
|
+
"save": "Kaydet",
|
|
14
|
+
"reset": "Sıfırla",
|
|
15
|
+
|
|
16
|
+
"creator_title": "Anket Oluştur",
|
|
17
|
+
"poll_title": "Anket Başlığı",
|
|
18
|
+
"poll_title_placeholder": "Anket başlığını buraya girin",
|
|
19
|
+
"options_title": "Seçenekler",
|
|
20
|
+
"options_add": "Seçenek ekle",
|
|
21
|
+
"max_votes_placeholder": "Bir sayı girin",
|
|
22
|
+
"auto_end_title": "Anketi otomatik olarak bitir",
|
|
23
|
+
"auto_end_placeholder": "Tarih ve saat bilgisini buraya girin",
|
|
24
|
+
"auto_end_help": "Burayı boş bırakırsanız anket hiç sonlanmayacak.",
|
|
25
|
+
|
|
26
|
+
"error.max_options": "Sadece %1 seçenek oluşturabilirsiniz.",
|
|
27
|
+
"error.no_options": "En az bir seçenek oluşturmalısınız",
|
|
28
|
+
"error.valid_date": "Lütfen geçerli bir tarih girin",
|
|
29
|
+
|
|
30
|
+
"error.not_main": "Anketi sadece ilk iletiye ekleyebilirsiniz.",
|
|
31
|
+
"error.privilege.create": "Anket oluşturmaya yetkiniz yok",
|
|
32
|
+
|
|
33
|
+
"warning.redactor": "Redactor kullanıyorsunuz. Markup'ı manuel olarak <strong>değiştirmeyin</strong>. Bunun yerine anket oluşturucuyu yeniden açın.",
|
|
34
|
+
|
|
35
|
+
"vote": "Oy ver",
|
|
36
|
+
"update_vote": "Oyu güncelle",
|
|
37
|
+
"remove_vote": "Oyu kaldır",
|
|
38
|
+
"to_voting": "Seçeneklere geri dön",
|
|
39
|
+
"to_results": "Sonuçlar",
|
|
40
|
+
"voting_unavailable_title": "Oy vermek artık mümkün değil",
|
|
41
|
+
"voting_unavailable_message": "Bu anket bitti veya silindi. Sonuçları hala görmeniz mümkün",
|
|
42
|
+
"voting_update_disallowed_message": "Daha önce oy verdiniz. Bu anket için oyunuzu değiştiremezsiniz",
|
|
43
|
+
"vote_is_final": "Bu ankette oy değiştirmeniz mümkün değildir, o nedenle seçiminiz son karardır",
|
|
44
|
+
|
|
45
|
+
"vote_count": "kişi bu seçenek için oy kullandı",
|
|
46
|
+
"votes": "oy"
|
|
47
|
+
}
|
package/lib/config.js
CHANGED
|
@@ -28,9 +28,20 @@ Config.defaults = {
|
|
|
28
28
|
|
|
29
29
|
Config.getSettings = async function () {
|
|
30
30
|
const settings = await meta.settings.get(Config.plugin.id);
|
|
31
|
-
|
|
31
|
+
const merged = {
|
|
32
32
|
...Config.defaults,
|
|
33
33
|
...settings,
|
|
34
34
|
};
|
|
35
|
+
// NodeBB's settings helper stores checkboxes as 'on'/'off'; normalize to a boolean
|
|
36
|
+
// so parseInt(...) === 1 checks don't silently fail.
|
|
37
|
+
merged.allowGuestsToViewResults = ['1', 1, 'on', true].includes(merged.allowGuestsToViewResults);
|
|
38
|
+
// NodeBB's settings helper stores blank number fields as ''; fall back to defaults
|
|
39
|
+
// so an empty ACP field doesn't override the configured default.
|
|
40
|
+
merged.maxOptions = parseInt(merged.maxOptions, 10) || Config.defaults.maxOptions;
|
|
41
|
+
merged.maximumVotesPerUser = parseInt(merged.maximumVotesPerUser, 10) || Config.defaults.maximumVotesPerUser;
|
|
42
|
+
// NodeBB's settings helper stores blank text fields as ''; fall back to the
|
|
43
|
+
// configured default so an empty ACP field doesn't override the built-in default.
|
|
44
|
+
merged.defaultTitle = merged.defaultTitle || Config.defaults.defaultTitle;
|
|
45
|
+
return merged;
|
|
35
46
|
};
|
|
36
47
|
|
package/lib/poll.js
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
const db = nodebb.require('./src/database');
|
|
4
4
|
const privileges = nodebb.require('./src/privileges');
|
|
5
5
|
|
|
6
|
+
const Config = require('./config');
|
|
6
7
|
const Vote = require('./vote');
|
|
7
8
|
|
|
8
9
|
const Poll = exports;
|
|
@@ -10,6 +11,7 @@ const Poll = exports;
|
|
|
10
11
|
Poll.add = async function (postData, polls) {
|
|
11
12
|
// postData is what is being saved to db
|
|
12
13
|
// polls is submited by user
|
|
14
|
+
await validateMaxOptions(polls);
|
|
13
15
|
const savePolls = [];
|
|
14
16
|
for (const pollData of polls) {
|
|
15
17
|
// eslint-disable-next-line no-await-in-loop
|
|
@@ -75,6 +77,9 @@ Poll.edit = async function (postData, newPolls) {
|
|
|
75
77
|
const toDelete = currentPollIds.filter(pollId => !newPollIds.includes(String(pollId)));
|
|
76
78
|
const currentPolls = await db.getObjects(currentPollIds.map(pollId => `poll:${pollId}`));
|
|
77
79
|
|
|
80
|
+
await validateMaxOptions(toAdd);
|
|
81
|
+
await validateMaxOptionsForEdit(toUpdate, currentPolls);
|
|
82
|
+
|
|
78
83
|
await Poll.deletePolls(toDelete);
|
|
79
84
|
|
|
80
85
|
const savedPolls = await Poll.add(postData, toAdd);
|
|
@@ -245,6 +250,46 @@ function tryParseOptions(optionsJson) {
|
|
|
245
250
|
}
|
|
246
251
|
}
|
|
247
252
|
|
|
253
|
+
async function validateMaxOptions(polls) {
|
|
254
|
+
if (!Array.isArray(polls) || !polls.length) {
|
|
255
|
+
return;
|
|
256
|
+
}
|
|
257
|
+
const settings = await Config.getSettings();
|
|
258
|
+
const { maxOptions } = settings;
|
|
259
|
+
polls.forEach((poll) => {
|
|
260
|
+
if (!poll) {
|
|
261
|
+
return;
|
|
262
|
+
}
|
|
263
|
+
const optionCount = Array.isArray(poll.options) ? poll.options.length : 0;
|
|
264
|
+
if (optionCount > maxOptions) {
|
|
265
|
+
throw new Error(`[[poll:error.max_options, ${maxOptions}]]`);
|
|
266
|
+
}
|
|
267
|
+
});
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
async function validateMaxOptionsForEdit(updatedPolls, currentPolls) {
|
|
271
|
+
if (!Array.isArray(updatedPolls) || !updatedPolls.length) {
|
|
272
|
+
return;
|
|
273
|
+
}
|
|
274
|
+
const settings = await Config.getSettings();
|
|
275
|
+
const { maxOptions } = settings;
|
|
276
|
+
updatedPolls.forEach((poll) => {
|
|
277
|
+
if (!poll) {
|
|
278
|
+
return;
|
|
279
|
+
}
|
|
280
|
+
const newOptionCount = Array.isArray(poll.options) ? poll.options.length : 0;
|
|
281
|
+
if (newOptionCount <= maxOptions) {
|
|
282
|
+
return;
|
|
283
|
+
}
|
|
284
|
+
const currentPoll = currentPolls.find(p => p && String(p.pollId) === String(poll.pollId));
|
|
285
|
+
const currentOptions = currentPoll ? tryParseOptions(currentPoll.options) : [];
|
|
286
|
+
const currentOptionCount = Array.isArray(currentOptions) ? currentOptions.length : 0;
|
|
287
|
+
if (newOptionCount > currentOptionCount) {
|
|
288
|
+
throw new Error(`[[poll:error.max_options, ${maxOptions}]]`);
|
|
289
|
+
}
|
|
290
|
+
});
|
|
291
|
+
}
|
|
292
|
+
|
|
248
293
|
Poll.getOption = async function (pollId, option, withVotes = false) {
|
|
249
294
|
const [optionsJson, votes, voteCount] = await Promise.all([
|
|
250
295
|
db.getObjectField(`poll:${pollId}`, 'options'),
|
package/lib/sockets.js
CHANGED
|
@@ -15,7 +15,7 @@ const Sockets = exports;
|
|
|
15
15
|
|
|
16
16
|
Sockets.get = async function (socket, data) {
|
|
17
17
|
const settings = await Config.getSettings();
|
|
18
|
-
const allowAnon = settings.allowGuestsToViewResults
|
|
18
|
+
const allowAnon = !!settings.allowGuestsToViewResults;
|
|
19
19
|
|
|
20
20
|
if (!data) {
|
|
21
21
|
throw new Error('Invalid request, request data is not defined');
|
|
@@ -27,13 +27,13 @@ Sockets.get = async function (socket, data) {
|
|
|
27
27
|
return [];
|
|
28
28
|
}
|
|
29
29
|
return await Promise.all(data.pollIds.map(async (pollId) => {
|
|
30
|
-
const withVotes =
|
|
30
|
+
const withVotes = !!socket.uid;
|
|
31
31
|
const pollData = await Poll.get(pollId, socket.uid, withVotes);
|
|
32
32
|
if (!pollData) {
|
|
33
33
|
return null;
|
|
34
34
|
}
|
|
35
35
|
|
|
36
|
-
if (parseInt(pollData.info.allowAnonVoting, 10) === 1) {
|
|
36
|
+
if (withVotes && parseInt(pollData.info.allowAnonVoting, 10) === 1) {
|
|
37
37
|
await anonymizeVoters(socket.uid, pollId, pollData.options);
|
|
38
38
|
}
|
|
39
39
|
|
|
@@ -145,20 +145,18 @@ Sockets.removeVote = async function (socket, data) {
|
|
|
145
145
|
};
|
|
146
146
|
|
|
147
147
|
Sockets.getOptionDetails = async function (socket, data) {
|
|
148
|
-
if (!data || isNaN(parseInt(data.pollId, 10)) || isNaN(parseInt(data.optionId, 10))) {
|
|
148
|
+
if (!socket.uid || !data || isNaN(parseInt(data.pollId, 10)) || isNaN(parseInt(data.optionId, 10))) {
|
|
149
149
|
throw new Error('Invalid request');
|
|
150
150
|
}
|
|
151
|
-
const [poll, option
|
|
151
|
+
const [poll, option] = await Promise.all([
|
|
152
152
|
Poll.getInfo(data.pollId),
|
|
153
153
|
Poll.getOption(data.pollId, data.optionId, true),
|
|
154
|
-
Config.getSettings(),
|
|
155
154
|
]);
|
|
156
155
|
if (!poll) {
|
|
157
156
|
throw new Error('[[error:invalid-data]]');
|
|
158
157
|
}
|
|
159
|
-
const allowAnon = settings.allowGuestsToViewResults === 'on';
|
|
160
158
|
const canRead = await privileges.posts.can('topics:read', poll.pid, socket.uid);
|
|
161
|
-
if (!canRead
|
|
159
|
+
if (!canRead) {
|
|
162
160
|
throw new Error('[[error:no-privileges]]');
|
|
163
161
|
}
|
|
164
162
|
if (!option || !option.votes || !option.votes.length) {
|
|
@@ -170,7 +168,7 @@ Sockets.getOptionDetails = async function (socket, data) {
|
|
|
170
168
|
}
|
|
171
169
|
|
|
172
170
|
const userData = await NodeBB.User.getUsersFields(option.votes, [
|
|
173
|
-
'uid', 'username', 'userslug', 'picture',
|
|
171
|
+
'uid', 'username', 'userslug', 'picture', 'displayname', 'icon:text', 'icon:bgColor',
|
|
174
172
|
]);
|
|
175
173
|
option.votes = userData;
|
|
176
174
|
|
|
@@ -205,7 +203,7 @@ async function anonymizeVoters(callerUid, pollId, options) {
|
|
|
205
203
|
}
|
|
206
204
|
const uidToIsAnon = _.zipObject(uids, isAnon);
|
|
207
205
|
options.forEach((opt) => {
|
|
208
|
-
opt.votes = opt.votes.map(
|
|
206
|
+
opt.votes = (opt.votes || []).map(
|
|
209
207
|
uid => (uidToIsAnon[uid] && String(callerUid) !== uid ? 0 : uid)
|
|
210
208
|
);
|
|
211
209
|
});
|
package/library.js
CHANGED
|
@@ -92,7 +92,7 @@ Plugin.renderPollWidget = async function (widget) {
|
|
|
92
92
|
const Poll = require('./lib/poll');
|
|
93
93
|
|
|
94
94
|
const settings = await Config.getSettings();
|
|
95
|
-
const allowAnon = settings.allowGuestsToViewResults
|
|
95
|
+
const allowAnon = !!settings.allowGuestsToViewResults;
|
|
96
96
|
|
|
97
97
|
if (!widget.uid && !allowAnon) {
|
|
98
98
|
return null;
|
package/package.json
CHANGED
|
@@ -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,15 @@
|
|
|
190
198
|
},
|
|
191
199
|
});
|
|
192
200
|
|
|
201
|
+
setTimezoneLabel(modal);
|
|
193
202
|
handleOptionSort({ modal });
|
|
194
203
|
|
|
195
204
|
// Add option adder
|
|
196
205
|
modal.find('#pollAddOption')
|
|
197
206
|
.off('click')
|
|
198
|
-
.on('click', async function (
|
|
199
|
-
const
|
|
200
|
-
if (config.maxOptions <=
|
|
207
|
+
.on('click', async function () {
|
|
208
|
+
const optionCount = modal.find('#poll-options-container [component="post/poll/option/item"]').length;
|
|
209
|
+
if (config.maxOptions <= optionCount) {
|
|
201
210
|
clearErrors();
|
|
202
211
|
error(`[[poll:error.max_options, ${config.maxOptions}]]`);
|
|
203
212
|
return false;
|
|
@@ -230,6 +239,14 @@
|
|
|
230
239
|
});
|
|
231
240
|
}
|
|
232
241
|
|
|
242
|
+
function setTimezoneLabel(modal) {
|
|
243
|
+
const offsetMinutes = -new Date().getTimezoneOffset();
|
|
244
|
+
const sign = offsetMinutes >= 0 ? '+' : '-';
|
|
245
|
+
const hours = String(Math.floor(Math.abs(offsetMinutes) / 60)).padStart(2, '0');
|
|
246
|
+
const minutes = String(Math.abs(offsetMinutes) % 60).padStart(2, '0');
|
|
247
|
+
modal.find('[data-timezone-label]').text(`(UTC${sign}${hours}:${minutes})`);
|
|
248
|
+
}
|
|
249
|
+
|
|
233
250
|
async function error(message) {
|
|
234
251
|
const errorBox = $('#pollErrorBox');
|
|
235
252
|
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
|
@@ -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,7 @@
|
|
|
201
204
|
};
|
|
202
205
|
|
|
203
206
|
View.prototype.voteUpdateAllowed = function () {
|
|
204
|
-
return parseInt(this.pollData.disallowVoteUpdate, 10) !== 1;
|
|
207
|
+
return parseInt(this.pollData.info.disallowVoteUpdate, 10) !== 1;
|
|
205
208
|
};
|
|
206
209
|
|
|
207
210
|
View.prototype.pollEndedOrDeleted = function () {
|
|
@@ -257,20 +260,26 @@
|
|
|
257
260
|
this.dom.messages.addClass('hidden');
|
|
258
261
|
};
|
|
259
262
|
|
|
260
|
-
View.prototype.showOptionDetails = function (details) {
|
|
261
|
-
require(['
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
263
|
+
View.prototype.showOptionDetails = async function (details) {
|
|
264
|
+
const [modals, Benchpress] = await app.require(['modals', 'benchpressjs']);
|
|
265
|
+
if (details.votes) {
|
|
266
|
+
const translator = await app.require('translator');
|
|
267
|
+
await Promise.all(details.votes.map(async (voter) => {
|
|
268
|
+
if (!parseInt(voter.uid, 10) && voter.displayname) {
|
|
269
|
+
voter.displayname = await translator.translate(voter.displayname);
|
|
270
|
+
}
|
|
271
|
+
}));
|
|
272
|
+
}
|
|
273
|
+
const html = await Benchpress.render('poll/view/details', details);
|
|
274
|
+
await modals.dialog({
|
|
275
|
+
title: `[[poll:x-users-voted-for-this-option, ${details.voteCount}]]`,
|
|
276
|
+
message: html,
|
|
277
|
+
backdrop: true,
|
|
278
|
+
buttons: {
|
|
279
|
+
close: {
|
|
280
|
+
label: '[[global:close]]',
|
|
281
|
+
},
|
|
282
|
+
},
|
|
274
283
|
});
|
|
275
284
|
};
|
|
276
285
|
|
|
@@ -301,7 +310,7 @@
|
|
|
301
310
|
}
|
|
302
311
|
} else {
|
|
303
312
|
this.showVoteButton();
|
|
304
|
-
if (this.pollData
|
|
313
|
+
if (this.pollData.info.allowAnonVoting) {
|
|
305
314
|
this.showVoteAnonButton();
|
|
306
315
|
}
|
|
307
316
|
}
|
|
@@ -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,33 @@
|
|
|
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
39
|
</div>
|
|
40
40
|
|
|
41
41
|
<div class="mb-3">
|
|
42
|
-
<label class="form-label" for="pollInputEnd">
|
|
42
|
+
<label class="form-label" for="pollInputEnd">{{tx("poll:auto_end_title")}}</label>
|
|
43
43
|
<div class="input-group date">
|
|
44
|
-
<input id="pollInputEnd" placeholder="
|
|
44
|
+
<input id="pollInputEnd" placeholder="{{tx("poll:auto_end_placeholder")}}" name="end" class="form-control" value="{poll.end}" type="datetime-local">
|
|
45
|
+
<span class="input-group-text poll-timezone-label" data-timezone-label></span>
|
|
45
46
|
</div>
|
|
46
|
-
<p class="form-text">
|
|
47
|
+
<p class="form-text">{{tx("poll:auto_end_help")}}</p>
|
|
47
48
|
</div>
|
|
48
49
|
</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 }}}
|