nodebb-plugin-poll 3.2.8 → 4.0.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/README.md +14 -32
- package/languages/en_GB/poll.json +12 -4
- package/languages/fr/poll.json +46 -38
- package/lib/config.js +13 -29
- package/lib/hooks.js +82 -67
- package/lib/nodebb.js +0 -1
- package/lib/poll.js +242 -121
- package/lib/sockets.js +60 -44
- package/lib/vote.js +29 -16
- package/library.js +27 -25
- package/package.json +4 -4
- package/plugin.json +8 -5
- package/public/js/admin.js +3 -32
- package/public/js/poll/creator.js +230 -177
- package/public/js/poll/main.js +64 -28
- package/public/js/poll/view.js +67 -81
- package/public/scss/style.scss +1 -4
- package/schema.md +24 -0
- package/templates/admin/partials/widgets/poll.tpl +2 -2
- package/templates/admin/plugins/poll.tpl +63 -48
- package/templates/poll/creator.tpl +18 -22
- package/templates/poll/manage.tpl +18 -0
- package/templates/poll/option-input.tpl +6 -0
- package/templates/poll/view/details.tpl +7 -6
- package/templates/poll/view/voting.tpl +4 -4
- package/templates/poll/view.tpl +14 -9
- package/upgrades/poll-4.0-upgrade.js +128 -0
- package/lib/scheduler.js +0 -52
- package/lib/serializer.js +0 -4
- package/public/js/poll/serializer.js +0 -173
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const db = require.main.require('./src/database');
|
|
4
|
+
const batch = require.main.require('./src/batch');
|
|
5
|
+
|
|
6
|
+
module.exports = {
|
|
7
|
+
name: 'Convert data structures for v4.0',
|
|
8
|
+
timestamp: Date.UTC(2026, 2, 7),
|
|
9
|
+
method: async function () {
|
|
10
|
+
const { progress } = this;
|
|
11
|
+
|
|
12
|
+
await convertSettings();
|
|
13
|
+
|
|
14
|
+
await db.delete('polls:scheduled');
|
|
15
|
+
|
|
16
|
+
const pollIds = [...new Set(await db.getListRange('polls', 0, -1))];
|
|
17
|
+
progress.total = pollIds.length;
|
|
18
|
+
|
|
19
|
+
await batch.processArray(pollIds, async (pollIds) => {
|
|
20
|
+
let polls = await db.getObjects(pollIds.map(pollId => `poll:${pollId}`));
|
|
21
|
+
|
|
22
|
+
await moveSettingsToPollHash(pollIds);
|
|
23
|
+
|
|
24
|
+
await addPollsToSortedSet(polls, pollIds);
|
|
25
|
+
|
|
26
|
+
await savePollOptionsInPollHash(pollIds);
|
|
27
|
+
|
|
28
|
+
await updatePollIdsInPosts(polls, pollIds);
|
|
29
|
+
|
|
30
|
+
progress.incr(pollIds.length);
|
|
31
|
+
}, {
|
|
32
|
+
batch: 500,
|
|
33
|
+
});
|
|
34
|
+
await db.delete('polls');
|
|
35
|
+
},
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
async function convertSettings() {
|
|
39
|
+
const settings = await db.getObject('settings:poll');
|
|
40
|
+
if (settings) {
|
|
41
|
+
try {
|
|
42
|
+
const parsed = JSON.parse(settings._);
|
|
43
|
+
const newSettings = {};
|
|
44
|
+
if (parsed?.toggles?.allowAnon) {
|
|
45
|
+
newSettings.allowGuestsToViewResults = parsed.toggles.allowAnon;
|
|
46
|
+
}
|
|
47
|
+
if (parsed?.limits?.maxOptions) {
|
|
48
|
+
newSettings.maxOptions = parsed.limits.maxOptions;
|
|
49
|
+
}
|
|
50
|
+
if (parsed?.defaults?.title) {
|
|
51
|
+
newSettings.defaultTitle = parsed.defaults.title;
|
|
52
|
+
}
|
|
53
|
+
if (parsed?.defaults?.maxvotes) {
|
|
54
|
+
newSettings.maximumVotesPerUser = parsed.defaults.maxvotes;
|
|
55
|
+
}
|
|
56
|
+
await db.setObject('settings:poll', newSettings);
|
|
57
|
+
} catch (err) {
|
|
58
|
+
console.info('[Poll Plugin] Error parsing settings, resetting to defaults');
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
async function moveSettingsToPollHash(pollIds) {
|
|
64
|
+
const pollSettings = await db.getObjects(pollIds.map(pollId => `poll:${pollId}:settings`));
|
|
65
|
+
const bulkSet = [];
|
|
66
|
+
pollSettings.forEach((s, i) => {
|
|
67
|
+
const pollId = pollIds[i];
|
|
68
|
+
if (s && pollId) {
|
|
69
|
+
bulkSet.push([`poll:${pollId}`, {
|
|
70
|
+
title: Object.hasOwn(s, 'title') ? s.title : 'Poll',
|
|
71
|
+
end: Object.hasOwn(s, 'end') ? s.end : 0,
|
|
72
|
+
disallowVoteUpdate: Object.hasOwn(s, 'disallowVoteUpdate') ? s.disallowVoteUpdate : 0,
|
|
73
|
+
allowAnonVoting: Object.hasOwn(s, 'allowAnonVoting') ? s.allowAnonVoting : 0,
|
|
74
|
+
maximumVotesPerUser: Object.hasOwn(s, 'maxvotes') ? s.maxvotes : 0,
|
|
75
|
+
}]);
|
|
76
|
+
}
|
|
77
|
+
});
|
|
78
|
+
await db.setObjectBulk(bulkSet);
|
|
79
|
+
await db.deleteAll(pollIds.map(pollId => `poll:${pollId}:settings`));
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
async function addPollsToSortedSet(polls, pollIds) {
|
|
83
|
+
const bulkAdd = [];
|
|
84
|
+
polls.forEach((poll, index) => {
|
|
85
|
+
if (poll) {
|
|
86
|
+
bulkAdd.push([`polls:createtime`, poll.timestamp || 0, pollIds[index]]);
|
|
87
|
+
}
|
|
88
|
+
});
|
|
89
|
+
await db.sortedSetAddBulk(bulkAdd);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
async function savePollOptionsInPollHash(pollIds) {
|
|
93
|
+
const pollOptions = await db.getSetsMembers(pollIds.map(pollId => `poll:${pollId}:options`));
|
|
94
|
+
const bulkSet = [];
|
|
95
|
+
await Promise.all(pollOptions.map(async (options, index) => {
|
|
96
|
+
const pollId = pollIds[index];
|
|
97
|
+
if (pollId && options?.length) {
|
|
98
|
+
const keys = options.map((option, i) => `poll:${pollId}:options:${options[i]}`)
|
|
99
|
+
const optionData = await db.getObjects(keys);
|
|
100
|
+
bulkSet.push([
|
|
101
|
+
`poll:${pollId}`,
|
|
102
|
+
{
|
|
103
|
+
pollId: pollId, // some old polls had "pollid" field, this fixes that
|
|
104
|
+
options: JSON.stringify(optionData.map(o => ({ id: o.id, title: o.title })))
|
|
105
|
+
},
|
|
106
|
+
]);
|
|
107
|
+
}
|
|
108
|
+
}));
|
|
109
|
+
|
|
110
|
+
await db.setObjectBulk(bulkSet);
|
|
111
|
+
await db.deleteAll(pollIds.map(pollId => `poll:${pollId}:options`))
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
async function updatePollIdsInPosts(polls, pollIds) {
|
|
115
|
+
const pids = polls.map(poll => poll && poll.pid);
|
|
116
|
+
const bulkSet = [];
|
|
117
|
+
polls.forEach((poll, index) => {
|
|
118
|
+
if (poll && poll.pid) {
|
|
119
|
+
bulkSet.push([
|
|
120
|
+
`post:${poll.pid}`,
|
|
121
|
+
{ pollIds: JSON.stringify([pollIds[index]]) }
|
|
122
|
+
]);
|
|
123
|
+
}
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
await db.setObjectBulk(bulkSet);
|
|
127
|
+
await db.deleteObjectFields(pids.map(pid => `post:${pid}`), ['pollId']);
|
|
128
|
+
}
|
package/lib/scheduler.js
DELETED
|
@@ -1,52 +0,0 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
|
|
3
|
-
const cron = require('cron').CronJob;
|
|
4
|
-
const NodeBB = require('./nodebb');
|
|
5
|
-
|
|
6
|
-
const Poll = require('./poll');
|
|
7
|
-
|
|
8
|
-
const Scheduler = exports;
|
|
9
|
-
const jobs = {};
|
|
10
|
-
|
|
11
|
-
Scheduler.start = async function () {
|
|
12
|
-
const pollIds = await Poll.getScheduled();
|
|
13
|
-
await Promise.all(pollIds.map(Scheduler.add));
|
|
14
|
-
};
|
|
15
|
-
|
|
16
|
-
Scheduler.add = async function (pollId) {
|
|
17
|
-
if (Object.keys(jobs).indexOf(pollId.toString()) !== -1) {
|
|
18
|
-
return;
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
const settings = await Poll.getSettings(pollId);
|
|
22
|
-
if (!settings) {
|
|
23
|
-
return NodeBB.winston.error(`[nodebb-plugin-poll/scheduler] Poll ID ${pollId} has no settings!`);
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
const now = Date.now();
|
|
27
|
-
const end = parseInt(settings.end, 10);
|
|
28
|
-
|
|
29
|
-
if (end < now) {
|
|
30
|
-
Scheduler.end(pollId);
|
|
31
|
-
} else {
|
|
32
|
-
const date = new Date(end);
|
|
33
|
-
NodeBB.winston.verbose(`[nodebb-plugin-poll/scheduler] Starting scheduler for poll with ID ${pollId} to end on ${date}`);
|
|
34
|
-
jobs[pollId] = new cron(date, (() => {
|
|
35
|
-
Scheduler.end(pollId);
|
|
36
|
-
}), null, true);
|
|
37
|
-
}
|
|
38
|
-
};
|
|
39
|
-
|
|
40
|
-
Scheduler.end = async function (pollId) {
|
|
41
|
-
NodeBB.winston.verbose(`[nodebb-plugin-poll/scheduler] Ending poll with ID ${pollId}`);
|
|
42
|
-
|
|
43
|
-
const index = Object.keys(jobs).indexOf(pollId.toString());
|
|
44
|
-
|
|
45
|
-
if (index !== -1 && jobs[pollId] !== undefined) {
|
|
46
|
-
jobs[pollId].stop();
|
|
47
|
-
delete jobs[pollId];
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
await Poll.end(pollId);
|
|
51
|
-
};
|
|
52
|
-
|
package/lib/serializer.js
DELETED
|
@@ -1,173 +0,0 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
|
|
3
|
-
module.exports = function (utils) {
|
|
4
|
-
var Serializer = {};
|
|
5
|
-
|
|
6
|
-
var pollRegex = /(?:(?:\[poll(?<settings>.*?)\])(?:\\n|\n|<br \/>)(?<content>(?:-.+?(?:\\n|\n|<br \/>))+)(?:\[\/poll\]))/g;
|
|
7
|
-
var settingsRegex = /(?<key>.+?)=(?:"|"|\)(?<value>.+?)(?:"|"|\)/g;
|
|
8
|
-
var settingsValidators = {
|
|
9
|
-
title: {
|
|
10
|
-
test: function (value) {
|
|
11
|
-
return value.length > 0;
|
|
12
|
-
},
|
|
13
|
-
parse: function (value) {
|
|
14
|
-
return utils.stripHTMLTags(value).trim();
|
|
15
|
-
},
|
|
16
|
-
},
|
|
17
|
-
maxvotes: {
|
|
18
|
-
test: function (value) {
|
|
19
|
-
return !isNaN(value);
|
|
20
|
-
},
|
|
21
|
-
parse: function (value) {
|
|
22
|
-
return parseInt(value, 10);
|
|
23
|
-
},
|
|
24
|
-
},
|
|
25
|
-
disallowVoteUpdate: {
|
|
26
|
-
test: function (value) {
|
|
27
|
-
return /true|false/.test(value);
|
|
28
|
-
},
|
|
29
|
-
parse: function (value) {
|
|
30
|
-
return value === 'true' || value === true ? 1 : 0;
|
|
31
|
-
},
|
|
32
|
-
},
|
|
33
|
-
allowAnonVoting: {
|
|
34
|
-
test: function (value) {
|
|
35
|
-
return /true|false/.test(value);
|
|
36
|
-
},
|
|
37
|
-
parse: function (value) {
|
|
38
|
-
return value === 'true' || value === true ? 1 : 0;
|
|
39
|
-
},
|
|
40
|
-
},
|
|
41
|
-
end: {
|
|
42
|
-
test: function (value) {
|
|
43
|
-
return (!isNaN(value) && parseInt(value, 10) > Date.now());
|
|
44
|
-
},
|
|
45
|
-
parse: function (value) {
|
|
46
|
-
return parseInt(value, 10);
|
|
47
|
-
},
|
|
48
|
-
},
|
|
49
|
-
};
|
|
50
|
-
|
|
51
|
-
Serializer.canSerialize = function (post) {
|
|
52
|
-
pollRegex.lastIndex = 0;
|
|
53
|
-
return pollRegex.test(post);
|
|
54
|
-
};
|
|
55
|
-
|
|
56
|
-
Serializer.removeMarkup = function (content, replace) {
|
|
57
|
-
return content.replace(pollRegex, replace || '');
|
|
58
|
-
};
|
|
59
|
-
|
|
60
|
-
Serializer.hasMarkup = function (content) {
|
|
61
|
-
pollRegex.lastIndex = 0;
|
|
62
|
-
const has = pollRegex.test(content);
|
|
63
|
-
return has;
|
|
64
|
-
};
|
|
65
|
-
|
|
66
|
-
Serializer.serialize = function (post, config) {
|
|
67
|
-
pollRegex.lastIndex = 0;
|
|
68
|
-
var match = pollRegex.exec(post);
|
|
69
|
-
|
|
70
|
-
if (match === null) {
|
|
71
|
-
return null;
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
return {
|
|
75
|
-
options: serializeOptions(match.groups.content, config),
|
|
76
|
-
settings: serializeSettings(match.groups.settings, config),
|
|
77
|
-
};
|
|
78
|
-
};
|
|
79
|
-
|
|
80
|
-
Serializer.deserialize = function (poll, config) {
|
|
81
|
-
var options = deserializeOptions(poll.options, config);
|
|
82
|
-
var settings = deserializeSettings(poll.settings, config);
|
|
83
|
-
|
|
84
|
-
return '[poll' + settings + ']\n' + options + '\n[/poll]';
|
|
85
|
-
};
|
|
86
|
-
|
|
87
|
-
function serializeOptions(raw, config) {
|
|
88
|
-
// Depending on composer, the line breaks can either be \n or <br /> so handle both
|
|
89
|
-
var pollOptions = [];
|
|
90
|
-
var rawOptions = raw.split(/(?:\\n|\n|<br \/>)/);
|
|
91
|
-
rawOptions.map(raw => utils.stripHTMLTags(raw));
|
|
92
|
-
var maxOptions = parseInt(config.limits.maxOptions, 10);
|
|
93
|
-
|
|
94
|
-
rawOptions.forEach(function (option) {
|
|
95
|
-
if (option.length) {
|
|
96
|
-
option = option.split('-').slice(1).join('-').trim();
|
|
97
|
-
|
|
98
|
-
if (option.length) {
|
|
99
|
-
pollOptions.push(option);
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
});
|
|
103
|
-
|
|
104
|
-
if (pollOptions.length > maxOptions) {
|
|
105
|
-
pollOptions = pollOptions.slice(0, maxOptions - 1);
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
return pollOptions;
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
function deserializeOptions(options, config) {
|
|
112
|
-
var maxOptions = config.limits.maxOptions;
|
|
113
|
-
|
|
114
|
-
options = options.map(function (option) {
|
|
115
|
-
return utils.stripHTMLTags(option).trim();
|
|
116
|
-
}).filter(function (option) {
|
|
117
|
-
return option.length;
|
|
118
|
-
});
|
|
119
|
-
|
|
120
|
-
if (options.length > maxOptions) {
|
|
121
|
-
options = options.slice(0, maxOptions - 1);
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
return options.length ? '- ' + options.join('\n- ') : '';
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
function serializeSettings(raw, config) {
|
|
128
|
-
var settings = {};
|
|
129
|
-
|
|
130
|
-
Object.keys(config.defaults).forEach(function (key) {
|
|
131
|
-
settings[key] = config.defaults[key];
|
|
132
|
-
});
|
|
133
|
-
|
|
134
|
-
const stripped = utils.stripHTMLTags(raw).replace(/\\/g, '\');
|
|
135
|
-
let match;
|
|
136
|
-
while ((match = settingsRegex.exec(stripped)) !== null) {
|
|
137
|
-
var key = match.groups.key.trim();
|
|
138
|
-
var value = match.groups.value.trim();
|
|
139
|
-
|
|
140
|
-
if (key.length && value.length && settingsValidators.hasOwnProperty(key)) {
|
|
141
|
-
if (settingsValidators[key].test(value)) {
|
|
142
|
-
settings[key] = settingsValidators[key].parse(value);
|
|
143
|
-
}
|
|
144
|
-
}
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
return settings;
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
function deserializeSettings(settings, config) {
|
|
151
|
-
var deserialized = '';
|
|
152
|
-
|
|
153
|
-
for (var k of Object.keys(settings)) {
|
|
154
|
-
if (settings.hasOwnProperty(k) && config.defaults.hasOwnProperty(k)) {
|
|
155
|
-
var key = utils.stripHTMLTags(k).trim();
|
|
156
|
-
var value = utils.stripHTMLTags(settings[k]).trim();
|
|
157
|
-
|
|
158
|
-
if (key.length && value.length && settingsValidators.hasOwnProperty(key)) {
|
|
159
|
-
if (settingsValidators[key].test(value)) {
|
|
160
|
-
deserialized += ' ' + key + '="' + value + '"';
|
|
161
|
-
}
|
|
162
|
-
}
|
|
163
|
-
}
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
return deserialized;
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
if (typeof window !== 'undefined') {
|
|
170
|
-
window.Poll.serializer = Serializer;
|
|
171
|
-
}
|
|
172
|
-
return Serializer;
|
|
173
|
-
};
|