FEATURE: automatically close a poll on a given date and time

This commit is contained in:
Régis Hanol
2018-05-03 02:12:19 +02:00
parent de6dd2dc02
commit ba14c80b9c
15 changed files with 204 additions and 217 deletions

View File

@@ -13,13 +13,11 @@ export default Ember.Controller.extend({
@computed("regularPollType", "numberPollType", "multiplePollType")
pollTypes(regularPollType, numberPollType, multiplePollType) {
let types = [];
types.push({ name: I18n.t("poll.ui_builder.poll_type.regular"), value: regularPollType });
types.push({ name: I18n.t("poll.ui_builder.poll_type.number"), value: numberPollType });
types.push({ name: I18n.t("poll.ui_builder.poll_type.multiple"), value: multiplePollType });
return types;
return [
{ name: I18n.t("poll.ui_builder.poll_type.regular"), value: regularPollType },
{ name: I18n.t("poll.ui_builder.poll_type.number"), value: numberPollType },
{ name: I18n.t("poll.ui_builder.poll_type.multiple"), value: multiplePollType },
];
},
@computed("pollType", "regularPollType")
@@ -101,8 +99,8 @@ export default Ember.Controller.extend({
return this._comboboxOptions(1, (parseInt(pollMax) || 1) + 1);
},
@computed("isNumber", "showMinMax", "pollType", "publicPoll", "pollOptions", "pollMin", "pollMax", "pollStep")
pollOutput(isNumber, showMinMax, pollType, publicPoll, pollOptions, pollMin, pollMax, pollStep) {
@computed("isNumber", "showMinMax", "pollType", "publicPoll", "pollOptions", "pollMin", "pollMax", "pollStep", "autoClose", "date", "time")
pollOutput(isNumber, showMinMax, pollType, publicPoll, pollOptions, pollMin, pollMax, pollStep, autoClose, date, time) {
let pollHeader = '[poll';
let output = '';
@@ -113,15 +111,15 @@ export default Ember.Controller.extend({
};
let step = pollStep;
if (step < 1) {
step = 1;
}
if (step < 1) { step = 1; }
if (pollType) pollHeader += ` type=${pollType}`;
if (pollMin && showMinMax) pollHeader += ` min=${pollMin}`;
if (pollMax) pollHeader += ` max=${pollMax}`;
if (isNumber) pollHeader += ` step=${step}`;
if (publicPoll) pollHeader += ' public=true';
if (publicPoll) pollHeader += ` public=true`;
if (autoClose) pollHeader += ` close=${moment(date + " " + time, "YYYY-MM-DD HH:mm").toISOString()}`;
pollHeader += ']';
output += `${pollHeader}\n`;
@@ -186,7 +184,10 @@ export default Ember.Controller.extend({
pollOptions: '',
pollMin: 1,
pollMax: null,
pollStep: 1
pollStep: 1,
autoClose: false,
date: moment().add(1, "day").format("YYYY-DD-MM"),
time: moment().add(1, "hour").format("HH:mm"),
});
},

View File

@@ -40,13 +40,6 @@
{{/if}}
{{/if}}
<div class="input-group">
<label>
{{input type='checkbox' checked=publicPoll}}
{{i18n "poll.ui_builder.poll_public.label"}}
</label>
</div>
{{#unless isNumber}}
<div class="input-group">
<label>{{i18n 'poll.ui_builder.poll_options.label'}}</label>
@@ -54,6 +47,28 @@
{{textarea value=pollOptions}}
</div>
{{/unless}}
<div class="input-group">
<label>
{{input type='checkbox' checked=publicPoll}}
{{i18n "poll.ui_builder.poll_public.label"}}
</label>
</div>
<div class="input-group">
<label>
{{input type="checkbox" checked=autoClose}}
{{i18n "poll.ui_builder.automatic_close.label"}}
</label>
</div>
{{#if autoClose}}
<div class="input-group">
{{date-picker-future value=date containerId="date-container"}}
{{input type="time" value=time}}
<div id="date-container"></div>
</div>
{{/if}}
</form>
{{/d-modal-body}}

View File

@@ -22,6 +22,13 @@ function initializePolls(api) {
}
});
let _glued = [];
let _interval = null;
function rerender() {
_glued.forEach(g => g.queueRerender());
}
api.modifyClass('model:post', {
_polls: null,
pollsObject: null,
@@ -41,12 +48,12 @@ function initializePolls(api) {
}
});
this.set("pollsObject", this._polls);
_glued.forEach(g => g.queueRerender());
rerender();
}
}
});
const _glued = [];
function attachPolls($elem, helper) {
const $polls = $('.poll', $elem);
if (!$polls.length) { return; }
@@ -60,6 +67,8 @@ function initializePolls(api) {
const polls = post.get("pollsObject");
if (!polls) { return; }
_interval = _interval || setInterval(rerender, 30000);
$polls.each((idx, pollElem) => {
const $poll = $(pollElem);
const pollName = $poll.data("poll-name");
@@ -81,7 +90,13 @@ function initializePolls(api) {
}
function cleanUpPolls() {
if (_interval) {
clearInterval(_interval);
_interval = null;
}
_glued.forEach(g => g.cleanUp());
_glued = [];
}
api.includePostAttributes("polls", "polls_votes");

View File

@@ -2,35 +2,7 @@
const DATA_PREFIX = "data-poll-";
const DEFAULT_POLL_NAME = "poll";
const WHITELISTED_ATTRIBUTES = ["type", "name", "min", "max", "step", "order", "status", "public"];
function getHelpText(count, min, max) {
// default values
if (isNaN(min) || min < 1) { min = 1; }
if (isNaN(max) || max > count) { max = count; }
// add some help text
let help;
if (max > 0) {
if (min === max) {
if (min > 1) {
help = I18n.t("poll.multiple.help.x_options", { count: min });
}
} else if (min > 1) {
if (max < count) {
help = I18n.t("poll.multiple.help.between_min_and_max_options", { min: min, max: max });
} else {
help = I18n.t("poll.multiple.help.at_least_min_options", { count: min });
}
} else if (max <= count) {
help = I18n.t("poll.multiple.help.up_to_max_options", { count: max });
}
}
return help;
}
const WHITELISTED_ATTRIBUTES = ["type", "name", "min", "max", "step", "order", "status", "public", "close"];
function replaceToken(tokens, target, list) {
let pos = tokens.indexOf(target);
@@ -50,7 +22,6 @@ function replaceToken(tokens, target, list) {
// analyzes the block to that we have poll options
function getListItems(tokens, startToken) {
let i = tokens.length-1;
let listItems = [];
let buffer = [];
@@ -217,63 +188,13 @@ const rule = {
token = state.push('span_open', 'span', 1);
token.block = false;
token.attrs = [['class', 'info-text']];
token.attrs = [['class', 'info-label']];
token = state.push('text', '', 0);
token.content = I18n.t("poll.voters", { count: 0 });
state.push('span_close', 'span', -1);
state.push('paragraph_close', 'p', -1);
// multiple help text
if (attributes[DATA_PREFIX + "type"] === "multiple") {
let help = getHelpText(items.length, min, max);
if (help) {
state.push('paragraph_open', 'p', 1);
token = state.push('html_inline', '', 0);
token.content = help;
state.push('paragraph_close', 'p', -1);
}
}
if (attributes[DATA_PREFIX + 'public'] === 'true') {
state.push('paragraph_open', 'p', 1);
token = state.push('text', '', 0);
token.content = I18n.t('poll.public.title');
state.push('paragraph_close', 'p', -1);
}
state.push('poll_close', 'div', -1);
state.push('poll_close', 'div', -1);
token = state.push('poll_open', 'div', 1);
token.attrs = [['class', 'poll-buttons']];
if (attributes[DATA_PREFIX + 'type'] === 'multiple') {
token = state.push('link_open', 'a', 1);
token.block = false;
token.attrs = [
['class', 'button cast-votes'],
['title', I18n.t('poll.cast-votes.title')]
];
token = state.push('text', '', 0);
token.content = I18n.t('poll.cast-votes.label');
state.push('link_close', 'a', -1);
}
token = state.push('link_open', 'a', 1);
token.block = false;
token.attrs = [
['class', 'button toggle-results'],
['title', I18n.t('poll.show-results.title')]
];
token = state.push('text', '', 0);
token.content = I18n.t("poll.show-results.label");
state.push('link_close', 'a', -1);
state.push('poll_close', 'div', -1);
state.push('poll_close', 'div', -1);
}
@@ -299,6 +220,7 @@ export function setup(helper) {
'div[data-*]',
'span.info-number',
'span.info-text',
'span.info-label',
'a.button.cast-votes',
'a.button.toggle-results',
'li[data-*]'

View File

@@ -34,7 +34,6 @@ createWidget('discourse-poll-option', {
html(attrs) {
const result = [];
const { option, vote } = attrs;
const chosen = vote.indexOf(option.id) !== -1;
@@ -45,6 +44,7 @@ createWidget('discourse-poll-option', {
}
result.push(' ');
result.push(optionHtml(option));
return result;
},
@@ -235,7 +235,6 @@ createWidget('discourse-poll-number-results', {
const { attrs, state } = this;
if (state.loaded === 'new') {
fetchVoters({
post_id: attrs.post.id,
poll_name: attrs.poll.get('name')
@@ -258,8 +257,7 @@ createWidget('discourse-poll-number-results', {
const voters = poll.voters;
const average = voters === 0 ? 0 : round(totalScore / voters, -2);
const averageRating = I18n.t("poll.average_rating", { average });
const results = [h('div.poll-results-number-rating',
new RawHtml({ html: `<span>${averageRating}</span>` }))];
const results = [h('div.poll-results-number-rating', new RawHtml({ html: `<span>${averageRating}</span>` }))];
if (isPublic) {
this.fetchVoters();
@@ -283,7 +281,7 @@ createWidget('discourse-poll-container', {
html(attrs) {
const { poll } = attrs;
if (attrs.showResults) {
if (attrs.showResults || attrs.isClosed) {
const type = poll.get('type') === 'number' ? 'number' : 'standard';
return this.attach(`discourse-poll-${type}-results`, attrs);
}
@@ -327,29 +325,37 @@ createWidget('discourse-poll-info', {
const count = poll.get('voters');
const result = [h('p', [
h('span.info-number', count.toString()),
h('span.info-text', I18n.t('poll.voters', { count }))
h('span.info-label', I18n.t('poll.voters', { count }))
])];
if (attrs.isMultiple) {
if (attrs.showResults) {
if (attrs.showResults || attrs.isClosed) {
const totalVotes = poll.get('options').reduce((total, o) => {
return total + parseInt(o.votes, 10);
}, 0);
result.push(h('p', [
h('span.info-number', totalVotes.toString()),
h('span.info-text', I18n.t("poll.total_votes", { count: totalVotes }))
h('span.info-label', I18n.t("poll.total_votes", { count: totalVotes }))
]));
} else {
const help = this.multipleHelpText(attrs.min, attrs.max, poll.get('options.length'));
if (help) {
result.push(new RawHtml({ html: `<span>${help}</span>` }));
result.push(new RawHtml({ html: `<span class="info-text">${help}</span>` }));
}
}
}
if (!attrs.showResults && attrs.poll.get('public')) {
result.push(h('p', I18n.t('poll.public.title')));
if (!attrs.isClosed) {
if (!attrs.showResults && poll.get('public')) {
result.push(h('span.info-text', I18n.t('poll.public.title')));
}
if (poll.close) {
const closeDate = moment.utc(poll.close);
const timeLeft = moment().to(closeDate.local(), true);
result.push(new RawHtml({ html: `<span class="info-text" title="${closeDate.format("LLL")}">${I18n.t("poll.automatic_close.closes_in", { timeLeft })}</span>` }));
}
}
return result;
@@ -363,8 +369,8 @@ createWidget('discourse-poll-buttons', {
const results = [];
const { poll, post } = attrs;
const topicArchived = post.get('topic.archived');
const isClosed = poll.get('status') === 'closed';
const hideResultsDisabled = isClosed || topicArchived;
const closed = attrs.isClosed;
const hideResultsDisabled = closed || topicArchived;
if (attrs.isMultiple && !hideResultsDisabled) {
const castVotesDisabled = !attrs.canCastVotes;
@@ -378,7 +384,7 @@ createWidget('discourse-poll-buttons', {
results.push(' ');
}
if (attrs.showResults) {
if (attrs.showResults || hideResultsDisabled) {
results.push(this.attach('button', {
className: 'btn toggle-results',
label: 'poll.hide-results.label',
@@ -403,14 +409,16 @@ createWidget('discourse-poll-buttons', {
this.currentUser.get("staff")) &&
!topicArchived) {
if (isClosed) {
results.push(this.attach('button', {
className: 'btn toggle-status',
label: 'poll.open.label',
title: 'poll.open.title',
icon: 'unlock-alt',
action: 'toggleStatus'
}));
if (closed) {
if (!attrs.isAutomaticallyClosed) {
results.push(this.attach('button', {
className: 'btn toggle-status',
label: 'poll.open.label',
title: 'poll.open.title',
icon: 'unlock-alt',
action: 'toggleStatus'
}));
}
} else {
results.push(this.attach('button', {
className: 'btn toggle-status btn-danger',
@@ -422,7 +430,6 @@ createWidget('discourse-poll-buttons', {
}
}
return results;
}
});
@@ -437,14 +444,14 @@ export default createWidget('discourse-poll', {
"data-poll-type": poll.get('type'),
"data-poll-name": poll.get('name'),
"data-poll-status": poll.get('status'),
"data-poll-public": poll.get('public')
"data-poll-public": poll.get('public'),
"data-poll-close": poll.get('close'),
};
},
defaultState(attrs) {
const { poll, post } = attrs;
return { loading: false,
showResults: poll.get('isClosed') || post.get('topic.archived') };
const showResults = this.isClosed() || attrs.post.get('topic.archived');
return { loading: false, showResults };
},
html(attrs, state) {
@@ -452,9 +459,12 @@ export default createWidget('discourse-poll', {
const newAttrs = jQuery.extend({}, attrs, {
showResults,
canCastVotes: this.canCastVotes(),
isClosed: this.isClosed(),
isAutomaticallyClosed: this.isAutomaticallyClosed(),
min: this.min(),
max: this.max()
});
return h('div', [
this.attach('discourse-poll-container', newAttrs),
this.attach('discourse-poll-info', newAttrs),
@@ -462,10 +472,6 @@ export default createWidget('discourse-poll', {
]);
},
isClosed() {
return this.attrs.poll.get('status') === "closed";
},
min() {
let min = parseInt(this.attrs.poll.min, 10);
if (isNaN(min) || min < 1) { min = 1; }
@@ -479,37 +485,51 @@ export default createWidget('discourse-poll', {
return max;
},
isAutomaticallyClosed() {
const { poll } = this.attrs;
return poll.get("close") && moment.utc(poll.get("close")) <= moment();
},
isClosed() {
const { poll } = this.attrs;
return poll.get("status") === "closed" || this.isAutomaticallyClosed();
},
canCastVotes() {
const { state, attrs } = this;
if (this.isClosed() || state.showResults || state.loading) {
return false;
}
const selectedOptionCount = attrs.vote.length;
if (attrs.isMultiple) {
return selectedOptionCount >= this.min() && selectedOptionCount <= this.max();
}
return selectedOptionCount > 0;
},
toggleStatus() {
const { state, attrs } = this;
const { poll } = attrs;
const isClosed = poll.get('status') === 'closed';
const { post, poll } = attrs;
if (this.isAutomaticallyClosed()) { return; }
bootbox.confirm(
I18n.t(isClosed ? "poll.open.confirm" : "poll.close.confirm"),
I18n.t(this.isClosed() ? "poll.open.confirm" : "poll.close.confirm"),
I18n.t("no_value"),
I18n.t("yes_value"),
confirmed => {
if (confirmed) {
state.loading = true;
const status = this.isClosed() ? "open" : "closed";
const status = isClosed ? "open" : "closed";
ajax("/polls/toggle_status", {
type: "PUT",
data: {
post_id: attrs.post.get('id'),
post_id: post.get('id'),
poll_name: poll.get('name'),
status,
}
@@ -535,15 +555,15 @@ export default createWidget('discourse-poll', {
},
showLogin() {
const appRoute = this.register.lookup('route:application');
appRoute.send('showLogin');
this.register.lookup('route:application').send('showLogin');
},
toggleOption(option) {
const { attrs } = this;
if (this.isClosed()) { return; }
if (!this.currentUser) { this.showLogin(); }
const { attrs } = this;
const { vote } = attrs;
const chosenIdx = vote.indexOf(option.id);