Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion languages/cs/poll.json
Original file line number Diff line number Diff line change
Expand Up @@ -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.",

Expand Down
2 changes: 1 addition & 1 deletion languages/de/poll.json
Original file line number Diff line number Diff line change
Expand Up @@ -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.",

Expand Down
2 changes: 1 addition & 1 deletion languages/en_US/poll.json
Original file line number Diff line number Diff line change
Expand Up @@ -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.",

Expand Down
2 changes: 1 addition & 1 deletion languages/es/poll.json
Original file line number Diff line number Diff line change
Expand Up @@ -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.",

Expand Down
2 changes: 1 addition & 1 deletion languages/fr/poll.json
Original file line number Diff line number Diff line change
Expand Up @@ -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.",
Expand Down
2 changes: 1 addition & 1 deletion languages/gl/poll.json
Original file line number Diff line number Diff line change
Expand Up @@ -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.",

Expand Down
2 changes: 1 addition & 1 deletion languages/he/poll.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
"auto_end_placeholder": "הקש להגדיר תאריך ושעה",
"auto_end_help": "באם לא נבחר תאריך הסקר לא יפוג לעולם",

"error.max_options": "אתה יכול ליצור רק %d אפשרויות בחירה",
"error.max_options": "אתה יכול ליצור רק %1 אפשרויות בחירה",
"error.no_options": "צור אפשרות אחת לפחות",
"error.valid_date": "נא להזין תאריך חוקי",

Expand Down
2 changes: 1 addition & 1 deletion languages/it/poll.json
Original file line number Diff line number Diff line change
Expand Up @@ -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.",

Expand Down
2 changes: 1 addition & 1 deletion languages/pl/poll.json
Original file line number Diff line number Diff line change
Expand Up @@ -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ę.",

Expand Down
2 changes: 1 addition & 1 deletion languages/pt-PT/poll.json
Original file line number Diff line number Diff line change
Expand Up @@ -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.",

Expand Down
2 changes: 1 addition & 1 deletion languages/ro/poll.json
Original file line number Diff line number Diff line change
Expand Up @@ -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ă.",

Expand Down
2 changes: 1 addition & 1 deletion languages/ru/poll.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
"auto_end_placeholder": "Нажмите, чтобы ввести дату и время",
"auto_end_help": "Если оставить данное поле пустым, то опрос будет всегда открыт.",

"error.max_options": "Вы можете создать только %d опций.",
"error.max_options": "Вы можете создать только %1 опций.",
"error.no_options": "Создайте хотя бы один вариант ответа.",
"error.valid_date": "Введите корректную дату.",

Expand Down
2 changes: 1 addition & 1 deletion languages/tr/poll.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",

Expand Down
13 changes: 12 additions & 1 deletion lib/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
};

14 changes: 0 additions & 14 deletions lib/hooks.js
Original file line number Diff line number Diff line change
Expand Up @@ -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)));
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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}]]`);
}
});
}

45 changes: 45 additions & 0 deletions lib/poll.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,15 @@
const db = nodebb.require('./src/database');
const privileges = nodebb.require('./src/privileges');

const Config = require('./config');
const Vote = require('./vote');

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
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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'),
Expand Down
18 changes: 8 additions & 10 deletions lib/sockets.js
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand All @@ -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);
}

Expand Down Expand Up @@ -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) {
Expand All @@ -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;

Expand Down Expand Up @@ -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)
);
});
Expand Down
2 changes: 1 addition & 1 deletion library.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
25 changes: 21 additions & 4 deletions public/js/poll/creator.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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');
Expand Down
Loading