mirror of
https://hk.gh-proxy.com/https://github.com/NodeBB/nodebb-plugin-poll.git
synced 2025-10-03 04:31:04 +08:00
I need to commit more often on these big rewrites
- Reworked server code - Reworked poll creator - Moved a couple of files around - Cleaned up a lot of code
This commit is contained in:
parent
f19d411628
commit
fdab27dbf0
31 changed files with 1462 additions and 1255 deletions
8
.editorconfig
Normal file
8
.editorconfig
Normal file
|
@ -0,0 +1,8 @@
|
|||
root = true
|
||||
|
||||
[{*.js, *.css, *.tpl, *.json}]
|
||||
indent_style = tab
|
||||
end_of_line = lf
|
||||
charset = utf-8
|
||||
trim_trailing_whitespace = true
|
||||
insert_final_newline = false
|
2
public/language/en_GB/poll.json → languages/en_GB/poll.json
Normal file → Executable file
2
public/language/en_GB/poll.json → languages/en_GB/poll.json
Normal file → Executable file
|
@ -6,7 +6,6 @@
|
|||
"reset": "Reset",
|
||||
"save": "Save",
|
||||
"toggles": "Toggles",
|
||||
"allow_guests": "Allow guests to view poll results",
|
||||
"limits": "Limits",
|
||||
"max_options": "Maximum number of options per poll",
|
||||
"defaults": "Defaults",
|
||||
|
@ -17,7 +16,6 @@
|
|||
"vote": "Vote",
|
||||
"results": "Results",
|
||||
"vote_count": "users voted for this option",
|
||||
"results": "Results",
|
||||
"votes": "votes",
|
||||
"poll_title": "Poll Title",
|
||||
"enter_poll_title": "Enter poll title",
|
0
public/language/en_US/poll.json → languages/en_US/poll.json
Normal file → Executable file
0
public/language/en_US/poll.json → languages/en_US/poll.json
Normal file → Executable file
0
public/language/fr/poll.json → languages/fr/poll.json
Normal file → Executable file
0
public/language/fr/poll.json → languages/fr/poll.json
Normal file → Executable file
0
public/language/ru/poll.json → languages/ru/poll.json
Normal file → Executable file
0
public/language/ru/poll.json → languages/ru/poll.json
Normal file → Executable file
|
@ -1,5 +0,0 @@
|
|||
var Admin = {
|
||||
|
||||
};
|
||||
|
||||
module.exports = Admin;
|
275
lib/backend.js
275
lib/backend.js
|
@ -1,275 +0,0 @@
|
|||
var NodeBB = require('./nodebb'),
|
||||
db = NodeBB.db,
|
||||
|
||||
async = require('async');
|
||||
|
||||
//To whoever reads this
|
||||
//Please help improve
|
||||
var Backend = {
|
||||
addPoll: function(pollData, callback) {
|
||||
db.incrObjectField('global', 'nextPollid', function(err, pollid) {
|
||||
if (err) {
|
||||
return callback(err, -1);
|
||||
}
|
||||
|
||||
//These are separately saved, so we need to remove them from the main poll data
|
||||
var pollOptions = pollData.options,
|
||||
pollSettings = pollData.settings;
|
||||
|
||||
pollData.options = undefined;
|
||||
pollData.settings = undefined;
|
||||
pollData.pollid = pollid;
|
||||
|
||||
//Build new pollData without the options and settings keys
|
||||
var poll = {};
|
||||
for (var p in pollData) {
|
||||
if (pollData.hasOwnProperty(p) && pollData[p] !== undefined) {
|
||||
poll[p] = pollData[p];
|
||||
}
|
||||
}
|
||||
|
||||
//Save all the options to the database
|
||||
for(var i = 0, l = pollOptions.length; i < l; i++) {
|
||||
db.setObject('poll:' + pollid + ':options:' + i, pollOptions[i]);
|
||||
db.setAdd('poll:' + pollid + ':options', i);
|
||||
}
|
||||
|
||||
//Save the poll and settings to the database
|
||||
db.setObject('poll:' + pollid, poll);
|
||||
db.setObject('poll:' + pollid + ':settings', pollSettings);
|
||||
db.listAppend('polls', pollid, function(){});
|
||||
|
||||
//Register poll with a topic and post
|
||||
db.setObjectField('topic:' + poll.tid, 'poll:id', pollid);
|
||||
db.setObjectField('post:' + poll.pid, 'poll:id', pollid);
|
||||
|
||||
//Check if this poll is scheduled to end
|
||||
if (parseInt(pollSettings.end, 10) > 0) {
|
||||
Backend.schedulePoll(pollid);
|
||||
}
|
||||
|
||||
return callback(null, pollid);
|
||||
});
|
||||
},
|
||||
getPoll: function(data, callback) {
|
||||
var pollid = data.pollid,
|
||||
uid = data.uid || false,
|
||||
withVotes = (data.anon ? false : !!data.withVotes);
|
||||
|
||||
async.parallel({
|
||||
info: function(next) {
|
||||
Backend.getPollInfo(pollid, next);
|
||||
},
|
||||
options: function(next) {
|
||||
Backend.getPollOptions(pollid, withVotes, next);
|
||||
},
|
||||
settings: function(next) {
|
||||
Backend.getPollSettings(pollid, next);
|
||||
},
|
||||
hasvoted: function(next) {
|
||||
if (uid) {
|
||||
Backend.hasUidVoted(uid, pollid, next);
|
||||
} else {
|
||||
next(null, false);
|
||||
}
|
||||
}
|
||||
}, callback);
|
||||
},
|
||||
getPollIdByTid: function(tid, callback) {
|
||||
db.getObjectField('topic:' + tid, 'poll:id', callback);
|
||||
},
|
||||
getPollIdByPid: function(pid, callback) {
|
||||
db.getObjectField('post:' + pid, 'poll:id', callback);
|
||||
},
|
||||
getPollInfo: function(pollid, callback) {
|
||||
db.getObject('poll:' + pollid, callback);
|
||||
},
|
||||
getPollOptions: function(pollid, withVotes, callback) {
|
||||
if (typeof withVotes === 'function') {
|
||||
callback = withVotes;
|
||||
withVotes = false;
|
||||
}
|
||||
|
||||
db.getSetMembers('poll:' + pollid + ':options', function(err, options) {
|
||||
async.map(options, function(option, next) {
|
||||
Backend.getPollOption(pollid, option, withVotes, next);
|
||||
}, callback);
|
||||
});
|
||||
},
|
||||
getPollOption: function(pollid, option, withVotes, callback) {
|
||||
async.parallel({
|
||||
option: function(next) {
|
||||
db.getObject('poll:' + pollid + ':options:' + option, next);
|
||||
},
|
||||
votes: function(next) {
|
||||
if (withVotes) {
|
||||
db.getSetMembers('poll:' + pollid + ':options:' + option + ':votes', next);
|
||||
} else {
|
||||
next();
|
||||
}
|
||||
}
|
||||
}, function(err, results) {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
results.option = results.option || {title: 'option ' + option};
|
||||
if (results.votes) {
|
||||
results.option.votes = results.votes;
|
||||
}
|
||||
results.option.votecount = results.option.votecount || 0;
|
||||
callback(null, results.option);
|
||||
});
|
||||
},
|
||||
getPollSettings: function(pollid, callback) {
|
||||
db.getObject('poll:' + pollid + ':settings', callback);
|
||||
},
|
||||
pollHasOption: function(pollid, option, callback) {
|
||||
db.isSetMember('poll:' + pollid + ':options', option, callback);
|
||||
},
|
||||
pollHasOptions: function(pollid, options, callback) {
|
||||
db.isSetMembers('poll:' + pollid + ':options', options, callback);
|
||||
},
|
||||
hasPollEnded: function(pollid, callback) {
|
||||
Backend.getPollField(pollid, 'ended', function(err, result) {
|
||||
callback(err, parseInt(result, 10) === 1);
|
||||
});
|
||||
},
|
||||
endPoll: function(pollid) {
|
||||
db.setRemove('polls:scheduled', pollid);
|
||||
Backend.setPollField(pollid, 'ended', 1);
|
||||
},
|
||||
isPollDeleted: function(pollid, callback) {
|
||||
Backend.getPollField(pollid, 'deleted', function(err, result) {
|
||||
callback(err, parseInt(result, 10) === 1);
|
||||
});
|
||||
},
|
||||
deletePoll: function(pollid) {
|
||||
Backend.setPollField(pollid, 'deleted', 1);
|
||||
},
|
||||
restorePoll: function(pollid) {
|
||||
Backend.setPollField(pollid, 'edited', 0);
|
||||
Backend.setPollField(pollid, 'deleted', 0);
|
||||
},
|
||||
schedulePoll: function(pollid) {
|
||||
db.setAdd('polls:scheduled', pollid);
|
||||
require('./utils').scheduler.add(pollid);
|
||||
},
|
||||
getScheduledPolls: function(callback) {
|
||||
db.getSetMembers('polls:scheduled', callback);
|
||||
},
|
||||
changePid: function(pollid, pid, callback) {
|
||||
async.parallel([function(next) {
|
||||
Backend.setPollField(pollid, 'pid', pid, next);
|
||||
}, function(next) {
|
||||
db.setObjectField('post:' + pid, 'poll:id', pollid, next);
|
||||
}], callback);
|
||||
},
|
||||
changeTid: function(pollid, tid, callback) {
|
||||
async.parallel([function(next) {
|
||||
Backend.setPollField(pollid, 'tid', tid, next);
|
||||
}, function(next) {
|
||||
db.setObjectField('topic:' + tid, 'poll:id', pollid, next);
|
||||
}], callback);
|
||||
},
|
||||
setPollField: function(pollid, field, value, callback) {
|
||||
db.setObjectField('poll:' + pollid, field, value, callback);
|
||||
},
|
||||
setPollFields: function(pollid, fields, values, callback) {
|
||||
db.setObjectFields('poll:' + pollid, fields, values, callback);
|
||||
},
|
||||
getPollField: function(pollid, field, callback) {
|
||||
db.getObjectField('poll:' + pollid, field, callback);
|
||||
},
|
||||
getPollFields: function(pollid, fields, callback) {
|
||||
db.getObjectFields('poll:' + pollid, fields, callback);
|
||||
},
|
||||
/***************************
|
||||
* Vote methods start here *
|
||||
***************************/
|
||||
addVote: function(voteData, callback) {
|
||||
var pollid = voteData.pollid,
|
||||
options = voteData.options,
|
||||
uid = voteData.uid;
|
||||
|
||||
async.parallel({
|
||||
options: function(next) {
|
||||
async.each(options, function(option, next) {
|
||||
//Increase option vote count
|
||||
//next is called here because the option votecount has been updated, it doesn't matter when the uid is added
|
||||
db.incrObjectField('poll:' + pollid + ':options:' + option, 'votecount', next);
|
||||
//Add uid to list of votes
|
||||
db.setAdd('poll:' + pollid + ':options:' + option + ':votes', uid);
|
||||
}, function(err) {
|
||||
//Get poll options for callback
|
||||
Backend.getPollOptions(pollid, next);
|
||||
});
|
||||
},
|
||||
info: function(next) {
|
||||
//Add uid to poll voters
|
||||
db.setAdd('poll:' + pollid + ':voters', uid);
|
||||
//Increase poll vote count
|
||||
db.incrObjectFieldBy('poll:' + pollid, 'votecount', options.length, function(err, result){
|
||||
next(err, {
|
||||
votecount: result
|
||||
})
|
||||
});
|
||||
}
|
||||
}, callback);
|
||||
},
|
||||
removeVote: function(voteData, callback) {
|
||||
var pollid = voteData.pollid,
|
||||
options = voteData.options,
|
||||
uid = voteData.uid;
|
||||
|
||||
async.parallel({
|
||||
options: function(next) {
|
||||
async.each(options, function(option, next) {
|
||||
//Decrease option vote count
|
||||
//next is called here because the option votecount has been updated, it doesn't matter when the uid is added
|
||||
db.decrObjectField('poll:' + pollid + ':options:' + option, 'votecount', next);
|
||||
//Remove uid from list of votes
|
||||
db.setRemove('poll:' + pollid + ':options:' + option + ':votes', uid);
|
||||
}, function(err) {
|
||||
//Get poll options for callback
|
||||
Backend.getPollOptions(pollid, next);
|
||||
});
|
||||
},
|
||||
info: function(next) {
|
||||
//Remove uid from poll voters
|
||||
db.setRemove('poll:' + pollid + ':voters', uid);
|
||||
//Decrease poll vote count
|
||||
db.decrObjectFieldBy('poll:' + pollid, 'votecount', options.length, function(err, result){
|
||||
next(err, {
|
||||
votecount: result
|
||||
})
|
||||
});
|
||||
}
|
||||
}, callback);
|
||||
},
|
||||
canVote: function(voteData, callback) {
|
||||
async.parallel([
|
||||
function(next) {
|
||||
//hasended
|
||||
Backend.hasPollEnded(voteData.pollid, next);
|
||||
},
|
||||
function(next) {
|
||||
//isdeleted
|
||||
Backend.isPollDeleted(voteData.pollid, next);
|
||||
},
|
||||
function(next) {
|
||||
//hasvoted
|
||||
Backend.hasUidVoted(voteData.uid, voteData.pollid, next);
|
||||
}
|
||||
], function(err, result) {
|
||||
callback(err, result.indexOf(true) === -1);
|
||||
});
|
||||
},
|
||||
hasUidVoted: function(uid, pollid, callback) {
|
||||
db.isSetMember('poll:' + pollid + ':voters', uid, callback);
|
||||
},
|
||||
hasUidVotedOnOption: function(uid, pollid, option, callback) {
|
||||
db.isSetMember('poll:' + pollid + ':options:' + option + ':votes', uid, callback);
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = Backend;
|
41
lib/config.js
Normal file → Executable file
41
lib/config.js
Normal file → Executable file
|
@ -1,17 +1,19 @@
|
|||
var NodeBB = require('./nodebb'),
|
||||
Settings = NodeBB.settings,
|
||||
"use strict";
|
||||
|
||||
pjson = require('../package.json'),
|
||||
var NodeBB = require('./nodebb'),
|
||||
|
||||
packageInfo = require('../package.json'),
|
||||
pluginInfo = require('../plugin.json'),
|
||||
pluginId = pluginInfo.id.replace('nodebb-plugin-', ''),
|
||||
|
||||
Config = {};
|
||||
|
||||
Config.plugin = {
|
||||
name: 'Poll',
|
||||
id: 'poll',
|
||||
version: pjson.version,
|
||||
description: pjson.description,
|
||||
icon: 'fa-bar-chart-o',
|
||||
route: '/poll'
|
||||
name: pluginInfo.name,
|
||||
id: pluginId,
|
||||
version: packageInfo.version,
|
||||
description: packageInfo.description,
|
||||
icon: 'fa-bar-chart-o'
|
||||
};
|
||||
|
||||
Config.defaults = {
|
||||
|
@ -25,12 +27,27 @@ Config.defaults = {
|
|||
title: 'Poll',
|
||||
maxvotes: 1,
|
||||
end: 0
|
||||
}
|
||||
},
|
||||
version: ''
|
||||
};
|
||||
|
||||
Config.settings = new Settings(Config.plugin.id, Config.plugin.version, Config.defaults);
|
||||
Config.settings = {};
|
||||
|
||||
Config.settingSockets = {
|
||||
Config.init = function(callback) {
|
||||
Config.settings = new NodeBB.Settings(Config.plugin.id, Config.plugin.version, Config.defaults, function() {
|
||||
var oldVersion = Config.settings.get('version');
|
||||
|
||||
if (oldVersion < Config.settings.version) {
|
||||
Config.settings.set('version', Config.plugin.version);
|
||||
Config.settings.persist();
|
||||
callback();
|
||||
} else {
|
||||
callback();
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
Config.adminSockets = {
|
||||
sync: function() {
|
||||
Config.settings.sync();
|
||||
},
|
||||
|
|
142
lib/hooks.js
Normal file → Executable file
142
lib/hooks.js
Normal file → Executable file
|
@ -2,79 +2,81 @@ var XRegExp = require('xregexp').XRegExp,
|
|||
|
||||
NodeBB = require('./nodebb'),
|
||||
|
||||
Backend = require('./backend'),
|
||||
Poll = require('./poll'),
|
||||
Serializer = require('./serializer'),
|
||||
Utils = require('./utils');
|
||||
|
||||
//Todo: add hooks for post / topic changes like delete
|
||||
var Hooks = {
|
||||
filter: {
|
||||
postSave: function(postData, callback) {
|
||||
//Is this the first post?
|
||||
Utils.isFirstPost(postData.pid, postData.tid, function(err, isFirstPost) {
|
||||
if (isFirstPost) {
|
||||
Utils.parsePoll(postData.content, function(err, pollData) {
|
||||
//Check if there's poll markup
|
||||
if (pollData && pollData.options.length > 0) {
|
||||
var poll = Utils.preparePoll(postData, pollData);
|
||||
(function(Hooks) {
|
||||
|
||||
Backend.addPoll(poll, function(err, pollid) {
|
||||
postData.content = Utils.removeMarkup(postData.content);
|
||||
//Done
|
||||
callback(null, postData);
|
||||
});
|
||||
} else {
|
||||
return callback(null, postData);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
Hooks.filter = {};
|
||||
Hooks.action = {};
|
||||
|
||||
Hooks.filter.parseRaw = function(raw, callback) {
|
||||
callback(null, Serializer.removeMarkup(raw, '[Poll]'));
|
||||
};
|
||||
|
||||
Hooks.filter.postSave = function(postData, callback) {
|
||||
// Is this the first post?
|
||||
Utils.isFirstPost(postData.pid, postData.tid, function(err, isFirstPost) {
|
||||
if (!isFirstPost) {
|
||||
return callback(null, postData);
|
||||
}
|
||||
|
||||
Serializer.serialize(postData.content, function(err, pollData) {
|
||||
//Check if there's poll markup
|
||||
if (!pollData || !pollData.options.length) {
|
||||
return callback(null, postData);
|
||||
}
|
||||
});
|
||||
},
|
||||
getPosts: function(data, callback) {
|
||||
if (Array.isArray(data.posts) && data.posts.length && data.posts[0] && data.posts[0]['poll:id']) {
|
||||
//Render the client notification and add to post
|
||||
Utils.app.render('poll/notify', { pollid: data.posts[0]['poll:id'] }, function(err, html) {
|
||||
NodeBB.translator.translate(html, function(html) {
|
||||
data.posts[0].content += html;
|
||||
return callback(null, data);
|
||||
});
|
||||
});
|
||||
} else {
|
||||
return callback(null, data);
|
||||
}
|
||||
}
|
||||
},
|
||||
action: {
|
||||
postDelete: function(pid) {
|
||||
Backend.getPollIdByPid(pid, function(err, pollid) {
|
||||
if (pollid) {
|
||||
Backend.deletePoll(pollid);
|
||||
}
|
||||
});
|
||||
},
|
||||
postRestore: function(postData) {
|
||||
Backend.getPollIdByPid(postData.pid, function(err, pollid) {
|
||||
if (pollid) {
|
||||
Backend.restorePoll(pollid);
|
||||
}
|
||||
});
|
||||
},
|
||||
topicDelete: function(tid) {
|
||||
Backend.getPollIdByTid(tid, function(err, pollid) {
|
||||
if (pollid) {
|
||||
Backend.deletePoll(pollid);
|
||||
}
|
||||
});
|
||||
},
|
||||
topicRestore: function(tid) {
|
||||
Backend.getPollIdByTid(tid, function(err, pollid) {
|
||||
if (pollid) {
|
||||
Backend.restorePoll(pollid);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = Hooks;
|
||||
Poll.add(pollData, postData, function(err, pollId) {
|
||||
postData.content = Serializer.removeMarkup(postData.content);
|
||||
callback(null, postData);
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
Hooks.filter.getPosts = function(data, callback) {
|
||||
if (!Array.isArray(data.posts) || !data.posts.length || !data.posts[0] || !data.posts[0]['poll:id']) {
|
||||
return callback(null, data);
|
||||
}
|
||||
|
||||
// Render the client notification and add to post
|
||||
Utils.app.render('poll/notify', { pollId: data.posts[0]['poll:id'] }, function(err, html) {
|
||||
data.posts[0].content += html;
|
||||
return callback(null, data);
|
||||
});
|
||||
};
|
||||
|
||||
Hooks.action.postDelete = function(pid) {
|
||||
Poll.getPollIdByPid(pid, function(err, pollId) {
|
||||
if (pollId) {
|
||||
Poll.delete(pollId);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
Hooks.action.postRestore = function(postData) {
|
||||
Poll.getPollIdByPid(postData.pid, function(err, pollId) {
|
||||
if (pollId) {
|
||||
Poll.restore(pollId);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
Hooks.action.topicDelete = function(tid) {
|
||||
Poll.getPollIdByTid(tid, function(err, pollId) {
|
||||
if (pollId) {
|
||||
Poll.delete(pollId);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
Hooks.action.topicRestore = function(tid) {
|
||||
Poll.getPollIdByTid(tid, function(err, pollId) {
|
||||
if (pollId) {
|
||||
Poll.restore(pollId);
|
||||
}
|
||||
});
|
||||
};
|
||||
})(exports);
|
||||
|
|
16
lib/nodebb.js
Normal file → Executable file
16
lib/nodebb.js
Normal file → Executable file
|
@ -3,13 +3,13 @@ var NodeBB = {};
|
|||
(function(parent) {
|
||||
NodeBB = module.exports = {
|
||||
db: parent.require('./database'),
|
||||
settings: parent.require('./settings'),
|
||||
meta: parent.require('./meta'),
|
||||
user: parent.require('./user'),
|
||||
topics: parent.require('./topics'),
|
||||
pluginSockets: parent.require('./socket.io/plugins'),
|
||||
adminSockets: parent.require('./socket.io/admin').plugins,
|
||||
socketIndex: parent.require('./socket.io/index'),
|
||||
translator: parent.require('../public/src/modules/translator')
|
||||
Settings: parent.require('./settings'),
|
||||
Meta: parent.require('./meta'),
|
||||
User: parent.require('./user'),
|
||||
Topics: parent.require('./topics'),
|
||||
PluginSockets: parent.require('./socket.io/plugins'),
|
||||
AdminSockets: parent.require('./socket.io/admin').plugins,
|
||||
SocketIndex: parent.require('./socket.io/index'),
|
||||
Translator: parent.require('../public/src/modules/translator')
|
||||
};
|
||||
}(module.parent.parent));
|
||||
|
|
217
lib/poll.js
Executable file
217
lib/poll.js
Executable file
|
@ -0,0 +1,217 @@
|
|||
"use strict";
|
||||
|
||||
var async = require('async'),
|
||||
|
||||
NodeBB = require('./nodebb'),
|
||||
|
||||
Vote = require('./vote'),
|
||||
Scheduler = require('./scheduler');
|
||||
|
||||
(function(Poll) {
|
||||
|
||||
Poll.add = function(pollData, postData, callback) {
|
||||
NodeBB.db.incrObjectField('global', 'nextPollId', function(err, pollId) {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
var poll = {
|
||||
title: pollData.title,
|
||||
pollId: pollId,
|
||||
uid: postData.uid,
|
||||
tid: postData.tid,
|
||||
pid: postData.pid,
|
||||
deleted: 0,
|
||||
ended: 0,
|
||||
timestamp: postData.timestamp
|
||||
};
|
||||
|
||||
pollData.options = pollData.options.map(function(val, i) {
|
||||
return {
|
||||
id: i,
|
||||
title: val
|
||||
};
|
||||
});
|
||||
|
||||
// async this bitch up
|
||||
async.parallel([
|
||||
async.apply(async.each, pollData.options, function(option, next) {
|
||||
async.parallel([
|
||||
async.apply(NodeBB.db.setObject, 'poll:' + pollId + ':options:' + option.id, option),
|
||||
async.apply(NodeBB.db.setAdd, 'poll:' + pollId + ':options', option.id)
|
||||
], next);
|
||||
}),
|
||||
async.apply(NodeBB.db.setObject, 'poll:' + pollId, poll),
|
||||
async.apply(NodeBB.db.setObject, 'poll:' + pollId + ':settings', pollData.settings),
|
||||
async.apply(NodeBB.db.listAppend, 'polls', pollId),
|
||||
async.apply(NodeBB.db.setObjectField, 'topic:' + poll.tid, 'pollId', pollId),
|
||||
async.apply(NodeBB.db.setObjectField, 'post:' + poll.pid, 'pollId', pollId)
|
||||
], function(err) {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
// Check if this poll is scheduled to end
|
||||
if (parseInt(pollData.settings.end, 10) > 0) {
|
||||
Poll.schedulePoll(pollId);
|
||||
}
|
||||
|
||||
return callback(null, pollId);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
Poll.get = function(data, callback) {
|
||||
var pollId = data.pollId,
|
||||
uid = data.uid || false,
|
||||
withVotes = (data.anon ? false : !!data.withVotes);
|
||||
|
||||
async.parallel({
|
||||
info: function(next) {
|
||||
Poll.getInfo(pollId, next);
|
||||
},
|
||||
options: function(next) {
|
||||
Poll.getOptions(pollId, withVotes, next);
|
||||
},
|
||||
settings: function(next) {
|
||||
Poll.getSettings(pollId, next);
|
||||
},
|
||||
hasVoted: function(next) {
|
||||
if (uid) {
|
||||
Vote.hasUidVoted(uid, pollId, next);
|
||||
} else {
|
||||
next(null, false);
|
||||
}
|
||||
}
|
||||
}, callback);
|
||||
};
|
||||
|
||||
Poll.getPollIdByTid = function(tid, callback) {
|
||||
NodeBB.db.getObjectField('topic:' + tid, 'pollId', callback);
|
||||
};
|
||||
|
||||
Poll.getPollIdByPid = function(pid, callback) {
|
||||
NodeBB.db.getObjectField('post:' + pid, 'pollId', callback);
|
||||
};
|
||||
|
||||
Poll.getInfo = function(pollId, callback) {
|
||||
NodeBB.db.getObject('poll:' + pollId, callback);
|
||||
};
|
||||
|
||||
Poll.getOptions = function(pollId, withVotes, callback) {
|
||||
if (typeof withVotes === 'function') {
|
||||
callback = withVotes;
|
||||
withVotes = false;
|
||||
}
|
||||
|
||||
NodeBB.db.getSetMembers('poll:' + pollId + ':options', function(err, options) {
|
||||
async.map(options, function(option, next) {
|
||||
Poll.getOption(pollId, option, withVotes, next);
|
||||
}, callback);
|
||||
});
|
||||
};
|
||||
|
||||
Poll.getOption = function(pollId, option, withVotes, callback) {
|
||||
async.parallel({
|
||||
option: async.apply(NodeBB.db.getObject, 'poll:' + pollId + ':options:' + option),
|
||||
votes: function(next) {
|
||||
if (withVotes) {
|
||||
NodeBB.db.getSetMembers('poll:' + pollId + ':options:' + option + ':votes', next);
|
||||
} else {
|
||||
next();
|
||||
}
|
||||
}
|
||||
}, function(err, results) {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
if (results.votes) {
|
||||
results.option.votes = results.votes;
|
||||
}
|
||||
results.option.voteCount = results.option.voteCount || 0;
|
||||
|
||||
callback(null, results.option);
|
||||
});
|
||||
};
|
||||
|
||||
Poll.hasOption = function(pollId, option, callback) {
|
||||
NodeBB.db.isSetMember('poll:' + pollId + ':options', option, callback);
|
||||
};
|
||||
|
||||
Poll.hasOptions = function(pollId, options, callback) {
|
||||
NodeBB.db.isSetMembers('poll:' + pollId + ':options', options, callback);
|
||||
};
|
||||
|
||||
Poll.getSettings = function(pollId, callback) {
|
||||
NodeBB.db.getObject('poll:' + pollId + ':settings', callback);
|
||||
};
|
||||
|
||||
Poll.isDeleted = function(pollId, callback) {
|
||||
Poll.getField(pollId, 'deleted', function(err, result) {
|
||||
callback(err, parseInt(result, 10) === 1);
|
||||
});
|
||||
};
|
||||
|
||||
Poll.delete = function(pollId) {
|
||||
Poll.setField(pollId, 'deleted', 1);
|
||||
};
|
||||
|
||||
Poll.restore = function(pollId) {
|
||||
Poll.setPollField(pollId, 'edited', 0);
|
||||
Poll.setPollField(pollId, 'deleted', 0);
|
||||
};
|
||||
|
||||
Poll.schedule = function(pollId) {
|
||||
NodeBB.db.setAdd('polls:scheduled', pollId);
|
||||
Scheduler.add(pollId);
|
||||
};
|
||||
|
||||
Poll.getScheduled = function(callback) {
|
||||
NodeBB.db.getSetMembers('polls:scheduled', callback);
|
||||
};
|
||||
|
||||
Poll.hasEnded = function(pollId, callback) {
|
||||
Poll.getField(pollId, 'ended', function(err, result) {
|
||||
callback(err, parseInt(result, 10) === 1);
|
||||
});
|
||||
};
|
||||
|
||||
Poll.end = function(pollId) {
|
||||
NodeBB.db.setRemove('polls:scheduled', pollId);
|
||||
Poll.setField(pollId, 'ended', 1);
|
||||
};
|
||||
|
||||
Poll.changePid = function(pollId, pid, callback) {
|
||||
async.parallel([function(next) {
|
||||
Poll.setField(pollId, 'pid', pid, next);
|
||||
}, function(next) {
|
||||
NodeBB.db.setObjectField('post:' + pid, 'pollId', pollId, next);
|
||||
}], callback);
|
||||
};
|
||||
|
||||
Poll.changeTid = function(pollId, tid, callback) {
|
||||
async.parallel([function(next) {
|
||||
Poll.setField(pollId, 'tid', tid, next);
|
||||
}, function(next) {
|
||||
NodeBB.db.setObjectField('topic:' + tid, 'polIid', pollId, next);
|
||||
}], callback);
|
||||
};
|
||||
|
||||
Poll.setField = function(pollId, field, value, callback) {
|
||||
NodeBB.db.setObjectField('poll:' + pollId, field, value, callback);
|
||||
};
|
||||
|
||||
Poll.setFields = function(pollId, fields, values, callback) {
|
||||
NodeBB.db.setObjectFields('poll:' + pollId, fields, values, callback);
|
||||
};
|
||||
|
||||
Poll.getField = function(pollId, field, callback) {
|
||||
NodeBB.db.getObjectField('poll:' + pollId, field, callback);
|
||||
};
|
||||
|
||||
Poll.getFields = function(pollId, fields, callback) {
|
||||
NodeBB.db.getObjectFields('poll:' + pollId, fields, callback);
|
||||
};
|
||||
|
||||
})(exports);
|
54
lib/scheduler.js
Executable file
54
lib/scheduler.js
Executable file
|
@ -0,0 +1,54 @@
|
|||
"use strict";
|
||||
|
||||
var Poll = require('./poll'),
|
||||
|
||||
jobs = {},
|
||||
|
||||
Scheduler = {};
|
||||
|
||||
Scheduler.start = function() {
|
||||
Poll.getScheduled(function(err, pollIds) {
|
||||
pollIds.forEach(function(pollId) {
|
||||
Scheduler.add(pollId);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
Scheduler.add = function(pollId) {
|
||||
if (Object.keys(jobs).indexOf(pollId.toString()) !== -1) {
|
||||
return;
|
||||
}
|
||||
|
||||
Poll.getSettings(pollId, function(err, settings) {
|
||||
if (err) {
|
||||
return console.log(err.stack);
|
||||
}
|
||||
if (!settings) {
|
||||
return console.log('Poll ID ' + pollId + ' has no settings!');
|
||||
}
|
||||
|
||||
var now = Date.now(),
|
||||
end = parseInt(settings.end, 10);
|
||||
|
||||
if (end < now) {
|
||||
Scheduler.end(pollId);
|
||||
} else {
|
||||
jobs[pollId] = new cron(new Date(end), function() {
|
||||
Scheduler.end(pollId);
|
||||
}, null, true);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
Scheduler.end = function(pollId) {
|
||||
var index = Object.keys(jobs).indexOf(pollId.toString());
|
||||
|
||||
if (index !== -1 && jobs[pollId] !== undefined) {
|
||||
jobs[pollId].stop();
|
||||
delete jobs[pollId];
|
||||
}
|
||||
|
||||
Poll.end(pollId);
|
||||
};
|
||||
|
||||
module.exports = Scheduler;
|
3
lib/serializer.js
Executable file
3
lib/serializer.js
Executable file
|
@ -0,0 +1,3 @@
|
|||
"use strict";
|
||||
|
||||
module.exports = require('../public/js/poll/serializer');
|
169
lib/sockets.js
Normal file → Executable file
169
lib/sockets.js
Normal file → Executable file
|
@ -1,85 +1,100 @@
|
|||
"use strict";
|
||||
|
||||
var async = require('async'),
|
||||
|
||||
Backend = require('./backend'),
|
||||
Config = require('./config'),
|
||||
|
||||
NodeBB = require('./nodebb'),
|
||||
SocketIndex = NodeBB.socketIndex,
|
||||
User = NodeBB.user;
|
||||
|
||||
var Sockets = {
|
||||
load: function(socket, data, callback) {
|
||||
Config = require('./config'),
|
||||
Poll = require('./poll'),
|
||||
Vote = require('./vote');
|
||||
|
||||
(function(Sockets) {
|
||||
|
||||
Sockets.load = function(socket, data, callback) {
|
||||
var allowAnon = Config.settings.get('toggles.allowAnon');
|
||||
if (socket.uid || allowAnon) {
|
||||
if (data && data.pollid) {
|
||||
data.uid = socket.uid;
|
||||
data.anon = (!socket.uid && allowAnon);
|
||||
Backend.getPoll(data, function(err, result) {
|
||||
callback(err, result);
|
||||
});
|
||||
} else {
|
||||
callback(new Error('Invalid poll request'));
|
||||
|
||||
if (!socket.uid && !allowAnon) {
|
||||
return callback(new Error('Not logged in'));
|
||||
}
|
||||
|
||||
if (!data || !data.pollid) {
|
||||
return callback(new Error('Invalid poll request'));
|
||||
}
|
||||
|
||||
data.uid = socket.uid;
|
||||
data.anon = (!socket.uid && allowAnon);
|
||||
|
||||
Poll.get(data, function(err, result) {
|
||||
callback(err, result);
|
||||
});
|
||||
};
|
||||
|
||||
Sockets.vote = function(socket, data, callback) {
|
||||
if (!socket.uid || !data || isNaN(parseInt(data.pollId, 10)) || !data.options || !data.options.length) {
|
||||
return callback(new Error('Invalid vote'));
|
||||
}
|
||||
|
||||
data.uid = socket.uid;
|
||||
|
||||
async.parallel({
|
||||
canVote: function(next) {
|
||||
Vote.canVote(data.uid, data.pollId, next);
|
||||
},
|
||||
optionsFilter: function(next) {
|
||||
Poll.hasOptions(data.pollid, data.options, next);
|
||||
},
|
||||
settings: function(next) {
|
||||
Poll.getSettings(data.pollid, next);
|
||||
}
|
||||
} else {
|
||||
callback(new Error('Not logged in'));
|
||||
}
|
||||
},
|
||||
vote: function(socket, data, callback) {
|
||||
if (socket.uid && data && !isNaN(parseInt(data.pollid, 10)) && data.options && data.options.length > 0) {
|
||||
data.uid = socket.uid;
|
||||
async.parallel({
|
||||
canvote: function(next) {
|
||||
Backend.canVote(data, next);
|
||||
},
|
||||
optionsFilter: function(next) {
|
||||
Backend.pollHasOptions(data.pollid, data.options, next);
|
||||
},
|
||||
settings: function(next) {
|
||||
Backend.getPollSettings(data.pollid, next);
|
||||
}
|
||||
}, function(err, result) {
|
||||
//Filter the options on their existence, then slice out the max allowed votes
|
||||
data.options = data.options.filter(function(el, index) {
|
||||
return result.optionsFilter[index];
|
||||
})/*.slice(0, result.settings.maxvotes);*/
|
||||
|
||||
//Instead of slicing we'll give them an error
|
||||
if (data.options.length > parseInt(result.settings.maxvotes, 10)) {
|
||||
callback(new Error('You can only vote for ' + result.settings.maxvotes + ' options on this poll!'));
|
||||
} else {
|
||||
if (!err && (result.canvote && data.options.length > 0)) {
|
||||
Backend.addVote(data, function(err, result) {
|
||||
result.pollid = data.pollid;
|
||||
SocketIndex.server.sockets.emit('event:poll.votechange', result);
|
||||
callback();
|
||||
});
|
||||
} else {
|
||||
callback(new Error('Already voted or invalid option'));
|
||||
}
|
||||
}
|
||||
}, function(err, result) {
|
||||
// Filter the options on their existence
|
||||
data.options = data.options.filter(function(el, index) {
|
||||
return result.optionsFilter[index];
|
||||
});
|
||||
} else {
|
||||
callback(new Error('Invalid vote'));
|
||||
}
|
||||
},
|
||||
optionDetails: function(socket, data, callback) {
|
||||
if (socket.uid && data && !isNaN(parseInt(data.pollid, 10)) && !isNaN(parseInt(data.option, 10))) {
|
||||
Backend.getPollOption(data.pollid, data.option, true, function(err, result) {
|
||||
if (err) {
|
||||
callback(new Error('Something went wrong!'));
|
||||
} else if (result.votes && result.votes.length > 0) {
|
||||
User.getMultipleUserFields(result.votes, ['username', 'userslug', 'picture'], function(err, userData) {
|
||||
result.votes = userData;
|
||||
callback(null, result);
|
||||
})
|
||||
} else {
|
||||
callback(null, result);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
callback(new Error('Invalid request'));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = Sockets;
|
||||
// Give an error if there are too many votes
|
||||
if (data.options.length > parseInt(result.settings.maxvotes, 10)) {
|
||||
return callback(new Error('You can only vote for ' + result.settings.maxvotes + ' options on this poll.'));
|
||||
}
|
||||
|
||||
if (err || !result.canVote || !data.options.length) {
|
||||
return callback(new Error('Already voted or invalid option'));
|
||||
|
||||
}
|
||||
|
||||
Vote.add(data, function(err, result) {
|
||||
result.pollId = data.pollId;
|
||||
|
||||
NodeBB.SocketIndex.server.sockets.emit('event:poll.voteChange', result);
|
||||
|
||||
callback();
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
Sockets.getOptionDetails = function(socket, data, callback) {
|
||||
if (!socket.uid || !data || isNaN(parseInt(data.pollId, 10)) || isNaN(parseInt(data.option, 10))) {
|
||||
return callback(new Error('Invalid request'));
|
||||
}
|
||||
|
||||
Poll.getOption(data.pollid, data.option, true, function(err, result) {
|
||||
if (err) {
|
||||
return callback(new Error('Something went wrong!'));
|
||||
}
|
||||
|
||||
if (!result.votes || !result.votes.length) {
|
||||
return callback(null, result);
|
||||
}
|
||||
|
||||
NodeBB.User.getMultipleUserFields(result.votes, ['username', 'userslug', 'picture'], function(err, userData) {
|
||||
result.votes = userData;
|
||||
callback(null, result);
|
||||
})
|
||||
});
|
||||
};
|
||||
|
||||
Sockets.getConfig = function(socket, data, callback) {
|
||||
callback(null, Config.settings.get());
|
||||
};
|
||||
|
||||
})(exports);
|
196
lib/utils.js
Normal file → Executable file
196
lib/utils.js
Normal file → Executable file
|
@ -1,198 +1,20 @@
|
|||
var S = require('string'),
|
||||
XRegExp = require('xregexp').XRegExp,
|
||||
async = require('async'),
|
||||
cron = require('cron').CronJob,
|
||||
"use strict";
|
||||
|
||||
NodeBB = require('./nodebb'),
|
||||
db = NodeBB.db,
|
||||
fs = module.parent.parent.require('fs'),
|
||||
path = module.parent.parent.require('path'),
|
||||
var NodeBB = require('./nodebb');
|
||||
|
||||
Config = require('./config'),
|
||||
Backend = require('./backend'),
|
||||
(function(Utils) {
|
||||
|
||||
pollRegex = XRegExp('(?:(?:\\[poll(?<settings>.*?)\\])\n(?<content>(?:-.+?\n)+)(?:\\[\/poll\\]))', 'g'),
|
||||
pollSettingsRegex = XRegExp('(?<key>.+?)="(?<value>.+?)"', 'g'),
|
||||
translations,
|
||||
Utils.app = null;
|
||||
|
||||
pollSettingsMap = {
|
||||
max: {
|
||||
key: 'maxvotes',
|
||||
test: function(value) {
|
||||
return !isNaN(value);
|
||||
}
|
||||
},
|
||||
title: {
|
||||
key: 'title',
|
||||
test: function(value) {
|
||||
return value.length > 0;
|
||||
}
|
||||
},
|
||||
end: {
|
||||
key: 'end',
|
||||
test: function(value) {
|
||||
return (!isNaN(value) && parseInt(value, 10) > Date.now());
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
var Utils = {
|
||||
app: null,
|
||||
hasPoll: function(post) {
|
||||
return XRegExp.exec(post, pollRegex) !== null;
|
||||
},
|
||||
parsePoll: function(post, callback) {
|
||||
var match = XRegExp.exec(post, pollRegex);
|
||||
if (match !== null) {
|
||||
async.parallel({
|
||||
options: function(next) {
|
||||
Utils.parseOptions(match.content, next);
|
||||
},
|
||||
settings: function(next) {
|
||||
Utils.parseSettings(match.settings, next);
|
||||
}
|
||||
}, callback);
|
||||
} else {
|
||||
callback(null, null);
|
||||
}
|
||||
},
|
||||
parseOptions: function(raw, callback) {
|
||||
var maxOptions = Config.settings.get('limits.maxOptions'),
|
||||
pollOptions = [],
|
||||
rawOptions = S(raw).stripTags().s.split('\n');
|
||||
|
||||
for (var i = 0, l = rawOptions.length; i < l; i++) {
|
||||
if (rawOptions[i].length > 0) {
|
||||
var option = S(rawOptions[i].split('-')[1]).trim().s;
|
||||
if (option.length > 0) {
|
||||
pollOptions.push(option);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (pollOptions.length > maxOptions) {
|
||||
pollOptions = pollOptions.slice(0, maxOptions - 1);
|
||||
}
|
||||
|
||||
callback(null, pollOptions);
|
||||
},
|
||||
parseSettings: function(raw, callback) {
|
||||
var pollSettings = Config.settings.get('defaults');
|
||||
|
||||
raw = S(raw).stripTags().s;
|
||||
|
||||
callback(null, XRegExp.forEach(raw, pollSettingsRegex, function(match) {
|
||||
var key = S(match.key).trim().s,
|
||||
value = S(match.value).trim().s;
|
||||
|
||||
if (key.length > 0 && value.length > 0 && pollSettingsMap.hasOwnProperty(key)) {
|
||||
if (pollSettingsMap[key].test(value)) {
|
||||
this[pollSettingsMap[key].key] = value;
|
||||
}
|
||||
}
|
||||
}, pollSettings));
|
||||
},
|
||||
preparePoll: function(postData, pollData) {
|
||||
return {
|
||||
title: pollData.title,
|
||||
uid: postData.uid,
|
||||
tid: postData.tid,
|
||||
pid: postData.pid,
|
||||
deleted: 0,
|
||||
ended: 0,
|
||||
timestamp: postData.timestamp,
|
||||
settings: pollData.settings,
|
||||
options: pollData.options.map(function(val, i) {
|
||||
return {
|
||||
id: i,
|
||||
title: val
|
||||
};
|
||||
})
|
||||
};
|
||||
},
|
||||
isFirstPost: function(pid, tid, callback) {
|
||||
//Check if topic is empty or if post is first post
|
||||
db.getSortedSetRange('tid:' + tid + ':posts', 0, 0, function(err, pids) {
|
||||
Utils.isFirstPost = function(pid, tid, callback) {
|
||||
// Check if topic is empty or if post is first post
|
||||
NodeBB.db.getSortedSetRange('tid:' + tid + ':posts', 0, 0, function(err, pids) {
|
||||
if(err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
callback(null, pids.length === 0 || parseInt(pids[0], 10) === parseInt(pid, 10));
|
||||
});
|
||||
},
|
||||
removeMarkup: function(content) {
|
||||
return XRegExp.replace(content, pollRegex, '');
|
||||
},
|
||||
scheduler: {
|
||||
init: function() {
|
||||
Backend.getScheduledPolls(function(err, pollids) {
|
||||
if (pollids.length > 0) {
|
||||
for (var i = 0, l = pollids.length; i < l; i++) {
|
||||
Utils.scheduler.add(pollids[i]);
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
add: function(pollid) {
|
||||
if (Object.keys(Utils.scheduler.jobs).indexOf(pollid.toString()) !== -1) {
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
Backend.getPollSettings(pollid, function(err, settings) {
|
||||
if (err) {
|
||||
return console.log(err.stack);
|
||||
}
|
||||
if (!settings) {
|
||||
return console.log('Poll ID - ' + pollid + ' has no settings!');
|
||||
}
|
||||
var now = Date.now(),
|
||||
end = parseInt(settings.end, 10);
|
||||
if (end < now) {
|
||||
Utils.scheduler.end(pollid);
|
||||
} else {
|
||||
Utils.scheduler.jobs[pollid] = new cron(new Date(end), function() {
|
||||
Utils.scheduler.end(pollid);
|
||||
}, null, true);
|
||||
}
|
||||
});
|
||||
},
|
||||
end: function(pollid) {
|
||||
var index = Object.keys(Utils.scheduler.jobs).indexOf(pollid.toString());
|
||||
if (index !== -1 && Utils.scheduler.jobs[pollid] !== undefined) {
|
||||
Utils.scheduler.jobs[pollid].stop();
|
||||
Utils.scheduler.jobs[pollid] = undefined;
|
||||
}
|
||||
Backend.endPoll(pollid);
|
||||
},
|
||||
jobs: {}
|
||||
},
|
||||
loadTranslations: function(){
|
||||
Utils.translations = { };
|
||||
var languagesPath = path.resolve(__dirname, '../public/language');
|
||||
|
||||
var langs = fs.readdirSync(languagesPath);
|
||||
for (var l in langs) {
|
||||
var lang = langs[l],
|
||||
langPath = languagesPath + '/' + lang;
|
||||
|
||||
if (fs.lstatSync(langPath).isDirectory()) {
|
||||
var files = fs.readdirSync(langPath);
|
||||
for (var f in files) {
|
||||
var file = files[f],
|
||||
filePath = langPath + '/' + file;
|
||||
|
||||
if (!fs.lstatSync(filePath).isDirectory() && file.slice(-5) === '.json') {
|
||||
try {
|
||||
Utils.translations[langs[l]] = JSON.parse(fs.readFileSync(filePath), 'utf8');
|
||||
NodeBB.translator.addTranslation(lang, file.slice(0, -5), Utils.translations[langs[l]]);
|
||||
}catch (e){
|
||||
console.log("Poll: Error reading " + filePath + ": " + e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = Utils;
|
||||
})(exports);
|
106
lib/vote.js
Executable file
106
lib/vote.js
Executable file
|
@ -0,0 +1,106 @@
|
|||
"use strict";
|
||||
|
||||
var async = require('async'),
|
||||
|
||||
NodeBB = require('./nodebb'),
|
||||
|
||||
Poll = require('./poll');
|
||||
|
||||
(function(Vote) {
|
||||
|
||||
Vote.add = function(voteData, callback) {
|
||||
var pollId = voteData.pollId,
|
||||
options = voteData.options,
|
||||
uid = voteData.uid;
|
||||
|
||||
async.parallel({
|
||||
options: function(next) {
|
||||
async.each(options, function(option, next) {
|
||||
async.parallel([
|
||||
// Increase option vote count
|
||||
async.apply(NodeBB.db.incrObjectField, 'poll:' + pollId + ':options:' + option),
|
||||
// Add uid to list of votes
|
||||
async.apply(NodeBB.db.setAdd, 'poll:' + pollId + ':options:' + option + ':votes', uid)
|
||||
], next);
|
||||
}, function(err) {
|
||||
// Get poll options for callback
|
||||
Poll.getOptions(pollId, next);
|
||||
});
|
||||
},
|
||||
info: function(next) {
|
||||
async.parallel([
|
||||
// Add uid to poll voters
|
||||
async.apply(NodeBB.db.setAdd, 'poll:' + pollId + ':voters', uid),
|
||||
// Increase poll vote count
|
||||
async.apply(NodeBB.db.incrObjectFieldBy, 'poll:' + pollId, 'votecount', options.length)
|
||||
], function(err, result) {
|
||||
next(err, {
|
||||
voteCount: result[1]
|
||||
})
|
||||
});
|
||||
}
|
||||
}, callback);
|
||||
};
|
||||
|
||||
Vote.remove = function(voteData, callback) {
|
||||
var pollId = voteData.pollId,
|
||||
options = voteData.options,
|
||||
uid = voteData.uid;
|
||||
|
||||
async.parallel({
|
||||
options: function(next) {
|
||||
async.each(options, function(option, next) {
|
||||
async.parallel([
|
||||
// Decrease option vote count
|
||||
async.apply(NodeBB.db.decrObjectField, 'poll:' + pollId + ':options:' + option, 'votecount'),
|
||||
// Remove uid from list of votes
|
||||
async.apply(NodeBB.db.setRemove, 'poll:' + pollId + ':options:' + option + ':votes', uid)
|
||||
], next);
|
||||
}, function(err) {
|
||||
//Get poll options for callback
|
||||
Poll.getOptions(pollId, next);
|
||||
});
|
||||
},
|
||||
info: function(next) {
|
||||
async.parallel([
|
||||
// Remove uid from poll voters
|
||||
async.apply(NodeBB.db.setRemove, 'poll:' + pollId + ':voters', uid),
|
||||
// Decrease poll vote count
|
||||
async.apply(NodeBB.db.decrObjectFieldBy, 'poll:' + pollId, 'votecount', options.length)
|
||||
], function(err, result) {
|
||||
next(err, {
|
||||
voteCount: result[1]
|
||||
})
|
||||
});
|
||||
}
|
||||
}, callback);
|
||||
};
|
||||
|
||||
Vote.canVote = function(uid, pollId, callback) {
|
||||
async.parallel([
|
||||
function(next) {
|
||||
// Ended?
|
||||
Poll.hasEnded(pollId, next);
|
||||
},
|
||||
function(next) {
|
||||
// Deleted?
|
||||
Poll.isDeleted(pollId, next);
|
||||
},
|
||||
function(next) {
|
||||
// Already voted?
|
||||
Vote.hasUidVoted(uid, pollId, next);
|
||||
}
|
||||
], function(err, result) {
|
||||
callback(err, result.indexOf(true) === -1);
|
||||
});
|
||||
};
|
||||
|
||||
Vote.hasUidVoted = function(uid, pollId, callback) {
|
||||
NodeBB.db.isSetMember('poll:' + pollId + ':voters', uid, callback);
|
||||
};
|
||||
|
||||
Vote.hasUidVotedOnOption = function(uid, pollId, option, callback) {
|
||||
NodeBB.db.isSetMember('poll:' + pollId + ':options:' + option + ':votes', uid, callback);
|
||||
};
|
||||
|
||||
})(exports);
|
64
library.js
Normal file → Executable file
64
library.js
Normal file → Executable file
|
@ -1,47 +1,41 @@
|
|||
"use strict";
|
||||
|
||||
var NodeBB = require('./lib/nodebb'),
|
||||
Config = require('./lib/config'),
|
||||
Sockets = require('./lib/sockets'),
|
||||
Hooks = require('./lib/hooks'),
|
||||
Utils = require('./lib/utils'),
|
||||
Admin = require('./lib/admin'),
|
||||
Scheduler = require('./lib/scheduler'),
|
||||
Utils = require('./lib/utils');
|
||||
|
||||
PluginSockets = NodeBB.pluginSockets,
|
||||
AdminSockets = NodeBB.adminSockets,
|
||||
(function(Plugin) {
|
||||
|
||||
app;
|
||||
Plugin.hooks = Hooks;
|
||||
|
||||
var Poll = {};
|
||||
|
||||
Poll.init = {
|
||||
load: function(data, callback) {
|
||||
app = data.app;
|
||||
Plugin.load = function(params, callback) {
|
||||
function renderAdmin(req, res, next) {
|
||||
//Config.api(function(data) {
|
||||
res.render('poll/admin', {});
|
||||
//});
|
||||
res.render('admin/plugins/' + Config.plugin.id, {});
|
||||
}
|
||||
Utils.loadTranslations();
|
||||
data.router.get('/admin/poll', data.middleware.admin.buildHeader, renderAdmin);
|
||||
data.router.get('/api/admin/poll', renderAdmin);
|
||||
PluginSockets.poll = Sockets;
|
||||
AdminSockets.poll = Config.settingSockets;
|
||||
Utils.app = data.router;
|
||||
Utils.scheduler.init();
|
||||
callback();
|
||||
},
|
||||
admin: {
|
||||
addNavigation: function(custom_header, callback) {
|
||||
custom_header.plugins.push({
|
||||
route: Config.plugin.route,
|
||||
icon: Config.plugin.icon,
|
||||
name: Config.plugin.name
|
||||
});
|
||||
|
||||
callback(null, custom_header);
|
||||
}
|
||||
}
|
||||
};
|
||||
params.router.get('/admin/plugins/' + Config.plugin.id, params.middleware.admin.buildHeader, renderAdmin);
|
||||
params.router.get('/api/admin/plugins/' + Config.plugin.id, renderAdmin);
|
||||
|
||||
Poll.hooks = Hooks;
|
||||
NodeBB.PluginSockets[Config.plugin.id] = Sockets;
|
||||
NodeBB.AdminSockets[Config.plugin.id] = Config.adminSockets;
|
||||
|
||||
module.exports = Poll;
|
||||
Utils.app = params.app;
|
||||
Scheduler.start();
|
||||
|
||||
Config.init(callback);
|
||||
};
|
||||
|
||||
Plugin.addAdminNavigation = function(adminHeader, callback) {
|
||||
adminHeader.plugins.push({
|
||||
route: '/plugins/' + Config.plugin.id,
|
||||
icon: Config.plugin.icon,
|
||||
name: Config.plugin.name
|
||||
});
|
||||
|
||||
callback(null, adminHeader);
|
||||
};
|
||||
|
||||
})(exports);
|
8
package.json
Normal file → Executable file
8
package.json
Normal file → Executable file
|
@ -21,10 +21,10 @@
|
|||
"url": "https://github.com/Schamper/nodebb-plugin-poll/issues"
|
||||
},
|
||||
"dependencies": {
|
||||
"async": "~0.2.9",
|
||||
"string": "~1.8.0",
|
||||
"xregexp": "~2.0.0",
|
||||
"cron": "~1.0.4"
|
||||
"async": "~1.5.0",
|
||||
"string": "~3.3.1",
|
||||
"xregexp": "~3.0.0",
|
||||
"cron": "~1.0.9"
|
||||
},
|
||||
"nbbpm": {
|
||||
"compatibility": "^0.7.0 || ^0.8.0 || ^0.9.0"
|
||||
|
|
20
plugin.json
Normal file → Executable file
20
plugin.json
Normal file → Executable file
|
@ -3,10 +3,11 @@
|
|||
"name": "Poll",
|
||||
"description": "NodeBB Poll Plugin",
|
||||
"url": "https://github.com/Schamper/nodebb-plugin-poll",
|
||||
"library": "./library.js",
|
||||
"library": "library.js",
|
||||
"hooks": [
|
||||
{ "hook": "static:app.load", "method": "init.load" },
|
||||
{ "hook": "filter:admin.header.build", "method": "init.admin.addNavigation" },
|
||||
{ "hook": "static:app.load", "method": "load" },
|
||||
{ "hook": "filter:admin.header.build", "method": "addAdminNavigation" },
|
||||
{ "hook": "filter:parse.raw", "method": "hooks.filter.parseRaw", "priority": 1 },
|
||||
{ "hook": "filter:post.save", "method": "hooks.filter.postSave" },
|
||||
{ "hook": "filter:post.getPosts", "method": "hooks.filter.getPosts" },
|
||||
{ "hook": "action:post.delete", "method": "hooks.action.postDelete" },
|
||||
|
@ -15,20 +16,23 @@
|
|||
{ "hook": "action:topic.restore", "method": "hooks.action.topicRestore" }
|
||||
],
|
||||
"staticDirs": {
|
||||
"public": "./public"
|
||||
"public": "public"
|
||||
},
|
||||
"less": [
|
||||
"public/less/style.less",
|
||||
"public/less/vendor/bootstrap-datetimepicker.less"
|
||||
],
|
||||
"scripts": [
|
||||
"public/js/admin.js",
|
||||
"public/js/poll/main.js",
|
||||
"public/js/poll/serializer.js",
|
||||
"public/js/poll/sockets.js",
|
||||
"public/js/poll/view.js",
|
||||
"public/js/poll/creator.js",
|
||||
"public/js/vendor/moment.min.js",
|
||||
"public/js/vendor/bootstrap-datetimepicker.min.js"
|
||||
"public/js/vendor/bootstrap-datetimepicker.min.js",
|
||||
"public/js/vendor/jquery.deserialize.min.js",
|
||||
"public/js/vendor/jquery.serialize-object.min.js"
|
||||
],
|
||||
"templates": "./templates",
|
||||
"languages": "public/language"
|
||||
"templates": "templates",
|
||||
"languages": "languages"
|
||||
}
|
||||
|
|
42
public/js/admin.js
Executable file
42
public/js/admin.js
Executable file
|
@ -0,0 +1,42 @@
|
|||
'use strict';
|
||||
/* globals $, app, socket, define, bootbox */
|
||||
|
||||
define('admin/plugins/poll', ['settings'], function(Settings) {
|
||||
var wrapper;
|
||||
|
||||
var ACP = {};
|
||||
|
||||
ACP.init = function() {
|
||||
wrapper = $('.poll-settings');
|
||||
|
||||
Settings.sync('poll', wrapper);
|
||||
|
||||
$('#save').on('click', function() {
|
||||
save();
|
||||
});
|
||||
|
||||
$('#reset').click(function() {
|
||||
reset();
|
||||
});
|
||||
};
|
||||
|
||||
function save() {
|
||||
Settings.persist('poll', wrapper, function() {
|
||||
socket.emit('admin.plugins.poll.sync');
|
||||
});
|
||||
}
|
||||
|
||||
function reset() {
|
||||
bootbox.confirm('Are you sure you wish to reset the settings?', function(sure) {
|
||||
if (sure) {
|
||||
socket.emit('admin.plugins.poll.getDefaults', null, function (err, data) {
|
||||
Settings.set('poll', data, wrapper, function(){
|
||||
socket.emit('admin.plugins.poll.sync');
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return ACP;
|
||||
});
|
284
public/js/poll/creator.js
Normal file → Executable file
284
public/js/poll/creator.js
Normal file → Executable file
|
@ -1,139 +1,185 @@
|
|||
"use strict";
|
||||
/* globals $, app, bootbox, define */
|
||||
|
||||
(function(Poll) {
|
||||
var S,
|
||||
settings = {
|
||||
max: {
|
||||
test: function(value) {
|
||||
return !isNaN(value);
|
||||
}
|
||||
},
|
||||
title: {
|
||||
test: function(value) {
|
||||
return value.length > 0;
|
||||
}
|
||||
},
|
||||
end: {
|
||||
test: function(value) {
|
||||
return moment(value).isValid();
|
||||
},
|
||||
parse: function(value) {
|
||||
return moment(value).valueOf();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
//Todo: load settings (like option limit) from server
|
||||
var Creator = {};
|
||||
var config;
|
||||
|
||||
function initialise() {
|
||||
require(['composer', 'string'], function(composer, String) {
|
||||
S = String;
|
||||
composer.addButton('fa fa-bar-chart-o', Poll.creator.show);
|
||||
function init() {
|
||||
require(['composer'], function(composer) {
|
||||
composer.addButton('fa fa-bar-chart-o', Creator.show);
|
||||
});
|
||||
}
|
||||
|
||||
initialise();
|
||||
Creator.show = function(textarea) {
|
||||
Poll.sockets.getConfig(null, function(err, c) {
|
||||
config = c;
|
||||
|
||||
Poll.creator = {
|
||||
show: function(textarea) {
|
||||
window.templates.parse('poll/creator', {}, function(html) {
|
||||
require(['translator'], function(translator) {
|
||||
translator.translate(html, config.userLang, function(html) {
|
||||
bootbox.dialog({
|
||||
title: 'Create a poll',
|
||||
message: html,
|
||||
className: 'poll-creator',
|
||||
buttons: {
|
||||
cancel: {
|
||||
label: 'Cancel',
|
||||
className: 'btn-default',
|
||||
callback: function(e) {
|
||||
return Poll.creator.cancel(e, textarea);
|
||||
}
|
||||
},
|
||||
save: {
|
||||
label: 'Done',
|
||||
className: 'btn-primary',
|
||||
callback: function(e) {
|
||||
return Poll.creator.save(e, textarea);
|
||||
}
|
||||
}
|
||||
}
|
||||
}).find('#pollInputEnd').datetimepicker({
|
||||
useSeconds: false,
|
||||
useCurrent: false,
|
||||
minDate: new Date(),
|
||||
icons: {
|
||||
time: "fa fa-clock-o",
|
||||
date: "fa fa-calendar",
|
||||
up: "fa fa-arrow-up",
|
||||
down: "fa fa-arrow-down"
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
},
|
||||
cancel: function(e, textarea) {
|
||||
return true;
|
||||
},
|
||||
save: function(e, textarea) {
|
||||
var modal = $(e.currentTarget).parents('.bootbox'),
|
||||
errorBox = modal.find('#pollErrorBox');
|
||||
var poll = {};
|
||||
|
||||
errorBox.addClass('hidden').html('');
|
||||
// If there's already a poll in the post, serialize it for editing
|
||||
if (Poll.serializer.canSerialize(textarea.value)) {
|
||||
poll = Poll.serializer.serialize(textarea.value, config);
|
||||
|
||||
var result = Creator.parse(modal);
|
||||
if (result.err) {
|
||||
return Poll.creator.error(errorBox, result.err);
|
||||
} else {
|
||||
if (textarea.value.charAt(textarea.value.length - 1) !== '\n') {
|
||||
result.markup = '\n' + result.markup;
|
||||
if (poll.settings.end === 0) {
|
||||
delete poll.settings.end;
|
||||
} else {
|
||||
poll.settings.end = parseInt(poll.settings.end, 10);
|
||||
}
|
||||
textarea.value += result.markup;
|
||||
return true;
|
||||
}
|
||||
},
|
||||
error: function(errorBox, message) {
|
||||
errorBox.removeClass('hidden');
|
||||
errorBox.append(message + '<br>');
|
||||
return false;
|
||||
}
|
||||
|
||||
showModal(poll, config, textarea);
|
||||
});
|
||||
};
|
||||
|
||||
var Creator = {
|
||||
parse: function(modal) {
|
||||
var options = S(modal.find('#pollInputOptions').val()).stripTags().s.split('\n').filter(function(o) {
|
||||
return o.length == 0 ? false : o;
|
||||
}),
|
||||
settingMarkup = '',
|
||||
result = {
|
||||
err: null,
|
||||
markup: null
|
||||
};
|
||||
|
||||
if (options.length == 0) {
|
||||
result.err = 'Create at least one option!';
|
||||
return result;
|
||||
}
|
||||
|
||||
for (var s in settings) {
|
||||
if (settings.hasOwnProperty(s)) {
|
||||
var value = S(modal.find('[data-poll-setting="' + s + '"]').val()).stripTags().trim().s;
|
||||
if (value.length > 0 && settings[s].test(value)) {
|
||||
if (typeof settings[s].parse === 'function') {
|
||||
value = settings[s].parse(value);
|
||||
function showModal(poll, config, textarea) {
|
||||
app.parseAndTranslate('poll/creator', { poll: poll, config: config }, function(html) {
|
||||
// Initialise modal
|
||||
var modal = bootbox.dialog({
|
||||
title: 'Create a poll',
|
||||
message: html,
|
||||
className: 'poll-creator',
|
||||
buttons: {
|
||||
cancel: {
|
||||
label: 'Cancel',
|
||||
className: 'btn-default',
|
||||
callback: function() {
|
||||
return true
|
||||
}
|
||||
},
|
||||
save: {
|
||||
label: 'Done',
|
||||
className: 'btn-primary',
|
||||
callback: function(e) {
|
||||
return save(e, textarea);
|
||||
}
|
||||
settingMarkup += ' ' + s + '="' + value + '"';
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
result.markup = '[poll' + settingMarkup + ']\n';
|
||||
for (var i = 0, l = options.length; i < l; i++) {
|
||||
result.markup += '- ' + options[i] + '\n';
|
||||
}
|
||||
result.markup += '[/poll]\n';
|
||||
// Add option adder
|
||||
modal.find('#pollAddOption')
|
||||
.off('click')
|
||||
.on('click', function(e) {
|
||||
var el = $(e.currentTarget);
|
||||
var prevOption = el.prev();
|
||||
|
||||
return result;
|
||||
if (config.limits.maxOptions <= el.prevAll('input').length) {
|
||||
clearErrors();
|
||||
return error('You can only create ' + config.limits.maxOptions + ' options.');
|
||||
}
|
||||
|
||||
if (prevOption.val().length != 0) {
|
||||
prevOption.clone().val('').insertBefore(el).focus();
|
||||
}
|
||||
});
|
||||
|
||||
var datetimepicker = modal.find('#pollInputEnd')
|
||||
.datetimepicker({
|
||||
sideBySide: true,
|
||||
showClear: true,
|
||||
useCurrent: true,
|
||||
ignoreReadonly: true,
|
||||
allowInputToggle: true,
|
||||
toolbarPlacement: 'top',
|
||||
minDate: moment().add(5, 'minutes'),
|
||||
icons: {
|
||||
time: "fa fa-clock-o",
|
||||
date: "fa fa-calendar",
|
||||
up: "fa fa-chevron-up",
|
||||
down: "fa fa-chevron-down",
|
||||
previous: 'fa fa-chevron-left',
|
||||
next: 'fa fa-chevron-right',
|
||||
today: 'fa fa-calendar',
|
||||
clear: 'fa fa-trash-o',
|
||||
close: 'fa fa-times'
|
||||
}
|
||||
}).data('DateTimePicker');
|
||||
|
||||
if (poll.settings.end) {
|
||||
datetimepicker.date(moment(poll.settings.end));
|
||||
} else {
|
||||
datetimepicker.clear();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function save(e, textarea) {
|
||||
clearErrors();
|
||||
|
||||
var form = $(e.currentTarget).parents('.bootbox').find('#pollCreator');
|
||||
var obj = form.serializeObject();
|
||||
|
||||
// Let's be nice and at least show an error if there are no options
|
||||
obj.options.filter(function(obj) {
|
||||
return obj.length;
|
||||
});
|
||||
|
||||
if (obj.options.length == 0) {
|
||||
return error('Create at least one option.');
|
||||
}
|
||||
};
|
||||
|
||||
if (obj.settings.end && !moment(obj.settings.end).isValid()) {
|
||||
return error('Please enter a valid date.');
|
||||
} else if (obj.settings.end) {
|
||||
obj.settings.end = moment(obj.settings.end).valueOf();
|
||||
}
|
||||
|
||||
// Anything invalid will be discarded by the serializer
|
||||
var markup = Poll.serializer.deserialize(obj, config);
|
||||
|
||||
// Remove any existing poll markup
|
||||
textarea.value = Poll.serializer.removeMarkup(textarea.value);
|
||||
|
||||
// Insert the poll markup at the bottom
|
||||
if (textarea.value.charAt(textarea.value.length - 1) !== '\n') {
|
||||
markup = '\n' + markup;
|
||||
}
|
||||
|
||||
textarea.value += markup;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
function error(message) {
|
||||
var errorBox = $('#pollErrorBox');
|
||||
|
||||
errorBox.removeClass('hidden');
|
||||
errorBox.append(message + '<br>');
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
function clearErrors() {
|
||||
$('#pollErrorBox').addClass('hidden').html('');
|
||||
}
|
||||
|
||||
function dumbifyObject(obj) {
|
||||
var result = {};
|
||||
|
||||
for (var key in obj) {
|
||||
if (obj.hasOwnProperty(key)) {
|
||||
var val = obj[key];
|
||||
|
||||
if (jQuery.isPlainObject(val)) {
|
||||
var obj1 = dumbifyObject(val);
|
||||
for (var k1 in obj1) {
|
||||
if (obj1.hasOwnProperty(k1)) {
|
||||
result[key + '.' + k1] = obj1[k1];
|
||||
}
|
||||
}
|
||||
} else {
|
||||
result[key] = val;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
Poll.creator = Creator;
|
||||
|
||||
init();
|
||||
|
||||
})(window.Poll);
|
42
public/js/poll/main.js
Normal file → Executable file
42
public/js/poll/main.js
Normal file → Executable file
|
@ -1,20 +1,26 @@
|
|||
"use strict";
|
||||
|
||||
var Poll = {};
|
||||
|
||||
(function() {
|
||||
window.Poll = {
|
||||
load: function(pollid) {
|
||||
Poll.sockets.emit.load(pollid, function(err, poll) {
|
||||
if (!err) {
|
||||
Poll.view.init(poll, function(pollView) {
|
||||
if (parseInt(poll.info.deleted, 10) === 1 || parseInt(poll.info.ended, 10) === 1) {
|
||||
Poll.view.showMessage({
|
||||
title: 'Voting unavailable',
|
||||
content: 'This poll has ended or has been marked as deleted. You can still view the results.'
|
||||
}, pollView);
|
||||
}
|
||||
});
|
||||
} else if (err.message != 'Not logged in') {
|
||||
app.alertError('Something went wrong while getting the poll!');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Poll.load = function(pollId) {
|
||||
console.log(pollId);
|
||||
|
||||
//Poll.sockets.emit.load(pollid, function(err, poll) {
|
||||
// if (!err) {
|
||||
// Poll.view.init(poll, function(pollView) {
|
||||
// if (parseInt(poll.info.deleted, 10) === 1 || parseInt(poll.info.ended, 10) === 1) {
|
||||
// Poll.view.showMessage({
|
||||
// title: 'Voting unavailable',
|
||||
// content: 'This poll has ended or has been marked as deleted. You can still view the results.'
|
||||
// }, pollView);
|
||||
// }
|
||||
// });
|
||||
// } else if (err.message != 'Not logged in') {
|
||||
// app.alertError('Something went wrong while getting the poll!');
|
||||
// }
|
||||
//});
|
||||
};
|
||||
})();
|
||||
|
||||
})();
|
150
public/js/poll/serializer.js
Normal file
150
public/js/poll/serializer.js
Normal file
|
@ -0,0 +1,150 @@
|
|||
"use strict";
|
||||
/* globals require */
|
||||
|
||||
(function(module) {
|
||||
|
||||
var XRegExp, S;
|
||||
var Serializer = {};
|
||||
|
||||
if ('undefined' === typeof window) {
|
||||
XRegExp = require('xregexp').XRegExp;
|
||||
} else {
|
||||
XRegExp = window.XRegExp;
|
||||
require(['string'], function(string){ S = string });
|
||||
}
|
||||
|
||||
var pollRegex = XRegExp('(?:(?:\\[poll(?<settings>.*?)\\])\n(?<content>(?:-.+?\n)+)(?:\\[\/poll\\]))', 'g');
|
||||
var settingsRegex = XRegExp('(?<key>.+?)="(?<value>.+?)"', 'g');
|
||||
var settingsValidators = {
|
||||
title: {
|
||||
test: function (value) {
|
||||
return value.length > 0;
|
||||
},
|
||||
parse: function(value) {
|
||||
return S(value).stripTags().trim().s;
|
||||
}
|
||||
},
|
||||
maxvotes: {
|
||||
test: function (value) {
|
||||
return !isNaN(value);
|
||||
},
|
||||
parse: function(value) {
|
||||
return parseInt(value, 10);
|
||||
}
|
||||
},
|
||||
end: {
|
||||
test: function (value) {
|
||||
return (!isNaN(value) && parseInt(value, 10) > Date.now());
|
||||
},
|
||||
parse: function(value) {
|
||||
return parseInt(value, 10);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
Serializer.canSerialize = function(post) {
|
||||
return XRegExp.exec(post, pollRegex) !== null;
|
||||
};
|
||||
|
||||
Serializer.removeMarkup = function(content, replace) {
|
||||
return XRegExp.replace(content, pollRegex, replace || '');
|
||||
};
|
||||
|
||||
Serializer.serialize = function(post, config) {
|
||||
var match = XRegExp.exec(post, pollRegex);
|
||||
|
||||
return {
|
||||
options: serializeOptions(match.content, config),
|
||||
settings: serializeSettings(match.settings, config)
|
||||
};
|
||||
};
|
||||
|
||||
Serializer.deserialize = function(poll, config) {
|
||||
var options = deserializeOptions(poll.options, config);
|
||||
var settings = deserializeSettings(poll.settings, config);
|
||||
|
||||
return '[poll' + settings +']\n' + options + '\n[/poll]';
|
||||
};
|
||||
|
||||
function serializeOptions(raw, config) {
|
||||
var pollOptions = [];
|
||||
var rawOptions = S(raw).stripTags().s.split('\n');
|
||||
var maxOptions = parseInt(config.limits.maxOptions, 10);
|
||||
|
||||
rawOptions.forEach(function(option) {
|
||||
if (option.length) {
|
||||
option = S(option.split('-')[1]).trim().s;
|
||||
|
||||
if (option.length) {
|
||||
pollOptions.push(option);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (pollOptions.length > maxOptions) {
|
||||
pollOptions = pollOptions.slice(0, maxOptions - 1);
|
||||
}
|
||||
|
||||
return pollOptions;
|
||||
}
|
||||
|
||||
function deserializeOptions(options, config) {
|
||||
var maxOptions = config.limits.maxOptions;
|
||||
|
||||
options = options.map(function (option) {
|
||||
return S(option).stripTags().trim().s;
|
||||
}).filter(function (option) {
|
||||
return option.length;
|
||||
});
|
||||
|
||||
if (options.length > maxOptions) {
|
||||
options = options.slice(0, maxOptions - 1);
|
||||
}
|
||||
|
||||
return options.length ? '- ' + options.join('\n- ') : '';
|
||||
}
|
||||
|
||||
function serializeSettings(raw, config) {
|
||||
return XRegExp.forEach(S(raw).stripTags().s, settingsRegex, function(match) {
|
||||
var key = S(match.key).trim().s;
|
||||
var value = S(match.value).trim().s;
|
||||
|
||||
if (key.length && value.length && settingsValidators.hasOwnProperty(key)) {
|
||||
if (settingsValidators[key].test(value)) {
|
||||
this[key] = settingsValidators[key].parse(value);
|
||||
}
|
||||
}
|
||||
}, config.defaults);
|
||||
}
|
||||
|
||||
function deserializeSettings(settings, config) {
|
||||
var pollSettings = config.defaults;
|
||||
var deserialized = '';
|
||||
|
||||
for (var k in settings) {
|
||||
if (settings.hasOwnProperty(k)) {
|
||||
var key = S(k).stripTags().trim().s;
|
||||
var value = S(settings[k]).stripTags().trim().s;
|
||||
|
||||
if (key.length && value.length && settingsValidators.hasOwnProperty(key)) {
|
||||
if (settingsValidators[key].test(value)) {
|
||||
deserialized += ' ' + key + '="' + value + '"';
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return deserialized;
|
||||
}
|
||||
|
||||
module.exports = Serializer;
|
||||
|
||||
if ('undefined' !== typeof window) {
|
||||
Poll.serializer = module.exports;
|
||||
}
|
||||
|
||||
})('undefined' === typeof module ? {
|
||||
module: {
|
||||
exports: {}
|
||||
}
|
||||
} : module);
|
68
public/js/poll/sockets.js
Normal file → Executable file
68
public/js/poll/sockets.js
Normal file → Executable file
|
@ -1,47 +1,39 @@
|
|||
"use strict";
|
||||
/* globals socket */
|
||||
|
||||
(function(Poll) {
|
||||
var Sockets = {
|
||||
events: {
|
||||
load: 'plugins.poll.load',
|
||||
vote: 'plugins.poll.vote',
|
||||
details: 'plugins.poll.optionDetails',
|
||||
onvotechange: 'event:poll.votechange'
|
||||
},
|
||||
on: {
|
||||
votechange: {
|
||||
register: function() {
|
||||
if (socket.listeners(Sockets.events.onvotechange).length === 0) {
|
||||
socket.on(Sockets.events.onvotechange, this.handle);
|
||||
}
|
||||
},
|
||||
handle: function(data) {
|
||||
Poll.view.updateResults(data, $('#poll-id-' + data.pollid));
|
||||
}
|
||||
}
|
||||
},
|
||||
emit: {
|
||||
load: function(pollid, callback) {
|
||||
socket.emit(Sockets.events.load, { pollid: pollid }, callback);
|
||||
},
|
||||
vote: function(voteData, callback) {
|
||||
socket.emit(Sockets.events.vote, voteData, callback);
|
||||
},
|
||||
getDetails: function(data, callback) {
|
||||
socket.emit(Sockets.events.details, data, callback);
|
||||
}
|
||||
}
|
||||
var messages = {
|
||||
load: 'plugins.poll.load',
|
||||
vote: 'plugins.poll.vote',
|
||||
getDetails: 'plugins.poll.getOptionDetails',
|
||||
getConfig: 'plugins.poll.getConfig'
|
||||
};
|
||||
|
||||
function initialise() {
|
||||
for (var e in Sockets.on) {
|
||||
if (Sockets.on.hasOwnProperty(e)) {
|
||||
Sockets.on[e].register();
|
||||
var handlers = [{
|
||||
event: 'event:poll.votechange',
|
||||
handle: function(data) {
|
||||
Poll.view.updateResults(data, $('#poll-id-' + data.pollid));
|
||||
}
|
||||
}];
|
||||
|
||||
function init() {
|
||||
handlers.forEach(function(handler) {
|
||||
if (socket.listeners(handler.event).length === 0) {
|
||||
socket.on(handler.event, handler.handle);
|
||||
}
|
||||
});
|
||||
|
||||
for (var m in messages) {
|
||||
if (messages.hasOwnProperty(m)) {
|
||||
Poll.sockets[m] = function(data, callback) {
|
||||
socket.emit(messages[m], data, callback);
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
initialise();
|
||||
Poll.sockets = {};
|
||||
|
||||
init();
|
||||
|
||||
Poll.sockets = {
|
||||
emit: Sockets.emit
|
||||
};
|
||||
})(window.Poll);
|
||||
|
|
106
public/js/vendor/bootstrap-datetimepicker.min.js
vendored
Normal file → Executable file
106
public/js/vendor/bootstrap-datetimepicker.min.js
vendored
Normal file → Executable file
File diff suppressed because one or more lines are too long
8
public/js/vendor/jquery.serialize-object.min.js
vendored
Normal file
8
public/js/vendor/jquery.serialize-object.min.js
vendored
Normal file
|
@ -0,0 +1,8 @@
|
|||
/**
|
||||
* jQuery serializeObject
|
||||
* @copyright 2014, macek <paulmacek@gmail.com>
|
||||
* @link https://github.com/macek/jquery-serialize-object
|
||||
* @license BSD
|
||||
* @version 2.5.0
|
||||
*/
|
||||
!function(e,i){i(e,e.jQuery||e.Zepto||e.ender||e.$)}(this,function(e,i){function r(e,r){function n(e,i,r){return e[i]=r,e}function a(e,i){for(var r,a=e.match(t.key);void 0!==(r=a.pop());)if(t.push.test(r)){var u=s(e.replace(/\[\]$/,""));i=n([],u,i)}else t.fixed.test(r)?i=n([],r,i):t.named.test(r)&&(i=n({},r,i));return i}function s(e){return void 0===h[e]&&(h[e]=0),h[e]++}function u(e){switch(i('[name="'+e.name+'"]',r).attr("type")){case"checkbox":return"on"===e.value?!0:e.value;default:return e.value}}function f(i){if(!t.validate.test(i.name))return this;var r=a(i.name,u(i));return l=e.extend(!0,l,r),this}function d(i){if(!e.isArray(i))throw new Error("formSerializer.addPairs expects an Array");for(var r=0,t=i.length;t>r;r++)this.addPair(i[r]);return this}function o(){return l}function c(){return JSON.stringify(o())}var l={},h={};this.addPair=f,this.addPairs=d,this.serialize=o,this.serializeJSON=c}var t={validate:/^[a-z_][a-z0-9_]*(?:\[(?:\d*|[a-z0-9_]+)\])*$/i,key:/[a-z0-9_]+|(?=\[\])/gi,push:/^$/,fixed:/^\d+$/,named:/^[a-z0-9_]+$/i};return r.patterns=t,r.serializeObject=function(){return new r(i,this).addPairs(this.serializeArray()).serialize()},r.serializeJSON=function(){return new r(i,this).addPairs(this.serializeArray()).serializeJSON()},"undefined"!=typeof i.fn&&(i.fn.serializeObject=r.serializeObject,i.fn.serializeJSON=r.serializeJSON),e.FormSerializer=r,r});
|
5
public/js/vendor/moment.min.js
vendored
Normal file → Executable file
5
public/js/vendor/moment.min.js
vendored
Normal file → Executable file
File diff suppressed because one or more lines are too long
502
public/less/vendor/bootstrap-datetimepicker.less
vendored
Normal file → Executable file
502
public/less/vendor/bootstrap-datetimepicker.less
vendored
Normal file → Executable file
|
@ -1,254 +1,352 @@
|
|||
/*!
|
||||
* Datetimepicker for Bootstrap v3
|
||||
* Datetimepicker for Bootstrap 3
|
||||
* version : 4.17.37
|
||||
* https://github.com/Eonasdan/bootstrap-datetimepicker/
|
||||
*/
|
||||
@bs-datetimepicker-timepicker-font-size: 1.2em;
|
||||
@bs-datetimepicker-active-bg: @btn-primary-bg;
|
||||
@bs-datetimepicker-active-color: @btn-primary-color;
|
||||
@bs-datetimepicker-border-radius: @border-radius-base;
|
||||
@bs-datetimepicker-btn-hover-bg: @gray-lighter;
|
||||
@bs-datetimepicker-disabled-color: @gray-light;
|
||||
@bs-datetimepicker-alternate-color: @gray-light;
|
||||
@bs-datetimepicker-secondary-border-color: #ccc;
|
||||
@bs-datetimepicker-secondary-border-color-rgba: rgba(0, 0, 0, 0.2);
|
||||
@bs-datetimepicker-primary-border-color: white;
|
||||
@bs-datetimepicker-text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.25);
|
||||
|
||||
.bootstrap-datetimepicker-widget {
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 250px;
|
||||
padding: 4px;
|
||||
margin-top: 1px;
|
||||
z-index: 99999 !important;
|
||||
border-radius: 4px;
|
||||
list-style: none;
|
||||
|
||||
&.timepicker-sbs {
|
||||
width: 600px;
|
||||
}
|
||||
&.dropdown-menu {
|
||||
margin: 2px 0;
|
||||
padding: 4px;
|
||||
width: 19em;
|
||||
|
||||
&.bottom {
|
||||
&:before {
|
||||
&.timepicker-sbs {
|
||||
@media (min-width: @screen-sm-min) {
|
||||
width: 38em;
|
||||
}
|
||||
|
||||
@media (min-width: @screen-md-min) {
|
||||
width: 38em;
|
||||
}
|
||||
|
||||
@media (min-width: @screen-lg-min) {
|
||||
width: 38em;
|
||||
}
|
||||
}
|
||||
|
||||
&:before, &:after {
|
||||
content: '';
|
||||
display: inline-block;
|
||||
border-left: 7px solid transparent;
|
||||
border-right: 7px solid transparent;
|
||||
border-bottom: 7px solid #ccc;
|
||||
border-bottom-color: rgba(0,0,0,.2);
|
||||
position: absolute;
|
||||
top: -7px;
|
||||
left: 7px;
|
||||
}
|
||||
|
||||
&:after {
|
||||
content: '';
|
||||
display: inline-block;
|
||||
border-left: 6px solid transparent;
|
||||
border-right: 6px solid transparent;
|
||||
border-bottom: 6px solid white;
|
||||
position: absolute;
|
||||
top: -6px;
|
||||
left: 8px;
|
||||
&.bottom {
|
||||
&:before {
|
||||
border-left: 7px solid transparent;
|
||||
border-right: 7px solid transparent;
|
||||
border-bottom: 7px solid @bs-datetimepicker-secondary-border-color;
|
||||
border-bottom-color: @bs-datetimepicker-secondary-border-color-rgba;
|
||||
top: -7px;
|
||||
left: 7px;
|
||||
}
|
||||
|
||||
&:after {
|
||||
border-left: 6px solid transparent;
|
||||
border-right: 6px solid transparent;
|
||||
border-bottom: 6px solid @bs-datetimepicker-primary-border-color;
|
||||
top: -6px;
|
||||
left: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
&.top {
|
||||
&:before {
|
||||
border-left: 7px solid transparent;
|
||||
border-right: 7px solid transparent;
|
||||
border-top: 7px solid @bs-datetimepicker-secondary-border-color;
|
||||
border-top-color: @bs-datetimepicker-secondary-border-color-rgba;
|
||||
bottom: -7px;
|
||||
left: 6px;
|
||||
}
|
||||
|
||||
&:after {
|
||||
border-left: 6px solid transparent;
|
||||
border-right: 6px solid transparent;
|
||||
border-top: 6px solid @bs-datetimepicker-primary-border-color;
|
||||
bottom: -6px;
|
||||
left: 7px;
|
||||
}
|
||||
}
|
||||
|
||||
&.pull-right {
|
||||
&:before {
|
||||
left: auto;
|
||||
right: 6px;
|
||||
}
|
||||
|
||||
&:after {
|
||||
left: auto;
|
||||
right: 7px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.top {
|
||||
&:before {
|
||||
content: '';
|
||||
display: inline-block;
|
||||
border-left: 7px solid transparent;
|
||||
border-right: 7px solid transparent;
|
||||
border-top: 7px solid #ccc;
|
||||
border-top-color: rgba(0,0,0,.2);
|
||||
position: absolute;
|
||||
bottom: -7px;
|
||||
left: 6px;
|
||||
}
|
||||
|
||||
&:after {
|
||||
content: '';
|
||||
display: inline-block;
|
||||
border-left: 6px solid transparent;
|
||||
border-right: 6px solid transparent;
|
||||
border-top: 6px solid white;
|
||||
position: absolute;
|
||||
bottom: -6px;
|
||||
left: 7px;
|
||||
}
|
||||
}
|
||||
|
||||
& .dow {
|
||||
width: 14.2857%;
|
||||
}
|
||||
|
||||
&.pull-right {
|
||||
&:before {
|
||||
left: auto;
|
||||
right: 6px;
|
||||
}
|
||||
|
||||
&:after {
|
||||
left: auto;
|
||||
right: 7px;
|
||||
}
|
||||
}
|
||||
|
||||
>ul {
|
||||
list-style-type: none;
|
||||
.list-unstyled {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
a[data-action] {
|
||||
padding: 6px 0;
|
||||
}
|
||||
|
||||
a[data-action]:active {
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.timepicker-hour, .timepicker-minute, .timepicker-second {
|
||||
width: 100%;
|
||||
width: 54px;
|
||||
font-weight: bold;
|
||||
font-size: 1.2em;
|
||||
}
|
||||
|
||||
table[data-hour-format="12"] .separator {
|
||||
width: 4px;
|
||||
padding: 0;
|
||||
font-size: @bs-datetimepicker-timepicker-font-size;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.datepicker > div {
|
||||
display: none;
|
||||
button[data-action] {
|
||||
padding: 6px;
|
||||
}
|
||||
|
||||
.btn[data-action="incrementHours"]::after {
|
||||
.sr-only();
|
||||
content: "Increment Hours";
|
||||
}
|
||||
|
||||
.btn[data-action="incrementMinutes"]::after {
|
||||
.sr-only();
|
||||
content: "Increment Minutes";
|
||||
}
|
||||
|
||||
.btn[data-action="decrementHours"]::after {
|
||||
.sr-only();
|
||||
content: "Decrement Hours";
|
||||
}
|
||||
|
||||
.btn[data-action="decrementMinutes"]::after {
|
||||
.sr-only();
|
||||
content: "Decrement Minutes";
|
||||
}
|
||||
|
||||
.btn[data-action="showHours"]::after {
|
||||
.sr-only();
|
||||
content: "Show Hours";
|
||||
}
|
||||
|
||||
.btn[data-action="showMinutes"]::after {
|
||||
.sr-only();
|
||||
content: "Show Minutes";
|
||||
}
|
||||
|
||||
.btn[data-action="togglePeriod"]::after {
|
||||
.sr-only();
|
||||
content: "Toggle AM/PM";
|
||||
}
|
||||
|
||||
.btn[data-action="clear"]::after {
|
||||
.sr-only();
|
||||
content: "Clear the picker";
|
||||
}
|
||||
|
||||
.btn[data-action="today"]::after {
|
||||
.sr-only();
|
||||
content: "Set the date to today";
|
||||
}
|
||||
|
||||
.picker-switch {
|
||||
text-align: center;
|
||||
|
||||
&::after {
|
||||
.sr-only();
|
||||
content: "Toggle Date and Time Screens";
|
||||
}
|
||||
|
||||
td {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
height: auto;
|
||||
width: auto;
|
||||
line-height: inherit;
|
||||
|
||||
span {
|
||||
line-height: 2.5;
|
||||
height: 2.5em;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
table {
|
||||
width: 100%;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
td,
|
||||
th {
|
||||
text-align: center;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
td {
|
||||
&.day:hover,
|
||||
&.hour:hover,
|
||||
&.minute:hover,
|
||||
&.second:hover {
|
||||
background: @gray-lighter;
|
||||
cursor: pointer;
|
||||
& td,
|
||||
& th {
|
||||
text-align: center;
|
||||
border-radius: @bs-datetimepicker-border-radius;
|
||||
}
|
||||
|
||||
&.old,
|
||||
&.new {
|
||||
color: @gray-light;
|
||||
}
|
||||
& th {
|
||||
height: 20px;
|
||||
line-height: 20px;
|
||||
width: 20px;
|
||||
|
||||
&.today {
|
||||
position: relative;
|
||||
|
||||
&:before {
|
||||
content: '';
|
||||
display: inline-block;
|
||||
border-left: 7px solid transparent;
|
||||
border-bottom: 7px solid @btn-primary-bg;
|
||||
border-top-color: rgba(0, 0, 0, 0.2);
|
||||
position: absolute;
|
||||
bottom: 4px;
|
||||
right: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
&.active,
|
||||
&.active:hover {
|
||||
background-color: @btn-primary-bg;
|
||||
color: #fff;
|
||||
text-shadow: 0 -1px 0 rgba(0,0,0,.25);
|
||||
}
|
||||
|
||||
&.active.today:before {
|
||||
border-bottom-color: #fff;
|
||||
}
|
||||
|
||||
&.disabled,
|
||||
&.disabled:hover {
|
||||
background: none;
|
||||
color: @gray-light;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
span {
|
||||
display: block;
|
||||
width: 47px;
|
||||
height: 54px;
|
||||
line-height: 54px;
|
||||
float: left;
|
||||
margin: 2px;
|
||||
cursor: pointer;
|
||||
border-radius: 4px;
|
||||
|
||||
&:hover {
|
||||
background: @gray-lighter;
|
||||
}
|
||||
|
||||
&.active {
|
||||
background-color: @btn-primary-bg;
|
||||
color: #fff;
|
||||
text-shadow: 0 -1px 0 rgba(0,0,0,.25);
|
||||
}
|
||||
|
||||
&.old {
|
||||
color: @gray-light;
|
||||
&.picker-switch {
|
||||
width: 145px;
|
||||
}
|
||||
|
||||
&.disabled,
|
||||
&.disabled:hover {
|
||||
background: none;
|
||||
color: @gray-light;
|
||||
color: @bs-datetimepicker-disabled-color;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
&.prev::after {
|
||||
.sr-only();
|
||||
content: "Previous Month";
|
||||
}
|
||||
|
||||
&.next::after {
|
||||
.sr-only();
|
||||
content: "Next Month";
|
||||
}
|
||||
}
|
||||
|
||||
& thead tr:first-child th {
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
background: @bs-datetimepicker-btn-hover-bg;
|
||||
}
|
||||
}
|
||||
|
||||
& td {
|
||||
height: 54px;
|
||||
line-height: 54px;
|
||||
width: 54px;
|
||||
|
||||
&.cw {
|
||||
font-size: .8em;
|
||||
height: 20px;
|
||||
line-height: 20px;
|
||||
color: @bs-datetimepicker-alternate-color;
|
||||
}
|
||||
|
||||
&.day {
|
||||
height: 20px;
|
||||
line-height: 20px;
|
||||
width: 20px;
|
||||
}
|
||||
|
||||
&.day:hover,
|
||||
&.hour:hover,
|
||||
&.minute:hover,
|
||||
&.second:hover {
|
||||
background: @bs-datetimepicker-btn-hover-bg;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
&.old,
|
||||
&.new {
|
||||
color: @bs-datetimepicker-alternate-color;
|
||||
}
|
||||
|
||||
&.today {
|
||||
position: relative;
|
||||
|
||||
&:before {
|
||||
content: '';
|
||||
display: inline-block;
|
||||
border: solid transparent;
|
||||
border-width: 0 0 7px 7px;
|
||||
border-bottom-color: @bs-datetimepicker-active-bg;
|
||||
border-top-color: @bs-datetimepicker-secondary-border-color-rgba;
|
||||
position: absolute;
|
||||
bottom: 4px;
|
||||
right: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
&.active,
|
||||
&.active:hover {
|
||||
background-color: @bs-datetimepicker-active-bg;
|
||||
color: @bs-datetimepicker-active-color;
|
||||
text-shadow: @bs-datetimepicker-text-shadow;
|
||||
}
|
||||
|
||||
&.active.today:before {
|
||||
border-bottom-color: #fff;
|
||||
}
|
||||
|
||||
&.disabled,
|
||||
&.disabled:hover {
|
||||
background: none;
|
||||
color: @bs-datetimepicker-disabled-color;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
span {
|
||||
display: inline-block;
|
||||
width: 54px;
|
||||
height: 54px;
|
||||
line-height: 54px;
|
||||
margin: 2px 1.5px;
|
||||
cursor: pointer;
|
||||
border-radius: @bs-datetimepicker-border-radius;
|
||||
|
||||
&:hover {
|
||||
background: @bs-datetimepicker-btn-hover-bg;
|
||||
}
|
||||
|
||||
&.active {
|
||||
background-color: @bs-datetimepicker-active-bg;
|
||||
color: @bs-datetimepicker-active-color;
|
||||
text-shadow: @bs-datetimepicker-text-shadow;
|
||||
}
|
||||
|
||||
&.old {
|
||||
color: @bs-datetimepicker-alternate-color;
|
||||
}
|
||||
|
||||
&.disabled,
|
||||
&.disabled:hover {
|
||||
background: none;
|
||||
color: @bs-datetimepicker-disabled-color;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
th {
|
||||
&.switch {
|
||||
width: 145px;
|
||||
}
|
||||
|
||||
&.next,
|
||||
&.prev {
|
||||
font-size: @font-size-base * 1.5;
|
||||
}
|
||||
|
||||
&.disabled,
|
||||
&.disabled:hover {
|
||||
background: none;
|
||||
color: @gray-light;
|
||||
cursor: not-allowed;
|
||||
&.usetwentyfour {
|
||||
td.hour {
|
||||
height: 27px;
|
||||
line-height: 27px;
|
||||
}
|
||||
}
|
||||
|
||||
thead tr:first-child th {
|
||||
&.wider {
|
||||
width: 21em;
|
||||
}
|
||||
|
||||
& .datepicker-decades .decade {
|
||||
line-height: 1.8em !important;
|
||||
}
|
||||
}
|
||||
|
||||
.input-group.date {
|
||||
& .input-group-addon {
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
background: @gray-lighter;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.input-group {
|
||||
&.date {
|
||||
.input-group-addon span {
|
||||
display: block;
|
||||
cursor: pointer;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.bootstrap-datetimepicker-widget.left-oriented {
|
||||
&:before {
|
||||
left: auto;
|
||||
right: 6px;
|
||||
}
|
||||
|
||||
&:after {
|
||||
left: auto;
|
||||
right: 7px;
|
||||
}
|
||||
}
|
||||
|
||||
.bootstrap-datetimepicker-widget ul.list-unstyled li div.timepicker div.timepicker-picker table.table-condensed tbody > tr > td {
|
||||
padding: 0px !important;
|
||||
}
|
||||
|
|
59
templates/admin/plugins/poll.tpl
Executable file
59
templates/admin/plugins/poll.tpl
Executable file
|
@ -0,0 +1,59 @@
|
|||
<div class="row">
|
||||
<div class="col-lg-9">
|
||||
<form class="form poll-settings">
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">[[poll:poll]]</div>
|
||||
<div class="panel-body">
|
||||
<div class="row">
|
||||
<div class="col-lg-6">
|
||||
<strong>[[poll:toggles]]</strong>
|
||||
<div class="form-group">
|
||||
<div class="checkbox">
|
||||
<label>
|
||||
<input type="checkbox" data-key="toggles.allowAnon" data-trim="false"> [[poll:allow_guests]]
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-lg-6">
|
||||
<strong>[[poll:limits]]</strong>
|
||||
<div class="form-group">
|
||||
<label for="maxPollOptions">[[poll:max_options]]</label>
|
||||
<input type="number" class="form-control" id="maxPollOptions" placeholder="10" min="1" max="100" data-key="limits.maxOptions">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">[[poll:defaults]]</div>
|
||||
<div class="panel-body">
|
||||
<div class="row">
|
||||
<div class="col-lg-6">
|
||||
<div class="form-group">
|
||||
<label for="defaultsTitle">[[poll:default_title]]</label>
|
||||
<input type="text" class="form-control" id="defaultsTitle" placeholder="Poll" data-key="defaults.title">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="defaultsMaxVotes">[[poll:max_votes]]</label>
|
||||
<input type="number" class="form-control" id="defaultsMaxVotes" placeholder="1" min="1" max="100" data-key="defaults.maxvotes">
|
||||
<p class="help-block">[[poll:info_choices]]</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div class="col-lg-3">
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">[[poll:settings]]</div>
|
||||
<div class="panel-body">
|
||||
<button id="save" class="btn btn-primary btn-block">[[poll:save]]</button>
|
||||
<button id="reset" class="btn btn-warning btn-block">[[poll:reset]]</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
|
@ -1,70 +0,0 @@
|
|||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<h1>[[poll:poll]]</h1>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-xs-6 pull-left">
|
||||
<h2>[[poll:settings]]
|
||||
<small>[[poll:change_settings]]</small>
|
||||
<button id="reset" class="btn btn-warning btn-xs pull-right">[[poll:reset]]</button>
|
||||
<button id="save" class="btn btn-success btn-xs pull-right">[[poll:save]]</button>
|
||||
</h2>
|
||||
<hr>
|
||||
<form class="form" id="pollSettingsForm">
|
||||
<h3>[[poll:toggles]]</h3>
|
||||
<div class="form-group">
|
||||
<div class="checkbox">
|
||||
<label>
|
||||
<input type="checkbox" data-key="toggles.allowAnon"> [[poll:allow_guests]]
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<h3>[[poll:limits]]</h3>
|
||||
<div class="form-group">
|
||||
<label for="maxPollOptions">[[poll:max_options]]</label>
|
||||
<input type="number" class="form-control" id="maxPollOptions" placeholder="10" min="1" max="100" data-key="limits.maxOptions">
|
||||
</div>
|
||||
<h3>[[poll:defaults]]</h3>
|
||||
<div class="form-group">
|
||||
<label for="defaultsTitle">[[poll:default_title]]</label>
|
||||
<input type="text" class="form-control" id="defaultsTitle" placeholder="Poll" data-key="defaults.title">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="defaultsMaxVotes">[[poll:max_votes]]</label>
|
||||
<input type="number" class="form-control" id="defaultsMaxVotes" placeholder="1" min="1" max="100" data-key="defaults.maxvotes">
|
||||
<p class="help-block">[[poll:info_choices]]</p>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div class="col-xs-6 pull-right">
|
||||
<h2>[[poll:actions]]</h2>
|
||||
<hr>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
require(['settings'], function (settings) {
|
||||
var wrapper = $('#pollSettingsForm');
|
||||
settings.sync('poll', wrapper);
|
||||
$('#save').click(function(event) {
|
||||
event.preventDefault();
|
||||
settings.persist('poll', wrapper, function(){
|
||||
socket.emit('admin.plugins.poll.sync');
|
||||
});
|
||||
});
|
||||
$('#reset').click(function(event) {
|
||||
event.preventDefault();
|
||||
bootbox.confirm('Are you sure you wish to reset the settings?', function(sure) {
|
||||
if (sure) {
|
||||
socket.emit('admin.plugins.poll.getDefaults', null, function (err, data) {
|
||||
settings.set('poll', data, wrapper, function(){
|
||||
socket.emit('admin.plugins.poll.sync');
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
</script>
|
55
templates/poll/creator.tpl
Normal file → Executable file
55
templates/poll/creator.tpl
Normal file → Executable file
|
@ -1,28 +1,39 @@
|
|||
<div id="pollErrorBox" class="alert alert-danger hidden"></div>
|
||||
<form class="form" id="pollCreator">
|
||||
<div id="pollErrorBox" class="alert alert-danger hidden"></div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="pollInputTitle">[[poll:poll_title]]</label>
|
||||
<input data-poll-setting="title" type="text" class="form-control" id="pollInputTitle" placeholder="[[poll:enter_poll_title]]">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="pollInputTitle">[[poll:poll_title]]</label>
|
||||
<input type="text" name="settings[title]" id="pollInputTitle" value="{poll.settings.title}" placeholder="[[poll:enter_poll_title]]" class="form-control">
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="pollInputOptions">[[poll:options]]</label>
|
||||
<textarea id="pollInputOptions" class="form-control" rows="5" placeholder="[[poll:options_placeholder]]"></textarea>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="pollInputOptions">[[poll:options]]</label>
|
||||
<!-- IF poll.options.length -->
|
||||
<!-- BEGIN poll.options -->
|
||||
<input type="text" name="options[]" id="pollInputOptions" value="@value" class="form-control"/>
|
||||
<!-- END poll.options -->
|
||||
<!-- ELSE -->
|
||||
<input type="text" name="options[]" id="pollInputOptions" class="form-control"/>
|
||||
<!-- ENDIF poll.options.length -->
|
||||
<button type="button" id="pollAddOption" class="btn btn-primary btn-sm btn-block">Add option</button>
|
||||
</div>
|
||||
|
||||
<h3>[[poll:settings]]</h3>
|
||||
<hr>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="pollInputAmount">[[poll:max_votes]]</label>
|
||||
<!-- TODO change this to defaults -->
|
||||
<input data-poll-setting="max" type="number" min="1" max="10" step="1" class="form-control" id="pollInputAmount" placeholder="[[poll:enter_amount]]">
|
||||
<p class="help-block">[[poll:info_choices]]</p>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="pollInputAmount">[[poll:max_votes]]</label>
|
||||
<input type="number" name="settings[maxvotes]" id="pollInputAmount" value="{poll.settings.maxvotes}" min="1" max="10" step="1" placeholder="[[poll:enter_amount]]" class="form-control">
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="pollInputAmount">[[poll:auto_end]]</label>
|
||||
<input data-poll-setting="end" type="text" class="form-control" id="pollInputEnd" placeholder="[[poll:date_placeholder]]">
|
||||
<p class="help-block">[[poll:auto_end_help]]</p>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="pollInputEnd">[[poll:auto_end]]</label>
|
||||
|
||||
<div id="dtBox"></div>
|
||||
<div class='input-group date' id='pollInputEnd'>
|
||||
<input type="text" name="settings[end]" value="{poll.settings.end}" placeholder="[[poll:date_placeholder]]" class="form-control" readonly>
|
||||
<span class="input-group-addon">
|
||||
<span class="fa fa-calendar"></span>
|
||||
</span>
|
||||
</div>
|
||||
<p class="help-block">[[poll:auto_end_help]]</p>
|
||||
</div>
|
||||
</form>
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue