mirror of
https://github.com/discourse/discourse.git
synced 2024-12-04 12:43:40 +08:00
573a71fdd9
In some conditions, pages were skipped. This was implemented in the past inf490a8d
, but then reverted in04ec543
, because sometimes it was stuck reloading the first page. The code that loads more results was simplified and a lot of duplicate code was removed. The logic to remove users who changed their vote was also introduced again, but just for the regular polls.
980 lines
25 KiB
JavaScript
980 lines
25 KiB
JavaScript
import I18n from "I18n";
|
|
import { PIE_CHART_TYPE } from "discourse/plugins/poll/controllers/poll-ui-builder";
|
|
import RawHtml from "discourse/widgets/raw-html";
|
|
import { ajax } from "discourse/lib/ajax";
|
|
import { avatarFor } from "discourse/widgets/post";
|
|
import { createWidget } from "discourse/widgets/widget";
|
|
import evenRound from "discourse/plugins/poll/lib/even-round";
|
|
import { getColors } from "discourse/plugins/poll/lib/chart-colors";
|
|
import { h } from "virtual-dom";
|
|
import { iconNode } from "discourse-common/lib/icon-library";
|
|
import loadScript from "discourse/lib/load-script";
|
|
import { popupAjaxError } from "discourse/lib/ajax-error";
|
|
import { relativeAge } from "discourse/lib/formatter";
|
|
import round from "discourse/lib/round";
|
|
import showModal from "discourse/lib/show-modal";
|
|
|
|
const FETCH_VOTERS_COUNT = 25;
|
|
|
|
function optionHtml(option) {
|
|
const $node = $(`<span>${option.html}</span>`);
|
|
|
|
$node.find(".discourse-local-date").each((_index, elem) => {
|
|
$(elem).applyLocalDates();
|
|
});
|
|
|
|
return new RawHtml({ html: `<span>${$node.html()}</span>` });
|
|
}
|
|
|
|
function infoTextHtml(text) {
|
|
return new RawHtml({
|
|
html: `<span class="info-text">${text}</span>`,
|
|
});
|
|
}
|
|
|
|
function checkUserGroups(user, poll) {
|
|
const pollGroups =
|
|
poll && poll.groups && poll.groups.split(",").map((g) => g.toLowerCase());
|
|
|
|
if (!pollGroups) {
|
|
return true;
|
|
}
|
|
|
|
const userGroups =
|
|
user && user.groups && user.groups.map((g) => g.name.toLowerCase());
|
|
|
|
return userGroups && pollGroups.some((g) => userGroups.includes(g));
|
|
}
|
|
|
|
createWidget("discourse-poll-option", {
|
|
tagName: "li",
|
|
|
|
buildAttributes(attrs) {
|
|
return { tabindex: 0, "data-poll-option-id": attrs.option.id };
|
|
},
|
|
|
|
html(attrs) {
|
|
const contents = [];
|
|
const { option, vote } = attrs;
|
|
const chosen = vote.includes(option.id);
|
|
|
|
if (attrs.isMultiple) {
|
|
contents.push(iconNode(chosen ? "far-check-square" : "far-square"));
|
|
} else {
|
|
contents.push(iconNode(chosen ? "circle" : "far-circle"));
|
|
}
|
|
|
|
contents.push(" ");
|
|
contents.push(optionHtml(option));
|
|
|
|
return contents;
|
|
},
|
|
|
|
click(e) {
|
|
if ($(e.target).closest("a").length === 0) {
|
|
this.sendWidgetAction("toggleOption", this.attrs.option);
|
|
}
|
|
},
|
|
|
|
keyDown(e) {
|
|
if (e.keyCode === 13) {
|
|
this.click(e);
|
|
}
|
|
},
|
|
});
|
|
|
|
createWidget("discourse-poll-load-more", {
|
|
tagName: "div.poll-voters-toggle-expand",
|
|
buildKey: (attrs) => `load-more-${attrs.optionId}`,
|
|
|
|
defaultState() {
|
|
return { loading: false };
|
|
},
|
|
|
|
html(attrs, state) {
|
|
return state.loading
|
|
? h("div.spinner.small")
|
|
: h("a", iconNode("chevron-down"));
|
|
},
|
|
|
|
click() {
|
|
const { state, attrs } = this;
|
|
|
|
if (state.loading) {
|
|
return;
|
|
}
|
|
|
|
state.loading = true;
|
|
return this.sendWidgetAction("fetchVoters", attrs.optionId).finally(
|
|
() => (state.loading = false)
|
|
);
|
|
},
|
|
});
|
|
|
|
createWidget("discourse-poll-voters", {
|
|
tagName: "ul.poll-voters-list",
|
|
buildKey: (attrs) => `poll-voters-${attrs.optionId}`,
|
|
|
|
html(attrs) {
|
|
const contents = attrs.voters.map((user) =>
|
|
h("li", [
|
|
avatarFor("tiny", {
|
|
username: user.username,
|
|
template: user.avatar_template,
|
|
}),
|
|
" ",
|
|
])
|
|
);
|
|
|
|
if (attrs.voters.length < attrs.totalVotes) {
|
|
contents.push(this.attach("discourse-poll-load-more", attrs));
|
|
}
|
|
|
|
return h("div.poll-voters", contents);
|
|
},
|
|
});
|
|
|
|
createWidget("discourse-poll-standard-results", {
|
|
tagName: "ul.results",
|
|
buildKey: (attrs) => `poll-standard-results-${attrs.id}`,
|
|
|
|
html(attrs) {
|
|
const { poll } = attrs;
|
|
const options = poll.options;
|
|
|
|
if (options) {
|
|
const voters = poll.voters;
|
|
const isPublic = poll.public;
|
|
|
|
const ordered = [...options].sort((a, b) => {
|
|
if (a.votes < b.votes) {
|
|
return 1;
|
|
} else if (a.votes === b.votes) {
|
|
if (a.html < b.html) {
|
|
return -1;
|
|
} else {
|
|
return 1;
|
|
}
|
|
} else {
|
|
return -1;
|
|
}
|
|
});
|
|
|
|
const percentages =
|
|
voters === 0
|
|
? Array(ordered.length).fill(0)
|
|
: ordered.map((o) => (100 * o.votes) / voters);
|
|
|
|
const rounded = attrs.isMultiple
|
|
? percentages.map(Math.floor)
|
|
: evenRound(percentages);
|
|
|
|
return ordered.map((option, idx) => {
|
|
const contents = [];
|
|
const per = rounded[idx].toString();
|
|
const chosen = (attrs.vote || []).includes(option.id);
|
|
|
|
contents.push(
|
|
h(
|
|
"div.option",
|
|
h("p", [h("span.percentage", `${per}%`), optionHtml(option)])
|
|
)
|
|
);
|
|
|
|
contents.push(
|
|
h(
|
|
"div.bar-back",
|
|
h("div.bar", { attributes: { style: `width:${per}%` } })
|
|
)
|
|
);
|
|
|
|
if (isPublic) {
|
|
contents.push(
|
|
this.attach("discourse-poll-voters", {
|
|
postId: attrs.post.id,
|
|
optionId: option.id,
|
|
pollName: poll.name,
|
|
totalVotes: option.votes,
|
|
voters: (attrs.voters && attrs.voters[option.id]) || [],
|
|
})
|
|
);
|
|
}
|
|
|
|
return h("li", { className: `${chosen ? "chosen" : ""}` }, contents);
|
|
});
|
|
}
|
|
},
|
|
});
|
|
|
|
createWidget("discourse-poll-number-results", {
|
|
buildKey: (attrs) => `poll-number-results-${attrs.id}`,
|
|
|
|
html(attrs) {
|
|
const { poll } = attrs;
|
|
|
|
const totalScore = poll.options.reduce((total, o) => {
|
|
return total + parseInt(o.html, 10) * parseInt(o.votes, 10);
|
|
}, 0);
|
|
|
|
const voters = poll.voters;
|
|
const average = voters === 0 ? 0 : round(totalScore / voters, -2);
|
|
const averageRating = I18n.t("poll.average_rating", { average });
|
|
const contents = [
|
|
h(
|
|
"div.poll-results-number-rating",
|
|
new RawHtml({ html: `<span>${averageRating}</span>` })
|
|
),
|
|
];
|
|
|
|
if (poll.public) {
|
|
contents.push(
|
|
this.attach("discourse-poll-voters", {
|
|
totalVotes: poll.voters,
|
|
voters: attrs.voters || [],
|
|
postId: attrs.post.id,
|
|
pollName: poll.name,
|
|
pollType: poll.type,
|
|
})
|
|
);
|
|
}
|
|
|
|
return contents;
|
|
},
|
|
});
|
|
|
|
createWidget("discourse-poll-container", {
|
|
tagName: "div.poll-container",
|
|
buildKey: (attrs) => `poll-container-${attrs.id}`,
|
|
|
|
defaultState() {
|
|
return { voters: [] };
|
|
},
|
|
|
|
html(attrs, state) {
|
|
const { poll } = attrs;
|
|
const options = poll.options;
|
|
|
|
if (attrs.showResults) {
|
|
const contents = [];
|
|
|
|
if (attrs.titleHTML) {
|
|
contents.push(new RawHtml({ html: attrs.titleHTML }));
|
|
}
|
|
|
|
if (poll.public) {
|
|
state.voters = poll.preloaded_voters;
|
|
}
|
|
|
|
const type = poll.type === "number" ? "number" : "standard";
|
|
const resultsWidget =
|
|
type === "number" || attrs.poll.chart_type !== PIE_CHART_TYPE
|
|
? `discourse-poll-${type}-results`
|
|
: "discourse-poll-pie-chart";
|
|
contents.push(
|
|
this.attach(
|
|
resultsWidget,
|
|
Object.assign({}, attrs, { voters: state.voters })
|
|
)
|
|
);
|
|
|
|
return contents;
|
|
} else if (options) {
|
|
const contents = [];
|
|
|
|
if (attrs.titleHTML) {
|
|
contents.push(new RawHtml({ html: attrs.titleHTML }));
|
|
}
|
|
|
|
if (!checkUserGroups(this.currentUser, poll)) {
|
|
contents.push(
|
|
h(
|
|
"div.alert.alert-danger",
|
|
I18n.t("poll.results.groups.title", { groups: poll.groups })
|
|
)
|
|
);
|
|
}
|
|
|
|
contents.push(
|
|
h(
|
|
"ul",
|
|
options.map((option) => {
|
|
return this.attach("discourse-poll-option", {
|
|
option,
|
|
isMultiple: attrs.isMultiple,
|
|
vote: attrs.vote,
|
|
});
|
|
})
|
|
)
|
|
);
|
|
|
|
return contents;
|
|
}
|
|
},
|
|
|
|
fetchVoters(optionId) {
|
|
const { attrs, state } = this;
|
|
let votersCount;
|
|
|
|
if (optionId) {
|
|
if (!state.voters) {
|
|
state.voters = {};
|
|
}
|
|
|
|
if (!state.voters[optionId]) {
|
|
state.voters[optionId] = [];
|
|
}
|
|
|
|
votersCount = state.voters[optionId].length;
|
|
} else {
|
|
if (!state.voters) {
|
|
state.voters = [];
|
|
}
|
|
|
|
votersCount = state.voters.length;
|
|
}
|
|
|
|
return ajax("/polls/voters.json", {
|
|
data: {
|
|
post_id: attrs.post.id,
|
|
poll_name: attrs.poll.name,
|
|
option_id: optionId,
|
|
page: Math.floor(votersCount / FETCH_VOTERS_COUNT) + 1,
|
|
limit: FETCH_VOTERS_COUNT,
|
|
},
|
|
})
|
|
.then((result) => {
|
|
const voters = optionId ? state.voters[optionId] : state.voters;
|
|
const newVoters = optionId ? result.voters[optionId] : result.voters;
|
|
|
|
const votersSet = new Set(voters.map((voter) => voter.username));
|
|
newVoters.forEach((voter) => {
|
|
if (!votersSet.has(voter.username)) {
|
|
votersSet.add(voter.username);
|
|
voters.push(voter);
|
|
}
|
|
});
|
|
|
|
// remove users who changed their vote
|
|
if (attrs.poll.type === "regular") {
|
|
Object.keys(state.voters).forEach((otherOptionId) => {
|
|
if (optionId !== otherOptionId) {
|
|
state.voters[otherOptionId] = state.voters[otherOptionId].filter(
|
|
(voter) => !votersSet.has(voter.username)
|
|
);
|
|
}
|
|
});
|
|
}
|
|
|
|
this.scheduleRerender();
|
|
})
|
|
.catch((error) => {
|
|
if (error) {
|
|
popupAjaxError(error);
|
|
} else {
|
|
bootbox.alert(I18n.t("poll.error_while_fetching_voters"));
|
|
}
|
|
});
|
|
},
|
|
});
|
|
|
|
createWidget("discourse-poll-info", {
|
|
tagName: "div.poll-info",
|
|
|
|
multipleHelpText(min, max, options) {
|
|
if (max > 0) {
|
|
if (min === max) {
|
|
if (min > 1) {
|
|
return I18n.t("poll.multiple.help.x_options", { count: min });
|
|
}
|
|
} else if (min > 1) {
|
|
if (max < options) {
|
|
return I18n.t("poll.multiple.help.between_min_and_max_options", {
|
|
min,
|
|
max,
|
|
});
|
|
} else {
|
|
return I18n.t("poll.multiple.help.at_least_min_options", {
|
|
count: min,
|
|
});
|
|
}
|
|
} else if (max <= options) {
|
|
return I18n.t("poll.multiple.help.up_to_max_options", { count: max });
|
|
}
|
|
}
|
|
},
|
|
|
|
html(attrs) {
|
|
const { poll } = attrs;
|
|
const count = poll.voters;
|
|
const contents = [
|
|
h("p", [
|
|
h("span.info-number", count.toString()),
|
|
h("span.info-label", I18n.t("poll.voters", { count })),
|
|
]),
|
|
];
|
|
|
|
if (attrs.isMultiple) {
|
|
if (attrs.showResults || attrs.isClosed) {
|
|
const totalVotes = poll.options.reduce((total, o) => {
|
|
return total + parseInt(o.votes, 10);
|
|
}, 0);
|
|
|
|
contents.push(
|
|
h("p", [
|
|
h("span.info-number", totalVotes.toString()),
|
|
h(
|
|
"span.info-label",
|
|
I18n.t("poll.total_votes", { count: totalVotes })
|
|
),
|
|
])
|
|
);
|
|
} else {
|
|
const help = this.multipleHelpText(
|
|
attrs.min,
|
|
attrs.max,
|
|
poll.options.length
|
|
);
|
|
if (help) {
|
|
contents.push(infoTextHtml(help));
|
|
}
|
|
}
|
|
}
|
|
|
|
if (
|
|
!attrs.isClosed &&
|
|
!attrs.showResults &&
|
|
poll.public &&
|
|
poll.results !== "staff_only"
|
|
) {
|
|
contents.push(infoTextHtml(I18n.t("poll.public.title")));
|
|
}
|
|
|
|
return contents;
|
|
},
|
|
});
|
|
|
|
function clearPieChart(id) {
|
|
let el = document.querySelector(`#poll-results-chart-${id}`);
|
|
el && el.parentNode.removeChild(el);
|
|
}
|
|
|
|
createWidget("discourse-poll-pie-canvas", {
|
|
tagName: "canvas.poll-results-canvas",
|
|
|
|
init(attrs) {
|
|
loadScript("/javascripts/Chart.min.js").then(() => {
|
|
const data = attrs.poll.options.mapBy("votes");
|
|
const labels = attrs.poll.options.mapBy("html");
|
|
const config = pieChartConfig(data, labels);
|
|
|
|
const el = document.getElementById(`poll-results-chart-${attrs.id}`);
|
|
// eslint-disable-next-line
|
|
let chart = new Chart(el.getContext("2d"), config);
|
|
document.getElementById(
|
|
`poll-results-legend-${attrs.id}`
|
|
).innerHTML = chart.generateLegend();
|
|
});
|
|
},
|
|
|
|
buildAttributes(attrs) {
|
|
return {
|
|
id: `poll-results-chart-${attrs.id}`,
|
|
};
|
|
},
|
|
});
|
|
|
|
createWidget("discourse-poll-pie-chart", {
|
|
tagName: "div.poll-results-chart",
|
|
|
|
html(attrs) {
|
|
const contents = [];
|
|
|
|
if (!attrs.showResults) {
|
|
clearPieChart(attrs.id);
|
|
return contents;
|
|
}
|
|
|
|
const chart = this.attach("discourse-poll-pie-canvas", attrs);
|
|
contents.push(chart);
|
|
|
|
contents.push(h(`div#poll-results-legend-${attrs.id}.pie-chart-legends`));
|
|
|
|
return contents;
|
|
},
|
|
});
|
|
|
|
function pieChartConfig(data, labels, opts = {}) {
|
|
const aspectRatio = "aspectRatio" in opts ? opts.aspectRatio : 2.2;
|
|
const strippedLabels = labels.map((l) => stripHtml(l));
|
|
return {
|
|
type: PIE_CHART_TYPE,
|
|
data: {
|
|
datasets: [
|
|
{
|
|
data,
|
|
backgroundColor: getColors(data.length),
|
|
},
|
|
],
|
|
labels: strippedLabels,
|
|
},
|
|
options: {
|
|
responsive: true,
|
|
aspectRatio,
|
|
animation: { duration: 0 },
|
|
legend: { display: false },
|
|
legendCallback: function (chart) {
|
|
let legends = "";
|
|
for (let i = 0; i < labels.length; i++) {
|
|
legends += `<div class="legend"><span class="swatch" style="background-color:
|
|
${chart.data.datasets[0].backgroundColor[i]}"></span>${labels[i]}</div>`;
|
|
}
|
|
return legends;
|
|
},
|
|
},
|
|
};
|
|
}
|
|
|
|
function stripHtml(html) {
|
|
let doc = new DOMParser().parseFromString(html, "text/html");
|
|
return doc.body.textContent || "";
|
|
}
|
|
|
|
createWidget("discourse-poll-buttons", {
|
|
tagName: "div.poll-buttons",
|
|
|
|
html(attrs) {
|
|
const contents = [];
|
|
const { poll, post } = attrs;
|
|
const topicArchived = post.get("topic.archived");
|
|
const closed = attrs.isClosed;
|
|
const staffOnly = poll.results === "staff_only";
|
|
const isStaff = this.currentUser && this.currentUser.staff;
|
|
const isAdmin = this.currentUser && this.currentUser.admin;
|
|
const isMe = this.currentUser && post.user_id === this.currentUser.id;
|
|
const dataExplorerEnabled = this.siteSettings.data_explorer_enabled;
|
|
const hideResultsDisabled = !staffOnly && (closed || topicArchived);
|
|
const exportQueryID = this.siteSettings.poll_export_data_explorer_query_id;
|
|
|
|
if (attrs.isMultiple && !hideResultsDisabled) {
|
|
const castVotesDisabled = !attrs.canCastVotes;
|
|
contents.push(
|
|
this.attach("button", {
|
|
className: `cast-votes ${
|
|
castVotesDisabled ? "btn-default" : "btn-primary"
|
|
}`,
|
|
label: "poll.cast-votes.label",
|
|
title: "poll.cast-votes.title",
|
|
disabled: castVotesDisabled,
|
|
action: "castVotes",
|
|
})
|
|
);
|
|
contents.push(" ");
|
|
}
|
|
|
|
if (attrs.showResults || hideResultsDisabled) {
|
|
contents.push(
|
|
this.attach("button", {
|
|
className: "btn-default toggle-results",
|
|
label: "poll.hide-results.label",
|
|
title: "poll.hide-results.title",
|
|
icon: "far-eye-slash",
|
|
disabled: hideResultsDisabled,
|
|
action: "toggleResults",
|
|
})
|
|
);
|
|
} else {
|
|
if (poll.results === "on_vote" && !attrs.hasVoted && !isMe) {
|
|
contents.push(infoTextHtml(I18n.t("poll.results.vote.title")));
|
|
} else if (poll.results === "on_close" && !closed) {
|
|
contents.push(infoTextHtml(I18n.t("poll.results.closed.title")));
|
|
} else if (poll.results === "staff_only" && !isStaff) {
|
|
contents.push(infoTextHtml(I18n.t("poll.results.staff.title")));
|
|
} else {
|
|
contents.push(
|
|
this.attach("button", {
|
|
className: "btn-default toggle-results",
|
|
label: "poll.show-results.label",
|
|
title: "poll.show-results.title",
|
|
icon: "far-eye",
|
|
disabled: poll.voters === 0,
|
|
action: "toggleResults",
|
|
})
|
|
);
|
|
}
|
|
}
|
|
|
|
if (attrs.groupableUserFields.length && poll.voters > 0) {
|
|
const button = this.attach("button", {
|
|
className: "btn-default poll-show-breakdown",
|
|
label: "poll.group-results.label",
|
|
title: "poll.group-results.title",
|
|
icon: "far-eye",
|
|
action: "showBreakdown",
|
|
});
|
|
|
|
contents.push(button);
|
|
}
|
|
|
|
if (isAdmin && dataExplorerEnabled && poll.voters > 0 && exportQueryID) {
|
|
contents.push(
|
|
this.attach("button", {
|
|
className: "btn btn-default export-results",
|
|
label: "poll.export-results.label",
|
|
title: "poll.export-results.title",
|
|
icon: "download",
|
|
disabled: poll.voters === 0,
|
|
action: "exportResults",
|
|
})
|
|
);
|
|
}
|
|
|
|
if (poll.close) {
|
|
const closeDate = moment(poll.close);
|
|
if (closeDate.isValid()) {
|
|
const title = closeDate.format("LLL");
|
|
let label;
|
|
|
|
if (attrs.isAutomaticallyClosed) {
|
|
const age = relativeAge(closeDate.toDate(), { addAgo: true });
|
|
label = I18n.t("poll.automatic_close.age", { age });
|
|
} else {
|
|
const timeLeft = moment().to(closeDate, true);
|
|
label = I18n.t("poll.automatic_close.closes_in", { timeLeft });
|
|
}
|
|
|
|
contents.push(
|
|
new RawHtml({
|
|
html: `<span class="info-text" title="${title}">${label}</span>`,
|
|
})
|
|
);
|
|
}
|
|
}
|
|
|
|
if (
|
|
this.currentUser &&
|
|
(this.currentUser.id === post.user_id || isStaff) &&
|
|
!topicArchived
|
|
) {
|
|
if (closed) {
|
|
if (!attrs.isAutomaticallyClosed) {
|
|
contents.push(
|
|
this.attach("button", {
|
|
className: "btn-default toggle-status",
|
|
label: "poll.open.label",
|
|
title: "poll.open.title",
|
|
icon: "unlock-alt",
|
|
action: "toggleStatus",
|
|
})
|
|
);
|
|
}
|
|
} else {
|
|
contents.push(
|
|
this.attach("button", {
|
|
className: "toggle-status btn-danger",
|
|
label: "poll.close.label",
|
|
title: "poll.close.title",
|
|
icon: "lock",
|
|
action: "toggleStatus",
|
|
})
|
|
);
|
|
}
|
|
}
|
|
|
|
return contents;
|
|
},
|
|
});
|
|
|
|
export default createWidget("discourse-poll", {
|
|
tagName: "div",
|
|
buildKey: (attrs) => `poll-${attrs.id}`,
|
|
|
|
buildAttributes(attrs) {
|
|
let cssClasses = "poll";
|
|
if (attrs.poll.chart_type === PIE_CHART_TYPE) {
|
|
cssClasses += " pie";
|
|
}
|
|
return {
|
|
class: cssClasses,
|
|
"data-poll-name": attrs.poll.name,
|
|
"data-poll-type": attrs.poll.type,
|
|
};
|
|
},
|
|
|
|
defaultState(attrs) {
|
|
const { poll } = attrs;
|
|
const staffOnly = attrs.poll.results === "staff_only";
|
|
|
|
const showResults =
|
|
poll.results !== "on_close" && this.hasVoted() && !staffOnly;
|
|
|
|
return { loading: false, showResults };
|
|
},
|
|
|
|
html(attrs, state) {
|
|
const staffOnly = attrs.poll.results === "staff_only";
|
|
const showResults =
|
|
state.showResults ||
|
|
(attrs.post.get("topic.archived") && !staffOnly) ||
|
|
(this.isClosed() && !staffOnly);
|
|
|
|
const newAttrs = jQuery.extend({}, attrs, {
|
|
canCastVotes: this.canCastVotes(),
|
|
hasVoted: this.hasVoted(),
|
|
isAutomaticallyClosed: this.isAutomaticallyClosed(),
|
|
isClosed: this.isClosed(),
|
|
isMultiple: this.isMultiple(),
|
|
max: this.max(),
|
|
min: this.min(),
|
|
showResults,
|
|
});
|
|
|
|
return h("div", [
|
|
this.attach("discourse-poll-container", newAttrs),
|
|
this.attach("discourse-poll-info", newAttrs),
|
|
this.attach("discourse-poll-buttons", newAttrs),
|
|
]);
|
|
},
|
|
|
|
min() {
|
|
let min = parseInt(this.attrs.poll.min, 10);
|
|
if (isNaN(min) || min < 0) {
|
|
min = 0;
|
|
}
|
|
return min;
|
|
},
|
|
|
|
max() {
|
|
let max = parseInt(this.attrs.poll.max, 10);
|
|
const numOptions = this.attrs.poll.options.length;
|
|
if (isNaN(max) || max > numOptions) {
|
|
max = numOptions;
|
|
}
|
|
return max;
|
|
},
|
|
|
|
isAutomaticallyClosed() {
|
|
const { poll } = this.attrs;
|
|
return poll.close && moment.utc(poll.close) <= moment();
|
|
},
|
|
|
|
isClosed() {
|
|
const { poll } = this.attrs;
|
|
return poll.status === "closed" || this.isAutomaticallyClosed();
|
|
},
|
|
|
|
isMultiple() {
|
|
const { poll } = this.attrs;
|
|
return poll.type === "multiple";
|
|
},
|
|
|
|
hasVoted() {
|
|
const { vote } = this.attrs;
|
|
return vote && vote.length > 0;
|
|
},
|
|
|
|
canCastVotes() {
|
|
const { state, attrs } = this;
|
|
|
|
if (this.isClosed() || state.showResults || state.loading) {
|
|
return false;
|
|
}
|
|
|
|
const selectedOptionCount = attrs.vote.length;
|
|
|
|
if (this.isMultiple()) {
|
|
return (
|
|
selectedOptionCount >= this.min() && selectedOptionCount <= this.max()
|
|
);
|
|
}
|
|
|
|
return selectedOptionCount > 0;
|
|
},
|
|
|
|
toggleStatus() {
|
|
const { state, attrs } = this;
|
|
const { post, poll } = attrs;
|
|
|
|
if (this.isAutomaticallyClosed()) {
|
|
return;
|
|
}
|
|
|
|
bootbox.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";
|
|
|
|
ajax("/polls/toggle_status", {
|
|
type: "PUT",
|
|
data: {
|
|
post_id: post.id,
|
|
poll_name: poll.name,
|
|
status,
|
|
},
|
|
})
|
|
.then(() => {
|
|
poll.set("status", status);
|
|
if (poll.results === "on_close") {
|
|
state.showResults = status === "closed";
|
|
}
|
|
this.scheduleRerender();
|
|
})
|
|
.catch((error) => {
|
|
if (error) {
|
|
popupAjaxError(error);
|
|
} else {
|
|
bootbox.alert(I18n.t("poll.error_while_toggling_status"));
|
|
}
|
|
})
|
|
.finally(() => {
|
|
state.loading = false;
|
|
});
|
|
}
|
|
}
|
|
);
|
|
},
|
|
|
|
toggleResults() {
|
|
this.state.showResults = !this.state.showResults;
|
|
},
|
|
|
|
exportResults() {
|
|
const { attrs } = this;
|
|
const queryID = this.siteSettings.poll_export_data_explorer_query_id;
|
|
|
|
// This uses the Data Explorer plugin export as CSV route
|
|
// There is detection to check if the plugin is enabled before showing the button
|
|
ajax(`/admin/plugins/explorer/queries/${queryID}/run.csv`, {
|
|
type: "POST",
|
|
data: {
|
|
// needed for data-explorer route compatibility
|
|
params: JSON.stringify({
|
|
poll_name: attrs.poll.name,
|
|
post_id: attrs.post.id.toString(), // needed for data-explorer route compatibility
|
|
}),
|
|
explain: false,
|
|
limit: 1000000,
|
|
download: 1,
|
|
},
|
|
})
|
|
.then((csvContent) => {
|
|
const downloadLink = document.createElement("a");
|
|
const blob = new Blob([csvContent], {
|
|
type: "text/csv;charset=utf-8;",
|
|
});
|
|
downloadLink.href = URL.createObjectURL(blob);
|
|
downloadLink.setAttribute(
|
|
"download",
|
|
`poll-export-${attrs.poll.name}-${attrs.post.id}.csv`
|
|
);
|
|
downloadLink.click();
|
|
downloadLink.remove();
|
|
})
|
|
.catch((error) => {
|
|
if (error) {
|
|
popupAjaxError(error);
|
|
} else {
|
|
bootbox.alert(I18n.t("poll.error_while_exporting_results"));
|
|
}
|
|
});
|
|
},
|
|
|
|
showLogin() {
|
|
this.register.lookup("route:application").send("showLogin");
|
|
},
|
|
|
|
_toggleOption(option) {
|
|
const { vote } = this.attrs;
|
|
const chosenIdx = vote.indexOf(option.id);
|
|
if (chosenIdx !== -1) {
|
|
vote.splice(chosenIdx, 1);
|
|
} else {
|
|
vote.push(option.id);
|
|
}
|
|
},
|
|
|
|
toggleOption(option) {
|
|
const { attrs } = this;
|
|
|
|
if (this.isClosed()) {
|
|
return;
|
|
}
|
|
if (!this.currentUser) {
|
|
return this.showLogin();
|
|
}
|
|
if (!checkUserGroups(this.currentUser, this.attrs.poll)) {
|
|
return;
|
|
}
|
|
|
|
const { vote } = attrs;
|
|
if (!this.isMultiple()) {
|
|
vote.length = 0;
|
|
}
|
|
|
|
this._toggleOption(option);
|
|
if (!this.isMultiple()) {
|
|
return this.castVotes().catch(() => this._toggleOption(option));
|
|
}
|
|
},
|
|
|
|
castVotes() {
|
|
if (!this.canCastVotes()) {
|
|
return;
|
|
}
|
|
if (!this.currentUser) {
|
|
return this.showLogin();
|
|
}
|
|
|
|
const { attrs, state } = this;
|
|
|
|
state.loading = true;
|
|
|
|
return ajax("/polls/vote", {
|
|
type: "PUT",
|
|
data: {
|
|
post_id: attrs.post.id,
|
|
poll_name: attrs.poll.name,
|
|
options: attrs.vote,
|
|
},
|
|
})
|
|
.then(({ poll }) => {
|
|
attrs.poll.setProperties(poll);
|
|
this.appEvents.trigger("poll:voted", poll, attrs.post, attrs.vote);
|
|
|
|
if (attrs.poll.results !== "on_close") {
|
|
state.showResults = true;
|
|
}
|
|
if (attrs.poll.results === "staff_only") {
|
|
if (this.currentUser && this.currentUser.staff) {
|
|
state.showResults = true;
|
|
} else {
|
|
state.showResults = false;
|
|
}
|
|
}
|
|
})
|
|
.catch((error) => {
|
|
if (error) {
|
|
popupAjaxError(error);
|
|
} else {
|
|
bootbox.alert(I18n.t("poll.error_while_casting_votes"));
|
|
}
|
|
})
|
|
.finally(() => {
|
|
state.loading = false;
|
|
});
|
|
},
|
|
|
|
showBreakdown() {
|
|
showModal("poll-breakdown", {
|
|
model: this.attrs,
|
|
panels: [
|
|
{ id: "percentage", title: "poll.breakdown.percentage" },
|
|
{ id: "count", title: "poll.breakdown.count" },
|
|
],
|
|
});
|
|
},
|
|
});
|