From b653ebd31eb28e9e7d8d111dd2e66569e0c47c56 Mon Sep 17 00:00:00 2001 From: Jason Date: Sat, 11 Jul 2026 09:30:59 +0800 Subject: [PATCH 1/3] Poll plugin refinement and bug fix Poll plugin refinement and bug fix targeted 4.14 and 4.15 --- languages/cs/poll.json | 2 +- languages/de/poll.json | 2 +- languages/en_US/poll.json | 2 +- languages/es/poll.json | 2 +- languages/fr/poll.json | 2 +- languages/gl/poll.json | 2 +- languages/he/poll.json | 2 +- languages/it/poll.json | 2 +- languages/pl/poll.json | 2 +- languages/pt-PT/poll.json | 2 +- languages/ro/poll.json | 2 +- languages/ru/poll.json | 2 +- languages/tr/poll.json | 2 +- lib/config.js | 15 +++++- lib/hooks.js | 22 ++------- lib/nodebb.js | 24 ++++----- lib/poll.js | 49 ++++++++++++++++++- lib/sockets.js | 28 +++++------ lib/vote.js | 4 +- library.js | 10 ++-- public/js/poll/creator.js | 25 ++++++++-- public/js/poll/main.js | 27 +++++++--- public/js/poll/view.js | 44 +++++++++++------ templates/admin/plugins/poll.tpl | 21 ++++---- templates/poll/creator.tpl | 25 +++++----- templates/poll/manage.tpl | 2 +- templates/poll/view.tpl | 16 +++--- templates/poll/view/messages.tpl | 4 +- templates/poll/view/results.tpl | 12 +++-- templates/poll/view/voting.tpl | 10 ++-- ...voters_and_options_votes_to_sorted_sets.js | 2 +- upgrades/fix_incorrect_poll_privilege.js | 4 +- ...tegories_poll_create_privs_to_reg_users.js | 2 +- upgrades/poll-4.0-upgrade.js | 4 +- 34 files changed, 231 insertions(+), 145 deletions(-) diff --git a/languages/cs/poll.json b/languages/cs/poll.json index 428780c..67d92c1 100755 --- a/languages/cs/poll.json +++ b/languages/cs/poll.json @@ -23,7 +23,7 @@ "auto_end_placeholder": "Kliknutím vyberete datum a čas", "auto_end_help": "Není-li vyplňeno, hlasování nebude ukončeno nikdy", - "error.max_options": "Hlasování může obsahovat maximálně %d možností.", + "error.max_options": "Hlasování může obsahovat maximálně %1 možností.", "error.no_options": "Přidejte alespoň jednu možnost.", "error.valid_date": "Vyplňte prosím platné datum.", diff --git a/languages/de/poll.json b/languages/de/poll.json index bf6b7bb..90e5f90 100644 --- a/languages/de/poll.json +++ b/languages/de/poll.json @@ -22,7 +22,7 @@ "auto_end_placeholder": "Datum und Zeit eingeben", "auto_end_help": "Lasse dieses Feld leer, wenn die Umfrage niemals enden soll.", - "error.max_options": "Du kannst höchstens %d Optionen angeben.", + "error.max_options": "Du kannst höchstens %1 Optionen angeben.", "error.no_options": "Erstelle mindestens eine Option.", "error.valid_date": "Bitte gib ein gültiges Datum an.", diff --git a/languages/en_US/poll.json b/languages/en_US/poll.json index 66d98c1..046081d 100755 --- a/languages/en_US/poll.json +++ b/languages/en_US/poll.json @@ -24,7 +24,7 @@ "auto_end_placeholder": "Click to enter date and time", "auto_end_help": "Leaving this empty will never end the poll.", - "error.max_options": "You can only create %d options.", + "error.max_options": "You can only create %1 options.", "error.no_options": "Create at least one option.", "error.valid_date": "Please enter a valid date.", diff --git a/languages/es/poll.json b/languages/es/poll.json index aed3da5..6a95385 100644 --- a/languages/es/poll.json +++ b/languages/es/poll.json @@ -22,7 +22,7 @@ "auto_end_placeholder": "Click para introducir fecha y hora", "auto_end_help": "Dejar esta opción vacia hará que la encuesta no acabe nunca.", - "error.max_options": "Solo puedes crear %d opciones.", + "error.max_options": "Solo puedes crear %1 opciones.", "error.no_options": "Crea una opción al menos.", "error.valid_date": "Por favor introduce una fecha valida.", diff --git a/languages/fr/poll.json b/languages/fr/poll.json index 37d240b..f49d852 100755 --- a/languages/fr/poll.json +++ b/languages/fr/poll.json @@ -22,7 +22,7 @@ "auto_end_title": "Terminer le sondage automatiquement", "auto_end_placeholder": "Cliquez pour entrer la date et l'heure", "auto_end_help": "Laisser ce champ vide pour que le sondage ne se termine jamais.", - "error.max_options": "Vous ne pouvez créer que %d options.", + "error.max_options": "Vous ne pouvez créer que %1 options.", "error.no_options": "Créez au moins une option.", "error.valid_date": "Veuillez entrer une date valide.", "error.not_main": "Le sondage ne peut être ajouté que dans le post principal.", diff --git a/languages/gl/poll.json b/languages/gl/poll.json index dc8c926..a523750 100644 --- a/languages/gl/poll.json +++ b/languages/gl/poll.json @@ -22,7 +22,7 @@ "auto_end_placeholder": "Fai clic aquí para introduci-la data e a hora", "auto_end_help": "Deixar esta opción baleira fará que a enquisa non remate nunca.", - "error.max_options": "You can only create %d options.", + "error.max_options": "You can only create %1 options.", "error.no_options": "Create at least one option.", "error.valid_date": "Please enter a valid date.", diff --git a/languages/he/poll.json b/languages/he/poll.json index 9351cb9..4f5b38e 100644 --- a/languages/he/poll.json +++ b/languages/he/poll.json @@ -23,7 +23,7 @@ "auto_end_placeholder": "הקש להגדיר תאריך ושעה", "auto_end_help": "באם לא נבחר תאריך הסקר לא יפוג לעולם", - "error.max_options": "אתה יכול ליצור רק %d אפשרויות בחירה", + "error.max_options": "אתה יכול ליצור רק %1 אפשרויות בחירה", "error.no_options": "צור אפשרות אחת לפחות", "error.valid_date": "נא להזין תאריך חוקי", diff --git a/languages/it/poll.json b/languages/it/poll.json index b91398b..e824c82 100644 --- a/languages/it/poll.json +++ b/languages/it/poll.json @@ -22,7 +22,7 @@ "auto_end_placeholder": "Clicca per inserire data e ora", "auto_end_help": "Lasciare vuoto per non far terminare il sondaggio", - "error.max_options": "Puoi creare al massimo %d opzioni.", + "error.max_options": "Puoi creare al massimo %1 opzioni.", "error.no_options": "Crea almeno una opzione.", "error.valid_date": "Inserisci una data valida.", diff --git a/languages/pl/poll.json b/languages/pl/poll.json index 6cef12e..5d1b705 100644 --- a/languages/pl/poll.json +++ b/languages/pl/poll.json @@ -24,7 +24,7 @@ "auto_end_placeholder": "Kliknij, aby wprowadzić datę i godzinę", "auto_end_help": "Pozostaw pole puste, aby sonda trwała bez końca.", - "error.max_options": "Możesz utworzyć nie więcej niż %d opcji.", + "error.max_options": "Możesz utworzyć nie więcej niż %1 opcji.", "error.no_options": "Wymagana jest co najmniej jedna opcja.", "error.valid_date": "Wprowadź poprawną datę.", diff --git a/languages/pt-PT/poll.json b/languages/pt-PT/poll.json index 3b1eedb..aaaea7e 100644 --- a/languages/pt-PT/poll.json +++ b/languages/pt-PT/poll.json @@ -22,7 +22,7 @@ "auto_end_placeholder": "Clica para escolheres uma data e uma hora", "auto_end_help": "Deixar este campo vazio irá fazer com que a votação nunca encerre.", - "error.max_options": "Apenas podes criar %d opções.", + "error.max_options": "Apenas podes criar %1 opções.", "error.no_options": "Primeiro tens de criar pelo menos uma opção.", "error.valid_date": "Por favor introduz uma data válida.", diff --git a/languages/ro/poll.json b/languages/ro/poll.json index a55ec8a..0959c81 100644 --- a/languages/ro/poll.json +++ b/languages/ro/poll.json @@ -22,7 +22,7 @@ "auto_end_placeholder": "Introduceți data si ora", "auto_end_help": "Lăsați acest câmp liber dacă sondajul nu are termen.", - "error.max_options": "Puteți introduce un maxim de %d opțiuni.", + "error.max_options": "Puteți introduce un maxim de %1 opțiuni.", "error.no_options": "Introduceți ce-l puțin o opțiune.", "error.valid_date": "Vă rog introduceți o dată valabilă.", diff --git a/languages/ru/poll.json b/languages/ru/poll.json index 0f6b6ee..ebff45e 100755 --- a/languages/ru/poll.json +++ b/languages/ru/poll.json @@ -23,7 +23,7 @@ "auto_end_placeholder": "Нажмите, чтобы ввести дату и время", "auto_end_help": "Если оставить данное поле пустым, то опрос будет всегда открыт.", - "error.max_options": "Вы можете создать только %d опций.", + "error.max_options": "Вы можете создать только %1 опций.", "error.no_options": "Создайте хотя бы один вариант ответа.", "error.valid_date": "Введите корректную дату.", diff --git a/languages/tr/poll.json b/languages/tr/poll.json index a5e3431..5953c0a 100644 --- a/languages/tr/poll.json +++ b/languages/tr/poll.json @@ -23,7 +23,7 @@ "auto_end_placeholder": "Tarih ve saat bilgisini buraya girin", "auto_end_help": "Burayı boş bırakırsanız anket hiç sonlanmayacak.", - "error.max_options": "Sadece %d seçenek oluşturabilirsiniz.", + "error.max_options": "Sadece %1 seçenek oluşturabilirsiniz.", "error.no_options": "En az bir seçenek oluşturmalısınız", "error.valid_date": "Lütfen geçerli bir tarih girin", diff --git a/lib/config.js b/lib/config.js index ff5fb9e..1c47527 100755 --- a/lib/config.js +++ b/lib/config.js @@ -1,6 +1,6 @@ 'use strict'; -const meta = nodebb.require('./src/meta'); +const meta = require.main.require('./src/meta'); const packageInfo = require('../package.json'); const pluginInfo = require('../plugin.json'); @@ -28,9 +28,20 @@ Config.defaults = { Config.getSettings = async function () { const settings = await meta.settings.get(Config.plugin.id); - return { + const merged = { ...Config.defaults, ...settings, }; + // NodeBB's settings helper stores checkboxes as 'on'/'off'; normalize to a boolean + // so parseInt(...) === 1 checks don't silently fail. + merged.allowGuestsToViewResults = ['1', 1, 'on', true].includes(merged.allowGuestsToViewResults); + // NodeBB's settings helper stores blank number fields as ''; fall back to defaults + // so an empty ACP field doesn't override the configured default. + merged.maxOptions = parseInt(merged.maxOptions, 10) || Config.defaults.maxOptions; + merged.maximumVotesPerUser = parseInt(merged.maximumVotesPerUser, 10) || Config.defaults.maximumVotesPerUser; + // NodeBB's settings helper stores blank text fields as ''; fall back to the + // configured default so an empty ACP field doesn't override the built-in default. + merged.defaultTitle = merged.defaultTitle || Config.defaults.defaultTitle; + return merged; }; diff --git a/lib/hooks.js b/lib/hooks.js index 545d499..86c5f84 100755 --- a/lib/hooks.js +++ b/lib/hooks.js @@ -1,9 +1,9 @@ 'use strict'; -const db = nodebb.require('./src/database'); -const topics = nodebb.require('./src/topics'); -const posts = nodebb.require('./src/posts'); -const privileges = nodebb.require('./src/privileges'); +const db = require.main.require('./src/database'); +const topics = require.main.require('./src/topics'); +const posts = require.main.require('./src/posts'); +const privileges = require.main.require('./src/privileges'); const Config = require('./config'); @@ -44,7 +44,6 @@ Hooks.filter.postCreate = async function (hookData) { // post is going to be saved to db, data is what is submitted by user const { post, data } = hookData; if (Array.isArray(data?.polls) && data.polls.length) { - await checkMaxOptions(data.polls); const savedPolls = await Poll.add(post, data.polls); if (savedPolls.length) { post.pollIds = JSON.stringify(savedPolls.map(p => String(p.pollId))); @@ -74,7 +73,6 @@ Hooks.filter.postEdit = async function (hookData) { return hookData; } const pollData = hookData.data.polls || []; - await checkMaxOptions(pollData); const toAdd = pollData.filter(p => !currentPollIds.includes(String(p.pollId))); if (toAdd.length) { @@ -136,15 +134,3 @@ async function canCreate(cid, uid) { } } -async function checkMaxOptions(polls) { - if (!Array.isArray(polls) || !polls.length) { - return; - } - const { maxOptions } = await Config.getSettings(); - polls.forEach((poll) => { - if (poll && poll.options.length > maxOptions) { - throw new Error(`[[poll:error.max_options, ${maxOptions}]]`); - } - }); -} - diff --git a/lib/nodebb.js b/lib/nodebb.js index 4e1c87f..da0e838 100755 --- a/lib/nodebb.js +++ b/lib/nodebb.js @@ -1,17 +1,17 @@ 'use strict'; module.exports = { - db: nodebb.require('./src/database'), - Settings: nodebb.require('./src/settings'), - Meta: nodebb.require('./src/meta'), - User: nodebb.require('./src/user'), - Posts: nodebb.require('./src/posts'), - Topics: nodebb.require('./src/topics'), - Privileges: nodebb.require('./src/privileges'), - Plugins: nodebb.require('./src/plugins'), - PluginSockets: nodebb.require('./src/socket.io/plugins'), - SocketIndex: nodebb.require('./src/socket.io/index'), - Translator: nodebb.require('./src/translator'), - winston: nodebb.require('winston'), + db: require.main.require('./src/database'), + Settings: require.main.require('./src/settings'), + Meta: require.main.require('./src/meta'), + User: require.main.require('./src/user'), + Posts: require.main.require('./src/posts'), + Topics: require.main.require('./src/topics'), + Privileges: require.main.require('./src/privileges'), + Plugins: require.main.require('./src/plugins'), + PluginSockets: require.main.require('./src/socket.io/plugins'), + SocketIndex: require.main.require('./src/socket.io/index'), + Translator: require.main.require('./src/translator'), + winston: require.main.require('winston'), app: null, }; diff --git a/lib/poll.js b/lib/poll.js index c3f955d..1385362 100755 --- a/lib/poll.js +++ b/lib/poll.js @@ -1,8 +1,9 @@ 'use strict'; -const db = nodebb.require('./src/database'); -const privileges = nodebb.require('./src/privileges'); +const db = require.main.require('./src/database'); +const privileges = require.main.require('./src/privileges'); +const Config = require('./config'); const Vote = require('./vote'); const Poll = exports; @@ -10,6 +11,7 @@ const Poll = exports; Poll.add = async function (postData, polls) { // postData is what is being saved to db // polls is submited by user + await validateMaxOptions(polls); const savePolls = []; for (const pollData of polls) { // eslint-disable-next-line no-await-in-loop @@ -75,6 +77,9 @@ Poll.edit = async function (postData, newPolls) { const toDelete = currentPollIds.filter(pollId => !newPollIds.includes(String(pollId))); const currentPolls = await db.getObjects(currentPollIds.map(pollId => `poll:${pollId}`)); + await validateMaxOptions(toAdd); + await validateMaxOptionsForEdit(toUpdate, currentPolls); + await Poll.deletePolls(toDelete); const savedPolls = await Poll.add(postData, toAdd); @@ -245,6 +250,46 @@ function tryParseOptions(optionsJson) { } } +async function validateMaxOptions(polls) { + if (!Array.isArray(polls) || !polls.length) { + return; + } + const settings = await Config.getSettings(); + const { maxOptions } = settings; + polls.forEach((poll) => { + if (!poll) { + return; + } + const optionCount = Array.isArray(poll.options) ? poll.options.length : 0; + if (optionCount > maxOptions) { + throw new Error(`[[poll:error.max_options, ${maxOptions}]]`); + } + }); +} + +async function validateMaxOptionsForEdit(updatedPolls, currentPolls) { + if (!Array.isArray(updatedPolls) || !updatedPolls.length) { + return; + } + const settings = await Config.getSettings(); + const { maxOptions } = settings; + updatedPolls.forEach((poll) => { + if (!poll) { + return; + } + const newOptionCount = Array.isArray(poll.options) ? poll.options.length : 0; + if (newOptionCount <= maxOptions) { + return; + } + const currentPoll = currentPolls.find(p => p && String(p.pollId) === String(poll.pollId)); + const currentOptions = currentPoll ? tryParseOptions(currentPoll.options) : []; + const currentOptionCount = Array.isArray(currentOptions) ? currentOptions.length : 0; + if (newOptionCount > currentOptionCount) { + throw new Error(`[[poll:error.max_options, ${maxOptions}]]`); + } + }); +} + Poll.getOption = async function (pollId, option, withVotes = false) { const [optionsJson, votes, voteCount] = await Promise.all([ db.getObjectField(`poll:${pollId}`, 'options'), diff --git a/lib/sockets.js b/lib/sockets.js index 8e09a35..58b7602 100755 --- a/lib/sockets.js +++ b/lib/sockets.js @@ -1,10 +1,10 @@ 'use strict'; -const _ = nodebb.require('lodash'); -const db = nodebb.require('./src/database'); -const posts = nodebb.require('./src/posts'); -const topics = nodebb.require('./src/topics'); -const privileges = nodebb.require('./src/privileges'); +const _ = require.main.require('lodash'); +const db = require.main.require('./src/database'); +const posts = require.main.require('./src/posts'); +const topics = require.main.require('./src/topics'); +const privileges = require.main.require('./src/privileges'); const NodeBB = require('./nodebb'); const Config = require('./config'); @@ -15,7 +15,7 @@ const Sockets = exports; Sockets.get = async function (socket, data) { const settings = await Config.getSettings(); - const allowAnon = settings.allowGuestsToViewResults === 'on'; + const allowAnon = !!settings.allowGuestsToViewResults; if (!data) { throw new Error('Invalid request, request data is not defined'); @@ -27,13 +27,13 @@ Sockets.get = async function (socket, data) { return []; } return await Promise.all(data.pollIds.map(async (pollId) => { - const withVotes = allowAnon || socket.uid > 0; + const withVotes = !!socket.uid; const pollData = await Poll.get(pollId, socket.uid, withVotes); if (!pollData) { return null; } - if (parseInt(pollData.info.allowAnonVoting, 10) === 1) { + if (withVotes && parseInt(pollData.info.allowAnonVoting, 10) === 1) { await anonymizeVoters(socket.uid, pollId, pollData.options); } @@ -145,20 +145,18 @@ Sockets.removeVote = async function (socket, data) { }; Sockets.getOptionDetails = async function (socket, data) { - if (!data || isNaN(parseInt(data.pollId, 10)) || isNaN(parseInt(data.optionId, 10))) { + if (!socket.uid || !data || isNaN(parseInt(data.pollId, 10)) || isNaN(parseInt(data.optionId, 10))) { throw new Error('Invalid request'); } - const [poll, option, settings] = await Promise.all([ + const [poll, option] = await Promise.all([ Poll.getInfo(data.pollId), Poll.getOption(data.pollId, data.optionId, true), - Config.getSettings(), ]); if (!poll) { throw new Error('[[error:invalid-data]]'); } - const allowAnon = settings.allowGuestsToViewResults === 'on'; const canRead = await privileges.posts.can('topics:read', poll.pid, socket.uid); - if (!canRead || (!allowAnon && socket.uid <= 0)) { + if (!canRead) { throw new Error('[[error:no-privileges]]'); } if (!option || !option.votes || !option.votes.length) { @@ -170,7 +168,7 @@ Sockets.getOptionDetails = async function (socket, data) { } const userData = await NodeBB.User.getUsersFields(option.votes, [ - 'uid', 'username', 'userslug', 'picture', + 'uid', 'username', 'userslug', 'picture', 'displayname', 'icon:text', 'icon:bgColor', ]); option.votes = userData; @@ -205,7 +203,7 @@ async function anonymizeVoters(callerUid, pollId, options) { } const uidToIsAnon = _.zipObject(uids, isAnon); options.forEach((opt) => { - opt.votes = opt.votes.map( + opt.votes = (opt.votes || []).map( uid => (uidToIsAnon[uid] && String(callerUid) !== uid ? 0 : uid) ); }); diff --git a/lib/vote.js b/lib/vote.js index 7a23628..33916ff 100755 --- a/lib/vote.js +++ b/lib/vote.js @@ -1,7 +1,7 @@ 'use strict'; -const db = nodebb.require('./src/database'); -const privileges = nodebb.require('./src/privileges'); +const db = require.main.require('./src/database'); +const privileges = require.main.require('./src/privileges'); const Poll = require('./poll'); diff --git a/library.js b/library.js index c319fc6..8962232 100755 --- a/library.js +++ b/library.js @@ -5,16 +5,16 @@ const Config = require('./lib/config'); const Sockets = require('./lib/sockets'); const Hooks = require('./lib/hooks'); const Poll = require('./lib/poll'); -const db = nodebb.require('./src/database'); -const pagination = nodebb.require('./src/pagination'); -const utils = nodebb.require('./src/utils'); +const db = require.main.require('./src/database'); +const pagination = require.main.require('./src/pagination'); +const utils = require.main.require('./src/utils'); const Plugin = module.exports; Plugin.hooks = Hooks; Plugin.load = async function (params) { - const routeHelpers = nodebb.require('./src/routes/helpers'); + const routeHelpers = require.main.require('./src/routes/helpers'); const { router } = params; routeHelpers.setupAdminPageRoute(router, `/admin/plugins/${Config.plugin.id}`, async (req, res) => { @@ -92,7 +92,7 @@ Plugin.renderPollWidget = async function (widget) { const Poll = require('./lib/poll'); const settings = await Config.getSettings(); - const allowAnon = settings.allowGuestsToViewResults === 'on'; + const allowAnon = !!settings.allowGuestsToViewResults; if (!widget.uid && !allowAnon) { return null; diff --git a/public/js/poll/creator.js b/public/js/poll/creator.js index d614ef1..f2f3af9 100755 --- a/public/js/poll/creator.js +++ b/public/js/poll/creator.js @@ -46,7 +46,6 @@ const html = await Benchpress.render('poll/manage', { post: postData }); if (modal) { - // modal.find('.bootbox-body').html(html[0].outerHTML); modal.find('.bootbox-body').html(html); handleSort({ modal, polls: postData.polls }); } else { @@ -158,7 +157,16 @@ Creator.show = function (modalTitle, poll, config) { return new Promise((resolve) => { app.require(['benchpressjs', 'modals']).then(([Benchpress, modals]) => { - Benchpress.render('poll/creator', { poll, config }).then(async function (html) { + const pollForTemplate = { ...poll }; + const end = parseInt(pollForTemplate.end, 10); + if (end > 0) { + const d = new Date(end); + const pad = n => String(n).padStart(2, '0'); + pollForTemplate.end = `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}T${pad(d.getHours())}:${pad(d.getMinutes())}`; + } else { + pollForTemplate.end = ''; + } + Benchpress.render('poll/creator', { poll: pollForTemplate, config }).then(async function (html) { const modal = await modals.dialog({ title: modalTitle, message: html, @@ -190,14 +198,15 @@ }, }); + setTimezoneLabel(modal); handleOptionSort({ modal }); // Add option adder modal.find('#pollAddOption') .off('click') .on('click', async function () { - const optionEls = modal.find('[component="post/poll/option/item"]'); - if (config.maxOptions <= optionEls.length) { + const optionCount = modal.find('#poll-options-container [component="post/poll/option/item"]').length; + if (config.maxOptions <= optionCount) { clearErrors(); error(`[[poll:error.max_options, ${config.maxOptions}]]`); return false; @@ -230,6 +239,14 @@ }); } + function setTimezoneLabel(modal) { + const offsetMinutes = -new Date().getTimezoneOffset(); + const sign = offsetMinutes >= 0 ? '+' : '-'; + const hours = String(Math.floor(Math.abs(offsetMinutes) / 60)).padStart(2, '0'); + const minutes = String(Math.abs(offsetMinutes) % 60).padStart(2, '0'); + modal.find('[data-timezone-label]').text(`(UTC${sign}${hours}:${minutes})`); + } + async function error(message) { const errorBox = $('#pollErrorBox'); const translator = await app.require('translator'); diff --git a/public/js/poll/main.js b/public/js/poll/main.js index 8d5aca5..8993a75 100755 --- a/public/js/poll/main.js +++ b/public/js/poll/main.js @@ -71,16 +71,27 @@ $(document).ready(function () { }); function getPollByPost(post) { - if (!post || !post.hasOwnProperty('pollIds') || !post.pollIds) return; - let pollIds; - try { - pollIds = JSON.parse(post.pollIds || '[]'); - } catch (err) { - pollIds = []; + if (!post) return; + let pollIds = []; + if (post.pollIds) { + try { + pollIds = JSON.parse(post.pollIds); + } catch (err) { + pollIds = []; + } } + const postEl = $(`[component="post"][data-pid="${post.pid}"]`); + const container = postEl.find('[component="post/content"]'); + const validPollIdSet = new Set((pollIds || []).map(id => String(id))); + container.find('[data-poll-id]').each(function () { + const existingPollId = $(this).attr('data-poll-id'); + if (!validPollIdSet.has(existingPollId)) { + $(this).remove(); + delete Poll.view.polls[existingPollId]; + } + }); if (pollIds.length) { - const postEl = $(`[component="post"][data-pid="${post.pid}"]`); - getPolls(pollIds, postEl.find('[component="post/content"]')); + getPolls(pollIds, container); } } diff --git a/public/js/poll/view.js b/public/js/poll/view.js index c44c582..0a2c7fc 100644 --- a/public/js/poll/view.js +++ b/public/js/poll/view.js @@ -129,6 +129,9 @@ { // Option details register: function (view) { + if (!app.user.uid) { + return; + } const self = this; view.dom.resultsPanel.off('click').on('click', '.poll-result-votecount', function (e) { self.handle(view, e); @@ -201,7 +204,7 @@ }; View.prototype.voteUpdateAllowed = function () { - return parseInt(this.pollData.disallowVoteUpdate, 10) !== 1; + return parseInt(this.pollData.info.disallowVoteUpdate, 10) !== 1; }; View.prototype.pollEndedOrDeleted = function () { @@ -257,20 +260,29 @@ this.dom.messages.addClass('hidden'); }; - View.prototype.showOptionDetails = function (details) { - require(['benchpress', 'modals'], function (benchpress, modals) { - benchpress.render('poll/view/details', details).then(function (html) { - modals.dialog({ - title: `[[poll:x-users-voted-for-this-option, ${details.voteCount}]]`, - message: html, - backdrop: true, - buttons: { - close: { - label: 'Close', - }, - }, - }); - }); + View.prototype.showOptionDetails = async function (details) { + const [modals, Benchpress] = await app.require(['modals', 'benchpressjs']); + if (details.votes) { + const translator = await app.require('translator'); + await Promise.all(details.votes.map(async (voter) => { + if (voter.displayname && voter.displayname.includes('[[')) { + voter.displayname = await translator.translate(voter.displayname); + } + if (voter.username && voter.username.includes('[[')) { + voter.username = await translator.translate(voter.username); + } + })); + } + const html = await Benchpress.render('poll/view/details', details); + await modals.dialog({ + title: `[[poll:x-users-voted-for-this-option, ${details.voteCount}]]`, + message: html, + backdrop: true, + buttons: { + close: { + label: '[[global:close]]', + }, + }, }); }; @@ -301,7 +313,7 @@ } } else { this.showVoteButton(); - if (this.pollData?.info?.allowAnonVoting) { + if (this.pollData.info.allowAnonVoting) { this.showVoteAnonButton(); } } diff --git a/templates/admin/plugins/poll.tpl b/templates/admin/plugins/poll.tpl index 44f2720..b9afbda 100755 --- a/templates/admin/plugins/poll.tpl +++ b/templates/admin/plugins/poll.tpl @@ -4,7 +4,7 @@

{title}

- +
@@ -15,10 +15,10 @@ ID - [[poll:poll_title]] - [[poll:created-time]] - [[poll:end-time]] - [[poll:votes]] + {{tx("poll:poll_title")}} + {{tx("poll:created-time")}} + {{tx("poll:end-time")}} + {{tx("poll:votes")}} @@ -47,27 +47,26 @@
- +
- +
- +
- + -

[[poll:info_choices]]

+

{{tx("poll:info_choices")}}

- diff --git a/templates/poll/creator.tpl b/templates/poll/creator.tpl index 730c199..ea65d48 100755 --- a/templates/poll/creator.tpl +++ b/templates/poll/creator.tpl @@ -4,12 +4,12 @@
- - + +
- +
{{{ if poll.options.length }}} {{{ each poll.options }}} @@ -17,32 +17,33 @@ {{{ end }}} {{{ end }}}
- +

- - -

[[poll:info_choices]]

+ + +

{{tx("poll:info_choices")}}

- +
- +
- +
- + +
-

[[poll:auto_end_help]]

+

{{tx("poll:auto_end_help")}}

diff --git a/templates/poll/manage.tpl b/templates/poll/manage.tpl index 787131b..f075241 100644 --- a/templates/poll/manage.tpl +++ b/templates/poll/manage.tpl @@ -1,6 +1,6 @@
{{{ if !post.polls.length }}} -
[[poll:no-polls]]
+
{{tx("poll:no-polls")}}
{{{ end }}} {{{ each post.polls }}}
diff --git a/templates/poll/view.tpl b/templates/poll/view.tpl index d20bfd1..890f12c 100644 --- a/templates/poll/view.tpl +++ b/templates/poll/view.tpl @@ -9,7 +9,7 @@ {{{ end }}} {{{ if (isAdmin && !poll.isWidget) }}} - [[poll:poll-id-x, {poll.info.pollId}]] + {{tx("poll:poll-id-x", poll.info.pollId)}} {{{ end }}}
@@ -19,16 +19,16 @@
-
[[poll:total-votes-x, {poll.info.voteCount}]]
+
{{tx("poll:total-votes-x", poll.info.voteCount)}}
- - - - - - + + + + + +
diff --git a/templates/poll/view/messages.tpl b/templates/poll/view/messages.tpl index a01875e..ce1bb21 100644 --- a/templates/poll/view/messages.tpl +++ b/templates/poll/view/messages.tpl @@ -1,3 +1,3 @@ -
{title}
-

{content}

+
{{tx(title)}}
+

{{tx(content)}}


\ No newline at end of file diff --git a/templates/poll/view/results.tpl b/templates/poll/view/results.tpl index d1e489a..b7c25d9 100644 --- a/templates/poll/view/results.tpl +++ b/templates/poll/view/results.tpl @@ -1,10 +1,16 @@ - +{{{ each poll.options }}}
{poll.options.title} + {{{ if config.loggedIn }}} - [[poll:x-votes, {poll.options.voteCount}]] + {{tx("poll:x-votes", poll.options.voteCount)}} + {{{ else }}} + + {{tx("poll:x-votes", poll.options.voteCount)}} + + {{{ end }}}
@@ -12,4 +18,4 @@
- \ No newline at end of file +{{{ end }}} \ No newline at end of file diff --git a/templates/poll/view/voting.tpl b/templates/poll/view/voting.tpl index 3fb7a92..00449cf 100644 --- a/templates/poll/view/voting.tpl +++ b/templates/poll/view/voting.tpl @@ -1,16 +1,16 @@
- + {{{ each poll.options }}}
- + {{{ end }}}
- +{{{ if poll.info.disallowVoteUpdate }}} - +{{{ end }}} diff --git a/upgrades/convert_poll_id_voters_and_options_votes_to_sorted_sets.js b/upgrades/convert_poll_id_voters_and_options_votes_to_sorted_sets.js index adaabdf..48bc4d8 100644 --- a/upgrades/convert_poll_id_voters_and_options_votes_to_sorted_sets.js +++ b/upgrades/convert_poll_id_voters_and_options_votes_to_sorted_sets.js @@ -1,6 +1,6 @@ 'use strict'; -const async = nodebb.require('async'); +const async = require.main.require('async'); const NodeBB = require('../lib/nodebb'); const timestamp = Date.UTC(2019, 3, 1); diff --git a/upgrades/fix_incorrect_poll_privilege.js b/upgrades/fix_incorrect_poll_privilege.js index 86b5b0e..a52ad50 100644 --- a/upgrades/fix_incorrect_poll_privilege.js +++ b/upgrades/fix_incorrect_poll_privilege.js @@ -1,7 +1,7 @@ 'use strict'; -const db = nodebb.require('./src/database'); -const privileges = nodebb.require('./src/privileges'); +const db = require.main.require('./src/database'); +const privileges = require.main.require('./src/privileges'); module.exports = { name: 'Fix incorrectly given poll privilege', diff --git a/upgrades/give_categories_poll_create_privs_to_reg_users.js b/upgrades/give_categories_poll_create_privs_to_reg_users.js index a3ca27b..c48684f 100644 --- a/upgrades/give_categories_poll_create_privs_to_reg_users.js +++ b/upgrades/give_categories_poll_create_privs_to_reg_users.js @@ -1,6 +1,6 @@ 'use strict'; -const async = nodebb.require('async'); +const async = require.main.require('async'); const NodeBB = require('../lib/nodebb'); module.exports = { diff --git a/upgrades/poll-4.0-upgrade.js b/upgrades/poll-4.0-upgrade.js index 32efe5c..67efb41 100644 --- a/upgrades/poll-4.0-upgrade.js +++ b/upgrades/poll-4.0-upgrade.js @@ -1,7 +1,7 @@ 'use strict'; -const db = nodebb.require('./src/database'); -const batch = nodebb.require('./src/batch'); +const db = require.main.require('./src/database'); +const batch = require.main.require('./src/batch'); module.exports = { name: 'Convert data structures for v4.0', From c665aef217071be9cb477001603a0ceb3c54a866 Mon Sep 17 00:00:00 2001 From: Jason Date: Sat, 11 Jul 2026 22:11:00 +0800 Subject: [PATCH 2/3] Fix 1 1. Use nodebb.require 2. Do not translate usernames --- lib/config.js | 2 +- lib/hooks.js | 8 +++---- lib/nodebb.js | 24 +++++++++---------- lib/poll.js | 4 ++-- lib/sockets.js | 10 ++++---- lib/vote.js | 4 ++-- library.js | 8 +++---- public/js/poll/view.js | 11 --------- ...voters_and_options_votes_to_sorted_sets.js | 2 +- upgrades/fix_incorrect_poll_privilege.js | 4 ++-- ...tegories_poll_create_privs_to_reg_users.js | 2 +- upgrades/poll-4.0-upgrade.js | 4 ++-- 12 files changed, 36 insertions(+), 47 deletions(-) diff --git a/lib/config.js b/lib/config.js index 1c47527..1b93569 100755 --- a/lib/config.js +++ b/lib/config.js @@ -1,6 +1,6 @@ 'use strict'; -const meta = require.main.require('./src/meta'); +const meta = nodebb.require('./src/meta'); const packageInfo = require('../package.json'); const pluginInfo = require('../plugin.json'); diff --git a/lib/hooks.js b/lib/hooks.js index 86c5f84..97d2ff6 100755 --- a/lib/hooks.js +++ b/lib/hooks.js @@ -1,9 +1,9 @@ 'use strict'; -const db = require.main.require('./src/database'); -const topics = require.main.require('./src/topics'); -const posts = require.main.require('./src/posts'); -const privileges = require.main.require('./src/privileges'); +const db = nodebb.require('./src/database'); +const topics = nodebb.require('./src/topics'); +const posts = nodebb.require('./src/posts'); +const privileges = nodebb.require('./src/privileges'); const Config = require('./config'); diff --git a/lib/nodebb.js b/lib/nodebb.js index da0e838..4e1c87f 100755 --- a/lib/nodebb.js +++ b/lib/nodebb.js @@ -1,17 +1,17 @@ 'use strict'; module.exports = { - db: require.main.require('./src/database'), - Settings: require.main.require('./src/settings'), - Meta: require.main.require('./src/meta'), - User: require.main.require('./src/user'), - Posts: require.main.require('./src/posts'), - Topics: require.main.require('./src/topics'), - Privileges: require.main.require('./src/privileges'), - Plugins: require.main.require('./src/plugins'), - PluginSockets: require.main.require('./src/socket.io/plugins'), - SocketIndex: require.main.require('./src/socket.io/index'), - Translator: require.main.require('./src/translator'), - winston: require.main.require('winston'), + db: nodebb.require('./src/database'), + Settings: nodebb.require('./src/settings'), + Meta: nodebb.require('./src/meta'), + User: nodebb.require('./src/user'), + Posts: nodebb.require('./src/posts'), + Topics: nodebb.require('./src/topics'), + Privileges: nodebb.require('./src/privileges'), + Plugins: nodebb.require('./src/plugins'), + PluginSockets: nodebb.require('./src/socket.io/plugins'), + SocketIndex: nodebb.require('./src/socket.io/index'), + Translator: nodebb.require('./src/translator'), + winston: nodebb.require('winston'), app: null, }; diff --git a/lib/poll.js b/lib/poll.js index 1385362..5d1aa7a 100755 --- a/lib/poll.js +++ b/lib/poll.js @@ -1,7 +1,7 @@ 'use strict'; -const db = require.main.require('./src/database'); -const privileges = require.main.require('./src/privileges'); +const db = nodebb.require('./src/database'); +const privileges = nodebb.require('./src/privileges'); const Config = require('./config'); const Vote = require('./vote'); diff --git a/lib/sockets.js b/lib/sockets.js index 58b7602..1791356 100755 --- a/lib/sockets.js +++ b/lib/sockets.js @@ -1,10 +1,10 @@ 'use strict'; -const _ = require.main.require('lodash'); -const db = require.main.require('./src/database'); -const posts = require.main.require('./src/posts'); -const topics = require.main.require('./src/topics'); -const privileges = require.main.require('./src/privileges'); +const _ = nodebb.require('lodash'); +const db = nodebb.require('./src/database'); +const posts = nodebb.require('./src/posts'); +const topics = nodebb.require('./src/topics'); +const privileges = nodebb.require('./src/privileges'); const NodeBB = require('./nodebb'); const Config = require('./config'); diff --git a/lib/vote.js b/lib/vote.js index 33916ff..7a23628 100755 --- a/lib/vote.js +++ b/lib/vote.js @@ -1,7 +1,7 @@ 'use strict'; -const db = require.main.require('./src/database'); -const privileges = require.main.require('./src/privileges'); +const db = nodebb.require('./src/database'); +const privileges = nodebb.require('./src/privileges'); const Poll = require('./poll'); diff --git a/library.js b/library.js index 8962232..f7b3262 100755 --- a/library.js +++ b/library.js @@ -5,16 +5,16 @@ const Config = require('./lib/config'); const Sockets = require('./lib/sockets'); const Hooks = require('./lib/hooks'); const Poll = require('./lib/poll'); -const db = require.main.require('./src/database'); -const pagination = require.main.require('./src/pagination'); -const utils = require.main.require('./src/utils'); +const db = nodebb.require('./src/database'); +const pagination = nodebb.require('./src/pagination'); +const utils = nodebb.require('./src/utils'); const Plugin = module.exports; Plugin.hooks = Hooks; Plugin.load = async function (params) { - const routeHelpers = require.main.require('./src/routes/helpers'); + const routeHelpers = nodebb.require('./src/routes/helpers'); const { router } = params; routeHelpers.setupAdminPageRoute(router, `/admin/plugins/${Config.plugin.id}`, async (req, res) => { diff --git a/public/js/poll/view.js b/public/js/poll/view.js index 0a2c7fc..42367f8 100644 --- a/public/js/poll/view.js +++ b/public/js/poll/view.js @@ -262,17 +262,6 @@ View.prototype.showOptionDetails = async function (details) { const [modals, Benchpress] = await app.require(['modals', 'benchpressjs']); - if (details.votes) { - const translator = await app.require('translator'); - await Promise.all(details.votes.map(async (voter) => { - if (voter.displayname && voter.displayname.includes('[[')) { - voter.displayname = await translator.translate(voter.displayname); - } - if (voter.username && voter.username.includes('[[')) { - voter.username = await translator.translate(voter.username); - } - })); - } const html = await Benchpress.render('poll/view/details', details); await modals.dialog({ title: `[[poll:x-users-voted-for-this-option, ${details.voteCount}]]`, diff --git a/upgrades/convert_poll_id_voters_and_options_votes_to_sorted_sets.js b/upgrades/convert_poll_id_voters_and_options_votes_to_sorted_sets.js index 48bc4d8..adaabdf 100644 --- a/upgrades/convert_poll_id_voters_and_options_votes_to_sorted_sets.js +++ b/upgrades/convert_poll_id_voters_and_options_votes_to_sorted_sets.js @@ -1,6 +1,6 @@ 'use strict'; -const async = require.main.require('async'); +const async = nodebb.require('async'); const NodeBB = require('../lib/nodebb'); const timestamp = Date.UTC(2019, 3, 1); diff --git a/upgrades/fix_incorrect_poll_privilege.js b/upgrades/fix_incorrect_poll_privilege.js index a52ad50..86b5b0e 100644 --- a/upgrades/fix_incorrect_poll_privilege.js +++ b/upgrades/fix_incorrect_poll_privilege.js @@ -1,7 +1,7 @@ 'use strict'; -const db = require.main.require('./src/database'); -const privileges = require.main.require('./src/privileges'); +const db = nodebb.require('./src/database'); +const privileges = nodebb.require('./src/privileges'); module.exports = { name: 'Fix incorrectly given poll privilege', diff --git a/upgrades/give_categories_poll_create_privs_to_reg_users.js b/upgrades/give_categories_poll_create_privs_to_reg_users.js index c48684f..a3ca27b 100644 --- a/upgrades/give_categories_poll_create_privs_to_reg_users.js +++ b/upgrades/give_categories_poll_create_privs_to_reg_users.js @@ -1,6 +1,6 @@ 'use strict'; -const async = require.main.require('async'); +const async = nodebb.require('async'); const NodeBB = require('../lib/nodebb'); module.exports = { diff --git a/upgrades/poll-4.0-upgrade.js b/upgrades/poll-4.0-upgrade.js index 67efb41..32efe5c 100644 --- a/upgrades/poll-4.0-upgrade.js +++ b/upgrades/poll-4.0-upgrade.js @@ -1,7 +1,7 @@ 'use strict'; -const db = require.main.require('./src/database'); -const batch = require.main.require('./src/batch'); +const db = nodebb.require('./src/database'); +const batch = nodebb.require('./src/batch'); module.exports = { name: 'Convert data structures for v4.0', From 71e27cd2ffc4cd1f305d63be4b451a50536c4387 Mon Sep 17 00:00:00 2001 From: Jason Date: Sat, 11 Jul 2026 22:45:43 +0800 Subject: [PATCH 3/3] Fix for guest translate Fix for guest translate --- public/js/poll/view.js | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/public/js/poll/view.js b/public/js/poll/view.js index 42367f8..98cd19a 100644 --- a/public/js/poll/view.js +++ b/public/js/poll/view.js @@ -262,6 +262,14 @@ View.prototype.showOptionDetails = async function (details) { const [modals, Benchpress] = await app.require(['modals', 'benchpressjs']); + if (details.votes) { + const translator = await app.require('translator'); + await Promise.all(details.votes.map(async (voter) => { + if (!parseInt(voter.uid, 10) && voter.displayname) { + voter.displayname = await translator.translate(voter.displayname); + } + })); + } const html = await Benchpress.render('poll/view/details', details); await modals.dialog({ title: `[[poll:x-users-voted-for-this-option, ${details.voteCount}]]`,